Implement s3 storage.

Use uuid as file id.
This commit is contained in:
2025-05-13 11:56:22 +08:00
parent 081b547c03
commit 04d679f3f4
14 changed files with 145 additions and 76 deletions

View File

@@ -140,12 +140,7 @@ func createRedirectFile(c fiber.Ctx) error {
}
func getFile(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return model.NewRequestError("Invalid file ID")
}
file, err := service.GetFile(uint(id))
file, err := service.GetFile(c.Params("id"))
if err != nil {
return err
}
@@ -159,11 +154,6 @@ func getFile(c fiber.Ctx) error {
func updateFile(c fiber.Ctx) error {
uid := c.Locals("uid").(uint)
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return model.NewRequestError("Invalid file ID")
}
type UpdateFileRequest struct {
Filename string `json:"filename"`
Description string `json:"description"`
@@ -174,7 +164,7 @@ func updateFile(c fiber.Ctx) error {
return model.NewRequestError("Invalid request parameters")
}
result, err := service.UpdateFile(uid, uint(id), req.Filename, req.Description)
result, err := service.UpdateFile(uid, c.Params("id"), req.Filename, req.Description)
if err != nil {
return err
}
@@ -188,12 +178,7 @@ func updateFile(c fiber.Ctx) error {
func deleteFile(c fiber.Ctx) error {
uid := c.Locals("uid").(uint)
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return model.NewRequestError("Invalid file ID")
}
if err := service.DeleteFile(uid, uint(id)); err != nil {
if err := service.DeleteFile(uid, c.Params("id")); err != nil {
return err
}
@@ -204,12 +189,7 @@ func deleteFile(c fiber.Ctx) error {
}
func downloadFile(c fiber.Ctx) error {
idStr, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return model.NewRequestError("Invalid file ID")
}
id := uint(idStr)
s, filename, err := service.DownloadFile(id)
s, filename, err := service.DownloadFile(c.Params("id"))
if err != nil {
return err
}

View File

@@ -2,6 +2,7 @@ package dao
import (
"errors"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"nysoure/server/model"
@@ -73,17 +74,19 @@ func GetUploadingFilesOlderThan(time time.Time) ([]model.UploadingFile, error) {
return files, nil
}
func CreateFile(filename string, description string, resourceID uint, storageID *uint, storageKey string, redirectUrl string) (*model.File, error) {
func CreateFile(filename string, description string, resourceID uint, storageID *uint, storageKey string, redirectUrl string, size int64) (*model.File, error) {
if storageID == nil && redirectUrl == "" {
return nil, errors.New("storageID and redirectUrl cannot be both empty")
}
f := &model.File{
UUID: uuid.NewString(),
Filename: filename,
Description: description,
ResourceID: resourceID,
StorageID: storageID,
RedirectUrl: redirectUrl,
StorageKey: storageKey,
Size: size,
}
if err := db.Create(f).Error; err != nil {
return nil, err
@@ -91,9 +94,9 @@ func CreateFile(filename string, description string, resourceID uint, storageID
return f, nil
}
func GetFile(id uint) (*model.File, error) {
func GetFile(id string) (*model.File, error) {
f := &model.File{}
if err := db.Preload("Storage").Where("id = ?", id).First(f).Error; err != nil {
if err := db.Preload("Storage").Where("uuid = ?", id).First(f).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, model.NewNotFoundError("file not found")
}
@@ -102,17 +105,9 @@ func GetFile(id uint) (*model.File, error) {
return f, nil
}
func GetFilesByResourceID(rID uint) ([]model.File, error) {
var files []model.File
if err := db.Where("resource_id = ?", rID).Find(&files).Error; err != nil {
return nil, err
}
return files, nil
}
func DeleteFile(id uint) error {
func DeleteFile(id string) error {
f := &model.File{}
if err := db.Where("id = ?", id).First(f).Error; err != nil {
if err := db.Where("uuid = ?", id).First(f).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return model.NewNotFoundError("file not found")
}
@@ -124,9 +119,9 @@ func DeleteFile(id uint) error {
return nil
}
func UpdateFile(id uint, filename string, description string) (*model.File, error) {
func UpdateFile(id string, filename string, description string) (*model.File, error) {
f := &model.File{}
if err := db.Where("id = ?", id).First(f).Error; err != nil {
if err := db.Where("uuid = ?", id).First(f).Error; err != nil {
return nil, err
}
if filename != "" {
@@ -144,9 +139,9 @@ func UpdateFile(id uint, filename string, description string) (*model.File, erro
return f, nil
}
func SetFileStorageKey(id uint, storageKey string) error {
func SetFileStorageKey(id string, storageKey string) error {
f := &model.File{}
if err := db.Where("id = ?", id).First(f).Error; err != nil {
if err := db.Where("uuid = ?", id).First(f).Error; err != nil {
return err
}
f.StorageKey = storageKey

View File

@@ -1,6 +1,10 @@
package dao
import "nysoure/server/model"
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"nysoure/server/model"
)
func CreateStorage(s model.Storage) (model.Storage, error) {
err := db.Model(&s).Create(&s).Error
@@ -22,3 +26,14 @@ func GetStorage(id uint) (model.Storage, error) {
err := db.Model(&model.Storage{}).Where("id = ?", id).First(&storage).Error
return storage, err
}
func AddStorageUsage(id uint, offset int64) error {
return db.Transaction(func(tx *gorm.DB) error {
var storage model.Storage
err := tx.Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}).Model(&model.Storage{}).Where("id = ?", id).First(&storage).Error
if err != nil {
return err
}
return tx.Model(&model.Storage{}).Where("id = ?", id).Update("current_size", storage.CurrentSize+offset).Error
})
}

View File

@@ -6,6 +6,7 @@ import (
type File struct {
gorm.Model
UUID string `gorm:"uniqueIndex;not null"`
Filename string
Description string
StorageKey string
@@ -16,17 +17,18 @@ type File struct {
Resource Resource `gorm:"foreignKey:ResourceID"`
UserID uint
User User `gorm:"foreignKey:UserID"`
Size int64
}
type FileView struct {
ID uint `json:"id"`
ID string `json:"id"`
Filename string `json:"filename"`
Description string `json:"description"`
}
func (f *File) ToView() *FileView {
return &FileView{
ID: f.ID,
ID: f.UUID,
Filename: f.Filename,
Description: f.Description,
}

View File

@@ -71,6 +71,9 @@ func CreateUploadingFile(uid uint, filename string, description string, fileSize
if filename == "" {
return nil, model.NewRequestError("filename is empty")
}
if len([]rune(filename)) > 128 {
return nil, model.NewRequestError("filename is too long")
}
canUpload, err := checkUserCanUpload(uid)
if err != nil {
log.Error("failed to check user permission: ", err)
@@ -214,7 +217,7 @@ func FinishUploadingFile(uid uint, fid uint) (*model.FileView, error) {
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
}
dbFile, err := dao.CreateFile(uploadingFile.Filename, uploadingFile.Description, uploadingFile.TargetResourceID, &uploadingFile.TargetStorageID, "", "")
dbFile, err := dao.CreateFile(uploadingFile.Filename, uploadingFile.Description, uploadingFile.TargetResourceID, &uploadingFile.TargetStorageID, "", "", uploadingFile.TotalSize)
if err != nil {
log.Error("failed to create file in db: ", err)
_ = os.Remove(resultFilePath)
@@ -225,14 +228,22 @@ func FinishUploadingFile(uid uint, fid uint) (*model.FileView, error) {
defer func() {
_ = os.Remove(resultFilePath)
}()
storageKey, err := iStorage.Upload(resultFilePath)
err := dao.AddStorageUsage(uploadingFile.TargetStorageID, uploadingFile.TotalSize)
if err != nil {
log.Error("failed to add storage usage: ", err)
_ = dao.DeleteFile(dbFile.UUID)
return
}
storageKey, err := iStorage.Upload(resultFilePath, uploadingFile.Filename)
if err != nil {
_ = dao.AddStorageUsage(uploadingFile.TargetStorageID, -uploadingFile.TotalSize)
log.Error("failed to upload file to storage: ", err)
} else {
err = dao.SetFileStorageKey(dbFile.ID, storageKey)
err = dao.SetFileStorageKey(dbFile.UUID, storageKey)
if err != nil {
_ = dao.AddStorageUsage(uploadingFile.TargetStorageID, -uploadingFile.TotalSize)
_ = iStorage.Delete(storageKey)
_ = dao.DeleteFile(dbFile.ID)
_ = dao.DeleteFile(dbFile.UUID)
log.Error("failed to set file storage key: ", err)
}
}
@@ -279,7 +290,7 @@ func CreateRedirectFile(uid uint, filename string, description string, resourceI
return nil, model.NewUnAuthorizedError("user cannot upload file")
}
file, err := dao.CreateFile(filename, description, resourceID, nil, "", redirectUrl)
file, err := dao.CreateFile(filename, description, resourceID, nil, "", redirectUrl, 0)
if err != nil {
log.Error("failed to create file in db: ", err)
return nil, model.NewInternalServerError("failed to create file in db")
@@ -287,7 +298,7 @@ func CreateRedirectFile(uid uint, filename string, description string, resourceI
return file.ToView(), nil
}
func DeleteFile(uid uint, fid uint) error {
func DeleteFile(uid uint, fid string) error {
file, err := dao.GetFile(fid)
if err != nil {
log.Error("failed to get file: ", err)
@@ -314,6 +325,7 @@ func DeleteFile(uid uint, fid uint) error {
log.Error("failed to delete file from storage: ", err)
return model.NewInternalServerError("failed to delete file from storage")
}
_ = dao.AddStorageUsage(*file.StorageID, -file.Size)
if err := dao.DeleteFile(fid); err != nil {
log.Error("failed to delete file from db: ", err)
@@ -323,7 +335,7 @@ func DeleteFile(uid uint, fid uint) error {
return nil
}
func UpdateFile(uid uint, fid uint, filename string, description string) (*model.FileView, error) {
func UpdateFile(uid uint, fid string, filename string, description string) (*model.FileView, error) {
file, err := dao.GetFile(fid)
if err != nil {
log.Error("failed to get file: ", err)
@@ -348,7 +360,7 @@ func UpdateFile(uid uint, fid uint, filename string, description string) (*model
return file.ToView(), nil
}
func GetFile(fid uint) (*model.FileView, error) {
func GetFile(fid string) (*model.FileView, error) {
file, err := dao.GetFile(fid)
if err != nil {
log.Error("failed to get file: ", err)
@@ -358,7 +370,7 @@ func GetFile(fid uint) (*model.FileView, error) {
return file.ToView(), nil
}
func DownloadFile(fid uint) (string, string, error) {
func DownloadFile(fid string) (string, string, error) {
file, err := dao.GetFile(fid)
if err != nil {
log.Error("failed to get file: ", err)
@@ -382,7 +394,7 @@ func DownloadFile(fid uint) (string, string, error) {
return "", "", model.NewRequestError("file is not available, please try again later")
}
path, err := iStorage.Download(file.StorageKey)
path, err := iStorage.Download(file.StorageKey, file.Filename)
return path, file.Filename, err
}

View File

@@ -11,7 +11,7 @@ type LocalStorage struct {
Path string
}
func (s *LocalStorage) Upload(filePath string) (string, error) {
func (s *LocalStorage) Upload(filePath string, _ string) (string, error) {
id := uuid.New().String()
inputPath := s.Path + "/" + id
input, err := os.OpenFile(inputPath, os.O_RDWR|os.O_CREATE, 0755)
@@ -31,7 +31,7 @@ func (s *LocalStorage) Upload(filePath string) (string, error) {
return id, nil
}
func (s *LocalStorage) Download(storageKey string) (string, error) {
func (s *LocalStorage) Download(storageKey string, _ string) (string, error) {
path := s.Path + "/" + storageKey
if _, err := os.Stat(path); os.IsNotExist(err) {
return "", ErrFileUnavailable

View File

@@ -1,8 +1,16 @@
package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gofiber/fiber/v3/log"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"net/url"
"time"
)
type S3Storage struct {
@@ -12,14 +20,45 @@ type S3Storage struct {
BucketName string
}
func (s *S3Storage) Upload(filePath string) (string, error) {
// TODO: Implement S3 upload logic here
return "", nil
func (s *S3Storage) Upload(filePath string, fileName string) (string, error) {
minioClient, err := minio.New(s.EndPoint, &minio.Options{
Creds: credentials.NewStaticV4(s.AccessKeyID, s.SecretAccessKey, ""),
Secure: true,
})
if err != nil {
log.Error("Failed to create S3 client: ", err)
return "", errors.New("failed to create S3 client")
}
ctx := context.Background()
objectKey := uuid.NewString()
objectKey += "/" + fileName
_, err = minioClient.FPutObject(ctx, s.BucketName, objectKey, filePath, minio.PutObjectOptions{})
if err != nil {
log.Error("Failed to upload file to S3: ", err)
return "", errors.New("failed to upload file to S3")
}
return objectKey, nil
}
func (s *S3Storage) Download(storageKey string) (string, error) {
// TODO: Implement S3 download logic here
return "", nil
func (s *S3Storage) Download(storageKey string, fileName string) (string, error) {
minioClient, err := minio.New(s.EndPoint, &minio.Options{
Creds: credentials.NewStaticV4(s.AccessKeyID, s.SecretAccessKey, ""),
Secure: true,
})
if err != nil {
log.Error("Failed to create S3 client: ", err)
return "", errors.New("failed to create S3 client")
}
reqParams := make(url.Values)
reqParams.Set("response-content-disposition", "attachment; filename=\""+fileName+"\"")
presignedURL, err := minioClient.PresignedGetObject(context.Background(), s.BucketName, storageKey, 5*time.Second, reqParams)
if err != nil {
fmt.Println(err)
return "", errors.New("failed to generate presigned URL")
}
return presignedURL.String(), nil
}
func (s *S3Storage) Delete(storageKey string) error {

View File

@@ -13,9 +13,9 @@ var (
type IStorage interface {
// Upload uploads a file to the storage and returns the storage key.
Upload(filePath string) (string, error)
Upload(filePath string, fileName string) (string, error)
// Download return the download url of the file with the given storage key.
Download(storageKey string) (string, error)
Download(storageKey string, fileName string) (string, error)
// Delete deletes the file with the given storage key.
Delete(storageKey string) error
// ToString returns the storage configuration as a string.