diff --git a/.env.dist b/.env.dist index 9d7d587..d77f2c3 100644 --- a/.env.dist +++ b/.env.dist @@ -1,2 +1,4 @@ AUTH_CONFIG_PATH= -FILE_STORAGE_PATH= \ No newline at end of file +FILE_STORAGE_PATH= +FILE_NAME_LENGTH= +FILE_META_DB_FILE= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8ea5b1e..24caf16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ -.env \ No newline at end of file +.env +.dev/ \ No newline at end of file diff --git a/README.md b/README.md index 2bcd2a8..f9b060a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/aqua/main.go b/cmd/aqua/main.go index d0076ec..dd75d86 100644 --- a/cmd/aqua/main.go +++ b/cmd/aqua/main.go @@ -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"}) diff --git a/internal/handler/upload.go b/internal/handler/upload.go index cad0d4d..53daaa4 100644 --- a/internal/handler/upload.go +++ b/internal/handler/upload.go @@ -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 +} diff --git a/internal/storage/files.go b/internal/storage/files.go new file mode 100644 index 0000000..788b7ee --- /dev/null +++ b/internal/storage/files.go @@ -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} +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 391aa5f..609e506 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -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 -}