Add extensions to support Discord embeds

This commit is contained in:
Tobias B 2021-11-18 02:35:04 +01:00
parent 1a2dbf266d
commit a81a33c17a
No known key found for this signature in database
GPG Key ID: 5EF4C92355A3B53D
10 changed files with 109 additions and 54 deletions

View File

@ -4,5 +4,7 @@ FILE_NAME_LENGTH=12
FILE_MAX_SIZE=100 FILE_MAX_SIZE=100
FILE_META_DB_PATH= FILE_META_DB_PATH=
FILE_EXPIRATION_CYCLE=10 FILE_EXPIRATION_CYCLE=10
METRICS_ENABLED=true
FILE_SERVING_ENABLED=true FILE_SERVING_ENABLED=true
FILE_EXTENSIONS_RESPONSE=true
FILE_EXTENSIONS_EXCLUDED=image/png,image/jpeg
METRICS_ENABLED=true

View File

@ -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_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_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_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. | | `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 ## Tokens
@ -182,7 +184,7 @@ After that you can import it to your custom upload goals in the ShareX UI.
"metadata": "{ \"expiration\": 3600 }" "metadata": "{ \"expiration\": 3600 }"
}, },
"FileFormName": "file", "FileFormName": "file",
"URL": "https://your-domain.com/$json:id$" "URL": "https://your-domain.com/$json:fileName$"
} }
``` ```

View File

@ -1 +1 @@
0.6.1 0.6.2

View File

@ -15,7 +15,7 @@ import (
func main() { func main() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
klog.Warningf("Error loading .env file: %v", err) klog.Warningf("Could not load .env file: %v", err)
} }
klog.Infoln("Hello World!") klog.Infoln("Hello World!")

View File

@ -6,6 +6,13 @@ import (
"os" "os"
) )
const (
ExpireNever = -1
EnvDefaultFileStoragePath = "/var/lib/aqua/files/"
EnvDefaultMetaDbPath = "/var/lib/aqua/"
)
type AuthConfig struct { type AuthConfig struct {
ValidTokens []*TokenConfig `yaml:"validTokens"` ValidTokens []*TokenConfig `yaml:"validTokens"`
} }

View File

@ -1,12 +1,12 @@
package handler package handler
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superioz/aqua/internal/config" "github.com/superioz/aqua/internal/config"
"github.com/superioz/aqua/internal/metrics" "github.com/superioz/aqua/internal/metrics"
"github.com/superioz/aqua/internal/mime" "github.com/superioz/aqua/internal/mime"
"github.com/superioz/aqua/internal/request"
"github.com/superioz/aqua/internal/storage" "github.com/superioz/aqua/internal/storage"
"github.com/superioz/aqua/pkg/env" "github.com/superioz/aqua/pkg/env"
"k8s.io/klog" "k8s.io/klog"
@ -20,27 +20,13 @@ const (
SizeMegaByte = 1 << (10 * 2) 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 { type UploadHandler struct {
AuthConfig *config.AuthConfig AuthConfig *config.AuthConfig
FileStorage *storage.FileStorage FileStorage *storage.FileStorage
// excluded as per defined by the environment variable
// FILE_EXTENSIONS_EXCEPT
exclMimeTypes []string
} }
func NewUploadHandler() *UploadHandler { func NewUploadHandler() *UploadHandler {
@ -48,6 +34,7 @@ func NewUploadHandler() *UploadHandler {
handler.ReloadAuthConfig() handler.ReloadAuthConfig()
handler.FileStorage = storage.NewFileStorage() handler.FileStorage = storage.NewFileStorage()
handler.exclMimeTypes = env.ListOrDefault("FILE_EXTENSIONS_EXCLUDED", []string{"image/png", "image/jpeg"})
return handler return handler
} }
@ -124,8 +111,8 @@ func (h *UploadHandler) Upload(c *gin.Context) {
} }
defer of.Close() defer of.Close()
metadata := getMetadata(form) metadata := request.GetMetadata(form)
rff := &RequestFormFile{ rff := &request.RequestFormFile{
File: of, File: of,
ContentType: ct, ContentType: ct,
ContentLength: c.Request.ContentLength, 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) klog.Infof("Stored file %s (expiresIn: %s)", sf.Id, expiresIn)
metrics.IncFilesUploaded() 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 { // isExtensionExcluded returns if the given mime type
metaRawList := form.Value["metadata"] // should be excluded by the extension response rule, which states if
if len(metaRawList) == 0 { // an extension should be appended to the file name when responding.
return emptyRequestMetadata func (h *UploadHandler) isExtensionExcluded(mimeType string) bool {
for _, exclMimeType := range h.exclMimeTypes {
if exclMimeType == mimeType {
return true
}
} }
metaRaw := metaRawList[0] return false
var metadata *RequestMetadata
err := json.Unmarshal([]byte(metaRaw), &metadata)
if err != nil {
return emptyRequestMetadata
}
return metadata
} }
// workaround for file Content-Type headers // 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 // HandleStaticFiles takes the files inside the configured file storage
// path and serves them to the client. // path and serves them to the client.
func HandleStaticFiles() gin.HandlerFunc { 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) { return func(c *gin.Context) {
fileName := c.Param("file") 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 fullPath := fileStoragePath + fileName
f, err := os.Open(fullPath) f, err := os.Open(fullPath)

View File

@ -28,7 +28,7 @@ var (
// IsValid checks if given type is inside Types map // IsValid checks if given type is inside Types map
func IsValid(t string) bool { func IsValid(t string) bool {
for _, mt := range Types { for mt := range Types {
if mt == t { if mt == t {
return true return true
} }

View File

@ -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
}

View File

@ -5,8 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid" "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/metrics"
"github.com/superioz/aqua/internal/request"
"github.com/superioz/aqua/pkg/env" "github.com/superioz/aqua/pkg/env"
"k8s.io/klog" "k8s.io/klog"
"strings" "strings"
@ -15,13 +16,6 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
const (
ExpireNever = -1
EnvDefaultFileStoragePath = "/var/lib/aqua/files/"
EnvDefaultMetaDbPath = "/var/lib/aqua/"
)
type StoredFile struct { type StoredFile struct {
Id string Id string
UploadedAt int64 UploadedAt int64
@ -43,14 +37,14 @@ type FileStorage struct {
} }
func NewFileStorage() *FileStorage { func NewFileStorage() *FileStorage {
metaDbFilePath := env.StringOrDefault("FILE_META_DB_PATH", EnvDefaultMetaDbPath) metaDbFilePath := env.StringOrDefault("FILE_META_DB_PATH", config.EnvDefaultMetaDbPath)
fileMetaDb := NewSqliteFileMetaDatabase(metaDbFilePath) fileMetaDb := NewSqliteFileMetaDatabase(metaDbFilePath)
err := fileMetaDb.Connect() err := fileMetaDb.Connect()
if err != nil { if err != nil {
klog.Errorf("Could not connect to file meta db: %v", err) 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) fileSystem := NewLocalFileStorage(fileStoragePath)
return &FileStorage{ return &FileStorage{
@ -102,7 +96,7 @@ func (fs *FileStorage) Cleanup() error {
return nil 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)) name, err := getRandomFileName(env.IntOrDefault("FILE_NAME_LENGTH", 8))
if err != nil { if err != nil {
return nil, errors.New("could not generate random name") 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() currentTime := time.Now().Unix()
expAt := currentTime + expiration expAt := currentTime + expiration
if expiration == ExpireNever { if expiration == config.ExpireNever {
expAt = ExpireNever expAt = config.ExpireNever
} }
sf := &StoredFile{ sf := &StoredFile{

14
pkg/env/env.go vendored
View File

@ -3,6 +3,7 @@ package env
import ( import (
"os" "os"
"strconv" "strconv"
"strings"
) )
func String(name string) (string, bool) { func String(name string) (string, bool) {
@ -57,3 +58,16 @@ func BoolOrDefault(name string, def bool) bool {
} }
return b 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
}