mirror of https://github.com/Superioz/aqua.git
Add abstraction layer for file system
This commit is contained in:
parent
3f5bc42ceb
commit
0b30282cb5
|
@ -1,2 +1,4 @@
|
|||
AUTH_CONFIG_PATH=
|
||||
FILE_STORAGE_PATH=
|
||||
FILE_STORAGE_PATH=
|
||||
FILE_NAME_LENGTH=
|
||||
FILE_META_DB_FILE=
|
|
@ -1,2 +1,3 @@
|
|||
.idea/
|
||||
.env
|
||||
.env
|
||||
.dev/
|
|
@ -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.
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue