mirror of https://github.com/cloudreve/Cloudreve
refactor(thumb): new thumb pipeline model to generate thumb on-demand
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…
Reference in New Issue