From 3d98f78caa0a1e9513c8f4f7efed9e547dc48017 Mon Sep 17 00:00:00 2001 From: MasonDye Date: Fri, 29 Aug 2025 12:01:05 +0800 Subject: [PATCH] update reset thumbnail feature --- routers/controllers/file.go | 38 ++++++++ routers/router.go | 11 +++ service/explorer/thumb.go | 188 ++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 service/explorer/thumb.go diff --git a/routers/controllers/file.go b/routers/controllers/file.go index e09a95f1..d6940122 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -412,3 +412,41 @@ func PatchView(c *gin.Context) { c.JSON(200, serializer.Response{}) } + +// ResetThumb resets thumbnail generation for files +func ResetThumb(c *gin.Context) { + service := ParametersFromContext[*explorer.ResetThumbService](c, explorer.ResetThumbParamCtx{}) + resp, err := service.Reset(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + c.Abort() + return + } + + if resp != nil && len(resp.Errors) > 0 { + // Return error response with detailed errors + c.JSON(200, serializer.Response{ + Code: -1, + Msg: "Reset thumbnail error", + Data: resp, + }) + return + } + + c.JSON(200, serializer.Response{Data: resp}) +} + +// ThumbExts gets supported thumbnail extensions +func ThumbExts(c *gin.Context) { + service := ParametersFromContext[*explorer.ThumbExtsService](c, explorer.ThumbExtsParamCtx{}) + resp, err := service.Get(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + c.Abort() + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} diff --git a/routers/router.go b/routers/router.go index a866d880..e974371e 100644 --- a/routers/router.go +++ b/routers/router.go @@ -620,6 +620,17 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { controllers.FromQuery[explorer.FileThumbService](explorer.FileThumbParameterCtx{}), controllers.Thumb, ) + // 重置缩略图 + file.POST("thumb/reset", + controllers.FromJSON[explorer.ResetThumbService](explorer.ResetThumbParamCtx{}), + middleware.ValidateBatchFileCount(dep, explorer.ResetThumbParamCtx{}), + controllers.ResetThumb, + ) + // 获取支持的缩略图扩展名 + file.GET("thumb/exts", + controllers.FromQuery[explorer.ThumbExtsService](explorer.ThumbExtsParamCtx{}), + controllers.ThumbExts, + ) // Delete files file.DELETE("", controllers.FromJSON[explorer.DeleteFileService](explorer.DeleteFileParameterCtx{}), diff --git a/service/explorer/thumb.go b/service/explorer/thumb.go new file mode 100644 index 00000000..8f75ef1c --- /dev/null +++ b/service/explorer/thumb.go @@ -0,0 +1,188 @@ +package explorer + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" + "github.com/cloudreve/Cloudreve/v4/pkg/serializer" + "github.com/cloudreve/Cloudreve/v4/pkg/thumb" +) + +type ( + // ResetThumbParamCtx defines context for ResetThumbService + ResetThumbParamCtx struct{} + + // ResetThumbService handles resetting thumbnail generation for files + ResetThumbService struct { + Uris []string `json:"uris" binding:"required,min=1"` + } + + // ResetThumbError represents an error for a specific URI + ResetThumbError struct { + URI string `json:"uri"` + Reason string `json:"reason"` + } + + // ResetThumbResponse represents the response for reset thumbnail operation + ResetThumbResponse struct { + Errors []ResetThumbError `json:"errors,omitempty"` + } +) + +func (s *ResetThumbService) GetUris() []string { + return s.Uris +} + +// Reset resets thumbnail generation for the specified files +func (s *ResetThumbService) Reset(c context.Context) (*ResetThumbResponse, error) { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + + uris, err := fs.NewUriFromStrings(s.Uris...) + if err != nil { + return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err) + } + + var errs []ResetThumbError + + for _, uri := range uris { + // Get the file to check if it exists and get current metadata + file, err := m.Get(c, uri, dbfs.WithFilePublicMetadata()) + if err != nil { + errs = append(errs, ResetThumbError{ + URI: uri.String(), + Reason: fmt.Sprintf("Reset thumbnail error: File does not exist"), + }) + continue + } + + // Check if thumb:disabled metadata exists + metadata := file.Metadata() + if _, exists := metadata[dbfs.ThumbDisabledKey]; exists { + // Remove the disabled mark directly via FileClient to bypass metadata validation + fileClient := dep.FileClient() + if dbfsFile, ok := file.(*dbfs.File); ok { + if err := fileClient.RemoveMetadata(c, dbfsFile.Model, dbfs.ThumbDisabledKey); err != nil { + errs = append(errs, ResetThumbError{ + URI: uri.String(), + Reason: fmt.Sprintf("Reset thumbnail error: Failed to update metadata - %s", err.Error()), + }) + continue + } + } else { + errs = append(errs, ResetThumbError{ + URI: uri.String(), + Reason: "Reset thumbnail error: Failed to access file model", + }) + continue + } + } + + // Trigger thumbnail regeneration by accessing the thumbnail + _, err = m.Thumbnail(c, uri) + if err != nil { + // Unsupported or not available + if errors.Is(err, thumb.ErrNotAvailable) || errors.Is(err, fs.ErrEntityNotExist) { + errs = append(errs, ResetThumbError{ + URI: uri.String(), + Reason: "Reset thumbnail error: File does not support thumbnail generation", + }) + } else { + errs = append(errs, ResetThumbError{ + URI: uri.String(), + Reason: fmt.Sprintf("Reset thumbnail error: Failed to generate thumbnail - %s", err.Error()), + }) + } + } + } + + if len(errs) > 0 { + return &ResetThumbResponse{Errors: errs}, nil + } + + return &ResetThumbResponse{}, nil +} + +type ( + // ThumbExtsParamCtx defines context for ThumbExtsService + ThumbExtsParamCtx struct{} + + // ThumbExtsService handles getting supported thumbnail extensions + ThumbExtsService struct{} + + // ThumbExtsResponse represents the response for supported extensions + ThumbExtsResponse struct { + Exts []string `json:"exts"` + } +) + +// Get returns all supported thumbnail extensions from enabled generators +func (s *ThumbExtsService) Get(c context.Context) (*ThumbExtsResponse, error) { + dep := dependency.FromContext(c) + settings := dep.SettingProvider() + + extensions := make(map[string]bool) + + // Built-in generator (always supports these if enabled) + if settings.BuiltinThumbGeneratorEnabled(c) { + for _, ext := range []string{"jpg", "jpeg", "png", "gif"} { + extensions[ext] = true + } + } + + // FFMpeg generator + if settings.FFMpegThumbGeneratorEnabled(c) { + for _, ext := range settings.FFMpegThumbExts(c) { + extensions[strings.ToLower(ext)] = true + } + } + + // Vips generator + if settings.VipsThumbGeneratorEnabled(c) { + for _, ext := range settings.VipsThumbExts(c) { + extensions[strings.ToLower(ext)] = true + } + } + + // LibreOffice generator + if settings.LibreOfficeThumbGeneratorEnabled(c) { + for _, ext := range settings.LibreOfficeThumbExts(c) { + extensions[strings.ToLower(ext)] = true + } + } + + // Music cover generator + if settings.MusicCoverThumbGeneratorEnabled(c) { + for _, ext := range settings.MusicCoverThumbExts(c) { + extensions[strings.ToLower(ext)] = true + } + } + + // LibRaw generator + if settings.LibRawThumbGeneratorEnabled(c) { + for _, ext := range settings.LibRawThumbExts(c) { + extensions[strings.ToLower(ext)] = true + } + } + + // Convert map to sorted slice + result := make([]string, 0, len(extensions)) + for ext := range extensions { + result = append(result, ext) + } + + // Sort extensions alphabetically using Go's built-in sort + sort.Strings(result) + + return &ThumbExtsResponse{Exts: result}, nil +}