diff --git a/.env.dist b/.env.dist index 9e2d4c1..c638ff8 100644 --- a/.env.dist +++ b/.env.dist @@ -4,5 +4,7 @@ FILE_NAME_LENGTH=12 FILE_MAX_SIZE=100 FILE_META_DB_PATH= FILE_EXPIRATION_CYCLE=10 -METRICS_ENABLED=true FILE_SERVING_ENABLED=true +FILE_EXTENSIONS_RESPONSE=true +FILE_EXTENSIONS_EXCLUDED=image/png,image/jpeg +METRICS_ENABLED=true diff --git a/README.md b/README.md index 2fd8ba8..f90fa0f 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,10 @@ For examplary usage of the environment variables, have a look at the `.env.dist` | `FILE_MAX_SIZE` | Maximum size for uploaded files in Megabytes. | | `FILE_META_DB_PATH` | Path to the directory, where the sqlite database for file metadata should be stored. Recommended to not be the same folder as `FILE_STORAGE_PATH` to prevent overlapping. | | `FILE_EXPIRATION_CYCLE` | Determines the interval of the expiration cycle. `5` means that every 5 seconds the files will be checked for expiration. | +| `FILE_SERVING_ENABLED` | Defaults to `true`, if `false`, the server won't serve the stored files. | +| `FILE_EXTENSIONS_RESPONSE` | Defaults to `true`. if the file name returned will have its extension added to it. | +| `FILE_EXTENSIONS_EXCLUDED` | Comma-seperated list of MIME types, that should be excluded from the extension response rule above. Defaults to `image/png,image/jpeg` | | `METRICS_ENABLED` | Is normally set to true, but otherwise disables the Prometheus metrics publishing. | -| `FILE_SERVING_ENABLED` | Defaults to true, when `false`, then the server won't serve the stored files. | ## Tokens @@ -182,7 +184,7 @@ After that you can import it to your custom upload goals in the ShareX UI. "metadata": "{ \"expiration\": 3600 }" }, "FileFormName": "file", - "URL": "https://your-domain.com/$json:id$" + "URL": "https://your-domain.com/$json:fileName$" } ``` diff --git a/VERSION b/VERSION index ee6cdce..b616048 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.6.2 diff --git a/cmd/aqua/main.go b/cmd/aqua/main.go index 5da0a9c..dffc439 100644 --- a/cmd/aqua/main.go +++ b/cmd/aqua/main.go @@ -15,7 +15,7 @@ import ( func main() { err := godotenv.Load() if err != nil { - klog.Warningf("Error loading .env file: %v", err) + klog.Warningf("Could not load .env file: %v", err) } klog.Infoln("Hello World!") diff --git a/internal/config/config.go b/internal/config/config.go index f2ff5dd..74fa2b7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,13 @@ import ( "os" ) +const ( + ExpireNever = -1 + + EnvDefaultFileStoragePath = "/var/lib/aqua/files/" + EnvDefaultMetaDbPath = "/var/lib/aqua/" +) + type AuthConfig struct { ValidTokens []*TokenConfig `yaml:"validTokens"` } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 8e0ddfd..4caeb9e 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,12 +1,12 @@ package handler import ( - "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/superioz/aqua/internal/config" "github.com/superioz/aqua/internal/metrics" "github.com/superioz/aqua/internal/mime" + "github.com/superioz/aqua/internal/request" "github.com/superioz/aqua/internal/storage" "github.com/superioz/aqua/pkg/env" "k8s.io/klog" @@ -20,27 +20,13 @@ const ( SizeMegaByte = 1 << (10 * 2) ) -var ( - emptyRequestMetadata = &RequestMetadata{Expiration: storage.ExpireNever} -) - -// TODO when accessing: `/N2YwODUx.mp4` => `/N2YwODUx` - -// RequestFormFile is the metadata we get from the file -// which is requested to be uploaded. -type RequestFormFile struct { - File multipart.File - ContentType string - ContentLength int64 -} - -type RequestMetadata struct { - Expiration int64 `json:"expiration"` -} - type UploadHandler struct { AuthConfig *config.AuthConfig FileStorage *storage.FileStorage + + // excluded as per defined by the environment variable + // FILE_EXTENSIONS_EXCEPT + exclMimeTypes []string } func NewUploadHandler() *UploadHandler { @@ -48,6 +34,7 @@ func NewUploadHandler() *UploadHandler { handler.ReloadAuthConfig() handler.FileStorage = storage.NewFileStorage() + handler.exclMimeTypes = env.ListOrDefault("FILE_EXTENSIONS_EXCLUDED", []string{"image/png", "image/jpeg"}) return handler } @@ -124,8 +111,8 @@ func (h *UploadHandler) Upload(c *gin.Context) { } defer of.Close() - metadata := getMetadata(form) - rff := &RequestFormFile{ + metadata := request.GetMetadata(form) + rff := &request.RequestFormFile{ File: of, ContentType: ct, ContentLength: c.Request.ContentLength, @@ -145,22 +132,26 @@ func (h *UploadHandler) Upload(c *gin.Context) { klog.Infof("Stored file %s (expiresIn: %s)", sf.Id, expiresIn) metrics.IncFilesUploaded() - c.JSON(http.StatusOK, gin.H{"id": sf.Id}) + // add extension to file name if it is enabled and + // the extension is not excluded + storedName := sf.Id + if env.BoolOrDefault("FILE_EXTENSIONS_RESPONSE", true) && !h.isExtensionExcluded(sf.MimeType) { + storedName = fmt.Sprintf("%s.%s", storedName, mime.GetExtension(sf.MimeType)) + } + + c.JSON(http.StatusOK, gin.H{"fileName": storedName}) } -func getMetadata(form *multipart.Form) *RequestMetadata { - metaRawList := form.Value["metadata"] - if len(metaRawList) == 0 { - return emptyRequestMetadata +// isExtensionExcluded returns if the given mime type +// should be excluded by the extension response rule, which states if +// an extension should be appended to the file name when responding. +func (h *UploadHandler) isExtensionExcluded(mimeType string) bool { + for _, exclMimeType := range h.exclMimeTypes { + if exclMimeType == mimeType { + return true + } } - metaRaw := metaRawList[0] - - var metadata *RequestMetadata - err := json.Unmarshal([]byte(metaRaw), &metadata) - if err != nil { - return emptyRequestMetadata - } - return metadata + return false } // workaround for file Content-Type headers @@ -191,10 +182,17 @@ func getToken(c *gin.Context) string { // HandleStaticFiles takes the files inside the configured file storage // path and serves them to the client. func HandleStaticFiles() gin.HandlerFunc { - fileStoragePath := env.StringOrDefault("FILE_STORAGE_PATH", storage.EnvDefaultFileStoragePath) + fileStoragePath := env.StringOrDefault("FILE_STORAGE_PATH", config.EnvDefaultFileStoragePath) return func(c *gin.Context) { fileName := c.Param("file") + + // the file name could contain the extension + // we split it and ship it. + if strings.Contains(fileName, ".") { + fileName = strings.Split(fileName, ".")[0] + } + fullPath := fileStoragePath + fileName f, err := os.Open(fullPath) diff --git a/internal/mime/mime.go b/internal/mime/mime.go index 883d3b7..2809eaf 100644 --- a/internal/mime/mime.go +++ b/internal/mime/mime.go @@ -28,7 +28,7 @@ var ( // IsValid checks if given type is inside Types map func IsValid(t string) bool { - for _, mt := range Types { + for mt := range Types { if mt == t { return true } diff --git a/internal/request/request.go b/internal/request/request.go new file mode 100644 index 0000000..2b14817 --- /dev/null +++ b/internal/request/request.go @@ -0,0 +1,38 @@ +package request + +import ( + "encoding/json" + "github.com/superioz/aqua/internal/config" + "mime/multipart" +) + +var ( + emptyRequestMetadata = &RequestMetadata{Expiration: config.ExpireNever} +) + +// RequestFormFile is the metadata we get from the file +// which is requested to be uploaded. +type RequestFormFile struct { + File multipart.File + ContentType string + ContentLength int64 +} + +type RequestMetadata struct { + Expiration int64 `json:"expiration"` +} + +func GetMetadata(form *multipart.Form) *RequestMetadata { + metaRawList := form.Value["metadata"] + if len(metaRawList) == 0 { + return emptyRequestMetadata + } + metaRaw := metaRawList[0] + + var metadata *RequestMetadata + err := json.Unmarshal([]byte(metaRaw), &metadata) + if err != nil { + return emptyRequestMetadata + } + return metadata +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 054e824..3ae4dbc 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" "github.com/google/uuid" - "github.com/superioz/aqua/internal/handler" + "github.com/superioz/aqua/internal/config" "github.com/superioz/aqua/internal/metrics" + "github.com/superioz/aqua/internal/request" "github.com/superioz/aqua/pkg/env" "k8s.io/klog" "strings" @@ -15,13 +16,6 @@ import ( _ "modernc.org/sqlite" ) -const ( - ExpireNever = -1 - - EnvDefaultFileStoragePath = "/var/lib/aqua/files/" - EnvDefaultMetaDbPath = "/var/lib/aqua/" -) - type StoredFile struct { Id string UploadedAt int64 @@ -43,14 +37,14 @@ type FileStorage struct { } func NewFileStorage() *FileStorage { - metaDbFilePath := env.StringOrDefault("FILE_META_DB_PATH", EnvDefaultMetaDbPath) + metaDbFilePath := env.StringOrDefault("FILE_META_DB_PATH", config.EnvDefaultMetaDbPath) fileMetaDb := NewSqliteFileMetaDatabase(metaDbFilePath) err := fileMetaDb.Connect() if err != nil { klog.Errorf("Could not connect to file meta db: %v", err) } - fileStoragePath := env.StringOrDefault("FILE_STORAGE_PATH", EnvDefaultFileStoragePath) + fileStoragePath := env.StringOrDefault("FILE_STORAGE_PATH", config.EnvDefaultFileStoragePath) fileSystem := NewLocalFileStorage(fileStoragePath) return &FileStorage{ @@ -102,7 +96,7 @@ func (fs *FileStorage) Cleanup() error { return nil } -func (fs *FileStorage) StoreFile(rff *handler.RequestFormFile, expiration int64) (*StoredFile, error) { +func (fs *FileStorage) StoreFile(rff *request.RequestFormFile, expiration int64) (*StoredFile, error) { name, err := getRandomFileName(env.IntOrDefault("FILE_NAME_LENGTH", 8)) if err != nil { return nil, errors.New("could not generate random name") @@ -116,8 +110,8 @@ func (fs *FileStorage) StoreFile(rff *handler.RequestFormFile, expiration int64) currentTime := time.Now().Unix() expAt := currentTime + expiration - if expiration == ExpireNever { - expAt = ExpireNever + if expiration == config.ExpireNever { + expAt = config.ExpireNever } sf := &StoredFile{ diff --git a/pkg/env/env.go b/pkg/env/env.go index 873269c..b1d3b13 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -3,6 +3,7 @@ package env import ( "os" "strconv" + "strings" ) func String(name string) (string, bool) { @@ -57,3 +58,16 @@ func BoolOrDefault(name string, def bool) bool { } return b } + +func ListOrDefault(name string, def []string) []string { + s, ok := String(name) + if !ok { + return def + } + + spl := strings.Split(s, ",") + if len(spl) == 0 { + return def + } + return spl +}