This commit is contained in:
ryan
2026-07-13 15:17:45 +08:00
parent 08e61ce833
commit e0eb4e5975
334 changed files with 17212 additions and 12321 deletions
+1 -1
View File
@@ -140,4 +140,4 @@ func parseCommaSeparatedWhitelist(value string) []string {
}
}
return whitelist
}
}
+1 -1
View File
@@ -284,4 +284,4 @@ func TestGetUploadByIDWorksWithRedisDisabled(t *testing.T) {
if gotCached.ID != upload.ID {
t.Fatal("expected RAM cache hit when redis is disabled")
}
}
}
+1 -1
View File
@@ -35,4 +35,4 @@ const (
ErrS3KeyStartsWithSlash = shared.ErrS3KeyStartsWithSlash
ErrS3KeyContainsNullBytes = shared.ErrS3KeyContainsNullBytes
ErrQueryUnusedUploadsFailed = shared.ErrQueryUnusedUploadsFailed
)
)
@@ -22,15 +22,15 @@ func TestGetDistinctUploadTypes(t *testing.T) {
dbConn.Create(&user)
customUpload := model.Upload{
ID: 9001,
UserID: user.ID,
FileName: "custom.txt",
FilePath: "uploads/custom.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Type: "custom_type_xyz",
Status: model.UploadStatusUsed,
ID: 9001,
UserID: user.ID,
FileName: "custom.txt",
FilePath: "uploads/custom.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Type: "custom_type_xyz",
Status: model.UploadStatusUsed,
}
dbConn.Create(&customUpload)
@@ -61,4 +61,4 @@ func TestGetDistinctUploadTypes(t *testing.T) {
if len(resp.Data) != 1 || resp.Data[0] != "custom_type_xyz" {
t.Errorf("expected only custom_type_xyz in types list, got: %v", resp.Data)
}
}
}
+1 -1
View File
@@ -83,4 +83,4 @@ func loadUploadStats(ctx context.Context) ([]model.UploadStat, error) {
func isRecordNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
}
+112 -112
View File
@@ -343,14 +343,14 @@ func TestDownloadFile(t *testing.T) {
// Seed upload records in DB
localUpload := model.Upload{
ID: 2001,
UserID: 1001,
FileName: "中文文件名.txt",
FilePath: "uploads/test_download.txt",
FileSize: 12,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2001,
UserID: 1001,
FileName: "中文文件名.txt",
FilePath: "uploads/test_download.txt",
FileSize: 12,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
}
// Create local file
@@ -410,44 +410,44 @@ func TestListFiles(t *testing.T) {
uploads := []model.Upload{
{
ID: 2101,
UserID: authUser.ID,
FileName: "first-report.txt",
FilePath: "uploads/first-report.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2101,
UserID: authUser.ID,
FileName: "first-report.txt",
FilePath: "uploads/first-report.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 2102,
UserID: authUser.ID,
FileName: "Second-Photo.PNG",
FilePath: "uploads/second-photo.png",
FileSize: 20,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
ID: 2102,
UserID: authUser.ID,
FileName: "Second-Photo.PNG",
FilePath: "uploads/second-photo.png",
FileSize: 20,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
},
{
ID: 2103,
UserID: authUser.ID,
FileName: "third-notes.md",
FilePath: "uploads/third-notes.md",
FileSize: 30,
MimeType: "text/markdown",
Extension: "md",
Status: model.UploadStatusUsed,
ID: 2103,
UserID: authUser.ID,
FileName: "third-notes.md",
FilePath: "uploads/third-notes.md",
FileSize: 30,
MimeType: "text/markdown",
Extension: "md",
Status: model.UploadStatusUsed,
},
{
ID: 2104,
UserID: 2002,
FileName: "other-user.txt",
FilePath: "uploads/other-user.txt",
FileSize: 40,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2104,
UserID: 2002,
FileName: "other-user.txt",
FilePath: "uploads/other-user.txt",
FileSize: 40,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
}
for i := range uploads {
@@ -562,34 +562,34 @@ func TestBatchDownloadFiles(t *testing.T) {
// Seed upload records. Note f2 and f3 have the same FileName "file_a.txt" to trigger name collision resolution.
uploads := []model.Upload{
{
ID: 3001,
UserID: 1001,
FileName: "file_a.txt",
FilePath: "uploads/f1.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3001,
UserID: 1001,
FileName: "file_a.txt",
FilePath: "uploads/f1.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 3002,
UserID: 1001,
FileName: "file_b.txt",
FilePath: "uploads/f2.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3002,
UserID: 1001,
FileName: "file_b.txt",
FilePath: "uploads/f2.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 3003,
UserID: 1001,
FileName: "file_a.txt", // COLLISION with 3001!
FilePath: "uploads/f3.txt",
FileSize: 28,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3003,
UserID: 1001,
FileName: "file_a.txt", // COLLISION with 3001!
FilePath: "uploads/f3.txt",
FileSize: 28,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
}
@@ -750,40 +750,40 @@ func TestGetFileStats(t *testing.T) {
// Insert some dummy uploads
uploads := []model.Upload{
{
ID: 3101,
UserID: authUser.ID,
FileName: "photo.png",
FilePath: "uploads/photo.png",
FileSize: 100,
MimeType: "image/png",
Extension: "png",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 3101,
UserID: authUser.ID,
FileName: "photo.png",
FilePath: "uploads/photo.png",
FileSize: 100,
MimeType: "image/png",
Extension: "png",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
},
{
ID: 3102,
UserID: authUser.ID,
FileName: "video.mp4",
FilePath: "uploads/video.mp4",
FileSize: 500,
MimeType: "video/mp4",
Extension: "mp4",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
ID: 3102,
UserID: authUser.ID,
FileName: "video.mp4",
FilePath: "uploads/video.mp4",
FileSize: 500,
MimeType: "video/mp4",
Extension: "mp4",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
},
{
ID: 3103,
UserID: authUser.ID,
FileName: "document.pdf",
FilePath: "uploads/document.pdf",
FileSize: 200,
MimeType: "application/pdf",
Extension: "pdf",
Type: "avatar", // different type
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
ID: 3103,
UserID: authUser.ID,
FileName: "document.pdf",
FilePath: "uploads/document.pdf",
FileSize: 200,
MimeType: "application/pdf",
Extension: "pdf",
Type: "avatar", // different type
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
},
}
@@ -865,26 +865,26 @@ func TestUserUploadManagement(t *testing.T) {
// Seed upload records
upload1 := model.Upload{
ID: 4001,
UserID: 1001,
FileName: "user1-file.txt",
FilePath: "uploads/user1-file.txt",
FileSize: 100,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 4001,
UserID: 1001,
FileName: "user1-file.txt",
FilePath: "uploads/user1-file.txt",
FileSize: 100,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
}
upload2 := model.Upload{
ID: 4002,
UserID: 1002,
FileName: "user2-file.png",
FilePath: "uploads/user2-file.png",
FileSize: 200,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 4002,
UserID: 1002,
FileName: "user2-file.png",
FilePath: "uploads/user2-file.png",
FileSize: 200,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
}
_ = dbConn.Create(&upload1)
+1 -1
View File
@@ -123,4 +123,4 @@ func GetFileStats(c *gin.Context) {
Categories: categories,
Types: types,
}))
}
}
+1 -1
View File
@@ -13,4 +13,4 @@ import (
var ErrForbidden = errors.New("upload forbidden")
// ErrStorageReadOnly indicates the storage backend is in migration read-only mode.
var ErrStorageReadOnly = errors.New(shared.ErrStorageReadOnly)
var ErrStorageReadOnly = errors.New(shared.ErrStorageReadOnly)
+1 -1
View File
@@ -195,4 +195,4 @@ func createNewUpload(ctx context.Context, req Request) (Result, error) {
Created: true,
Stored: true,
}, nil
}
}
+1 -1
View File
@@ -61,4 +61,4 @@ func ingestWithHashPolicy(ctx context.Context, req Request) (Result, error) {
}
return createNewUpload(ctx, req)
}
}
+1 -1
View File
@@ -326,4 +326,4 @@ func setupMockStorage(t *testing.T, putCount *int) (restore func(), disable func
storage.ResetCache()
}
return restore, disable
}
}
+1 -1
View File
@@ -55,4 +55,4 @@ func softDeleteUploadWithStats(ctx context.Context, upload *model.Upload) error
}
uploadcache.InvalidateUploadMetaCache(ctx, upload.ID)
return nil
}
}
+1 -1
View File
@@ -57,4 +57,4 @@ type Result struct {
Created bool
Stored bool
Resolved bool
}
}
+9 -9
View File
@@ -5,16 +5,16 @@ package shared
// Upload size, path, media quality, and cache constants shared across subpackages.
const (
MaxUploadSize = 32 * 1024 * 1024 // 32MB
DetectContentBytes = 512 // http.DetectContentType 需要的最小字节数
UploadDirPerm = 0755 // 上传目录权限
UploadFilePerm = 0644 // 上传文件权限
ImageQualityLow = "low"
ImageQualityMedium = "medium"
ImageQualityHigh = "high"
ImageQualityOrigin = "origin"
MaxUploadSize = 32 * 1024 * 1024 // 32MB
DetectContentBytes = 512 // http.DetectContentType 需要的最小字节数
UploadDirPerm = 0755 // 上传目录权限
UploadFilePerm = 0644 // 上传文件权限
ImageQualityLow = "low"
ImageQualityMedium = "medium"
ImageQualityHigh = "high"
ImageQualityOrigin = "origin"
DefaultPublicUploadType = "avatar"
FileStatsTrendDays = 7
MaxS3KeyLength = 1024
AccessCacheTTL = 5 // seconds; multiplied by time.Second at use site
)
)
+1 -1
View File
@@ -38,4 +38,4 @@ const (
ErrInvalidImageCacheWarmupQuality = "图片质量仅支持 low、medium、high"
ErrParseImageCacheWarmupPayload = "解析图片缓存预热参数失败: %w"
ErrQueryImagesForCacheWarmup = "查询待预热图片失败: %w"
)
)
+1 -1
View File
@@ -40,4 +40,4 @@ func GetFileCategory(mimeType, ext string) string {
return catDocument
}
return catOther
}
}
+1 -1
View File
@@ -128,4 +128,4 @@ func RecordUploadStatsRemove(ctx context.Context, upload *model.Upload) {
func isActiveUploadStatus(status model.UploadStatus) bool {
return status == model.UploadStatusPending || status == model.UploadStatusUsed
}
}
@@ -100,4 +100,4 @@ func loadUploadStats(ctx context.Context) (uploadStatsSnapshot, error) {
TotalCount: rows[0].FileCount,
TotalSize: rows[0].FileSize,
}, nil
}
}
+1 -1
View File
@@ -85,4 +85,4 @@ func buildMigrationAccessState(ctx context.Context) MigrationAccessState {
state.Target = target
state.HasTarget = true
return state
}
}
+1 -1
View File
@@ -90,4 +90,4 @@ func NormalizeMigrationPayload(ctx context.Context, payload []byte) ([]byte, sto
return nil, storage.Config{}, fmt.Errorf("marshal storage migration payload: %w", err)
}
return normalized, target, nil
}
}
+1 -1
View File
@@ -28,4 +28,4 @@ func OpenStoredObject(ctx context.Context, upload *model.Upload) (*storage.Objec
return nil, err
}
return backend.Get(ctx, upload.FilePath)
}
}
@@ -374,4 +374,4 @@ func isNotFoundError(err error) bool {
}
}
return false
}
}
@@ -60,16 +60,16 @@ func TestMigrationHandlerExecute(t *testing.T) {
}
upload := model.Upload{
ID: 99101,
UserID: 1,
FileName: "test.txt",
FilePath: "uploads/test.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "hash",
Type: "attachment",
Status: model.UploadStatusUsed,
ID: 99101,
UserID: 1,
FileName: "test.txt",
FilePath: "uploads/test.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "hash",
Type: "attachment",
Status: model.UploadStatusUsed,
}
if err := dbConn.Create(&upload).Error; err != nil {
t.Fatalf("Create(upload) returned error: %v", err)
@@ -157,16 +157,16 @@ func TestMigrationHandlerExecuteWithHashValidation(t *testing.T) {
// Case 1: Incorrect Hash (should fail validation)
uploadIncorrect := model.Upload{
ID: 99102,
UserID: 1,
FileName: "test-hash.txt",
FilePath: "uploads/test-hash.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
Type: "attachment",
Status: model.UploadStatusUsed,
ID: 99102,
UserID: 1,
FileName: "test-hash.txt",
FilePath: "uploads/test-hash.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
Type: "attachment",
Status: model.UploadStatusUsed,
}
if err := dbConn.Create(&uploadIncorrect).Error; err != nil {
t.Fatalf("Create(uploadIncorrect) returned error: %v", err)
+1 -1
View File
@@ -181,4 +181,4 @@ func (h *WarmImageCacheHandler) Execute(ctx context.Context, payload []byte) (*t
)
task.AppendLog(ctx, "%s", msg)
return &task.TaskResult{Message: msg}, nil
}
}
+28 -28
View File
@@ -280,40 +280,40 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
records := []model.Upload{
{
ID: 4101,
UserID: 1001,
FileName: "first.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
ID: 4101,
UserID: 1001,
FileName: "first.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
},
{
ID: 4102,
UserID: 1001,
FileName: "second.jpg",
FilePath: secondPath,
MimeType: "application/octet-stream",
Extension: "jpg",
Status: model.UploadStatusPending,
ID: 4102,
UserID: 1001,
FileName: "second.jpg",
FilePath: secondPath,
MimeType: "application/octet-stream",
Extension: "jpg",
Status: model.UploadStatusPending,
},
{
ID: 4103,
UserID: 1001,
FileName: "notes.txt",
FilePath: filepath.Join(testDir, "notes.txt"),
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 4103,
UserID: 1001,
FileName: "notes.txt",
FilePath: filepath.Join(testDir, "notes.txt"),
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 4104,
UserID: 1001,
FileName: "deleted.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusDeleted,
ID: 4104,
UserID: 1001,
FileName: "deleted.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusDeleted,
},
}
for i := range records {
+1 -1
View File
@@ -47,4 +47,4 @@ func NormalizeImageQuality(quality string) string {
default:
return shared.ImageQualityOrigin
}
}
}
+1 -1
View File
@@ -72,4 +72,4 @@ func CompressImageToWebP(srcReader io.Reader, quality string) ([]byte, error) {
}
return buf.Bytes(), nil
}
}