From 22637bcf6ebc46651a72cc203ae316331363d906 Mon Sep 17 00:00:00 2001 From: keven1024 Date: Fri, 27 Feb 2026 11:23:48 +0800 Subject: [PATCH] feat(worker): add image conversion functionality with Magick and support for additional formats --- worker/internal/services/image.go | 30 ++++++++++++++++++- worker/internal/tasks/file.go | 12 +++++++- worker/internal/tasks/image.go | 3 +- worker/test/services/image_test.go | 46 ++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/worker/internal/services/image.go b/worker/internal/services/image.go index 36fa586..48e3d95 100644 --- a/worker/internal/services/image.go +++ b/worker/internal/services/image.go @@ -3,8 +3,20 @@ package services import ( "errors" "worker/internal/utils" + + "github.com/samber/lo" ) +var ( + ErrUnsupportedMimeType = errors.New("UnsupportedMimeType") +) + +// 支持的图片扩展名列表 +var supportedImageExts = []string{ + ".jpg", ".jpeg", ".png", ".gif", ".webp", + // ".bmp", ".tiff", ".tif", ".svg", ".ico", +} + func CompressImage(filePath string, mimeType string) (string, error) { compressedPath := filePath + "_compressed" switch mimeType { @@ -23,7 +35,23 @@ func CompressImage(filePath string, mimeType string) (string, error) { return "", err } default: - return "", errors.New("不支持的文件类型") + return "", ErrUnsupportedMimeType } return compressedPath, nil } + +func ConvertImageWithMagick(filePath, mimeType, targetExt string) (string, error) { + // 验证目标扩展名是否合法 + if !lo.Contains(supportedImageExts, targetExt) { + return "", ErrUnsupportedMimeType + } + + outputPath := filePath + "_converted" + targetExt + + _, err := utils.RunCommand("magick", filePath, outputPath) + if err != nil { + return "", err + } + + return outputPath, nil +} diff --git a/worker/internal/tasks/file.go b/worker/internal/tasks/file.go index f71348a..f065ba5 100644 --- a/worker/internal/tasks/file.go +++ b/worker/internal/tasks/file.go @@ -21,9 +21,19 @@ func RemoveFile(ctx context.Context, task *asynq.Task) error { if err != nil { return err } - if fileInfo == nil || fileInfo.FileType == models.FileTypeUpload { + if fileInfo == nil { return nil } + // 如果文件是上传文件,则需要检查是否还有分享,考虑到比如文件转换这些一次性任务产生的文件需要销毁 + if fileInfo.FileType == models.FileTypeUpload { + shareIDs, err := models.GetRedisFileShareRelational(payload.FileId) + if err != nil { + return err + } + if len(shareIDs) > 0 { + return nil + } + } rdb, rctx := u.GetRedisClient() uploadPath, err := utils.GetUploadDirPath() diff --git a/worker/internal/tasks/image.go b/worker/internal/tasks/image.go index 579ee4d..0571ad6 100644 --- a/worker/internal/tasks/image.go +++ b/worker/internal/tasks/image.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "mime" "path/filepath" "pkg/models" "worker/internal/services" @@ -19,7 +20,7 @@ func CompressImage(ctx context.Context, task *asynq.Task) error { } originalFileInfo, _ := models.GetRedisFileInfo(payload.FileId) if originalFileInfo == nil || originalFileInfo.FileType != models.FileTypeUpload { - return errors.New("文件不存在") + return ErrNotFoundFile } uploadPath, err := utils.GetUploadDirPath() if err != nil { diff --git a/worker/test/services/image_test.go b/worker/test/services/image_test.go index a72eb31..44a930e 100644 --- a/worker/test/services/image_test.go +++ b/worker/test/services/image_test.go @@ -64,3 +64,49 @@ func TestCompressJPEGHappyPath(t *testing.T) { assert.NotEqual(t, origInfo.Size(), compInfo.Size()) fmt.Printf("原图: %d | 压缩后: %d | 压缩率: %f%%\n", origInfo.Size(), compInfo.Size(), float64(compInfo.Size())/float64(origInfo.Size())*100) } + +func TestConvertImageWithMagickPNGToJPG(t *testing.T) { + tmp := t.TempDir() + filePath := filepath.Join(tmp, "test.png") + // 从 test/resource 复制真实 PNG + _, self, _, _ := runtime.Caller(0) + srcPath := filepath.Join(filepath.Dir(self), "..", "resource", "test.png") + err := utils.CopyFile(srcPath, filePath) + if err != nil { + t.Fatal(err) + } + + // 测试 PNG 转 JPEG + got, err := services.ConvertImageWithMagick(filePath, "image/png", ".jpg") + if err != nil { + t.Fatal(err) + } + + // 验证输出文件路径 + assert.Equal(t, got, filePath+"_converted.jpg") + + // 验证输出文件存在 + info, err := os.Stat(got) + if err != nil { + t.Fatal(err) + } + assert.True(t, info.Size() > 0, "转换后的文件应该有内容") + + fmt.Printf("PNG 转 JPEG 成功: %s (大小: %d bytes)\n", got, info.Size()) +} + +func TestConvertImageWithMagickInvalidExt(t *testing.T) { + tmp := t.TempDir() + filePath := filepath.Join(tmp, "test.png") + _, self, _, _ := runtime.Caller(0) + srcPath := filepath.Join(filepath.Dir(self), "..", "resource", "test.png") + err := utils.CopyFile(srcPath, filePath) + if err != nil { + t.Fatal(err) + } + + // 测试非法扩展名(防止注入) + _, err = services.ConvertImageWithMagick(filePath, "image/png", ".exe") + assert.Error(t, err, "应该返回错误") + assert.Equal(t, services.ErrInvalidImageExt, err, "应该返回 ErrInvalidImageExt 错误") +}