mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 20:27:23 +00:00
Implement s3 storage.
Use uuid as file id.
This commit is contained in:
@@ -74,7 +74,7 @@ export interface Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RFile {
|
export interface RFile {
|
||||||
id: number;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
@@ -488,7 +488,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFile(fileId: number): Promise<Response<RFile>> {
|
async getFile(fileId: string): Promise<Response<RFile>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.apiBaseUrl}/files/${fileId}`);
|
const response = await axios.get(`${this.apiBaseUrl}/files/${fileId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -501,7 +501,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFile(fileId: number, filename: string, description: string): Promise<Response<RFile>> {
|
async updateFile(fileId: string, filename: string, description: string): Promise<Response<RFile>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`${this.apiBaseUrl}/files/${fileId}`, {
|
const response = await axios.put(`${this.apiBaseUrl}/files/${fileId}`, {
|
||||||
filename,
|
filename,
|
||||||
@@ -530,7 +530,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileDownloadLink(fileId: number): string {
|
getFileDownloadLink(fileId: string): string {
|
||||||
return `${this.apiBaseUrl}/files/download/${fileId}`;
|
return `${this.apiBaseUrl}/files/download/${fileId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@ export class UploadingTask extends Listenable {
|
|||||||
this.onFinished = onFinished;
|
this.onFinished = onFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(id: number) {
|
async upload() {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < this.blocks.length) {
|
while (index < this.blocks.length) {
|
||||||
if (this.blocks[index] || this.uploadingBlocks.includes(index)) {
|
if (this.blocks[index] || this.uploadingBlocks.includes(index)) {
|
||||||
@@ -67,7 +67,6 @@ export class UploadingTask extends Listenable {
|
|||||||
if (this.status !== UploadingStatus.UPLOADING) {
|
if (this.status !== UploadingStatus.UPLOADING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`${id}: uploading block ${index}`);
|
|
||||||
this.uploadingBlocks.push(index);
|
this.uploadingBlocks.push(index);
|
||||||
const start = index * this.blockSize;
|
const start = index * this.blockSize;
|
||||||
const end = Math.min(start + this.blockSize, this.file.size);
|
const end = Math.min(start + this.blockSize, this.file.size);
|
||||||
@@ -88,7 +87,6 @@ export class UploadingTask extends Listenable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`${id}: uploaded block ${index}`);
|
|
||||||
this.blocks[index] = true;
|
this.blocks[index] = true;
|
||||||
this.finishedBlocksCount++;
|
this.finishedBlocksCount++;
|
||||||
this.uploadingBlocks = this.uploadingBlocks.filter(i => i !== index);
|
this.uploadingBlocks = this.uploadingBlocks.filter(i => i !== index);
|
||||||
@@ -101,10 +99,10 @@ export class UploadingTask extends Listenable {
|
|||||||
this.status = UploadingStatus.UPLOADING;
|
this.status = UploadingStatus.UPLOADING;
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.upload(0),
|
this.upload(),
|
||||||
this.upload(1),
|
this.upload(),
|
||||||
this.upload(2),
|
this.upload(),
|
||||||
this.upload(3),
|
this.upload(),
|
||||||
])
|
])
|
||||||
if (this.status !== UploadingStatus.UPLOADING) {
|
if (this.status !== UploadingStatus.UPLOADING) {
|
||||||
return;
|
return;
|
||||||
|
@@ -71,7 +71,7 @@ function UserTable({ page, searchKeyword, totalPagesCallback }: { page: number,
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}, [page, searchKeyword]);
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const handleChanged = useCallback(async () => {
|
const handleChanged = useCallback(async () => {
|
||||||
setUsers(null);
|
setUsers(null);
|
||||||
|
11
go.mod
11
go.mod
@@ -12,6 +12,17 @@ require (
|
|||||||
|
|
||||||
require github.com/chai2010/webp v1.4.0
|
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 (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
|
17
go.sum
17
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
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 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
|
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=
|
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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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 h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||||
|
@@ -140,12 +140,7 @@ func createRedirectFile(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFile(c fiber.Ctx) error {
|
func getFile(c fiber.Ctx) error {
|
||||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
file, err := service.GetFile(c.Params("id"))
|
||||||
if err != nil {
|
|
||||||
return model.NewRequestError("Invalid file ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := service.GetFile(uint(id))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -159,11 +154,6 @@ func getFile(c fiber.Ctx) error {
|
|||||||
func updateFile(c fiber.Ctx) error {
|
func updateFile(c fiber.Ctx) error {
|
||||||
uid := c.Locals("uid").(uint)
|
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 {
|
type UpdateFileRequest struct {
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@@ -174,7 +164,7 @@ func updateFile(c fiber.Ctx) error {
|
|||||||
return model.NewRequestError("Invalid request parameters")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -188,12 +178,7 @@ func updateFile(c fiber.Ctx) error {
|
|||||||
func deleteFile(c fiber.Ctx) error {
|
func deleteFile(c fiber.Ctx) error {
|
||||||
uid := c.Locals("uid").(uint)
|
uid := c.Locals("uid").(uint)
|
||||||
|
|
||||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
if err := service.DeleteFile(uid, c.Params("id")); err != nil {
|
||||||
if err != nil {
|
|
||||||
return model.NewRequestError("Invalid file ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := service.DeleteFile(uid, uint(id)); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,12 +189,7 @@ func deleteFile(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(c fiber.Ctx) error {
|
func downloadFile(c fiber.Ctx) error {
|
||||||
idStr, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
s, filename, err := service.DownloadFile(c.Params("id"))
|
||||||
if err != nil {
|
|
||||||
return model.NewRequestError("Invalid file ID")
|
|
||||||
}
|
|
||||||
id := uint(idStr)
|
|
||||||
s, filename, err := service.DownloadFile(id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
@@ -73,17 +74,19 @@ func GetUploadingFilesOlderThan(time time.Time) ([]model.UploadingFile, error) {
|
|||||||
return files, nil
|
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 == "" {
|
if storageID == nil && redirectUrl == "" {
|
||||||
return nil, errors.New("storageID and redirectUrl cannot be both empty")
|
return nil, errors.New("storageID and redirectUrl cannot be both empty")
|
||||||
}
|
}
|
||||||
f := &model.File{
|
f := &model.File{
|
||||||
|
UUID: uuid.NewString(),
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Description: description,
|
Description: description,
|
||||||
ResourceID: resourceID,
|
ResourceID: resourceID,
|
||||||
StorageID: storageID,
|
StorageID: storageID,
|
||||||
RedirectUrl: redirectUrl,
|
RedirectUrl: redirectUrl,
|
||||||
StorageKey: storageKey,
|
StorageKey: storageKey,
|
||||||
|
Size: size,
|
||||||
}
|
}
|
||||||
if err := db.Create(f).Error; err != nil {
|
if err := db.Create(f).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -91,9 +94,9 @@ func CreateFile(filename string, description string, resourceID uint, storageID
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFile(id uint) (*model.File, error) {
|
func GetFile(id string) (*model.File, error) {
|
||||||
f := &model.File{}
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, model.NewNotFoundError("file not found")
|
return nil, model.NewNotFoundError("file not found")
|
||||||
}
|
}
|
||||||
@@ -102,17 +105,9 @@ func GetFile(id uint) (*model.File, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFilesByResourceID(rID uint) ([]model.File, error) {
|
func DeleteFile(id string) 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 {
|
|
||||||
f := &model.File{}
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return model.NewNotFoundError("file not found")
|
return model.NewNotFoundError("file not found")
|
||||||
}
|
}
|
||||||
@@ -124,9 +119,9 @@ func DeleteFile(id uint) error {
|
|||||||
return nil
|
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{}
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
@@ -144,9 +139,9 @@ func UpdateFile(id uint, filename string, description string) (*model.File, erro
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFileStorageKey(id uint, storageKey string) error {
|
func SetFileStorageKey(id string, storageKey string) error {
|
||||||
f := &model.File{}
|
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
|
return err
|
||||||
}
|
}
|
||||||
f.StorageKey = storageKey
|
f.StorageKey = storageKey
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package dao
|
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) {
|
func CreateStorage(s model.Storage) (model.Storage, error) {
|
||||||
err := db.Model(&s).Create(&s).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
|
err := db.Model(&model.Storage{}).Where("id = ?", id).First(&storage).Error
|
||||||
return storage, err
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
|
UUID string `gorm:"uniqueIndex;not null"`
|
||||||
Filename string
|
Filename string
|
||||||
Description string
|
Description string
|
||||||
StorageKey string
|
StorageKey string
|
||||||
@@ -16,17 +17,18 @@ type File struct {
|
|||||||
Resource Resource `gorm:"foreignKey:ResourceID"`
|
Resource Resource `gorm:"foreignKey:ResourceID"`
|
||||||
UserID uint
|
UserID uint
|
||||||
User User `gorm:"foreignKey:UserID"`
|
User User `gorm:"foreignKey:UserID"`
|
||||||
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileView struct {
|
type FileView struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) ToView() *FileView {
|
func (f *File) ToView() *FileView {
|
||||||
return &FileView{
|
return &FileView{
|
||||||
ID: f.ID,
|
ID: f.UUID,
|
||||||
Filename: f.Filename,
|
Filename: f.Filename,
|
||||||
Description: f.Description,
|
Description: f.Description,
|
||||||
}
|
}
|
||||||
|
@@ -71,6 +71,9 @@ func CreateUploadingFile(uid uint, filename string, description string, fileSize
|
|||||||
if filename == "" {
|
if filename == "" {
|
||||||
return nil, model.NewRequestError("filename is empty")
|
return nil, model.NewRequestError("filename is empty")
|
||||||
}
|
}
|
||||||
|
if len([]rune(filename)) > 128 {
|
||||||
|
return nil, model.NewRequestError("filename is too long")
|
||||||
|
}
|
||||||
canUpload, err := checkUserCanUpload(uid)
|
canUpload, err := checkUserCanUpload(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to check user permission: ", err)
|
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")
|
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 {
|
if err != nil {
|
||||||
log.Error("failed to create file in db: ", err)
|
log.Error("failed to create file in db: ", err)
|
||||||
_ = os.Remove(resultFilePath)
|
_ = os.Remove(resultFilePath)
|
||||||
@@ -225,14 +228,22 @@ func FinishUploadingFile(uid uint, fid uint) (*model.FileView, error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
_ = os.Remove(resultFilePath)
|
_ = os.Remove(resultFilePath)
|
||||||
}()
|
}()
|
||||||
storageKey, err := iStorage.Upload(resultFilePath)
|
err := dao.AddStorageUsage(uploadingFile.TargetStorageID, uploadingFile.TotalSize)
|
||||||
if err != nil {
|
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)
|
log.Error("failed to upload file to storage: ", err)
|
||||||
} else {
|
} else {
|
||||||
err = dao.SetFileStorageKey(dbFile.ID, storageKey)
|
err = dao.SetFileStorageKey(dbFile.UUID, storageKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = dao.AddStorageUsage(uploadingFile.TargetStorageID, -uploadingFile.TotalSize)
|
||||||
_ = iStorage.Delete(storageKey)
|
_ = iStorage.Delete(storageKey)
|
||||||
_ = dao.DeleteFile(dbFile.ID)
|
_ = dao.DeleteFile(dbFile.UUID)
|
||||||
log.Error("failed to set file storage key: ", err)
|
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")
|
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 {
|
if err != nil {
|
||||||
log.Error("failed to create file in db: ", err)
|
log.Error("failed to create file in db: ", err)
|
||||||
return nil, model.NewInternalServerError("failed to create file in db")
|
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
|
return file.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteFile(uid uint, fid uint) error {
|
func DeleteFile(uid uint, fid string) error {
|
||||||
file, err := dao.GetFile(fid)
|
file, err := dao.GetFile(fid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to get file: ", err)
|
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)
|
log.Error("failed to delete file from storage: ", err)
|
||||||
return model.NewInternalServerError("failed to delete file from storage")
|
return model.NewInternalServerError("failed to delete file from storage")
|
||||||
}
|
}
|
||||||
|
_ = dao.AddStorageUsage(*file.StorageID, -file.Size)
|
||||||
|
|
||||||
if err := dao.DeleteFile(fid); err != nil {
|
if err := dao.DeleteFile(fid); err != nil {
|
||||||
log.Error("failed to delete file from db: ", err)
|
log.Error("failed to delete file from db: ", err)
|
||||||
@@ -323,7 +335,7 @@ func DeleteFile(uid uint, fid uint) error {
|
|||||||
return nil
|
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)
|
file, err := dao.GetFile(fid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to get file: ", err)
|
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
|
return file.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFile(fid uint) (*model.FileView, error) {
|
func GetFile(fid string) (*model.FileView, error) {
|
||||||
file, err := dao.GetFile(fid)
|
file, err := dao.GetFile(fid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to get file: ", err)
|
log.Error("failed to get file: ", err)
|
||||||
@@ -358,7 +370,7 @@ func GetFile(fid uint) (*model.FileView, error) {
|
|||||||
return file.ToView(), nil
|
return file.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadFile(fid uint) (string, string, error) {
|
func DownloadFile(fid string) (string, string, error) {
|
||||||
file, err := dao.GetFile(fid)
|
file, err := dao.GetFile(fid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to get file: ", err)
|
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")
|
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
|
return path, file.Filename, err
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ type LocalStorage struct {
|
|||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LocalStorage) Upload(filePath string) (string, error) {
|
func (s *LocalStorage) Upload(filePath string, _ string) (string, error) {
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
inputPath := s.Path + "/" + id
|
inputPath := s.Path + "/" + id
|
||||||
input, err := os.OpenFile(inputPath, os.O_RDWR|os.O_CREATE, 0755)
|
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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LocalStorage) Download(storageKey string) (string, error) {
|
func (s *LocalStorage) Download(storageKey string, _ string) (string, error) {
|
||||||
path := s.Path + "/" + storageKey
|
path := s.Path + "/" + storageKey
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return "", ErrFileUnavailable
|
return "", ErrFileUnavailable
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"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 {
|
type S3Storage struct {
|
||||||
@@ -12,14 +20,45 @@ type S3Storage struct {
|
|||||||
BucketName string
|
BucketName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S3Storage) Upload(filePath string) (string, error) {
|
func (s *S3Storage) Upload(filePath string, fileName string) (string, error) {
|
||||||
// TODO: Implement S3 upload logic here
|
minioClient, err := minio.New(s.EndPoint, &minio.Options{
|
||||||
return "", nil
|
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) {
|
func (s *S3Storage) Download(storageKey string, fileName string) (string, error) {
|
||||||
// TODO: Implement S3 download logic here
|
minioClient, err := minio.New(s.EndPoint, &minio.Options{
|
||||||
return "", nil
|
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 {
|
func (s *S3Storage) Delete(storageKey string) error {
|
||||||
|
@@ -13,9 +13,9 @@ var (
|
|||||||
|
|
||||||
type IStorage interface {
|
type IStorage interface {
|
||||||
// Upload uploads a file to the storage and returns the storage key.
|
// 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 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 deletes the file with the given storage key.
|
||||||
Delete(storageKey string) error
|
Delete(storageKey string) error
|
||||||
// ToString returns the storage configuration as a string.
|
// ToString returns the storage configuration as a string.
|
||||||
|
Reference in New Issue
Block a user