From 04d679f3f4842f806bd978f1d27d35c3b3317726 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 13 May 2025 11:56:22 +0800 Subject: [PATCH] Implement s3 storage. Use uuid as file id. --- frontend/src/network/models.ts | 2 +- frontend/src/network/network.ts | 6 +-- frontend/src/network/uploading.ts | 12 +++--- frontend/src/pages/manage_user_page.tsx | 2 +- go.mod | 11 ++++++ go.sum | 17 +++++++++ server/api/file.go | 28 ++------------ server/dao/file.go | 29 ++++++-------- server/dao/storage.go | 17 ++++++++- server/model/file.go | 6 ++- server/service/file.go | 32 +++++++++++----- server/storage/local.go | 4 +- server/storage/s3.go | 51 ++++++++++++++++++++++--- server/storage/storage.go | 4 +- 14 files changed, 145 insertions(+), 76 deletions(-) diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index c33a1eb..6da5f66 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -74,7 +74,7 @@ export interface Storage { } export interface RFile { - id: number; + id: string; filename: string; description: string; } diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index a7dac4c..eb4b9f3 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -488,7 +488,7 @@ class Network { } } - async getFile(fileId: number): Promise> { + async getFile(fileId: string): Promise> { try { const response = await axios.get(`${this.apiBaseUrl}/files/${fileId}`); return response.data; @@ -501,7 +501,7 @@ class Network { } } - async updateFile(fileId: number, filename: string, description: string): Promise> { + async updateFile(fileId: string, filename: string, description: string): Promise> { try { const response = await axios.put(`${this.apiBaseUrl}/files/${fileId}`, { filename, @@ -530,7 +530,7 @@ class Network { } } - getFileDownloadLink(fileId: number): string { + getFileDownloadLink(fileId: string): string { return `${this.apiBaseUrl}/files/download/${fileId}`; } } diff --git a/frontend/src/network/uploading.ts b/frontend/src/network/uploading.ts index 1db264a..8d7ba8e 100644 --- a/frontend/src/network/uploading.ts +++ b/frontend/src/network/uploading.ts @@ -57,7 +57,7 @@ export class UploadingTask extends Listenable { this.onFinished = onFinished; } - async upload(id: number) { + async upload() { let index = 0; while (index < this.blocks.length) { if (this.blocks[index] || this.uploadingBlocks.includes(index)) { @@ -67,7 +67,6 @@ export class UploadingTask extends Listenable { if (this.status !== UploadingStatus.UPLOADING) { return; } - console.log(`${id}: uploading block ${index}`); this.uploadingBlocks.push(index); const start = index * this.blockSize; const end = Math.min(start + this.blockSize, this.file.size); @@ -88,7 +87,6 @@ export class UploadingTask extends Listenable { break; } } - console.log(`${id}: uploaded block ${index}`); this.blocks[index] = true; this.finishedBlocksCount++; this.uploadingBlocks = this.uploadingBlocks.filter(i => i !== index); @@ -101,10 +99,10 @@ export class UploadingTask extends Listenable { this.status = UploadingStatus.UPLOADING; this.notifyListeners(); await Promise.all([ - this.upload(0), - this.upload(1), - this.upload(2), - this.upload(3), + this.upload(), + this.upload(), + this.upload(), + this.upload(), ]) if (this.status !== UploadingStatus.UPLOADING) { return; diff --git a/frontend/src/pages/manage_user_page.tsx b/frontend/src/pages/manage_user_page.tsx index e6c7ca7..fe69046 100644 --- a/frontend/src/pages/manage_user_page.tsx +++ b/frontend/src/pages/manage_user_page.tsx @@ -71,7 +71,7 @@ function UserTable({ page, searchKeyword, totalPagesCallback }: { page: number, useEffect(() => { fetchUsers(); - }, [page, searchKeyword]); + }, [fetchUsers]); const handleChanged = useCallback(async () => { setUsers(null); diff --git a/go.mod b/go.mod index f56ccb1..c23c274 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,17 @@ require ( require github.com/chai2010/webp v1.4.0 +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.91 // indirect + github.com/rs/xid v1.6.0 // indirect +) + require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect diff --git a/go.sum b/go.sum index 4ea8d7c..6d6bc44 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,14 @@ github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko= github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= @@ -22,16 +28,27 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= +github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= diff --git a/server/api/file.go b/server/api/file.go index 16342e9..b00c952 100644 --- a/server/api/file.go +++ b/server/api/file.go @@ -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 } diff --git a/server/dao/file.go b/server/dao/file.go index 7dd1ebe..10b82fe 100644 --- a/server/dao/file.go +++ b/server/dao/file.go @@ -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 diff --git a/server/dao/storage.go b/server/dao/storage.go index a487dd3..4bad3bf 100644 --- a/server/dao/storage.go +++ b/server/dao/storage.go @@ -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 + }) +} diff --git a/server/model/file.go b/server/model/file.go index f2ed938..94691ce 100644 --- a/server/model/file.go +++ b/server/model/file.go @@ -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, } diff --git a/server/service/file.go b/server/service/file.go index 9aad720..8227b95 100644 --- a/server/service/file.go +++ b/server/service/file.go @@ -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 } diff --git a/server/storage/local.go b/server/storage/local.go index ac2ba0a..da3119c 100644 --- a/server/storage/local.go +++ b/server/storage/local.go @@ -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 diff --git a/server/storage/s3.go b/server/storage/s3.go index a8ab3fe..1f561f8 100644 --- a/server/storage/s3.go +++ b/server/storage/s3.go @@ -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 { diff --git a/server/storage/storage.go b/server/storage/storage.go index 9936a5d..50a11e6 100644 --- a/server/storage/storage.go +++ b/server/storage/storage.go @@ -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.