refactor(thumb): new thumb pipeline model to generate thumb on-demand

pull/1690/head
Aaron Liu 1 year ago
parent da1eaf2d1f
commit f36e39991d

@ -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"},

@ -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"
}
}

@ -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{},
)

@ -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 - 站点本身地址,

@ -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
}

@ -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 {

@ -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,
)

@ -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

@ -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))

@ -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("未实现")
}

@ -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
}

@ -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
}

@ -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,

@ -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)

@ -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
}

@ -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,

@ -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()

@ -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"`

@ -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"
}

@ -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
}

@ -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)
}

@ -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 {

Loading…
Cancel
Save