Add abstraction layer for file system

This commit is contained in:
Tobias B 2021-11-14 13:13:50 +01:00
parent 3f5bc42ceb
commit 0b30282cb5
No known key found for this signature in database
GPG Key ID: 5EF4C92355A3B53D
7 changed files with 119 additions and 86 deletions

View File

@ -1,2 +1,4 @@
AUTH_CONFIG_PATH=
FILE_STORAGE_PATH=
FILE_STORAGE_PATH=
FILE_NAME_LENGTH=
FILE_META_DB_FILE=

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea/
.env
.env
.dev/

View File

@ -4,6 +4,7 @@
---
**aqua** is a simple file uploading and sharing server for personal use. It is built to be easy to set up and host on your own server.
**aqua** is a simple file uploading and sharing server for personal use. It is built to be easy to set up and host on your own server, for example to use it in combination with
uploading tools like [ShareX](https://getsharex.com/).
It is the successor/rework of my previous project [lightf](https://github.com/Superioz/lightf), but this time without trying weird things out and building a complete product instead.

View File

@ -10,7 +10,6 @@ import (
)
// TODO Add cleanup process, to delete all images that are not in the sqlite or that are expired
// TODO make file storage (not metadata) an adapter (AddFile, RemoveFile)
func main() {
err := godotenv.Load()
@ -19,17 +18,15 @@ func main() {
}
klog.Infoln("Hello World!")
// init some stuff for the handler
// like config etc.
handler.Initialize()
r := gin.New()
r.Use(middleware.Logger(3 * time.Second))
// restrict to max 100mb
r.Use(middleware.RestrictBodySize(100 * handler.SizeMegaByte))
r.Use(gin.Recovery())
r.POST("/upload", handler.Upload)
// handler for receiving uploaded files
uh := handler.NewUploadHandler()
r.POST("/upload", uh.Upload)
r.GET("/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "UP"})

View File

@ -1,7 +1,9 @@
package handler
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/superioz/aqua/internal/config"
"github.com/superioz/aqua/internal/storage"
"github.com/superioz/aqua/pkg/env"
@ -9,6 +11,7 @@ import (
"mime/multipart"
"net/http"
"strings"
"time"
)
const (
@ -24,36 +27,44 @@ var (
"text/csv",
"text/plain",
}
authConfig *config.AuthConfig
metaDb storage.FileMetaDatabase
)
func Initialize() {
type UploadHandler struct {
authConfig *config.AuthConfig
fileMetaDb storage.FileMetaDatabase
fileStorage storage.FileStorage
}
func NewUploadHandler() *UploadHandler {
handler := &UploadHandler{}
path := env.StringOrDefault("AUTH_CONFIG_PATH", "/etc/aqua/auth.yml")
ac, err := config.FromLocalFile(path)
if err != nil {
// this is not good, but the system still works.
// nobody can upload a file though.
klog.Warningf("Could not open auth config at %s: %v", path, err)
authConfig = config.NewEmptyAuthConfig()
return
handler.authConfig = config.NewEmptyAuthConfig()
} else {
klog.Infof("Loaded %d valid tokens", len(ac.ValidTokens))
handler.authConfig = ac
}
authConfig = ac
metaDbFilePath := env.StringOrDefault("FILE_META_DB_FILE", "./files.db")
metaDb = storage.NewSqliteFileMetaDatabase(metaDbFilePath)
handler.fileMetaDb = storage.NewSqliteFileMetaDatabase(metaDbFilePath)
fileStoragePath := env.StringOrDefault("FILE_STORAGE_PATH", "/var/lib/aqua/")
handler.fileStorage = storage.NewLocalFileStorage(fileStoragePath)
return handler
}
func Upload(c *gin.Context) {
func (u *UploadHandler) Upload(c *gin.Context) {
// get token for auth
// empty string, if not given
token := getToken(c)
klog.Infof("Checking authentication for token=%s", token)
if !authConfig.HasToken(token) {
if !u.authConfig.HasToken(token) {
c.JSON(http.StatusUnauthorized, gin.H{"msg": "the token is not valid"})
return
}
@ -87,7 +98,7 @@ func Upload(c *gin.Context) {
return
}
if !authConfig.CanUpload(token, ct) {
if !u.authConfig.CanUpload(token, ct) {
c.JSON(http.StatusForbidden, gin.H{"msg": "you can not upload a file with this content type"})
return
}
@ -100,15 +111,28 @@ func Upload(c *gin.Context) {
}
defer of.Close()
sf, err := storage.StoreFile(of, storage.ExpireNever)
name, err := getRandomFileName(env.IntOrDefault("FILE_NAME_LENGTH", 8))
if err != nil {
klog.Error(err)
c.JSON(http.StatusInternalServerError, gin.H{"msg": "could not store file"})
c.JSON(http.StatusInternalServerError, gin.H{"msg": "could not generate random name"})
return
}
_, err = u.fileStorage.CreateFile(of, name)
if err != nil {
klog.Error(err)
c.JSON(http.StatusInternalServerError, gin.H{"msg": "could not save file"})
}
t := time.Now()
sf := &storage.StoredFile{
Id: name,
UploadedAt: t.Unix(),
ExpiresAt: t.Add(time.Duration(storage.ExpireNever)).Unix(),
}
// write to meta database
metaDb.WriteFile(sf)
u.fileMetaDb.WriteFile(sf)
c.JSON(http.StatusOK, gin.H{"id": sf.Id})
}
@ -146,3 +170,24 @@ func getToken(c *gin.Context) string {
spl := strings.Split(bearerToken, "Bearer ")
return spl[1]
}
// getRandomFileName returns a random string with a fixed size
// that is generated from an uuid.
// It also checks, that no file with that name already exists,
// if that is the case, it generates a new one.
func getRandomFileName(size int) (string, error) {
if size <= 1 {
return "", errors.New("size must be greater than 1")
}
id, err := uuid.NewRandom()
if err != nil {
return "", err
}
// strip '-' from uuid
str := strings.ReplaceAll(id.String(), "-", "")
if size >= len(str) {
return str, nil
}
return str[:size], nil
}

50
internal/storage/files.go Normal file
View File

@ -0,0 +1,50 @@
package storage
import (
"io"
"mime/multipart"
"os"
)
type FileStorage interface {
CreateFile(mf multipart.File, name string) (bool, error)
DeleteFile(id string) error
GetFile(id string) (*os.File, error)
}
type LocalFileStorage struct {
FolderPath string
}
func (l LocalFileStorage) CreateFile(mf multipart.File, name string) (bool, error) {
err := os.MkdirAll(l.FolderPath, os.ModePerm)
if err != nil {
return false, err
}
f, err := os.Create(l.FolderPath + name)
if err != nil {
return false, err
}
defer f.Close()
// use io.Copy so that we don't have to load all the image into the memory.
// they get copied in smaller 32kb chunks.
_, err = io.Copy(f, mf)
if err != nil {
return true, err
}
return true, nil
}
func (l LocalFileStorage) DeleteFile(id string) error {
panic("implement me")
}
func (l LocalFileStorage) GetFile(id string) (*os.File, error) {
panic("implement me")
}
func NewLocalFileStorage(path string) *LocalFileStorage {
return &LocalFileStorage{FolderPath: path}
}

View File

@ -1,14 +1,7 @@
package storage
import (
"errors"
"fmt"
"github.com/google/uuid"
"github.com/superioz/aqua/pkg/env"
"io"
"mime/multipart"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
@ -27,59 +20,3 @@ type StoredFile struct {
func (sf *StoredFile) String() string {
return fmt.Sprintf("StoredFile<%s, %s>", sf.Id, time.Unix(sf.UploadedAt, 0).String())
}
func StoreFile(mf multipart.File, expiration int64) (*StoredFile, error) {
name, err := getRandomFileName(env.IntOrDefault("FILE_NAME_LENGTH", 8))
if err != nil {
return nil, err
}
path := env.StringOrDefault("FILE_STORAGE_PATH", "/var/lib/aqua/")
err = os.MkdirAll(path, os.ModePerm)
if err != nil {
return nil, err
}
f, err := os.Create(path + name)
if err != nil {
return nil, err
}
defer f.Close()
// use io.Copy so that we don't have to load all the image into the memory.
// they get copied in smaller 32kb chunks.
_, err = io.Copy(f, mf)
if err != nil {
return nil, err
}
t := time.Now()
sf := &StoredFile{
Id: name,
UploadedAt: t.Unix(),
ExpiresAt: t.Add(time.Duration(expiration)).Unix(),
}
return sf, nil
}
// getRandomFileName returns a random string with a fixed size
// that is generated from an uuid.
// It also checks, that no file with that name already exists,
// if that is the case, it generates a new one.
func getRandomFileName(size int) (string, error) {
if size <= 1 {
return "", errors.New("size must be greater than 1")
}
id, err := uuid.NewRandom()
if err != nil {
return "", err
}
// strip '-' from uuid
str := strings.ReplaceAll(id.String(), "-", "")
if size >= len(str) {
return str, nil
}
return str[:size], nil
}