From f36e39991df28a4044985916a4b0b48266ea9a5e Mon Sep 17 00:00:00 2001 From: Aaron Liu <912394456@qq.com> Date: Fri, 7 Apr 2023 19:08:54 +0800 Subject: [PATCH] refactor(thumb): new thumb pipeline model to generate thumb on-demand --- models/defaults.go | 5 + models/file.go | 93 ++++++++++++++++-- pkg/filesystem/driver/cos/handler.go | 4 +- pkg/filesystem/driver/handler.go | 8 +- pkg/filesystem/driver/local/handler.go | 12 ++- pkg/filesystem/driver/onedrive/handler.go | 4 +- pkg/filesystem/driver/oss/handler.go | 4 +- pkg/filesystem/driver/qiniu/handler.go | 6 +- pkg/filesystem/driver/remote/handler.go | 4 +- pkg/filesystem/driver/s3/handler.go | 2 +- .../driver/shadow/masterinslave/handler.go | 2 +- .../driver/shadow/slaveinmaster/handler.go | 2 +- pkg/filesystem/driver/upyun/handler.go | 4 +- pkg/filesystem/hooks.go | 16 ---- pkg/filesystem/image.go | 96 +++++++++++-------- pkg/filesystem/manage.go | 3 +- pkg/filesystem/upload.go | 1 - pkg/serializer/explorer.go | 2 +- pkg/thumb/{image.go => builtin.go} | 51 +++++++--- pkg/thumb/{image_test.go => builtin_test.go} | 0 pkg/thumb/pipeline.go | 88 +++++++++++++++++ pkg/webdav/webdav.go | 1 - service/explorer/upload.go | 1 - 23 files changed, 308 insertions(+), 101 deletions(-) rename pkg/thumb/{image.go => builtin.go} (78%) rename pkg/thumb/{image_test.go => builtin_test.go} (100%) create mode 100644 pkg/thumb/pipeline.go diff --git a/models/defaults.go b/models/defaults.go index 6cffbb4..dbaecb4 100644 --- a/models/defaults.go +++ b/models/defaults.go @@ -106,6 +106,11 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "thumb_encode_method", Value: "jpg", Type: "thumb"}, {Name: "thumb_gc_after_gen", Value: "0", Type: "thumb"}, {Name: "thumb_encode_quality", Value: "85", Type: "thumb"}, + {Name: "thumb_builtin_enabled", Value: "1", Type: "thumb"}, + {Name: "thumb_vips_enabled", Value: "0", Type: "thumb"}, + {Name: "thumb_ffmpeg_enabled", Value: "0", Type: "thumb"}, + {Name: "thumb_vips_path", Value: "vips", Type: "thumb"}, + {Name: "thumb_ffmpeg_path", Value: "ffmpeg", Type: "thumb"}, {Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"}, {Name: "pwa_medium_icon", Value: "/static/img/logo192.png", Type: "pwa"}, {Name: "pwa_large_icon", Value: "/static/img/logo512.png", Type: "pwa"}, diff --git a/models/file.go b/models/file.go index 8a7b3c8..097028d 100644 --- a/models/file.go +++ b/models/file.go @@ -34,6 +34,15 @@ type File struct { MetadataSerialized map[string]string `gorm:"-"` } +// Thumb related metadata +const ( + ThumbStatusNotExist = "" + ThumbStatusExist = "exist" + ThumbStatusNotAvailable = "not_available" + + ThumbStatusMetadataKey = "thumb_status" +) + func init() { // 注册缓存用到的复杂结构 gob.Register(File{}) @@ -64,6 +73,8 @@ func (file *File) AfterFind() (err error) { // 反序列化文件元数据 if file.Metadata != "" { err = json.Unmarshal([]byte(file.Metadata), &file.MetadataSerialized) + } else { + file.MetadataSerialized = make(map[string]string) } return @@ -71,9 +82,13 @@ func (file *File) AfterFind() (err error) { // BeforeSave Save策略前的钩子 func (file *File) BeforeSave() (err error) { - metaValue, err := json.Marshal(&file.MetadataSerialized) - file.Metadata = string(metaValue) - return err + if len(file.MetadataSerialized) > 0 { + metaValue, err := json.Marshal(&file.MetadataSerialized) + file.Metadata = string(metaValue) + return err + } + + return nil } // GetChildFile 查找目录下名为name的子文件 @@ -279,7 +294,14 @@ func GetFilesByUploadSession(sessionID string, uid uint) (*File, error) { // Rename 重命名文件 func (file *File) Rename(new string) error { - return DB.Model(&file).UpdateColumn("name", new).Error + if err := file.resetThumb(); err != nil { + return err + } + + return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{ + "name": new, + "metadata": file.Metadata, + }).Error } // UpdatePicInfo 更新文件的图像信息 @@ -287,6 +309,23 @@ func (file *File) UpdatePicInfo(value string) error { return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{PicInfo: value}).Error } +// UpdateMetadata 新增或修改文件的元信息 +func (file *File) UpdateMetadata(data map[string]string) error { + if file.MetadataSerialized == nil { + file.MetadataSerialized = make(map[string]string) + } + + for k, v := range data { + file.MetadataSerialized[k] = v + } + metaValue, err := json.Marshal(&file.MetadataSerialized) + if err != nil { + return err + } + + return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{Metadata: string(metaValue)}).Error +} + // UpdateSize 更新文件的大小信息 // TODO: 全局锁 func (file *File) UpdateSize(value uint64) error { @@ -302,10 +341,18 @@ func (file *File) UpdateSize(value uint64) error { sizeDelta = file.Size - value } + if err := file.resetThumb(); err != nil { + tx.Rollback() + return err + } + if res := tx.Model(&file). Where("size = ?", file.Size). Set("gorm:association_autoupdate", false). - Update("size", value); res.Error != nil { + Updates(map[string]interface{}{ + "size": value, + "metadata": file.Metadata, + }); res.Error != nil { tx.Rollback() return res.Error } @@ -321,7 +368,14 @@ func (file *File) UpdateSize(value uint64) error { // UpdateSourceName 更新文件的源文件名 func (file *File) UpdateSourceName(value string) error { - return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("source_name", value).Error + if err := file.resetThumb(); err != nil { + return err + } + + return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{ + "source_name": value, + "metadata": file.Metadata, + }).Error } func (file *File) PopChunkToFile(lastModified *time.Time, picInfo string) error { @@ -361,6 +415,21 @@ func (file *File) CreateOrGetSourceLink() (*SourceLink, error) { return res, nil } +func (file *File) resetThumb() error { + if _, ok := file.MetadataSerialized[ThumbStatusMetadataKey]; !ok { + return nil + } + + delete(file.MetadataSerialized, ThumbStatusMetadataKey) + metaValue, err := json.Marshal(&file.MetadataSerialized) + if err != nil { + return err + } + + file.Metadata = string(metaValue) + return err +} + /* 实现 webdav.FileInfo 接口 */ @@ -383,3 +452,15 @@ func (file *File) IsDir() bool { func (file *File) GetPosition() string { return file.Position } + +// ShouldLoadThumb returns if file explorer should try to load thumbnail for this file. +// `True` does not guarantee the load request will success in next step, but the client +// should try to load and fallback to default placeholder in case error returned. +func (file *File) ShouldLoadThumb() bool { + switch file.GetPolicy().Type { + case "local": + return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable + default: + return file.PicInfo != "" && file.PicInfo != " " && file.PicInfo != "null,null" + } +} diff --git a/pkg/filesystem/driver/cos/handler.go b/pkg/filesystem/driver/cos/handler.go index 48ff09c..87a8124 100644 --- a/pkg/filesystem/driver/cos/handler.go +++ b/pkg/filesystem/driver/cos/handler.go @@ -222,7 +222,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err } // Thumb 获取文件缩略图 -func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { var ( thumbSize = [2]uint{400, 300} ok = false @@ -234,7 +234,7 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content source, err := handler.signSourceURL( ctx, - path, + file.SourceName, int64(model.GetIntSetting("preview_timeout", 60)), &urlOption{}, ) diff --git a/pkg/filesystem/driver/handler.go b/pkg/filesystem/driver/handler.go index a1caaf4..8b2a08d 100644 --- a/pkg/filesystem/driver/handler.go +++ b/pkg/filesystem/driver/handler.go @@ -2,12 +2,18 @@ package driver import ( "context" + "fmt" + model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/serializer" "net/url" ) +var ( + ErrorThumbNotExist = fmt.Errorf("thumb not exist") +) + // Handler 存储策略适配器 type Handler interface { // 上传文件, dst为文件存储路径,size 为文件大小。上下文关闭 @@ -22,7 +28,7 @@ type Handler interface { // 获取缩略图,可直接在ContentResponse中返回文件数据流,也可指 // 定为重定向 - Thumb(ctx context.Context, path string) (*response.ContentResponse, error) + Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) // 获取外链/下载地址, // url - 站点本身地址, diff --git a/pkg/filesystem/driver/local/handler.go b/pkg/filesystem/driver/local/handler.go index 2c1205c..50af796 100644 --- a/pkg/filesystem/driver/local/handler.go +++ b/pkg/filesystem/driver/local/handler.go @@ -12,6 +12,7 @@ import ( model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/auth" "github.com/cloudreve/Cloudreve/v3/pkg/cache" + "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/serializer" @@ -194,15 +195,20 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err } // Thumb 获取文件缩略图 -func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { - file, err := handler.Get(ctx, path+model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")) +func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { + if file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist { + // Tell invoker to generate a thumb + return nil, driver.ErrorThumbNotExist + } + + thumbFile, err := handler.Get(ctx, file.SourceName+model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")) if err != nil { return nil, err } return &response.ContentResponse{ Redirect: false, - Content: file, + Content: thumbFile, }, nil } diff --git a/pkg/filesystem/driver/onedrive/handler.go b/pkg/filesystem/driver/onedrive/handler.go index 389ede2..3b8eea7 100644 --- a/pkg/filesystem/driver/onedrive/handler.go +++ b/pkg/filesystem/driver/onedrive/handler.go @@ -136,7 +136,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err } // Thumb 获取文件缩略图 -func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { var ( thumbSize = [2]uint{400, 300} ok = false @@ -145,7 +145,7 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content return nil, errors.New("failed to get thumbnail size") } - res, err := handler.Client.GetThumbURL(ctx, path, thumbSize[0], thumbSize[1]) + res, err := handler.Client.GetThumbURL(ctx, file.SourceName, thumbSize[0], thumbSize[1]) if err != nil { // 如果出现异常,就清空文件的pic_info if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { diff --git a/pkg/filesystem/driver/oss/handler.go b/pkg/filesystem/driver/oss/handler.go index af27816..826942b 100644 --- a/pkg/filesystem/driver/oss/handler.go +++ b/pkg/filesystem/driver/oss/handler.go @@ -293,7 +293,7 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er } // Thumb 获取文件缩略图 -func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { // 初始化客户端 if err := handler.InitOSSClient(true); err != nil { return nil, err @@ -312,7 +312,7 @@ func (handler *Driver) Thumb(ctx context.Context, path string) (*response.Conten thumbOption := []oss.Option{oss.Process(thumbParam)} thumbURL, err := handler.signSourceURL( ctx, - path, + file.SourceName, int64(model.GetIntSetting("preview_timeout", 60)), thumbOption, ) diff --git a/pkg/filesystem/driver/qiniu/handler.go b/pkg/filesystem/driver/qiniu/handler.go index 3fed572..d201e5b 100644 --- a/pkg/filesystem/driver/qiniu/handler.go +++ b/pkg/filesystem/driver/qiniu/handler.go @@ -230,7 +230,7 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er } // Thumb 获取文件缩略图 -func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { var ( thumbSize = [2]uint{400, 300} ok = false @@ -239,12 +239,12 @@ func (handler *Driver) Thumb(ctx context.Context, path string) (*response.Conten return nil, errors.New("无法获取缩略图尺寸设置") } - path = fmt.Sprintf("%s?imageView2/1/w/%d/h/%d", path, thumbSize[0], thumbSize[1]) + thumb := fmt.Sprintf("%s?imageView2/1/w/%d/h/%d", file.SourceName, thumbSize[0], thumbSize[1]) return &response.ContentResponse{ Redirect: true, URL: handler.signSourceURL( ctx, - path, + thumb, int64(model.GetIntSetting("preview_timeout", 60)), ), }, nil diff --git a/pkg/filesystem/driver/remote/handler.go b/pkg/filesystem/driver/remote/handler.go index 9b88e8d..06c9efe 100644 --- a/pkg/filesystem/driver/remote/handler.go +++ b/pkg/filesystem/driver/remote/handler.go @@ -204,8 +204,8 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er } // Thumb 获取文件缩略图 -func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { - sourcePath := base64.RawURLEncoding.EncodeToString([]byte(path)) +func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { + sourcePath := base64.RawURLEncoding.EncodeToString([]byte(file.SourceName)) thumbURL := handler.getAPIUrl("thumb") + "/" + sourcePath ttl := model.GetIntSetting("preview_timeout", 60) signedThumbURL, err := auth.SignURI(handler.AuthInstance, thumbURL, int64(ttl)) diff --git a/pkg/filesystem/driver/s3/handler.go b/pkg/filesystem/driver/s3/handler.go index 9280a63..ef88e51 100644 --- a/pkg/filesystem/driver/s3/handler.go +++ b/pkg/filesystem/driver/s3/handler.go @@ -264,7 +264,7 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er } // Thumb 获取文件缩略图 -func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { return nil, errors.New("未实现") } diff --git a/pkg/filesystem/driver/shadow/masterinslave/handler.go b/pkg/filesystem/driver/shadow/masterinslave/handler.go index 0356ae3..782a4a9 100644 --- a/pkg/filesystem/driver/shadow/masterinslave/handler.go +++ b/pkg/filesystem/driver/shadow/masterinslave/handler.go @@ -39,7 +39,7 @@ func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error return nil, ErrNotImplemented } -func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { return nil, ErrNotImplemented } diff --git a/pkg/filesystem/driver/shadow/slaveinmaster/handler.go b/pkg/filesystem/driver/shadow/slaveinmaster/handler.go index 4dd9da8..15a7a8f 100644 --- a/pkg/filesystem/driver/shadow/slaveinmaster/handler.go +++ b/pkg/filesystem/driver/shadow/slaveinmaster/handler.go @@ -102,7 +102,7 @@ func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error return nil, ErrNotImplemented } -func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { return nil, ErrNotImplemented } diff --git a/pkg/filesystem/driver/upyun/handler.go b/pkg/filesystem/driver/upyun/handler.go index 8fed080..89192e5 100644 --- a/pkg/filesystem/driver/upyun/handler.go +++ b/pkg/filesystem/driver/upyun/handler.go @@ -220,7 +220,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err } // Thumb 获取文件缩略图 -func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { +func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { var ( thumbSize = [2]uint{400, 300} ok = false @@ -232,7 +232,7 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content thumbParam := fmt.Sprintf("!/fwfh/%dx%d", thumbSize[0], thumbSize[1]) thumbURL, err := handler.Source( ctx, - path+thumbParam, + file.SourceName+thumbParam, url.URL{}, int64(model.GetIntSetting("preview_timeout", 60)), false, diff --git a/pkg/filesystem/hooks.go b/pkg/filesystem/hooks.go index 4f3b9f2..bd34a64 100644 --- a/pkg/filesystem/hooks.go +++ b/pkg/filesystem/hooks.go @@ -184,7 +184,6 @@ func SlaveAfterUpload(session *serializer.UploadSession) Hook { Name: fileInfo.FileName, SourceName: fileInfo.SavePath, } - fs.GenerateThumbnail(ctx, &file) if session.Callback == "" { return nil @@ -231,21 +230,6 @@ func GenericAfterUpload(ctx context.Context, fs *FileSystem, fileHeader fsctx.Fi return nil } -// HookGenerateThumb 生成缩略图 -func HookGenerateThumb(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error { - // 异步尝试生成缩略图 - fileMode := fileHeader.Info().Model.(*model.File) - if fs.Policy.IsThumbGenerateNeeded() { - fs.recycleLock.Lock() - go func() { - defer fs.recycleLock.Unlock() - _, _ = fs.Handler.Delete(ctx, []string{fileMode.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")}) - fs.GenerateThumbnail(ctx, fileMode) - }() - } - return nil -} - // HookClearFileHeaderSize 将FileHeader大小设定为0 func HookClearFileHeaderSize(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error { fileHeader.SetSize(0) diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index 493f8d7..76c6550 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -2,13 +2,15 @@ package filesystem import ( "context" - "fmt" + "errors" + "io" "sync" "runtime" model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/conf" + "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/thumb" @@ -20,14 +22,11 @@ import ( ================ */ -// HandledExtension 可以生成缩略图的文件扩展名 -var HandledExtension = []string{"jpg", "jpeg", "png", "gif"} - // GetThumb 获取文件的缩略图 func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentResponse, error) { // 根据 ID 查找文件 err := fs.resetFileIDIfNotExist(ctx, id) - if err != nil || fs.FileTarget[0].PicInfo == "" { + if err != nil { return &response.ContentResponse{ Redirect: false, }, ErrObjectNotExist @@ -36,12 +35,11 @@ func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentR w, h := fs.GenerateThumbnailSize(0, 0) ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, [2]uint{w, h}) ctx = context.WithValue(ctx, fsctx.FileModelCtx, fs.FileTarget[0]) - res, err := fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName) - - // 本地存储策略出错时重新生成缩略图 - if err != nil && fs.Policy.Type == "local" { + res, err := fs.Handler.Thumb(ctx, &fs.FileTarget[0]) + if errors.Is(err, driver.ErrorThumbNotExist) { + // Regenerate thumb if the thumb is not initialized yet fs.GenerateThumbnail(ctx, &fs.FileTarget[0]) - res, err = fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName) + res, err = fs.Handler.Thumb(ctx, &fs.FileTarget[0]) } if err == nil && conf.SystemConfig.Mode == "master" { @@ -84,14 +82,8 @@ func (pool *Pool) releaseWorker() { <-pool.worker } -// GenerateThumbnail 尝试为本地策略文件生成缩略图并获取图像原始大小 -// TODO 失败时,如果之前还有图像信息,则清除 +// GenerateThumbnail generates thumb for given file, upload the thumb file back with given suffix func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { - // 判断是否可以生成缩略图 - if !IsInExtensionList(HandledExtension, file.Name) { - return - } - // 新建上下文 newCtx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -105,36 +97,50 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { getThumbWorker().addWorker() defer getThumbWorker().releaseWorker() - image, err := thumb.NewThumbFromFile(source, file.Name) - if err != nil { - util.Log().Warning("Cannot generate thumb because of failed to parse image %q: %s", file.SourceName, err) + r, w := io.Pipe() + defer w.Close() + + errChan := make(chan error, 1) + go func() { + errChan <- fs.Handler.Put(newCtx, &fsctx.FileStream{ + Mode: fsctx.Overwrite, + File: io.NopCloser(r), + Seeker: nil, + SavePath: file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"), + }) + }() + + if err = thumb.Generators.Generate(source, w, file.Name, model.GetSettingByNames( + "thumb_width", + "thumb_height", + "thumb_builtin_enabled", + "thumb_vips_enabled", + "thumb_ffmpeg_enabled", + "thumb_vips_path", + "thumb_ffmpeg_path", + )); err != nil { + util.Log().Warning("Failed to generate thumb for %s: %s", file.Name, err) + if errors.Is(err, thumb.ErrNotAvailable) { + // Mark this file as no thumb available + _ = updateThumbStatus(file, model.ThumbStatusNotAvailable) + } + return } - // 获取原始图像尺寸 - w, h := image.GetSize() + w.Close() + if err = <-errChan; err != nil { + util.Log().Warning("Failed to save thumb for %s: %s", file.Name, err) + return + } - // 生成缩略图 - image.GetThumb(fs.GenerateThumbnailSize(w, h)) - // 保存到文件 - err = image.Save(util.RelativePath(file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"))) - image = nil if model.IsTrueVal(model.GetSettingByName("thumb_gc_after_gen")) { util.Log().Debug("GenerateThumbnail runtime.GC") runtime.GC() } - if err != nil { - util.Log().Warning("Failed to save thumb: %s", err) - return - } - - // 更新文件的图像信息 - if file.Model.ID > 0 { - err = file.UpdatePicInfo(fmt.Sprintf("%d,%d", w, h)) - } else { - file.PicInfo = fmt.Sprintf("%d,%d", w, h) - } + // Mark this file as thumb available + err = updateThumbStatus(file, model.ThumbStatusExist) // 失败时删除缩略图文件 if err != nil { @@ -144,5 +150,17 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { // GenerateThumbnailSize 获取要生成的缩略图的尺寸 func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) { - return uint(model.GetIntSetting("thumb_width", 400)), uint(model.GetIntSetting("thumb_width", 300)) + return uint(model.GetIntSetting("thumb_width", 400)), uint(model.GetIntSetting("thumb_height", 300)) +} + +func updateThumbStatus(file *model.File, status string) error { + if file.Model.ID > 0 { + return file.UpdateMetadata(map[string]string{ + model.ThumbStatusMetadataKey: status, + }) + } else { + file.MetadataSerialized[model.ThumbStatusMetadataKey] = status + } + + return nil } diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go index 670f4cd..acf739c 100644 --- a/pkg/filesystem/manage.go +++ b/pkg/filesystem/manage.go @@ -341,7 +341,6 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo ID: hashid.HashID(subFolder.ID, hashid.FolderID), Name: subFolder.Name, Path: processedPath, - Pic: "", Size: 0, Type: "dir", Date: subFolder.UpdatedAt, @@ -363,7 +362,7 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo ID: hashid.HashID(file.ID, hashid.FileID), Name: file.Name, Path: processedPath, - Pic: file.PicInfo, + Thumb: file.ShouldLoadThumb(), Size: file.Size, Type: "file", Date: file.UpdatedAt, diff --git a/pkg/filesystem/upload.go b/pkg/filesystem/upload.go index 9073ab2..08dde53 100644 --- a/pkg/filesystem/upload.go +++ b/pkg/filesystem/upload.go @@ -210,7 +210,6 @@ func (fs *FileSystem) UploadFromStream(ctx context.Context, file *fsctx.FileStre fs.Use("BeforeUpload", HookValidateCapacity) fs.Use("AfterUploadCanceled", HookDeleteTempFile) fs.Use("AfterUpload", GenericAfterUpload) - fs.Use("AfterUpload", HookGenerateThumb) fs.Use("AfterValidateFailed", HookDeleteTempFile) } fs.Lock.Unlock() diff --git a/pkg/serializer/explorer.go b/pkg/serializer/explorer.go index be49edd..da3dc32 100644 --- a/pkg/serializer/explorer.go +++ b/pkg/serializer/explorer.go @@ -36,7 +36,7 @@ type Object struct { ID string `json:"id"` Name string `json:"name"` Path string `json:"path"` - Pic string `json:"pic"` + Thumb bool `json:"thumb"` Size uint64 `json:"size"` Type string `json:"type"` Date time.Time `json:"date"` diff --git a/pkg/thumb/image.go b/pkg/thumb/builtin.go similarity index 78% rename from pkg/thumb/image.go rename to pkg/thumb/builtin.go index cf851c3..5fe412d 100644 --- a/pkg/thumb/image.go +++ b/pkg/thumb/builtin.go @@ -1,7 +1,6 @@ package thumb import ( - "errors" "fmt" "image" "image/gif" @@ -18,6 +17,10 @@ import ( "golang.org/x/image/draw" ) +func init() { + RegisterGenerator(&Builtin{}) +} + // Thumb 缩略图 type Thumb struct { src image.Image @@ -30,7 +33,7 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) { ext := strings.ToLower(filepath.Ext(name)) // 无扩展名时 if len(ext) == 0 { - return nil, errors.New("未知的图像类型") + return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough) } var err error @@ -45,7 +48,7 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) { case "png": img, err = png.Decode(file) default: - return nil, errors.New("unknown image format") + return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough) } if err != nil { return nil, err @@ -70,18 +73,12 @@ func (image *Thumb) GetSize() (int, int) { } // Save 保存图像到给定路径 -func (image *Thumb) Save(path string) (err error) { - out, err := util.CreatNestedFile(path) - - if err != nil { - return err - } - defer out.Close() +func (image *Thumb) Save(w io.Writer) (err error) { switch model.GetSettingByNameWithDefault("thumb_encode_method", "jpg") { case "png": - err = png.Encode(out, image.src) + err = png.Encode(w, image.src) default: - err = jpeg.Encode(out, image.src, &jpeg.Options{Quality: model.GetIntSetting("thumb_encode_quality", 85)}) + err = jpeg.Encode(w, image.src, &jpeg.Options{Quality: model.GetIntSetting("thumb_encode_quality", 85)}) } return err @@ -141,9 +138,15 @@ func (image *Thumb) CreateAvatar(uid uint) error { // 生成头像缩略图 src := image.src for k, size := range []int{s, m, l} { - //image.src = resize.Resize(uint(size), uint(size), src, resize.Lanczos3) + out, err := util.CreatNestedFile(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k))) + + if err != nil { + return err + } + defer out.Close() + image.src = Resize(uint(size), uint(size), src) - err := image.Save(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k))) + err = image.Save(out) if err != nil { return err } @@ -152,3 +155,23 @@ func (image *Thumb) CreateAvatar(uid uint) error { return nil } + +type Builtin struct{} + +func (b Builtin) Generate(file io.Reader, w io.Writer, name string, options map[string]string) error { + img, err := NewThumbFromFile(file, name) + if err != nil { + return err + } + + img.GetThumb(thumbSize(options)) + return img.Save(w) +} + +func (b Builtin) Priority() int { + return 300 +} + +func (b Builtin) EnableFlag() string { + return "thumb_builtin_enabled" +} diff --git a/pkg/thumb/image_test.go b/pkg/thumb/builtin_test.go similarity index 100% rename from pkg/thumb/image_test.go rename to pkg/thumb/builtin_test.go diff --git a/pkg/thumb/pipeline.go b/pkg/thumb/pipeline.go new file mode 100644 index 0000000..e38b466 --- /dev/null +++ b/pkg/thumb/pipeline.go @@ -0,0 +1,88 @@ +package thumb + +import ( + "errors" + "fmt" + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/util" + "io" + "sort" + "strconv" +) + +// Generator generates a thumbnail for a given reader. +type Generator interface { + Generate(file io.Reader, w io.Writer, name string, options map[string]string) error + + // Priority of execution order, smaller value means higher priority. + Priority() int + + // EnableFlag returns the setting name to enable this generator. + EnableFlag() string +} + +type ( + GeneratorType string + GeneratorList []Generator +) + +var ( + Generators = GeneratorList{} + + ErrPassThrough = errors.New("pass through") + ErrNotAvailable = fmt.Errorf("thumbnail not available: %w", ErrPassThrough) +) + +func (g GeneratorList) Len() int { + return len(g) +} + +func (g GeneratorList) Less(i, j int) bool { + return g[i].Priority() < g[j].Priority() +} + +func (g GeneratorList) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} + +// RegisterGenerator registers a thumbnail generator. +func RegisterGenerator(generator Generator) { + Generators = append(Generators, generator) + sort.Sort(Generators) +} + +func (p GeneratorList) Generate(file io.Reader, w io.Writer, name string, options map[string]string) error { + for _, generator := range p { + if model.IsTrueVal(options[generator.EnableFlag()]) { + err := generator.Generate(file, w, name, options) + if errors.Is(err, ErrPassThrough) { + util.Log().Debug("Failed to generate thumbnail for %s: %s, passing through to next generator.", name, err) + continue + } + + return err + } + } + return ErrNotAvailable +} + +func (p GeneratorList) Priority() int { + return 0 +} + +func (p GeneratorList) EnableFlag() string { + return "" +} + +func thumbSize(options map[string]string) (uint, uint) { + w, h := uint(400), uint(300) + if wParsed, err := strconv.Atoi(options["thumb_width"]); err == nil { + w = uint(wParsed) + } + + if hParsed, err := strconv.Atoi(options["thumb_height"]); err == nil { + h = uint(hParsed) + } + + return w, h +} diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index 8ee02cf..b081932 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -386,7 +386,6 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst fs.Use("AfterUploadCanceled", filesystem.HookDeleteTempFile) fs.Use("AfterUploadCanceled", filesystem.HookCancelContext) fs.Use("AfterUpload", filesystem.GenericAfterUpload) - fs.Use("AfterUpload", filesystem.HookGenerateThumb) fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile) } diff --git a/service/explorer/upload.go b/service/explorer/upload.go index 0cac8df..d14f328 100644 --- a/service/explorer/upload.go +++ b/service/explorer/upload.go @@ -196,7 +196,6 @@ func processChunkUpload(ctx context.Context, c *gin.Context, fs *filesystem.File fs.Use("AfterValidateFailed", filesystem.HookChunkUploadFailed) if isLastChunk { fs.Use("AfterUpload", filesystem.HookPopPlaceholderToFile("")) - fs.Use("AfterUpload", filesystem.HookGenerateThumb) fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key)) } } else {