mirror of https://github.com/Superioz/aqua.git
Add extensions to support Discord embeds
This commit is contained in:
parent
1a2dbf266d
commit
a81a33c17a
|
@ -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
|
||||||
|
|
|
@ -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$"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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!")
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue