From 7566ddb5f21b354f514710b6fda2e86084017057 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 10:54:29 +0800 Subject: [PATCH 01/97] fix(pkg): simplify task scheduling by removing unnecessary duration conversion in SetFileRemoveTask --- pkg/services/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/file.go b/pkg/services/file.go index 764e0ad..e5b8d15 100644 --- a/pkg/services/file.go +++ b/pkg/services/file.go @@ -17,6 +17,6 @@ func SetFileRemoveTask(fileId string, expire time.Duration) error { if err != nil { return err } - _, err = client.Enqueue(asynq.NewTask("file:remove", json), asynq.ProcessIn(time.Duration(expire)*time.Second)) + _, err = client.Enqueue(asynq.NewTask("file:remove", json), asynq.ProcessIn(expire)) return err } From d3e7760aea4ce5981512c31a6f953f9ec5fb44be Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 11:13:27 +0800 Subject: [PATCH 02/97] fix(backend): handle missing share information in DownloadShare and refactor SetRedisShareInfo to use a function for updating view count --- backend/internal/controllers/download.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/internal/controllers/download.go b/backend/internal/controllers/download.go index 5171d40..8f3b1f5 100644 --- a/backend/internal/controllers/download.go +++ b/backend/internal/controllers/download.go @@ -33,6 +33,9 @@ func DownloadShare(c *echo.Context) error { return utils.HTTPErrorHandler(c, ErrInvalidRequest) } shareInfo, _ := models.GetRedisShareInfo(claims.ShareId) + if shareInfo == nil { + return utils.HTTPErrorHandler(c, ErrShareNotFound) + } if shareInfo.Type == models.ShareTypeFile { fileInfo, _ := models.GetRedisFileInfo(shareInfo.Data) From 55949d1f76649061215c079e8f5803a955fe8660 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 11:15:02 +0800 Subject: [PATCH 03/97] refactor(models): remove mergo dependency and update SetRedisFileInfo and SetRedisShareInfo to use handler functions for improved data handling --- pkg/models/file.go | 8 ++++---- pkg/models/go.mod | 5 +---- pkg/models/go.sum | 7 +++++-- pkg/models/share.go | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/models/file.go b/pkg/models/file.go index 599e4fc..32b62ef 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -4,7 +4,6 @@ import ( "encoding/json" "pkg/utils" - "dario.cat/mergo" "github.com/redis/rueidis" ) @@ -45,15 +44,16 @@ func GetRedisFileInfo(fileId string) (*RedisFileInfo, error) { return &fileInfoData, nil } -func SetRedisFileInfo(fileId string, fileInfo RedisFileInfo) error { +func SetRedisFileInfo(fileId string, handler func(fileInfo *RedisFileInfo) *RedisFileInfo) error { rdb, ctx := utils.GetRedisClient() old_fileInfo, err := GetRedisFileInfo(fileId) if err != nil { return err } - if old_fileInfo != nil { - mergo.Merge(&fileInfo, old_fileInfo) + if old_fileInfo == nil { + old_fileInfo = &RedisFileInfo{} } + fileInfo := handler(old_fileInfo) jsonData, _ := json.Marshal(fileInfo) return rdb.Do(ctx, rdb.B().Hset().Key("015:fileInfoMap").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error() } diff --git a/pkg/models/go.mod b/pkg/models/go.mod index 0e1333f..151bfb1 100644 --- a/pkg/models/go.mod +++ b/pkg/models/go.mod @@ -2,10 +2,7 @@ module pkg/models go 1.25.5 -require ( - dario.cat/mergo v1.0.2 - github.com/redis/rueidis v1.0.73 -) +require github.com/redis/rueidis v1.0.73 require ( golang.org/x/net v0.52.0 // indirect diff --git a/pkg/models/go.sum b/pkg/models/go.sum index 15bcb15..359e460 100644 --- a/pkg/models/go.sum +++ b/pkg/models/go.sum @@ -1,11 +1,14 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/redis/rueidis v1.0.73 h1:0Enrg0VuMdaYyNDDj0lLIheWY0uybCeQOh+jTp2GG3M= github.com/redis/rueidis v1.0.73/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= diff --git a/pkg/models/share.go b/pkg/models/share.go index 3dd0b47..5ae7f61 100644 --- a/pkg/models/share.go +++ b/pkg/models/share.go @@ -7,7 +7,6 @@ import ( "pkg/utils" - "dario.cat/mergo" "github.com/redis/rueidis" ) @@ -52,15 +51,16 @@ func GetRedisShareInfo(shareId string) (*RedisShareInfo, error) { return &shareInfoData, nil } -func SetRedisShareInfo(shareId string, shareInfo RedisShareInfo) error { +func SetRedisShareInfo(shareId string, handler func(shareInfo *RedisShareInfo) *RedisShareInfo) error { rdb, ctx := utils.GetRedisClient() old_shareInfo, err := GetRedisShareInfo(shareId) if err != nil { return err } - if old_shareInfo != nil { - mergo.Merge(&shareInfo, old_shareInfo) + if old_shareInfo == nil { + old_shareInfo = &RedisShareInfo{} } + shareInfo := handler(old_shareInfo) jsonData, _ := json.Marshal(shareInfo) return rdb.Do( ctx, From 17fa39b830d3270ba59008e32a160174e6012f43 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 11:23:35 +0800 Subject: [PATCH 04/97] refactor(backend): update SetRedisFileInfo to use handler functions for better data manipulation and simplify file merging process with io.CopyBuffer --- backend/internal/controllers/file.go | 9 ++++++--- backend/internal/services/file.go | 19 +++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/backend/internal/controllers/file.go b/backend/internal/controllers/file.go index 9facf2f..4409637 100644 --- a/backend/internal/controllers/file.go +++ b/backend/internal/controllers/file.go @@ -90,7 +90,9 @@ func CreateUploadTask(c *echo.Context) error { CreatedAt: time.Now().Unix(), Expire: uploadTaskExpire, } - err = models.SetRedisFileInfo(fileId, newFileInfo) + err = models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + return &newFileInfo + }) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -233,8 +235,9 @@ func FinishUploadTask(c *echo.Context) error { } // 更新文件信息 - err = models.SetRedisFileInfo(r.FileId, models.RedisFileInfo{ - FileType: models.FileTypeUpload, + err = models.SetRedisFileInfo(r.FileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + fileInfo.FileType = models.FileTypeUpload + return fileInfo }) if err != nil { return utils.HTTPErrorHandler(c, err) diff --git a/backend/internal/services/file.go b/backend/internal/services/file.go index 8ff5ad5..68ccafc 100644 --- a/backend/internal/services/file.go +++ b/backend/internal/services/file.go @@ -71,18 +71,13 @@ func MergeFileSlices(fileId string, uploadPath string) (string, error) { if err != nil { return "", fmt.Errorf("打开切片文件失败: %v", err) } - defer sf.Close() //nolint:errcheck - for { - n, err := sf.Read(buffer) - if err == io.EOF { - break - } - if err != nil { - return "", fmt.Errorf("读取切片文件失败: %v", err) - } - if _, err := destFile.Write(buffer[:n]); err != nil { - return "", fmt.Errorf("写入合并文件失败: %v", err) - } + + if _, err := io.CopyBuffer(destFile, sf, buffer); err != nil { + sf.Close() //nolint:errcheck + return "", fmt.Errorf("合并切片文件失败: %v", err) + } + if err := sf.Close(); err != nil { + return "", fmt.Errorf("关闭切片文件失败: %v", err) } } return mergeFilePath, nil From 83f6be04867bf4a407e3579d8948075f8501dbdc Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 11:26:38 +0800 Subject: [PATCH 05/97] fix(models): handle JSON marshaling errors in SetRedis functions to improve error handling and data integrity --- pkg/models/file.go | 5 ++++- pkg/models/file_share_relational.go | 5 ++++- pkg/models/share.go | 5 ++++- pkg/models/stat.go | 5 ++++- pkg/models/task.go | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/pkg/models/file.go b/pkg/models/file.go index 32b62ef..ff36f7a 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -54,7 +54,10 @@ func SetRedisFileInfo(fileId string, handler func(fileInfo *RedisFileInfo) *Redi old_fileInfo = &RedisFileInfo{} } fileInfo := handler(old_fileInfo) - jsonData, _ := json.Marshal(fileInfo) + jsonData, err := json.Marshal(fileInfo) + if err != nil { + return err + } return rdb.Do(ctx, rdb.B().Hset().Key("015:fileInfoMap").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error() } diff --git a/pkg/models/file_share_relational.go b/pkg/models/file_share_relational.go index 3e4fece..f7914bd 100644 --- a/pkg/models/file_share_relational.go +++ b/pkg/models/file_share_relational.go @@ -25,6 +25,9 @@ func GetRedisFileShareRelational(fileId string) ([]string, error) { func SetRedisFileShareRelational(fileId string, shareIDs []string) error { rdb, ctx := utils.GetRedisClient() - jsonData, _ := json.Marshal(shareIDs) + jsonData, err := json.Marshal(shareIDs) + if err != nil { + return err + } return rdb.Do(ctx, rdb.B().Hset().Key("015:fileShareRelational").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error() } diff --git a/pkg/models/share.go b/pkg/models/share.go index 5ae7f61..5e8f055 100644 --- a/pkg/models/share.go +++ b/pkg/models/share.go @@ -61,7 +61,10 @@ func SetRedisShareInfo(shareId string, handler func(shareInfo *RedisShareInfo) * old_shareInfo = &RedisShareInfo{} } shareInfo := handler(old_shareInfo) - jsonData, _ := json.Marshal(shareInfo) + jsonData, err := json.Marshal(shareInfo) + if err != nil { + return err + } return rdb.Do( ctx, rdb.B().Set(). diff --git a/pkg/models/stat.go b/pkg/models/stat.go index 3dbc75d..a1d0560 100644 --- a/pkg/models/stat.go +++ b/pkg/models/stat.go @@ -49,7 +49,10 @@ func SetRedisStat(key string, handler func(stat *StatData) *StatData) error { } } stat := handler(old_stat) - jsonData, _ := json.Marshal(stat) + jsonData, err := json.Marshal(stat) + if err != nil { + return err + } return rdb.Do(ctx, rdb.B().Hset().Key("015:stat").FieldValue().FieldValue(key, string(jsonData)).Build()).Error() }) } diff --git a/pkg/models/task.go b/pkg/models/task.go index a896ded..4531c9e 100644 --- a/pkg/models/task.go +++ b/pkg/models/task.go @@ -29,7 +29,10 @@ func GetRedisTaskInfo(taskId string) (*map[string]any, error) { func SetRedisTaskInfo(taskId string, taskInfo map[string]any) error { rdb, ctx := utils.GetRedisClient() - jsonData, _ := json.Marshal(taskInfo) + jsonData, err := json.Marshal(taskInfo) + if err != nil { + return err + } return rdb.Do( ctx, rdb.B().Set().Key(fmt.Sprintf("015:taskInfoMap:%s", taskId)).Value(string(jsonData)).Ex(time.Hour).Build(), From 1298bf79d3d2473db24bad81cc36fc35b26912d8 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 11:49:32 +0800 Subject: [PATCH 06/97] refactor(backend): enhance DownloadShare functionality with context management and improve Redis share info handling --- backend/internal/controllers/download.go | 99 ++++++++++++------------ pkg/models/file.go | 20 +++-- pkg/models/share.go | 19 +++-- pkg/models/stat.go | 22 +++--- worker/internal/services/file.go | 4 +- 5 files changed, 94 insertions(+), 70 deletions(-) diff --git a/backend/internal/controllers/download.go b/backend/internal/controllers/download.go index 8f3b1f5..b95ab1b 100644 --- a/backend/internal/controllers/download.go +++ b/backend/internal/controllers/download.go @@ -2,6 +2,7 @@ package controllers import ( "backend/internal/utils" + "context" "fmt" "pkg/models" u "pkg/utils" @@ -9,6 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v5" + "github.com/samber/lo" "github.com/spf13/cast" ) @@ -84,64 +86,65 @@ func VaildateShare(c *echo.Context) error { return utils.HTTPErrorHandler(c, ErrInvalidSharePassword) } } - // 如果下载次数为0,则设置为-1 防止空值问题 - if shareInfo.ViewNum < 1 { - return utils.HTTPErrorHandler(c, ErrInsufficientDownloadQuota) - } - downloadWindow := u.GetEnvWithDefault("share.download_window", "12") - token := jwt.NewWithClaims(jwt.SigningMethodHS256, DownloadShareClaims{ - ShareId: r.ShareId, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(cast.ToDuration(downloadWindow + "h"))), - }, - }) + return u.WithLocker(context.Background(), "015:shareInfoMap:"+r.ShareId, 0, func(ctx context.Context) error { + shareInfo, err := models.GetRedisShareInfo(r.ShareId) + if err != nil || shareInfo == nil { + return utils.HTTPErrorHandler(c, lo.Ternary(err != nil, err, ErrShareNotFound)) + } + if shareInfo.ViewNum < 1 { + return utils.HTTPErrorHandler(c, ErrInsufficientDownloadQuota) + } + downloadWindow := u.GetEnvWithDefault("share.download_window", "12") + token := jwt.NewWithClaims(jwt.SigningMethodHS256, DownloadShareClaims{ + ShareId: r.ShareId, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(cast.ToDuration(downloadWindow + "h"))), + }, + }) - // Sign and get the complete encoded token as a string using the secret - downloadToken, err := token.SignedString([]byte(u.GetEnv("share.download_secret"))) - if err != nil { - return utils.HTTPErrorHandler(c, err) - } - if shareInfo.Type == models.ShareTypeFile { - fileInfo, err := models.GetRedisFileInfo(shareInfo.Data) + // Sign and get the complete encoded token as a string using the secret + downloadToken, err := token.SignedString([]byte(u.GetEnv("share.download_secret"))) if err != nil { return utils.HTTPErrorHandler(c, err) } - if fileInfo == nil { - return utils.HTTPErrorHandler(c, ErrShareFileNotFound) + if shareInfo.Type == models.ShareTypeFile { + fileInfo, err := models.GetRedisFileInfo(shareInfo.Data) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + if fileInfo == nil { + return utils.HTTPErrorHandler(c, ErrShareFileNotFound) + } + if fileInfo.FileType != models.FileTypeUpload { + return utils.HTTPErrorHandler(c, ErrInvalidShareFileState) + } } - if fileInfo.FileType != models.FileTypeUpload { - return utils.HTTPErrorHandler(c, ErrInvalidShareFileState) + // download_nums 必须放在创建token的时候减掉,不然多线程下载会导致多次减掉 + err = models.SetRedisShareInfo(r.ShareId, func(shareInfo *models.RedisShareInfo) *models.RedisShareInfo { + shareInfo.ViewNum -= 1 + return shareInfo + }) + if err != nil { + return utils.HTTPErrorHandler(c, err) } - } - // download_nums 必须放在创建token的时候减掉,不然多线程下载会导致多次减掉 - latestViewNum := shareInfo.ViewNum - 1 - // 如果下载次数为0,则设置为-1 防止空值问题 - if latestViewNum < 1 { - latestViewNum = -1 - } - err = models.SetRedisShareInfo(r.ShareId, models.RedisShareInfo{ - ViewNum: latestViewNum, - }) - if err != nil { - return utils.HTTPErrorHandler(c, err) - } - // 统计分享数 - currentDate := time.Now().Format("2006-01-02") - err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { - stat.DownloadNum += 1 - return stat - }) - if err != nil { - return utils.HTTPErrorHandler(c, err) - } + // 统计分享数 + currentDate := time.Now().Format("2006-01-02") + err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { + stat.DownloadNum += 1 + return stat + }) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } - if shareInfo.Type == models.ShareTypeFile { + if shareInfo.Type == models.ShareTypeFile { + return utils.HTTPSuccessHandler(c, map[string]any{ + "token": downloadToken, + }) + } return utils.HTTPSuccessHandler(c, map[string]any{ "token": downloadToken, }) - } - return utils.HTTPSuccessHandler(c, map[string]any{ - "token": downloadToken, }) } diff --git a/pkg/models/file.go b/pkg/models/file.go index ff36f7a..10d5adc 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -3,8 +3,10 @@ package models import ( "encoding/json" "pkg/utils" + "time" "github.com/redis/rueidis" + "github.com/spf13/cast" ) type FileInfo struct { @@ -25,6 +27,7 @@ type RedisFileInfo struct { FileInfo FileType FileType `json:"type"` CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` Expire int64 `json:"expire"` // 只有上传文件(init)的时候有这个字段 } @@ -44,21 +47,28 @@ func GetRedisFileInfo(fileId string) (*RedisFileInfo, error) { return &fileInfoData, nil } -func SetRedisFileInfo(fileId string, handler func(fileInfo *RedisFileInfo) *RedisFileInfo) error { +func SetRedisFileInfo(fileId string, handler func(fileInfo *RedisFileInfo) *RedisFileInfo) (*RedisFileInfo, error) { rdb, ctx := utils.GetRedisClient() old_fileInfo, err := GetRedisFileInfo(fileId) if err != nil { - return err + return nil, err } if old_fileInfo == nil { - old_fileInfo = &RedisFileInfo{} + old_fileInfo = &RedisFileInfo{ + CreatedAt: time.Now().Unix(), + Expire: cast.ToInt64(utils.GetEnvWithDefault("upload.remove_expire", "2")) * 3600, + } } fileInfo := handler(old_fileInfo) + fileInfo.UpdatedAt = time.Now().Unix() jsonData, err := json.Marshal(fileInfo) if err != nil { - return err + return nil, err } - return rdb.Do(ctx, rdb.B().Hset().Key("015:fileInfoMap").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error() + if err := rdb.Do(ctx, rdb.B().Hset().Key("015:fileInfoMap").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error(); err != nil { + return nil, err + } + return fileInfo, nil } func GetRedisFileInfoAll() (map[string]string, error) { diff --git a/pkg/models/share.go b/pkg/models/share.go index 5e8f055..999d950 100644 --- a/pkg/models/share.go +++ b/pkg/models/share.go @@ -13,6 +13,7 @@ import ( type RedisShareInfo struct { // Id string `json:"id"` CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` Owner string `json:"owner"` Type ShareType `json:"type"` Data string `json:"data"` // 分享数据 文件分享为文件id 文本分享为文本内容 @@ -51,26 +52,32 @@ func GetRedisShareInfo(shareId string) (*RedisShareInfo, error) { return &shareInfoData, nil } -func SetRedisShareInfo(shareId string, handler func(shareInfo *RedisShareInfo) *RedisShareInfo) error { +func SetRedisShareInfo(shareId string, handler func(shareInfo *RedisShareInfo) *RedisShareInfo) (*RedisShareInfo, error) { rdb, ctx := utils.GetRedisClient() old_shareInfo, err := GetRedisShareInfo(shareId) if err != nil { - return err + return nil, err } if old_shareInfo == nil { - old_shareInfo = &RedisShareInfo{} + old_shareInfo = &RedisShareInfo{ + CreatedAt: time.Now().Unix(), + } } shareInfo := handler(old_shareInfo) + shareInfo.UpdatedAt = time.Now().Unix() jsonData, err := json.Marshal(shareInfo) if err != nil { - return err + return nil, err } - return rdb.Do( + if err := rdb.Do( ctx, rdb.B().Set(). Key(fmt.Sprintf("015:shareInfoMap:%s", shareId)). Value(string(jsonData)). Ex(time.Until(time.Unix(shareInfo.ExpireAt, 0))). Build(), - ).Error() + ).Error(); err != nil { + return nil, err + } + return shareInfo, nil } diff --git a/pkg/models/stat.go b/pkg/models/stat.go index a1d0560..3bbebfd 100644 --- a/pkg/models/stat.go +++ b/pkg/models/stat.go @@ -33,28 +33,32 @@ func GetRedisStat(key string) (*StatData, error) { return &stat, nil } -func SetRedisStat(key string, handler func(stat *StatData) *StatData) error { - return utils.WithLocker(context.Background(), "015:stat:"+key, 0, func(ctx context.Context) error { +func SetRedisStat(key string, handler func(stat *StatData) *StatData) (*StatData, error) { + var updatedStat *StatData + err := utils.WithLocker(context.Background(), "015:stat:"+key, 0, func(ctx context.Context) error { rdb, _ := utils.GetRedisClient() old_stat, err := GetRedisStat(key) if err != nil { return err } if old_stat == nil { - old_stat = &StatData{ - FileSize: 0, - FileNum: 0, - ShareNum: 0, - DownloadNum: 0, - } + old_stat = &StatData{} } stat := handler(old_stat) jsonData, err := json.Marshal(stat) if err != nil { return err } - return rdb.Do(ctx, rdb.B().Hset().Key("015:stat").FieldValue().FieldValue(key, string(jsonData)).Build()).Error() + if err := rdb.Do(ctx, rdb.B().Hset().Key("015:stat").FieldValue().FieldValue(key, string(jsonData)).Build()).Error(); err != nil { + return err + } + updatedStat = stat + return nil }) + if err != nil { + return nil, err + } + return updatedStat, nil } func GetRedisStatAll() (map[string]string, error) { diff --git a/worker/internal/services/file.go b/worker/internal/services/file.go index 189f3c9..8df06a9 100644 --- a/worker/internal/services/file.go +++ b/worker/internal/services/file.go @@ -53,8 +53,8 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e if err != nil { return GenStandardFileReturn{}, err } - if err := models.SetRedisFileInfo(fileId, models.RedisFileInfo{ - FileInfo: models.FileInfo{ + if err := models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + fileInfo.FileInfo = models.FileInfo{ FileSize: fileSize, FileHash: fileHash, MimeType: mimeType, From 499b931c043dc5b02ef32fe6ac226cbc3388ac71 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 12:05:00 +0800 Subject: [PATCH 07/97] refactor(backend): streamline Redis operations in file and share handling by utilizing handler functions for improved data management --- backend/internal/controllers/download.go | 4 +-- backend/internal/controllers/file.go | 34 ++++++++++-------------- backend/internal/controllers/share.go | 22 +++++++-------- worker/internal/services/file.go | 23 +++++++--------- 4 files changed, 37 insertions(+), 46 deletions(-) diff --git a/backend/internal/controllers/download.go b/backend/internal/controllers/download.go index b95ab1b..6e17a56 100644 --- a/backend/internal/controllers/download.go +++ b/backend/internal/controllers/download.go @@ -120,7 +120,7 @@ func VaildateShare(c *echo.Context) error { } } // download_nums 必须放在创建token的时候减掉,不然多线程下载会导致多次减掉 - err = models.SetRedisShareInfo(r.ShareId, func(shareInfo *models.RedisShareInfo) *models.RedisShareInfo { + _, err = models.SetRedisShareInfo(r.ShareId, func(shareInfo *models.RedisShareInfo) *models.RedisShareInfo { shareInfo.ViewNum -= 1 return shareInfo }) @@ -130,7 +130,7 @@ func VaildateShare(c *echo.Context) error { // 统计分享数 currentDate := time.Now().Format("2006-01-02") - err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { + _, err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { stat.DownloadNum += 1 return stat }) diff --git a/backend/internal/controllers/file.go b/backend/internal/controllers/file.go index 4409637..4bcde6d 100644 --- a/backend/internal/controllers/file.go +++ b/backend/internal/controllers/file.go @@ -13,7 +13,6 @@ import ( "time" "github.com/labstack/echo/v5" - "github.com/spf13/cast" ) func CreateUploadTask(c *echo.Context) error { @@ -78,38 +77,33 @@ func CreateUploadTask(c *echo.Context) error { for r.FileSize/ChunkSize > 1000 { ChunkSize *= 2 } - uploadTaskExpire := cast.ToInt64(u.GetEnvWithDefault("upload.remove_expire", "2")) * 3600 - newFileInfo := models.RedisFileInfo{ - FileType: models.FileTypeInit, - FileInfo: models.FileInfo{ + redisFileInfo, err := models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + fileInfo.FileType = models.FileTypeInit + fileInfo.FileInfo = models.FileInfo{ FileSize: r.FileSize, MimeType: r.MimeType, FileHash: r.FileHash, ChunkSize: ChunkSize, - }, - CreatedAt: time.Now().Unix(), - Expire: uploadTaskExpire, - } - err = models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { - return &newFileInfo + } + return fileInfo }) if err != nil { return utils.HTTPErrorHandler(c, err) } - err = s.SetFileRemoveTask(fileId, time.Duration(uploadTaskExpire)*time.Second) + err = s.SetFileRemoveTask(fileId, time.Duration(redisFileInfo.Expire)*time.Second) if err != nil { return utils.HTTPErrorHandler(c, err) } return utils.HTTPSuccessHandler(c, map[string]any{ - "size": newFileInfo.FileSize, - "mime_type": newFileInfo.MimeType, - "hash": newFileInfo.FileHash, - "type": newFileInfo.FileType, - "expire": newFileInfo.Expire, + "size": redisFileInfo.FileSize, + "mime_type": redisFileInfo.MimeType, + "hash": redisFileInfo.FileHash, + "type": redisFileInfo.FileType, + "expire": redisFileInfo.Expire, "id": fileId, - "chunk_size": newFileInfo.ChunkSize, + "chunk_size": redisFileInfo.ChunkSize, }) } @@ -235,7 +229,7 @@ func FinishUploadTask(c *echo.Context) error { } // 更新文件信息 - err = models.SetRedisFileInfo(r.FileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + fileInfo, err = models.SetRedisFileInfo(r.FileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { fileInfo.FileType = models.FileTypeUpload return fileInfo }) @@ -244,7 +238,7 @@ func FinishUploadTask(c *echo.Context) error { } // 统计 currentDate := time.Now().Format("2006-01-02") - err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { + _, err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { stat.FileSize += fileInfo.FileSize stat.FileNum += 1 return stat diff --git a/backend/internal/controllers/share.go b/backend/internal/controllers/share.go index 010e07a..47ac970 100644 --- a/backend/internal/controllers/share.go +++ b/backend/internal/controllers/share.go @@ -73,16 +73,16 @@ func CreateShareInfo(c *echo.Context) error { password = hash } - err = models.SetRedisShareInfo(id, models.RedisShareInfo{ - Data: r.Data, - Type: r.Type, - CreatedAt: time.Now().Unix(), - Owner: owner, - ViewNum: r.Config.ViewNum, - Password: password, - // NotifyEmail: r.Config.NotifyEmail, - FileName: r.FileName, - ExpireAt: ExpireTime.Unix(), + _, err = models.SetRedisShareInfo(id, func(shareInfo *models.RedisShareInfo) *models.RedisShareInfo { + shareInfo.Data = r.Data + shareInfo.Type = r.Type + shareInfo.CreatedAt = time.Now().Unix() + shareInfo.Owner = owner + shareInfo.ViewNum = r.Config.ViewNum + shareInfo.Password = password + shareInfo.FileName = r.FileName + shareInfo.ExpireAt = ExpireTime.Unix() + return shareInfo }) if err != nil { return utils.HTTPErrorHandler(c, err) @@ -128,7 +128,7 @@ func CreateShareInfo(c *echo.Context) error { // 统计分享数 currentDate := time.Now().Format("2006-01-02") - err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { + _, err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData { stat.ShareNum += 1 return stat }) diff --git a/worker/internal/services/file.go b/worker/internal/services/file.go index 8df06a9..e4d907a 100644 --- a/worker/internal/services/file.go +++ b/worker/internal/services/file.go @@ -7,8 +7,6 @@ import ( "pkg/services" u "pkg/utils" "time" - - "github.com/spf13/cast" ) type GenStandardFileReturn struct { @@ -48,21 +46,20 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e if err := os.Rename(filePath, newPath); err != nil { return GenStandardFileReturn{}, err } - expire := cast.ToInt64(u.GetEnvWithDefault("upload.remove_expire", "2")) * 3600 - err = services.SetFileRemoveTask(fileId, time.Duration(expire)*time.Second) - if err != nil { - return GenStandardFileReturn{}, err - } - if err := models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { + redisFileInfo, err := models.SetRedisFileInfo(fileId, func(fileInfo *models.RedisFileInfo) *models.RedisFileInfo { fileInfo.FileInfo = models.FileInfo{ FileSize: fileSize, FileHash: fileHash, MimeType: mimeType, - }, - FileType: models.FileTypeUpload, - CreatedAt: time.Now().Unix(), - Expire: expire, - }); err != nil { + } + fileInfo.FileType = models.FileTypeUpload + return fileInfo + }) + if err != nil { + return GenStandardFileReturn{}, err + } + err = services.SetFileRemoveTask(fileId, time.Duration(redisFileInfo.Expire)*time.Second) + if err != nil { return GenStandardFileReturn{}, err } return GenStandardFileReturn{ From a1808a64ccbbe473ea8a69a0e795137e6e1487a8 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 12:08:23 +0800 Subject: [PATCH 08/97] fix(backend): improve error handling in DownloadShare by consolidating token validation and share info retrieval --- backend/internal/controllers/download.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/internal/controllers/download.go b/backend/internal/controllers/download.go index 6e17a56..f1f4624 100644 --- a/backend/internal/controllers/download.go +++ b/backend/internal/controllers/download.go @@ -28,17 +28,13 @@ func DownloadShare(c *echo.Context) error { t, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { return []byte(u.GetEnv("share.download_secret")), nil }) - if err != nil { - return utils.HTTPErrorHandler(c, err) + if err != nil || !t.Valid { + return utils.HTTPErrorHandler(c, lo.Ternary(err != nil, err, ErrInvalidRequest)) } - if !t.Valid { - return utils.HTTPErrorHandler(c, ErrInvalidRequest) + shareInfo, err := models.GetRedisShareInfo(claims.ShareId) + if err != nil || shareInfo == nil { + return utils.HTTPErrorHandler(c, lo.Ternary(err != nil, err, ErrShareNotFound)) } - shareInfo, _ := models.GetRedisShareInfo(claims.ShareId) - if shareInfo == nil { - return utils.HTTPErrorHandler(c, ErrShareNotFound) - } - if shareInfo.Type == models.ShareTypeFile { fileInfo, _ := models.GetRedisFileInfo(shareInfo.Data) uploadPath, err := u.GetUploadDirPath() From 2af28b6a507c229a052cf48fdac916c2cb4be2aa Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 21:59:13 +0800 Subject: [PATCH 09/97] feat(frontend): enhance SelectField and Tiptap components with improved class handling and word count display --- front/components/Field/SelectField.vue | 15 ++++----------- front/components/Tiptap/Index.vue | 5 +++++ front/lib/countWords.ts | 9 +++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 front/lib/countWords.ts diff --git a/front/components/Field/SelectField.vue b/front/components/Field/SelectField.vue index ad40b26..744056b 100644 --- a/front/components/Field/SelectField.vue +++ b/front/components/Field/SelectField.vue @@ -1,13 +1,5 @@ \ No newline at end of file + diff --git a/front/components/Tiptap/Index.vue b/front/components/Tiptap/Index.vue index 222e86e..89df370 100644 --- a/front/components/Tiptap/Index.vue +++ b/front/components/Tiptap/Index.vue @@ -4,6 +4,7 @@ import StarterKit from '@tiptap/starter-kit' import { Markdown } from 'tiptap-markdown' import Placeholder from '@tiptap/extension-placeholder' import { cx } from 'class-variance-authority' +import countWords from '@/lib/countWords' const props = defineProps<{ modelValue?: string @@ -15,6 +16,7 @@ const emit = defineEmits<{ }>() const editor = ref(undefined) + onMounted(() => { editor.value = new Editor({ content: props.modelValue, @@ -58,4 +60,7 @@ onUnmounted(() => { > +
+ {{ `${modelValue?.length ?? 0} 长度 · ${countWords(modelValue ?? '')} 字符` }} +
diff --git a/front/lib/countWords.ts b/front/lib/countWords.ts new file mode 100644 index 0000000..b6fdd61 --- /dev/null +++ b/front/lib/countWords.ts @@ -0,0 +1,9 @@ +function countWords(text: string): number { + const trimmed = text?.trim() + if (!trimmed) return 0 + const cjk = trimmed.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g)?.length ?? 0 + const latin = trimmed.replace(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g, ' ').match(/\S+/g)?.length ?? 0 + return cjk + latin +} + +export default countWords From 0d4d89d4ecf4be32b73c1629035925834aa0a87a Mon Sep 17 00:00:00 2001 From: keven1024 Date: Mon, 6 Apr 2026 22:36:20 +0800 Subject: [PATCH 10/97] feat(front): add text-translate feature with corresponding UI components and action handlers --- front/components/Drawer/TextShareDrawer.vue | 9 +++++++++ front/components/Preprocessing/types.ts | 2 +- front/components/Result/ResultIndexView.vue | 2 ++ front/composables/useFeatureMeta.ts | 10 ++++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/front/components/Drawer/TextShareDrawer.vue b/front/components/Drawer/TextShareDrawer.vue index f0aaaa5..97c9a77 100644 --- a/front/components/Drawer/TextShareDrawer.vue +++ b/front/components/Drawer/TextShareDrawer.vue @@ -20,6 +20,15 @@ const actionHandlers: Partial> = { 'text-share': { onClick: () => showDrawer({ render: ({ hide }) => h(TextShareHandle, { ...props, hide }) }), }, + 'text-translate': { + onClick: () => + props.onTextHandle({ + type: 'text-translate', + config: { + source: 'auto', + }, + }), + }, // 'text-image-generate': { // label: '生成配图', icon: LucideImage, className: 'bg-red-300', // onClick: () => { console.log('复制链接') } diff --git a/front/components/Preprocessing/types.ts b/front/components/Preprocessing/types.ts index ede41ce..05d70ab 100644 --- a/front/components/Preprocessing/types.ts +++ b/front/components/Preprocessing/types.ts @@ -1,5 +1,5 @@ export type FileHandleKey = 'file-share' | 'file-image-compress' | 'file-image-convert' export type FileShareHandleProps = { type: FileHandleKey; config: Record } -export type TextHandleKey = 'text-share' +export type TextHandleKey = 'text-share' | 'text-translate' export type TextShareHandleProps = { type: TextHandleKey; config: Record } diff --git a/front/components/Result/ResultIndexView.vue b/front/components/Result/ResultIndexView.vue index fa4927e..94bc7a9 100644 --- a/front/components/Result/ResultIndexView.vue +++ b/front/components/Result/ResultIndexView.vue @@ -1,6 +1,7 @@ + + From 249b9f2350bdd4a226d98d83a23f8777ce733f5d Mon Sep 17 00:00:00 2001 From: keven1024 Date: Tue, 7 Apr 2026 22:53:13 +0800 Subject: [PATCH 14/97] feat(front): update I18nSwitchDrawer to support new locale structure and add German, French, Japanese, Korean, and Traditional Chinese translations --- front/components/Drawer/I18nSwitchDrawer.vue | 26 +- front/i18n/locales/de.json | 235 +++++++++++++++++++ front/i18n/locales/en.json | 22 +- front/i18n/locales/fr.json | 235 +++++++++++++++++++ front/i18n/locales/ja.json | 235 +++++++++++++++++++ front/i18n/locales/ko.json | 235 +++++++++++++++++++ front/i18n/locales/zh-CN.json | 22 +- front/i18n/locales/zh-TW.json | 235 +++++++++++++++++++ 8 files changed, 1226 insertions(+), 19 deletions(-) create mode 100644 front/i18n/locales/de.json create mode 100644 front/i18n/locales/fr.json create mode 100644 front/i18n/locales/ja.json create mode 100644 front/i18n/locales/ko.json create mode 100644 front/i18n/locales/zh-TW.json diff --git a/front/components/Drawer/I18nSwitchDrawer.vue b/front/components/Drawer/I18nSwitchDrawer.vue index c1cd77b..a422463 100644 --- a/front/components/Drawer/I18nSwitchDrawer.vue +++ b/front/components/Drawer/I18nSwitchDrawer.vue @@ -1,23 +1,15 @@ @@ -26,12 +18,12 @@ const switchLocale = async (locale: string) => {
{{ t('i18n.switchLocale') }}
- {{ localeMap?.[locale as keyof typeof localeMap] }} + {{ locale.name }}
diff --git a/front/i18n/locales/de.json b/front/i18n/locales/de.json new file mode 100644 index 0000000..3fd3f35 --- /dev/null +++ b/front/i18n/locales/de.json @@ -0,0 +1,235 @@ +{ + "navbar": { + "file": "Datei", + "text": "Text" + }, + "i18n": { + "switchLocale": "Sprache wechseln" + }, + "seo": { + "desc": "015 ist eine Plattform zum temporären Teilen von Dateien und unterstützt das hochladen großer Dateien in Blöcken, temporären Text-Upload, Download und Freigabe" + }, + "btn": { + "submit": "Absenden", + "backToHome": "Zurück zur Startseite" + }, + "page": { + "upload": { + "file": { + "uploadFile": "Datei hochladen", + "uploadFilePlaceholder": "Datei per Drag-and-drop hierher ziehen oder zum Hochladen klicken", + "addMore": "Mehr hinzufügen", + "handleType": { + "file-share": "Dateifreigabe", + "file-image-compress": "Bildkomprimierung", + "file-image-convert": "Formatkonvertierung" + } + }, + "text": { + "uploadText": "Text hochladen", + "uploadTextPlaceholder": "Mit unserem Textwerkzeug können Sie teilen, übersetzen, zusammenfassen, Bilder erzeugen und große Modelle befragen", + "handleType": { + "text-share": "Textfreigabe", + "text-translate": "Textübersetzung" + } + }, + "pickup": { + "title": "Abholcode eingeben", + "codeError": "Ungültiger Abholcode", + "btn": "Abholen" + } + }, + "shareOptions": { + "file": { + "title": "Freigabeoptionen", + "downloadNums": "Anzahl der Downloads", + "expireTime": "Ablaufzeit", + "or": "oder", + "expireAfter": "läuft ab nach", + "pickupCode": "Abholcode", + "passwordProtection": "Passwortschutz", + "downloadNotify": "Download-Benachrichtigung", + "passwordPlaceholder": "Passwort eingeben", + "emailPlaceholder": "E-Mail eingeben", + "downloadOptions": { + "xdownload": "{0} Downloads" + }, + "expireOptions": { + "5min": "5 Minuten", + "1hour": "1 Stunde", + "1day": "1 Tag", + "3days": "3 Tage" + } + }, + "imageConvert": { + "title": "Bildkonvertierung", + "targetFormat": "Zielformat" + }, + "text": { + "title": "Freigabeoptionen", + "viewNums": "Anzahl der Aufrufe", + "expireTime": "Ablaufzeit", + "or": "oder", + "expireAfter": "läuft ab nach", + "pickupCode": "Abholcode", + "passwordProtection": "Passwortschutz", + "readNotify": "Lesebestätigung", + "passwordPlaceholder": "Passwort eingeben", + "emailPlaceholder": "E-Mail eingeben", + "viewOptions": { + "xview": "{0} Aufrufe" + }, + "expireOptions": { + "5min": "5 Minuten", + "1hour": "1 Stunde", + "1day": "1 Tag", + "3days": "3 Tage" + } + } + }, + "progress": { + "file": { + "totalUploadProgress": "Gesamter Upload-Fortschritt", + "fileList": "Dateiliste", + "fileName": "Dateiname", + "fileSize": "Dateigröße", + "uploadSpeed": "Upload-Geschwindigkeit", + "progress": "Fortschritt", + "uploadDetails": "Upload-Details", + "chunk": "Block", + "completed": "Abgeschlossen", + "discarded": "Verworfen", + "pending": "Ausstehend", + "chunkProgress": "Block-Fortschrittsbalken", + "chunkHeatmap": "Block-Heatmap", + "heatmap": "Heatmap", + "progressBar": "Fortschrittsbalken", + "uploadError": "Upload-Fehler", + "chunkUploadFailed": "Block {1} der Datei {0} ist mehrfach fehlgeschlagen, der Upload wurde abgebrochen", + "chunkUploadRetry": "Block {1} der Datei {0} konnte nicht hochgeladen werden, wir versuchen es später erneut", + "fileUploadFailed": "Der Upload der Datei {0} ist fehlgeschlagen, bitte versuchen Sie es erneut", + "uploadFailed": "Upload fehlgeschlagen", + "processing": { + "hash": "Hash wird berechnet...", + "create": "Upload wird initialisiert...", + "upload": "Wird hochgeladen...", + "finish": "Upload abgeschlossen" + }, + "instantUploadSuccess": "Eine Datei mit demselben Hash existiert bereits in der Cloud, Sofort-Upload erfolgreich", + "uploadFailedRetry": "Upload fehlgeschlagen, bitte versuchen Sie es später erneut", + "uploadSpeedInfo": { + "title": "Wie wird die Upload-Geschwindigkeit berechnet?", + "desc": { + "base": "Die Upload-Geschwindigkeit wird anhand von {chunkNum} * {chunkSize} geschätzt, die in der aktuellen Sekunde hochgeladen wurden. Sie kann daher leicht von der tatsächlichen Geschwindigkeit abweichen und dient nur als Referenz", + "chunkNum": "Anzahl der Dateiblöcke", + "chunkSize": "Größe jedes Dateiblocks" + } + } + } + }, + "result": { + "file": { + "title": "Upload erfolgreich", + "fileList": "Dateiliste", + "info": "Informationen", + "downloadNums": "Anzahl der Downloads", + "expireTime": "Ablaufzeit", + "pickupCode": "Abholcode", + "link": "Link", + "copySuccess": "Erfolgreich kopiert" + }, + "imageCompress": { + "title": "Bildkomprimierung", + "totalSize": "Gesamtgröße", + "task": "Aufgabe", + "retry": "Wiederholen {0}/{1}", + "failed": "Fehlgeschlagen" + }, + "imageConvert": { + "title": "Bildkonvertierung", + "convert": "Konvertieren", + "task": "Aufgabe", + "retry": "Wiederholen {0}/{1}", + "failed": "Fehlgeschlagen" + }, + "text": { + "title": "Freigabe erfolgreich", + "info": "Informationen", + "viewNums": "Anzahl der Aufrufe", + "expireTime": "Ablaufzeit", + "pickupCode": "Abholcode", + "link": "Link", + "content": "Inhalt", + "copySuccess": "Erfolgreich kopiert" + }, + "textTranslate": { + "title": "Textübersetzung", + "sourceText": "Ausgangstext", + "translatedText": "Übersetzung", + "sourceLanguage": "Ausgangssprache", + "targetLanguage": "Zielsprache", + "provider": "Anbieter", + "retranslate": "Erneut übersetzen", + "empty": "Geben Sie Text ein und klicken Sie auf erneut übersetzen, um das Ergebnis anzuzeigen", + "copy": "Übersetzung kopieren", + "copySuccess": "Übersetzung kopiert", + "language": { + "auto": "Automatisch erkennen", + "zh-CN": "Chinesisch", + "en": "Englisch", + "ja": "Japanisch", + "ko": "Koreanisch" + } + }, + "qrCode": { + "title": "QR-Code zum Teilen" + } + }, + "shareView": { + "linkExpired": "Dieser Link ist abgelaufen.", + "passwall": { + "title": "Passwort eingeben", + "passwordError": "Falsches Passwort", + "passwordPlaceholder": "Passwort eingeben" + }, + "fileShare": { + "title": "Datei herunterladen", + "downloadBtn": "Herunterladen", + "needPassword": "Passwort erforderlich", + "expireTime": "Ablaufzeit", + "remainingDownloads": "Verbleibende Downloads", + "getTokenFailed": "Token konnte nicht abgerufen werden", + "durationFormat": "D [Tage] HH:mm:ss" + }, + "textShare": { + "title": "Text anzeigen", + "viewBtn": "Anzeigen", + "needPassword": "Passwort erforderlich", + "expireTime": "Ablaufzeit", + "remainingViews": "Verbleibende Aufrufe", + "durationFormat": "D [Tage] HH:mm:ss" + } + }, + "about": { + "powerBy": "Open-Source-Plattform zum selbst gehosteten temporären Teilen von Dateien, betrieben von {0}", + "file": "Datei", + "share": "Teilen", + "download": "Herunterladen", + "task": "Aufgabe", + "admin": "Administrator der Website", + "author": "Autor", + "title": "Über", + "about": "Über", + "systemInfo": "Systeminformationen", + "enabledFeatures": "Aktivierte Funktionen", + "enabledFeaturesEmpty": "Für diese Instanz sind derzeit keine zusätzlichen Funktionen aktiviert", + "systemVersion": "Systemversion", + "storage": "Speicher", + "analysis": "Analyse", + "fileSize": "Dateigröße", + "fileNum": "Anzahl der Dateien", + "processed": "Verarbeitet", + "failed": "Fehlgeschlagen" + } + } +} diff --git a/front/i18n/locales/en.json b/front/i18n/locales/en.json index e34e307..10f9730 100644 --- a/front/i18n/locales/en.json +++ b/front/i18n/locales/en.json @@ -29,7 +29,8 @@ "uploadText": "Upload Text", "uploadTextPlaceholder": "Share, translate, summarize, generate images, and ask large models with our text processor", "handleType": { - "text-share": "Text Share" + "text-share": "Text Share", + "text-translate": "Text Translate" } }, "pickup": { @@ -161,6 +162,25 @@ "content": "Content", "copySuccess": "Copy Success" }, + "textTranslate": { + "title": "Text Translation", + "sourceText": "Source Text", + "translatedText": "Translated Text", + "sourceLanguage": "Source Language", + "targetLanguage": "Target Language", + "provider": "Provider", + "retranslate": "Retranslate", + "empty": "Enter text and click retranslate to preview the result", + "copy": "Copy Translation", + "copySuccess": "Translation copied", + "language": { + "auto": "Auto Detect", + "zh-CN": "Chinese", + "en": "English", + "ja": "Japanese", + "ko": "Korean" + } + }, "qrCode": { "title": "Share QR code" } diff --git a/front/i18n/locales/fr.json b/front/i18n/locales/fr.json new file mode 100644 index 0000000..6f6b619 --- /dev/null +++ b/front/i18n/locales/fr.json @@ -0,0 +1,235 @@ +{ + "navbar": { + "file": "Fichier", + "text": "Texte" + }, + "i18n": { + "switchLocale": "Changer de langue" + }, + "seo": { + "desc": "015 est une plateforme de partage de fichiers temporaires prenant en charge le téléversement fractionné de gros fichiers, le téléversement de texte temporaire, le téléchargement et le partage" + }, + "btn": { + "submit": "Envoyer", + "backToHome": "Retour à l'accueil" + }, + "page": { + "upload": { + "file": { + "uploadFile": "Téléverser un fichier", + "uploadFilePlaceholder": "Glissez-déposez un fichier ou cliquez pour téléverser", + "addMore": "Ajouter plus", + "handleType": { + "file-share": "Partage de fichier", + "file-image-compress": "Compression d'image", + "file-image-convert": "Conversion de format" + } + }, + "text": { + "uploadText": "Téléverser du texte", + "uploadTextPlaceholder": "Partagez, traduisez, résumez, générez des images et interrogez de grands modèles avec notre processeur de texte", + "handleType": { + "text-share": "Partage de texte", + "text-translate": "Traduction de texte" + } + }, + "pickup": { + "title": "Saisir le code de retrait", + "codeError": "Code de retrait invalide", + "btn": "Récupérer" + } + }, + "shareOptions": { + "file": { + "title": "Options de partage", + "downloadNums": "Nombre de téléchargements", + "expireTime": "Date d'expiration", + "or": "ou", + "expireAfter": "expire après", + "pickupCode": "Code de retrait", + "passwordProtection": "Protection par mot de passe", + "downloadNotify": "Notification de téléchargement", + "passwordPlaceholder": "Saisissez le mot de passe", + "emailPlaceholder": "Saisissez l'adresse e-mail", + "downloadOptions": { + "xdownload": "{0} téléchargements" + }, + "expireOptions": { + "5min": "5 minutes", + "1hour": "1 heure", + "1day": "1 jour", + "3days": "3 jours" + } + }, + "imageConvert": { + "title": "Conversion d'image", + "targetFormat": "Format cible" + }, + "text": { + "title": "Options de partage", + "viewNums": "Nombre de vues", + "expireTime": "Date d'expiration", + "or": "ou", + "expireAfter": "expire après", + "pickupCode": "Code de retrait", + "passwordProtection": "Protection par mot de passe", + "readNotify": "Notification de lecture", + "passwordPlaceholder": "Saisissez le mot de passe", + "emailPlaceholder": "Saisissez l'adresse e-mail", + "viewOptions": { + "xview": "{0} vues" + }, + "expireOptions": { + "5min": "5 minutes", + "1hour": "1 heure", + "1day": "1 jour", + "3days": "3 jours" + } + } + }, + "progress": { + "file": { + "totalUploadProgress": "Progression totale du téléversement", + "fileList": "Liste des fichiers", + "fileName": "Nom du fichier", + "fileSize": "Taille du fichier", + "uploadSpeed": "Vitesse d'envoi", + "progress": "Progression", + "uploadDetails": "Détails du téléversement", + "chunk": "Bloc", + "completed": "Terminé", + "discarded": "Abandonné", + "pending": "En attente", + "chunkProgress": "Barre de progression des blocs", + "chunkHeatmap": "Carte thermique des blocs", + "heatmap": "Carte thermique", + "progressBar": "Barre de progression", + "uploadError": "Erreur de téléversement", + "chunkUploadFailed": "Le bloc {1} du fichier {0} a échoué plusieurs fois, le téléversement a été interrompu", + "chunkUploadRetry": "Le bloc {1} du fichier {0} n'a pas pu être téléversé, une nouvelle tentative sera effectuée plus tard", + "fileUploadFailed": "Le téléversement du fichier {0} a échoué, veuillez réessayer", + "uploadFailed": "Échec du téléversement", + "processing": { + "hash": "Calcul du hash...", + "create": "Initialisation du téléversement...", + "upload": "Téléversement en cours...", + "finish": "Téléversement terminé" + }, + "instantUploadSuccess": "Un fichier avec le même hash existe déjà dans le cloud, téléversement instantané réussi", + "uploadFailedRetry": "Le téléversement a échoué, veuillez réessayer plus tard", + "uploadSpeedInfo": { + "title": "Comment la vitesse de téléversement est-elle calculée ?", + "desc": { + "base": "La vitesse est estimée à partir de {chunkNum} * {chunkSize} téléversés pendant la seconde en cours, elle peut donc légèrement différer de la vitesse réelle et n'est fournie qu'à titre indicatif", + "chunkNum": "Nombre de blocs du fichier", + "chunkSize": "Taille de chaque bloc du fichier" + } + } + } + }, + "result": { + "file": { + "title": "Téléversement réussi", + "fileList": "Liste des fichiers", + "info": "Informations", + "downloadNums": "Nombre de téléchargements", + "expireTime": "Date d'expiration", + "pickupCode": "Code de retrait", + "link": "Lien", + "copySuccess": "Copié avec succès" + }, + "imageCompress": { + "title": "Compression d'image", + "totalSize": "Taille totale", + "task": "Tâche", + "retry": "Nouvelle tentative {0}/{1}", + "failed": "Échec" + }, + "imageConvert": { + "title": "Conversion d'image", + "convert": "Convertir", + "task": "Tâche", + "retry": "Nouvelle tentative {0}/{1}", + "failed": "Échec" + }, + "text": { + "title": "Partage réussi", + "info": "Informations", + "viewNums": "Nombre de vues", + "expireTime": "Date d'expiration", + "pickupCode": "Code de retrait", + "link": "Lien", + "content": "Contenu", + "copySuccess": "Copié avec succès" + }, + "textTranslate": { + "title": "Traduction de texte", + "sourceText": "Texte source", + "translatedText": "Texte traduit", + "sourceLanguage": "Langue source", + "targetLanguage": "Langue cible", + "provider": "Fournisseur", + "retranslate": "Traduire à nouveau", + "empty": "Saisissez du texte puis cliquez sur retraduire pour voir le résultat", + "copy": "Copier la traduction", + "copySuccess": "Traduction copiée", + "language": { + "auto": "Détection automatique", + "zh-CN": "Chinois", + "en": "Anglais", + "ja": "Japonais", + "ko": "Coréen" + } + }, + "qrCode": { + "title": "Code QR de partage" + } + }, + "shareView": { + "linkExpired": "Ce lien a expiré.", + "passwall": { + "title": "Saisir le mot de passe", + "passwordError": "Mot de passe incorrect", + "passwordPlaceholder": "Saisissez le mot de passe" + }, + "fileShare": { + "title": "Télécharger le fichier", + "downloadBtn": "Télécharger", + "needPassword": "Mot de passe requis", + "expireTime": "Date d'expiration", + "remainingDownloads": "Téléchargements restants", + "getTokenFailed": "Échec de récupération du jeton", + "durationFormat": "D [jours] HH:mm:ss" + }, + "textShare": { + "title": "Afficher le texte", + "viewBtn": "Afficher", + "needPassword": "Mot de passe requis", + "expireTime": "Date d'expiration", + "remainingViews": "Vues restantes", + "durationFormat": "D [jours] HH:mm:ss" + } + }, + "about": { + "powerBy": "Plateforme open source auto-hébergée de partage temporaire de fichiers propulsée par {0}", + "file": "Fichier", + "share": "Partager", + "download": "Télécharger", + "task": "Tâche", + "admin": "Administrateur du site", + "author": "Auteur", + "title": "À propos", + "about": "À propos", + "systemInfo": "Informations système", + "enabledFeatures": "Fonctionnalités de l'instance", + "enabledFeaturesEmpty": "Aucune fonctionnalité supplémentaire n'est activée pour cette instance", + "systemVersion": "Version du système", + "storage": "Stockage", + "analysis": "Analyse", + "fileSize": "Taille des fichiers", + "fileNum": "Nombre de fichiers", + "processed": "Traités", + "failed": "Échecs" + } + } +} diff --git a/front/i18n/locales/ja.json b/front/i18n/locales/ja.json new file mode 100644 index 0000000..5f874bb --- /dev/null +++ b/front/i18n/locales/ja.json @@ -0,0 +1,235 @@ +{ + "navbar": { + "file": "ファイル", + "text": "テキスト" + }, + "i18n": { + "switchLocale": "言語を切り替える" + }, + "seo": { + "desc": "015 は一時的なファイル共有プラットフォームであり、大容量ファイルの分割アップロード、一時テキストのアップロード、ダウンロード、共有をサポートします" + }, + "btn": { + "submit": "送信", + "backToHome": "ホームに戻る" + }, + "page": { + "upload": { + "file": { + "uploadFile": "ファイルをアップロード", + "uploadFilePlaceholder": "ファイルをドラッグ&ドロップ、またはクリックしてアップロード", + "addMore": "さらに追加", + "handleType": { + "file-share": "ファイル共有", + "file-image-compress": "画像圧縮", + "file-image-convert": "形式変換" + } + }, + "text": { + "uploadText": "テキストをアップロード", + "uploadTextPlaceholder": "テキスト処理機能で共有、翻訳、要約、画像生成、大規模モデルへの質問を簡単に行えます", + "handleType": { + "text-share": "テキスト共有", + "text-translate": "テキスト翻訳" + } + }, + "pickup": { + "title": "受取コードを入力", + "codeError": "受取コードが正しくありません", + "btn": "受け取る" + } + }, + "shareOptions": { + "file": { + "title": "共有オプション", + "downloadNums": "ダウンロード回数", + "expireTime": "有効期限", + "or": "または", + "expireAfter": "後に期限切れ", + "pickupCode": "受取コード", + "passwordProtection": "パスワード保護", + "downloadNotify": "ダウンロード通知", + "passwordPlaceholder": "パスワードを入力", + "emailPlaceholder": "メールアドレスを入力", + "downloadOptions": { + "xdownload": "{0} 回ダウンロード" + }, + "expireOptions": { + "5min": "5分", + "1hour": "1時間", + "1day": "1日", + "3days": "3日" + } + }, + "imageConvert": { + "title": "画像変換", + "targetFormat": "変換先形式" + }, + "text": { + "title": "共有オプション", + "viewNums": "閲覧回数", + "expireTime": "有効期限", + "or": "または", + "expireAfter": "後に期限切れ", + "pickupCode": "受取コード", + "passwordProtection": "パスワード保護", + "readNotify": "既読通知", + "passwordPlaceholder": "パスワードを入力", + "emailPlaceholder": "メールアドレスを入力", + "viewOptions": { + "xview": "{0} 回閲覧" + }, + "expireOptions": { + "5min": "5分", + "1hour": "1時間", + "1day": "1日", + "3days": "3日" + } + } + }, + "progress": { + "file": { + "totalUploadProgress": "全体のアップロード進捗", + "fileList": "ファイル一覧", + "fileName": "ファイル名", + "fileSize": "ファイルサイズ", + "uploadSpeed": "アップロード速度", + "progress": "進捗", + "uploadDetails": "アップロード詳細", + "chunk": "チャンク", + "completed": "完了", + "discarded": "破棄済み", + "pending": "保留中", + "chunkProgress": "チャンク進捗バー", + "chunkHeatmap": "チャンクヒートマップ", + "heatmap": "ヒートマップ", + "progressBar": "進捗バー", + "uploadError": "アップロードエラー", + "chunkUploadFailed": "ファイル {0} のチャンク {1} は複数回失敗したため、アップロードを中止しました", + "chunkUploadRetry": "ファイル {0} のチャンク {1} のアップロードに失敗しました。後でもう一度試します", + "fileUploadFailed": "ファイル {0} のアップロードに失敗しました。もう一度お試しください", + "uploadFailed": "アップロード失敗", + "processing": { + "hash": "ハッシュを計算中...", + "create": "アップロードを初期化中...", + "upload": "アップロード中...", + "finish": "アップロード完了" + }, + "instantUploadSuccess": "同じハッシュのファイルがクラウドに存在するため、秒でアップロード完了しました", + "uploadFailedRetry": "アップロードに失敗しました。しばらくしてから再試行してください", + "uploadSpeedInfo": { + "title": "アップロード速度はどのように計算されますか?", + "desc": { + "base": "アップロード速度は、現在の1秒間にアップロードされた {chunkNum} * {chunkSize} を元に推定しており、実際の速度と多少の誤差がある場合があります。参考値としてご利用ください", + "chunkNum": "ファイルチャンク数", + "chunkSize": "各ファイルチャンクのサイズ" + } + } + } + }, + "result": { + "file": { + "title": "アップロード成功", + "fileList": "ファイル一覧", + "info": "情報", + "downloadNums": "ダウンロード回数", + "expireTime": "有効期限", + "pickupCode": "受取コード", + "link": "リンク", + "copySuccess": "コピーしました" + }, + "imageCompress": { + "title": "画像圧縮", + "totalSize": "合計サイズ", + "task": "タスク", + "retry": "再試行 {0}/{1}", + "failed": "失敗" + }, + "imageConvert": { + "title": "画像変換", + "convert": "変換", + "task": "タスク", + "retry": "再試行 {0}/{1}", + "failed": "失敗" + }, + "text": { + "title": "共有成功", + "info": "情報", + "viewNums": "閲覧回数", + "expireTime": "有効期限", + "pickupCode": "受取コード", + "link": "リンク", + "content": "内容", + "copySuccess": "コピーしました" + }, + "textTranslate": { + "title": "テキスト翻訳", + "sourceText": "入力テキスト", + "translatedText": "翻訳結果", + "sourceLanguage": "元の言語", + "targetLanguage": "翻訳先の言語", + "provider": "プロバイダー", + "retranslate": "再翻訳", + "empty": "テキストを入力して再翻訳をクリックすると結果が表示されます", + "copy": "翻訳文をコピー", + "copySuccess": "翻訳文をコピーしました", + "language": { + "auto": "自動検出", + "zh-CN": "中国語", + "en": "英語", + "ja": "日本語", + "ko": "韓国語" + } + }, + "qrCode": { + "title": "共有QRコード" + } + }, + "shareView": { + "linkExpired": "このリンクは期限切れです。", + "passwall": { + "title": "パスワードを入力", + "passwordError": "パスワードが正しくありません", + "passwordPlaceholder": "パスワードを入力" + }, + "fileShare": { + "title": "ファイルをダウンロード", + "downloadBtn": "ダウンロード", + "needPassword": "パスワードが必要です", + "expireTime": "有効期限", + "remainingDownloads": "残りダウンロード回数", + "getTokenFailed": "トークンの取得に失敗しました", + "durationFormat": "D日 HH:mm:ss" + }, + "textShare": { + "title": "テキストを表示", + "viewBtn": "表示", + "needPassword": "パスワードが必要です", + "expireTime": "有効期限", + "remainingViews": "残り閲覧回数", + "durationFormat": "D日 HH:mm:ss" + } + }, + "about": { + "powerBy": "{0} によって支えられているオープンソースのセルフホスト型一時ファイル共有プラットフォーム", + "file": "ファイル", + "share": "共有", + "download": "ダウンロード", + "task": "タスク", + "admin": "サイト管理者", + "author": "作者", + "title": "このサイトについて", + "about": "概要", + "systemInfo": "システム情報", + "enabledFeatures": "有効な機能", + "enabledFeaturesEmpty": "このインスタンスでは追加機能は有効化されていません", + "systemVersion": "システムバージョン", + "storage": "保存済みファイル", + "analysis": "分析", + "fileSize": "ファイルサイズ", + "fileNum": "ファイル数", + "processed": "処理済み", + "failed": "失敗" + } + } +} diff --git a/front/i18n/locales/ko.json b/front/i18n/locales/ko.json new file mode 100644 index 0000000..00e6e5d --- /dev/null +++ b/front/i18n/locales/ko.json @@ -0,0 +1,235 @@ +{ + "navbar": { + "file": "파일", + "text": "텍스트" + }, + "i18n": { + "switchLocale": "언어 전환" + }, + "seo": { + "desc": "015는 임시 파일 공유 플랫폼으로, 대용량 파일 분할 업로드와 임시 텍스트 업로드, 다운로드 및 공유를 지원합니다" + }, + "btn": { + "submit": "제출", + "backToHome": "홈으로 돌아가기" + }, + "page": { + "upload": { + "file": { + "uploadFile": "파일 업로드", + "uploadFilePlaceholder": "파일을 드래그 앤 드롭하거나 클릭하여 업로드", + "addMore": "더 추가", + "handleType": { + "file-share": "파일 공유", + "file-image-compress": "이미지 압축", + "file-image-convert": "형식 변환" + } + }, + "text": { + "uploadText": "텍스트 업로드", + "uploadTextPlaceholder": "텍스트 처리기를 사용해 공유, 번역, 요약, 이미지 생성, 대형 모델 질의를 손쉽게 할 수 있습니다", + "handleType": { + "text-share": "텍스트 공유", + "text-translate": "텍스트 번역" + } + }, + "pickup": { + "title": "수령 코드 입력", + "codeError": "수령 코드가 올바르지 않습니다", + "btn": "수령" + } + }, + "shareOptions": { + "file": { + "title": "공유 옵션", + "downloadNums": "다운로드 횟수", + "expireTime": "만료 시간", + "or": "또는", + "expireAfter": "후 만료", + "pickupCode": "수령 코드", + "passwordProtection": "비밀번호 보호", + "downloadNotify": "다운로드 알림", + "passwordPlaceholder": "비밀번호를 입력하세요", + "emailPlaceholder": "이메일을 입력하세요", + "downloadOptions": { + "xdownload": "{0}회 다운로드" + }, + "expireOptions": { + "5min": "5분", + "1hour": "1시간", + "1day": "1일", + "3days": "3일" + } + }, + "imageConvert": { + "title": "이미지 변환", + "targetFormat": "대상 형식" + }, + "text": { + "title": "공유 옵션", + "viewNums": "조회 횟수", + "expireTime": "만료 시간", + "or": "또는", + "expireAfter": "후 만료", + "pickupCode": "수령 코드", + "passwordProtection": "비밀번호 보호", + "readNotify": "읽음 알림", + "passwordPlaceholder": "비밀번호를 입력하세요", + "emailPlaceholder": "이메일을 입력하세요", + "viewOptions": { + "xview": "{0}회 조회" + }, + "expireOptions": { + "5min": "5분", + "1hour": "1시간", + "1day": "1일", + "3days": "3일" + } + } + }, + "progress": { + "file": { + "totalUploadProgress": "전체 업로드 진행률", + "fileList": "파일 목록", + "fileName": "파일명", + "fileSize": "파일 크기", + "uploadSpeed": "업로드 속도", + "progress": "진행률", + "uploadDetails": "업로드 세부 정보", + "chunk": "청크", + "completed": "완료됨", + "discarded": "폐기됨", + "pending": "대기 중", + "chunkProgress": "청크 진행 막대", + "chunkHeatmap": "청크 히트맵", + "heatmap": "히트맵", + "progressBar": "진행 막대", + "uploadError": "업로드 오류", + "chunkUploadFailed": "파일 {0}의 청크 {1} 업로드가 여러 번 실패하여 업로드를 중단했습니다", + "chunkUploadRetry": "파일 {0}의 청크 {1} 업로드에 실패했습니다. 잠시 후 다시 시도합니다", + "fileUploadFailed": "파일 {0} 업로드에 실패했습니다. 다시 시도해 주세요", + "uploadFailed": "업로드 실패", + "processing": { + "hash": "해시 계산 중...", + "create": "업로드 초기화 중...", + "upload": "업로드 중...", + "finish": "업로드 완료" + }, + "instantUploadSuccess": "동일한 해시의 파일이 클라우드에 있어 즉시 업로드에 성공했습니다", + "uploadFailedRetry": "업로드에 실패했습니다. 잠시 후 다시 시도해 주세요", + "uploadSpeedInfo": { + "title": "업로드 속도는 어떻게 계산되나요?", + "desc": { + "base": "업로드 속도는 현재 1초 동안 업로드된 {chunkNum} * {chunkSize}를 기준으로 추정되며, 실제 속도와 약간의 차이가 있을 수 있습니다. 참고용으로만 사용해 주세요", + "chunkNum": "파일 청크 수", + "chunkSize": "각 파일 청크 크기" + } + } + } + }, + "result": { + "file": { + "title": "업로드 성공", + "fileList": "파일 목록", + "info": "정보", + "downloadNums": "다운로드 횟수", + "expireTime": "만료 시간", + "pickupCode": "수령 코드", + "link": "링크", + "copySuccess": "복사되었습니다" + }, + "imageCompress": { + "title": "이미지 압축", + "totalSize": "총 크기", + "task": "작업", + "retry": "재시도 {0}/{1}", + "failed": "실패" + }, + "imageConvert": { + "title": "이미지 변환", + "convert": "변환", + "task": "작업", + "retry": "재시도 {0}/{1}", + "failed": "실패" + }, + "text": { + "title": "공유 성공", + "info": "정보", + "viewNums": "조회 횟수", + "expireTime": "만료 시간", + "pickupCode": "수령 코드", + "link": "링크", + "content": "내용", + "copySuccess": "복사되었습니다" + }, + "textTranslate": { + "title": "텍스트 번역", + "sourceText": "입력 텍스트", + "translatedText": "번역 결과", + "sourceLanguage": "원본 언어", + "targetLanguage": "대상 언어", + "provider": "제공자", + "retranslate": "다시 번역", + "empty": "텍스트를 입력한 뒤 다시 번역을 클릭하면 결과를 볼 수 있습니다", + "copy": "번역문 복사", + "copySuccess": "번역문이 복사되었습니다", + "language": { + "auto": "자동 감지", + "zh-CN": "중국어", + "en": "영어", + "ja": "일본어", + "ko": "한국어" + } + }, + "qrCode": { + "title": "공유 QR 코드" + } + }, + "shareView": { + "linkExpired": "이 링크는 만료되었습니다.", + "passwall": { + "title": "비밀번호 입력", + "passwordError": "비밀번호가 올바르지 않습니다", + "passwordPlaceholder": "비밀번호를 입력하세요" + }, + "fileShare": { + "title": "파일 다운로드", + "downloadBtn": "다운로드", + "needPassword": "비밀번호가 필요합니다", + "expireTime": "만료 시간", + "remainingDownloads": "남은 다운로드 횟수", + "getTokenFailed": "토큰을 가져오지 못했습니다", + "durationFormat": "D일 HH:mm:ss" + }, + "textShare": { + "title": "텍스트 보기", + "viewBtn": "보기", + "needPassword": "비밀번호가 필요합니다", + "expireTime": "만료 시간", + "remainingViews": "남은 조회 횟수", + "durationFormat": "D일 HH:mm:ss" + } + }, + "about": { + "powerBy": "{0} 기반의 오픈 소스 셀프 호스팅 임시 파일 공유 플랫폼", + "file": "파일", + "share": "공유", + "download": "다운로드", + "task": "작업", + "admin": "사이트 관리자", + "author": "작성자", + "title": "소개", + "about": "소개", + "systemInfo": "시스템 정보", + "enabledFeatures": "활성화된 기능", + "enabledFeaturesEmpty": "현재 이 인스턴스에는 추가 기능이 활성화되어 있지 않습니다", + "systemVersion": "시스템 버전", + "storage": "저장소", + "analysis": "분석", + "fileSize": "파일 크기", + "fileNum": "파일 수", + "processed": "처리됨", + "failed": "실패" + } + } +} diff --git a/front/i18n/locales/zh-CN.json b/front/i18n/locales/zh-CN.json index a64a34a..0857bc7 100644 --- a/front/i18n/locales/zh-CN.json +++ b/front/i18n/locales/zh-CN.json @@ -29,7 +29,8 @@ "uploadText": "上传文本", "uploadTextPlaceholder": "使用我们的文本处理器轻松分享,翻译,总结,生成图片,询问大模型", "handleType": { - "text-share": "文本分享" + "text-share": "文本分享", + "text-translate": "文本翻译" } }, "pickup": { @@ -161,6 +162,25 @@ "content": "内容", "copySuccess": "复制成功" }, + "textTranslate": { + "title": "文本翻译", + "sourceText": "输入文本", + "translatedText": "翻译结果", + "sourceLanguage": "源语言", + "targetLanguage": "目标语言", + "provider": "提供商", + "retranslate": "重新翻译", + "empty": "输入文本后点击重新翻译查看结果", + "copy": "复制译文", + "copySuccess": "译文已复制", + "language": { + "auto": "自动检测", + "zh-CN": "中文", + "en": "英语", + "ja": "日语", + "ko": "韩语" + } + }, "qrCode": { "title": "分享二维码" } diff --git a/front/i18n/locales/zh-TW.json b/front/i18n/locales/zh-TW.json new file mode 100644 index 0000000..ec9cde5 --- /dev/null +++ b/front/i18n/locales/zh-TW.json @@ -0,0 +1,235 @@ +{ + "navbar": { + "file": "檔案", + "text": "文字" + }, + "i18n": { + "switchLocale": "切換語言" + }, + "seo": { + "desc": "015 是一個開源的臨時檔案分享平台專案,支援臨時大檔案分片上傳、臨時文字上傳、下載與分享" + }, + "btn": { + "submit": "提交", + "backToHome": "返回首頁" + }, + "page": { + "upload": { + "file": { + "uploadFile": "上傳檔案", + "uploadFilePlaceholder": "拖曳檔案或點擊上傳", + "addMore": "新增更多", + "handleType": { + "file-share": "檔案分享", + "file-image-compress": "圖片壓縮", + "file-image-convert": "格式轉換" + } + }, + "text": { + "uploadText": "上傳文字", + "uploadTextPlaceholder": "使用我們的文字處理器輕鬆分享、翻譯、總結、生成圖片,並向大型模型提問", + "handleType": { + "text-share": "文字分享", + "text-translate": "文字翻譯" + } + }, + "pickup": { + "title": "輸入取件碼", + "codeError": "取件碼錯誤", + "btn": "取件" + } + }, + "shareOptions": { + "file": { + "title": "分享選項", + "downloadNums": "下載次數", + "expireTime": "過期時間", + "or": "或", + "expireAfter": "後過期", + "pickupCode": "取件碼", + "passwordProtection": "密碼保護", + "downloadNotify": "下載通知", + "passwordPlaceholder": "請輸入密碼", + "emailPlaceholder": "請輸入電子郵件", + "downloadOptions": { + "xdownload": "{0} 次下載" + }, + "expireOptions": { + "5min": "5 分鐘", + "1hour": "1 小時", + "1day": "1 天", + "3days": "3 天" + } + }, + "imageConvert": { + "title": "圖片轉換", + "targetFormat": "目標格式" + }, + "text": { + "title": "分享選項", + "viewNums": "瀏覽次數", + "expireTime": "過期時間", + "or": "或", + "expireAfter": "後過期", + "pickupCode": "取件碼", + "passwordProtection": "密碼保護", + "readNotify": "已讀通知", + "passwordPlaceholder": "請輸入密碼", + "emailPlaceholder": "請輸入電子郵件", + "viewOptions": { + "xview": "{0} 次瀏覽" + }, + "expireOptions": { + "5min": "5 分鐘", + "1hour": "1 小時", + "1day": "1 天", + "3days": "3 天" + } + } + }, + "progress": { + "file": { + "totalUploadProgress": "總上傳進度", + "fileList": "檔案列表", + "fileName": "檔名", + "fileSize": "檔案大小", + "uploadSpeed": "上傳速度", + "progress": "進度", + "uploadDetails": "上傳詳情", + "chunk": "區塊", + "completed": "已完成", + "discarded": "已捨棄", + "pending": "待完成", + "chunkProgress": "區塊進度條", + "chunkHeatmap": "區塊熱力圖", + "heatmap": "熱力圖", + "progressBar": "進度條", + "uploadError": "上傳錯誤", + "chunkUploadFailed": "檔案 {0} 的第 {1} 個分塊多次上傳失敗,我們已終止該檔案上傳", + "chunkUploadRetry": "檔案 {0} 的第 {1} 個分塊上傳失敗,稍後將再次嘗試", + "fileUploadFailed": "檔案 {0} 上傳失敗,請重試", + "uploadFailed": "上傳失敗", + "processing": { + "hash": "計算雜湊中...", + "create": "初始化上傳中...", + "upload": "上傳中...", + "finish": "上傳完成" + }, + "instantUploadSuccess": "雲端已存在相同雜湊的檔案,秒傳成功", + "uploadFailedRetry": "上傳失敗,請稍後重試", + "uploadSpeedInfo": { + "title": "上傳速度如何計算", + "desc": { + "base": "上傳速度根據當前秒內上傳的 {chunkNum} * {chunkSize} 估算,可能與實際速度存在一定誤差,僅供參考", + "chunkNum": "檔案區塊數量", + "chunkSize": "每個檔案區塊大小" + } + } + } + }, + "result": { + "file": { + "title": "上傳成功", + "fileList": "檔案列表", + "info": "資訊", + "downloadNums": "下載次數", + "expireTime": "過期時間", + "pickupCode": "提取碼", + "link": "連結", + "copySuccess": "複製成功" + }, + "imageCompress": { + "title": "圖片壓縮", + "totalSize": "總大小", + "task": "任務", + "retry": "重試 {0}/{1}", + "failed": "失敗" + }, + "imageConvert": { + "title": "圖片轉換", + "convert": "轉換", + "task": "任務", + "retry": "重試 {0}/{1}", + "failed": "失敗" + }, + "text": { + "title": "分享成功", + "info": "資訊", + "viewNums": "瀏覽次數", + "expireTime": "過期時間", + "pickupCode": "提取碼", + "link": "連結", + "content": "內容", + "copySuccess": "複製成功" + }, + "textTranslate": { + "title": "文字翻譯", + "sourceText": "輸入文字", + "translatedText": "翻譯結果", + "sourceLanguage": "來源語言", + "targetLanguage": "目標語言", + "provider": "提供者", + "retranslate": "重新翻譯", + "empty": "輸入文字後點擊重新翻譯即可查看結果", + "copy": "複製譯文", + "copySuccess": "譯文已複製", + "language": { + "auto": "自動偵測", + "zh-CN": "中文", + "en": "英語", + "ja": "日語", + "ko": "韓語" + } + }, + "qrCode": { + "title": "分享 QR Code" + } + }, + "shareView": { + "linkExpired": "此連結已過期。", + "passwall": { + "title": "輸入密碼", + "passwordError": "密碼錯誤", + "passwordPlaceholder": "請輸入密碼" + }, + "fileShare": { + "title": "下載檔案", + "downloadBtn": "下載", + "needPassword": "需要密碼", + "expireTime": "過期時間", + "remainingDownloads": "剩餘下載次數", + "getTokenFailed": "取得 token 失敗", + "durationFormat": "D天 HH:mm:ss" + }, + "textShare": { + "title": "查看文字", + "viewBtn": "瀏覽", + "needPassword": "需要密碼", + "expireTime": "過期時間", + "remainingViews": "剩餘瀏覽次數", + "durationFormat": "D天 HH:mm:ss" + } + }, + "about": { + "powerBy": "由 {0} 驅動的開源自託管臨時檔案分享平台", + "file": "檔案", + "share": "分享", + "download": "下載", + "task": "任務", + "admin": "本站管理員", + "author": "作者", + "title": "關於", + "about": "關於", + "systemInfo": "系統資訊", + "enabledFeatures": "實例功能", + "enabledFeaturesEmpty": "目前此實例尚未啟用額外功能", + "systemVersion": "系統版本", + "storage": "已託管的檔案", + "analysis": "分析", + "fileSize": "檔案大小", + "fileNum": "檔案數量", + "processed": "處理數量", + "failed": "失敗數量" + } + } +} From a99790c9b124eb7bafdfe20833064cef3a347904 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Tue, 7 Apr 2026 22:53:58 +0800 Subject: [PATCH 15/97] feat(front): add support for additional locales including Japanese, Korean, French, German, and Traditional Chinese in nuxt.config.ts and update app config structure for text translation --- front/composables/useMyAppConfig.ts | 5 +++++ front/nuxt.config.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/front/composables/useMyAppConfig.ts b/front/composables/useMyAppConfig.ts index 37e6e07..11607fe 100644 --- a/front/composables/useMyAppConfig.ts +++ b/front/composables/useMyAppConfig.ts @@ -9,6 +9,11 @@ const useMyAppConfig = () => { version: string build_time: number features: string[] + text: { + translate: { + provider: string[] + } + } } }>('/api/config') return computed(() => data?.value?.data) diff --git a/front/nuxt.config.ts b/front/nuxt.config.ts index f9db451..0bf604e 100644 --- a/front/nuxt.config.ts +++ b/front/nuxt.config.ts @@ -24,6 +24,11 @@ export default defineNuxtConfig({ locales: [ { code: 'zh-CN', name: '中文(简体)', file: 'zh-CN.json' }, { code: 'en', name: 'English', file: 'en.json' }, + { code: 'ja', name: '日本語', file: 'ja.json' }, + { code: 'ko', name: '한국어', file: 'ko.json' }, + { code: 'fr', name: 'Français', file: 'fr.json' }, + { code: 'de', name: 'Deutsch', file: 'de.json' }, + { code: 'zh-TW', name: '中文(繁體)', file: 'zh-TW.json' }, ], }, vite: { From d0021f946896df5dbaf149b2694b1ba492e4d342 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Tue, 7 Apr 2026 22:54:13 +0800 Subject: [PATCH 16/97] feat(backend): refactor GetConfig to extract enabled feature keys and include text-translate provider configuration --- backend/internal/controllers/config.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/internal/controllers/config.go b/backend/internal/controllers/config.go index 30b3b30..8d3610e 100644 --- a/backend/internal/controllers/config.go +++ b/backend/internal/controllers/config.go @@ -10,12 +10,18 @@ import ( "github.com/spf13/cast" ) -func GetConfig(c *echo.Context) error { - featureConfig := u.GetEnvMap("features") - features := lo.FilterMap(lo.Entries(featureConfig), func(e lo.Entry[string, any], _ int) (string, bool) { +func getEnabledKeys(config map[string]any) []string { + return lo.FilterMap(lo.Entries(config), func(e lo.Entry[string, any], _ int) (string, bool) { node, ok := e.Value.(map[string]any) return e.Key, ok && cast.ToBool(node["enabled"]) }) +} + +func GetConfig(c *echo.Context) error { + featureConfig := u.GetEnvMap("features") + features := getEnabledKeys(featureConfig) + textTranslateProviderConfig := u.GetEnvMap("features.text-translate.provider") + textTranslateProviders := getEnabledKeys(textTranslateProviderConfig) return utils.HTTPSuccessHandler(c, map[string]any{ "site_title": u.GetEnvMap("site.title"), @@ -26,5 +32,10 @@ func GetConfig(c *echo.Context) error { "version": u.GetEnvWithDefault("VERSION", "dev"), "build_time": cast.ToInt(u.GetEnvWithDefault("BUILD_TIME", cast.ToString(time.Now().Unix()))), "features": features, + "config": map[string]any{ + "text-translate": map[string]any{ + "provider": textTranslateProviders, + }, + }, }) } From 788ef8df57c94695cfcb64d08ad2e3dea174c684 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Wed, 8 Apr 2026 22:42:16 +0800 Subject: [PATCH 17/97] feat(front): add FileIcon component for dynamic file previews and icons based on file type --- front/components/{FileIcon.vue => FileIcon/Index.vue} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename front/components/{FileIcon.vue => FileIcon/Index.vue} (100%) diff --git a/front/components/FileIcon.vue b/front/components/FileIcon/Index.vue similarity index 100% rename from front/components/FileIcon.vue rename to front/components/FileIcon/Index.vue From 58ba3f6d501b165a38a1f8350ee8e66700c4e76e Mon Sep 17 00:00:00 2001 From: keven1024 Date: Wed, 8 Apr 2026 22:42:47 +0800 Subject: [PATCH 18/97] feat(front): add language support for multiple locales including English, German, French, Japanese, Korean, and Chinese with updated translations for locale switching --- front/i18n/locales/de.json | 25 ++++++++++++++++++++++++- front/i18n/locales/en.json | 25 ++++++++++++++++++++++++- front/i18n/locales/fr.json | 25 ++++++++++++++++++++++++- front/i18n/locales/ja.json | 25 ++++++++++++++++++++++++- front/i18n/locales/ko.json | 25 ++++++++++++++++++++++++- front/i18n/locales/zh-CN.json | 25 ++++++++++++++++++++++++- front/i18n/locales/zh-TW.json | 25 ++++++++++++++++++++++++- 7 files changed, 168 insertions(+), 7 deletions(-) diff --git a/front/i18n/locales/de.json b/front/i18n/locales/de.json index 3fd3f35..f81062f 100644 --- a/front/i18n/locales/de.json +++ b/front/i18n/locales/de.json @@ -4,7 +4,30 @@ "text": "Text" }, "i18n": { - "switchLocale": "Sprache wechseln" + "switchLocale": "Sprache wechseln", + "language": { + "en": "Englisch", + "en-GB": "Englisch (Vereinigtes Königreich)", + "ja": "Japanisch", + "fr": "Französisch", + "de": "Deutsch", + "es-ES": "Spanisch (Spanien)", + "es-419": "Spanisch (Lateinamerika)", + "pt-BR": "Portugiesisch (Brasilien)", + "zh-CN": "Chinesisch (vereinfacht)", + "zh-TW": "Chinesisch (traditionell)", + "nl": "Niederländisch", + "no": "Norwegisch", + "sv": "Schwedisch", + "da": "Dänisch", + "fi": "Finnisch", + "ko": "Koreanisch", + "vi": "Vietnamesisch", + "th": "Thailändisch", + "id": "Indonesisch", + "ar": "Arabisch", + "he": "Hebräisch" + } }, "seo": { "desc": "015 ist eine Plattform zum temporären Teilen von Dateien und unterstützt das hochladen großer Dateien in Blöcken, temporären Text-Upload, Download und Freigabe" diff --git a/front/i18n/locales/en.json b/front/i18n/locales/en.json index 10f9730..36b51c3 100644 --- a/front/i18n/locales/en.json +++ b/front/i18n/locales/en.json @@ -4,7 +4,30 @@ "text": "Text" }, "i18n": { - "switchLocale": "Switch Language" + "switchLocale": "Switch Language", + "language": { + "en": "English", + "en-GB": "English (UK)", + "ja": "Japanese", + "fr": "French", + "de": "German", + "es-ES": "Spanish (Spain)", + "es-419": "Spanish (Latin America)", + "pt-BR": "Portuguese (Brazil)", + "zh-CN": "Chinese (Simplified)", + "zh-TW": "Chinese (Traditional)", + "nl": "Dutch", + "no": "Norwegian", + "sv": "Swedish", + "da": "Danish", + "fi": "Finnish", + "ko": "Korean", + "vi": "Vietnamese", + "th": "Thai", + "id": "Indonesian", + "ar": "Arabic", + "he": "Hebrew" + } }, "seo": { "desc": "015 is a temporary file sharing platform project, supporting temporary large file slicing upload, temporary text upload, download and share" diff --git a/front/i18n/locales/fr.json b/front/i18n/locales/fr.json index 6f6b619..6b0ea48 100644 --- a/front/i18n/locales/fr.json +++ b/front/i18n/locales/fr.json @@ -4,7 +4,30 @@ "text": "Texte" }, "i18n": { - "switchLocale": "Changer de langue" + "switchLocale": "Changer de langue", + "language": { + "en": "Anglais", + "en-GB": "Anglais (Royaume-Uni)", + "ja": "Japonais", + "fr": "Français", + "de": "Allemand", + "es-ES": "Espagnol (Espagne)", + "es-419": "Espagnol (Amérique latine)", + "pt-BR": "Portugais (Brésil)", + "zh-CN": "Chinois (simplifié)", + "zh-TW": "Chinois (traditionnel)", + "nl": "Néerlandais", + "no": "Norvégien", + "sv": "Suédois", + "da": "Danois", + "fi": "Finnois", + "ko": "Coréen", + "vi": "Vietnamien", + "th": "Thaï", + "id": "Indonésien", + "ar": "Arabe", + "he": "Hébreu" + } }, "seo": { "desc": "015 est une plateforme de partage de fichiers temporaires prenant en charge le téléversement fractionné de gros fichiers, le téléversement de texte temporaire, le téléchargement et le partage" diff --git a/front/i18n/locales/ja.json b/front/i18n/locales/ja.json index 5f874bb..171154b 100644 --- a/front/i18n/locales/ja.json +++ b/front/i18n/locales/ja.json @@ -4,7 +4,30 @@ "text": "テキスト" }, "i18n": { - "switchLocale": "言語を切り替える" + "switchLocale": "言語を切り替える", + "language": { + "en": "英語", + "en-GB": "英語(イギリス)", + "ja": "日本語", + "fr": "フランス語", + "de": "ドイツ語", + "es-ES": "スペイン語(スペイン)", + "es-419": "スペイン語(ラテンアメリカ)", + "pt-BR": "ポルトガル語(ブラジル)", + "zh-CN": "中国語(簡体字)", + "zh-TW": "中国語(繁体字)", + "nl": "オランダ語", + "no": "ノルウェー語", + "sv": "スウェーデン語", + "da": "デンマーク語", + "fi": "フィンランド語", + "ko": "韓国語", + "vi": "ベトナム語", + "th": "タイ語", + "id": "インドネシア語", + "ar": "アラビア語", + "he": "ヘブライ語" + } }, "seo": { "desc": "015 は一時的なファイル共有プラットフォームであり、大容量ファイルの分割アップロード、一時テキストのアップロード、ダウンロード、共有をサポートします" diff --git a/front/i18n/locales/ko.json b/front/i18n/locales/ko.json index 00e6e5d..f798a19 100644 --- a/front/i18n/locales/ko.json +++ b/front/i18n/locales/ko.json @@ -4,7 +4,30 @@ "text": "텍스트" }, "i18n": { - "switchLocale": "언어 전환" + "switchLocale": "언어 전환", + "language": { + "en": "영어", + "en-GB": "영어(영국)", + "ja": "일본어", + "fr": "프랑스어", + "de": "독일어", + "es-ES": "스페인어(스페인)", + "es-419": "스페인어(라틴아메리카)", + "pt-BR": "포르투갈어(브라질)", + "zh-CN": "중국어(간체)", + "zh-TW": "중국어(번체)", + "nl": "네덜란드어", + "no": "노르웨이어", + "sv": "스웨덴어", + "da": "덴마크어", + "fi": "핀란드어", + "ko": "한국어", + "vi": "베트남어", + "th": "태국어", + "id": "인도네시아어", + "ar": "아랍어", + "he": "히브리어" + } }, "seo": { "desc": "015는 임시 파일 공유 플랫폼으로, 대용량 파일 분할 업로드와 임시 텍스트 업로드, 다운로드 및 공유를 지원합니다" diff --git a/front/i18n/locales/zh-CN.json b/front/i18n/locales/zh-CN.json index 0857bc7..e68bd90 100644 --- a/front/i18n/locales/zh-CN.json +++ b/front/i18n/locales/zh-CN.json @@ -4,7 +4,30 @@ "text": "文本" }, "i18n": { - "switchLocale": "切换语言" + "switchLocale": "切换语言", + "language": { + "en": "英语", + "en-GB": "英语(英国)", + "ja": "日语", + "fr": "法语", + "de": "德语", + "es-ES": "西班牙语(西班牙)", + "es-419": "西班牙语(拉丁美洲)", + "pt-BR": "葡萄牙语(巴西)", + "zh-CN": "中文(简体)", + "zh-TW": "中文(繁体)", + "nl": "荷兰语", + "no": "挪威语", + "sv": "瑞典语", + "da": "丹麦语", + "fi": "芬兰语", + "ko": "韩语", + "vi": "越南语", + "th": "泰语", + "id": "印度尼西亚语", + "ar": "阿拉伯语", + "he": "希伯来语" + } }, "seo": { "desc": "015 是一个开源的临时文件分享平台项目,支持临时大文件切片上传,临时文本上传、下载、分享" diff --git a/front/i18n/locales/zh-TW.json b/front/i18n/locales/zh-TW.json index ec9cde5..6b00088 100644 --- a/front/i18n/locales/zh-TW.json +++ b/front/i18n/locales/zh-TW.json @@ -4,7 +4,30 @@ "text": "文字" }, "i18n": { - "switchLocale": "切換語言" + "switchLocale": "切換語言", + "language": { + "en": "英語", + "en-GB": "英語(英國)", + "ja": "日語", + "fr": "法語", + "de": "德語", + "es-ES": "西班牙語(西班牙)", + "es-419": "西班牙語(拉丁美洲)", + "pt-BR": "葡萄牙語(巴西)", + "zh-CN": "中文(簡體)", + "zh-TW": "中文(繁體)", + "nl": "荷蘭語", + "no": "挪威語", + "sv": "瑞典語", + "da": "丹麥語", + "fi": "芬蘭語", + "ko": "韓語", + "vi": "越南語", + "th": "泰語", + "id": "印尼語", + "ar": "阿拉伯語", + "he": "希伯來語" + } }, "seo": { "desc": "015 是一個開源的臨時檔案分享平台專案,支援臨時大檔案分片上傳、臨時文字上傳、下載與分享" From 881d8e111aec089a631c9e8a65ea25e5e3a0760d Mon Sep 17 00:00:00 2001 From: keven1024 Date: Wed, 8 Apr 2026 22:43:07 +0800 Subject: [PATCH 19/97] fix(front): update import path for filePreview type in FilePreviewView component --- front/components/FilePreviewView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/components/FilePreviewView.vue b/front/components/FilePreviewView.vue index 68b23eb..8312c60 100644 --- a/front/components/FilePreviewView.vue +++ b/front/components/FilePreviewView.vue @@ -1,6 +1,6 @@ + + diff --git a/front/components/FileIcon/Image.vue b/front/components/FileIcon/Image.vue new file mode 100644 index 0000000..5d390d2 --- /dev/null +++ b/front/components/FileIcon/Image.vue @@ -0,0 +1,24 @@ + + + diff --git a/front/components/FileIcon/Index.vue b/front/components/FileIcon/Index.vue index 278dbaa..1bd5cde 100644 --- a/front/components/FileIcon/Index.vue +++ b/front/components/FileIcon/Index.vue @@ -1,5 +1,8 @@ diff --git a/front/components/FileIcon/Video.vue b/front/components/FileIcon/Video.vue new file mode 100644 index 0000000..a28fbc8 --- /dev/null +++ b/front/components/FileIcon/Video.vue @@ -0,0 +1,75 @@ + + + From ae2fbcc216c9995bad40c06ae3f766d9bcbb12d0 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Wed, 8 Apr 2026 23:45:34 +0800 Subject: [PATCH 21/97] feat(front): enhance file hash calculation by introducing engine selection for large files using native or wasm methods --- .../File/FileUploadProgressView/index.vue | 5 +- front/lib/calcFileHash.ts | 53 +++++++------------ front/lib/calcFileHashWorker.ts | 6 +-- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/front/components/Home/File/FileUploadProgressView/index.vue b/front/components/Home/File/FileUploadProgressView/index.vue index 4a5b254..a0dc096 100644 --- a/front/components/Home/File/FileUploadProgressView/index.vue +++ b/front/components/Home/File/FileUploadProgressView/index.vue @@ -137,11 +137,14 @@ watchEffect(async () => { } }) +const LARGE_FILE_THRESHOLD = 500 * 1024 * 1024 // 500 MB + const handleHash = async (fileId: string) => { const uploadfile = uploadfiles.value.find((item) => item.fileId === fileId) if (!uploadfile?.file) return uploadfile.procressType = 'hash' - const res = await asyncWorker(calcFileHashWorker, { data: { file: uploadfile.file } }) + const engine = uploadfile.file.size >= LARGE_FILE_THRESHOLD ? 'wasm' : 'native' + const res = await asyncWorker(calcFileHashWorker, { data: { file: uploadfile.file, engine } }) const { hash } = res?.data || {} uploadfile.hash = hash } diff --git a/front/lib/calcFileHash.ts b/front/lib/calcFileHash.ts index 152e0c4..f7894c3 100644 --- a/front/lib/calcFileHash.ts +++ b/front/lib/calcFileHash.ts @@ -1,47 +1,34 @@ import { noop } from 'lodash-es' -import { md5 } from 'js-md5' +import { createSHA1 } from 'hash-wasm' interface CalcFileHashProps { file: File onProgress?: (current: number) => void chunkSize?: number + engine?: 'native' | 'wasm' } const calcFileHash = async (props: CalcFileHashProps) => { - const { file, onProgress = noop, chunkSize = 100 } = props || {} - const blob = await file.arrayBuffer() - const hash = md5(blob) - return hash - // const finalChunkSize = chunkSize * 1024 * 1024; - // const chunks = Math.ceil(file.size / finalChunkSize); - // const spark = new SparkMD5.ArrayBuffer(); // 使用 SparkMD5 增量计算哈希 - // const fileReader = new FileReader(); + const { file, onProgress = noop, chunkSize = 100, engine = 'native' } = props || {} - // const readChunk = (start: number): Promise => { - // return new Promise((resolve, reject) => { - // const chunk = file.slice(start, Math.min(start + finalChunkSize, file.size)); - // fileReader.onload = (e) => resolve(e.target?.result as ArrayBuffer); - // fileReader.onerror = reject; - // fileReader.readAsArrayBuffer(chunk); - // }); - // }; + if (engine === 'native') { + const buffer = await file.arrayBuffer() + const hashBuffer = await crypto.subtle.digest('SHA-1', buffer) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + } - // try { - // const progressCallback = (current: number) => { - // const percentage = Math.round((current / chunks) * 100); - // onProgress(percentage); - // }; - - // for (let i = 0; i < chunks; i++) { - // const chunk = await readChunk(i * chunkSize); - // spark.append(chunk); - // progressCallback(i + 1); - // } - - // return spark.end(); - // } catch (error) { - // throw error; - // } + const chunkBytes = chunkSize * 1024 * 1024 + const hasher = await createSHA1() + let offset = 0 + while (offset < file.size) { + const buffer = await file.slice(offset, offset + chunkBytes).arrayBuffer() + hasher.update(new Uint8Array(buffer)) + offset += chunkBytes + onProgress(Math.min(offset, file.size) / file.size) + } + return hasher.digest('hex') } export default calcFileHash diff --git a/front/lib/calcFileHashWorker.ts b/front/lib/calcFileHashWorker.ts index 423722d..5306810 100644 --- a/front/lib/calcFileHashWorker.ts +++ b/front/lib/calcFileHashWorker.ts @@ -1,8 +1,8 @@ import calcFileHash from './calcFileHash' // 监听主线程消息 -self.onmessage = async (e: MessageEvent<{ file: File }>) => { - const { file } = e.data || {} - const hash = await calcFileHash({ file }) +self.onmessage = async (e: MessageEvent<{ file: File; engine?: 'native' | 'wasm' }>) => { + const { file, engine } = e.data || {} + const hash = await calcFileHash({ file, engine }) self.postMessage({ hash }) } From 907f77aa662740c651acb99d991f352df6b7adab Mon Sep 17 00:00:00 2001 From: keven1024 Date: Wed, 8 Apr 2026 23:48:07 +0800 Subject: [PATCH 22/97] refactor(backend): update file hash calculation from MD5 to SHA1 and rename related error handling --- backend/internal/controllers/errors.go | 2 +- backend/internal/controllers/file.go | 6 +++--- pkg/utils/file.go | 6 +++--- worker/internal/services/file.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/internal/controllers/errors.go b/backend/internal/controllers/errors.go index bd290dc..0365056 100644 --- a/backend/internal/controllers/errors.go +++ b/backend/internal/controllers/errors.go @@ -17,7 +17,7 @@ var ( ErrInvalidFileSliceIndex = errors.New("InvalidFileSliceIndex") // 文件切片索引错误 ErrInvalidFileSliceSize = errors.New("InvalidFileSliceSize") // 文件切片大小错误 ErrIncompleteFileSlices = errors.New("IncompleteFileSlices") // 文件切片不完整 - ErrFileMD5Mismatch = errors.New("FileMD5Mismatch") // 文件MD5不一致 + ErrFileHashMismatch = errors.New("FileHashMismatch") // 文件Hash不一致 // 分享相关 ErrShareFileNotFound = errors.New("ShareFileNotFound") // 分享文件不存在 diff --git a/backend/internal/controllers/file.go b/backend/internal/controllers/file.go index 4bcde6d..29e6d4e 100644 --- a/backend/internal/controllers/file.go +++ b/backend/internal/controllers/file.go @@ -212,18 +212,18 @@ func FinishUploadTask(c *echo.Context) error { return utils.HTTPErrorHandler(c, err) } - // 计算文件MD5 + // 计算文件SHA1 file, err := os.Open(mergeFilePath) if err != nil { return utils.HTTPErrorHandler(c, err) } defer file.Close() //nolint:errcheck - file_hash, err := u.GetFileMd5(file) + file_hash, err := u.GetFileSHA1(file) if err != nil || file_hash != fileInfo.FileHash { defer os.Remove(mergeFilePath) //nolint:errcheck if err == nil { - return utils.HTTPErrorHandler(c, ErrFileMD5Mismatch) + return utils.HTTPErrorHandler(c, ErrFileHashMismatch) } return utils.HTTPErrorHandler(c, err) } diff --git a/pkg/utils/file.go b/pkg/utils/file.go index 0282eba..ac06b8e 100644 --- a/pkg/utils/file.go +++ b/pkg/utils/file.go @@ -2,7 +2,7 @@ package utils import ( "bufio" - "crypto/md5" + "crypto/sha1" "fmt" "io" "os" @@ -15,9 +15,9 @@ func GetFileId(fileHash string, fileSize int64) string { return fmt.Sprintf("%s_%d", fileHash, fileSize) } -func GetFileMd5(file io.Reader) (string, error) { +func GetFileSHA1(file io.Reader) (string, error) { const bufferSize = 1024 * 1000 // 1MB - hash := md5.New() + hash := sha1.New() buf := make([]byte, bufferSize) reader := bufio.NewReader(file) for { diff --git a/worker/internal/services/file.go b/worker/internal/services/file.go index e4d907a..5ed67fe 100644 --- a/worker/internal/services/file.go +++ b/worker/internal/services/file.go @@ -31,7 +31,7 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e } fileSize := fileInfo.Size() - fileHash, err := u.GetFileMd5(file) + fileHash, err := u.GetFileSHA1(file) if err != nil { return GenStandardFileReturn{}, err } From a5a01a667eab895987ed1f43def0fe5e78291051 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Thu, 9 Apr 2026 00:02:56 +0800 Subject: [PATCH 23/97] refactor(front): replace MD5 with SHA1 for user avatar generation and update related components to use native hash calculation --- front/components/About/AboutBaseInfo.vue | 13 ++++++++----- front/lib/calcFileHash.ts | 12 ++++++++---- front/package.json | 5 ++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/front/components/About/AboutBaseInfo.vue b/front/components/About/AboutBaseInfo.vue index 6f30756..e1ee345 100644 --- a/front/components/About/AboutBaseInfo.vue +++ b/front/components/About/AboutBaseInfo.vue @@ -2,12 +2,12 @@ import { useQuery } from '@tanstack/vue-query' import { Skeleton } from '@/components/ui/skeleton' import getFileSize from '~/lib/getFileSize' -import SparkMD5 from 'spark-md5' import useMyAppConfig from '@/composables/useMyAppConfig' import { useFeatureMeta } from '@/composables/useFeatureMeta' import Progress from '~/components/ui/progress/Progress.vue' import renderI18n from '~/lib/renderI18n' import { I18nT } from 'vue-i18n' +import { calcNativeHash } from '~/lib/calcFileHash' const { locale } = useI18n() const appConfig = useMyAppConfig() @@ -34,9 +34,12 @@ const { data, isLoading } = useQuery({ }) const { t } = useI18n() -const genUserAvatar = (email: string) => { - return `https://www.gravatar.com/avatar/${SparkMD5.hash(email)}?d=retro` -} +const { state: userAvatar } = useAsyncState(async () => { + if (!data?.value?.email) return null + const buffer = new TextEncoder().encode(data?.value?.email) + const hash = await calcNativeHash(buffer) + return `https://www.gravatar.com/avatar/${hash}?d=retro` +}, null)