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

This commit is contained in:
Aaron Liu
2023-04-07 19:08:54 +08:00
parent da1eaf2d1f
commit f36e39991d
23 changed files with 308 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

88
pkg/thumb/pipeline.go Normal file
View File

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

View File

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