refactor(thumb): thumb logic for slave policy

pull/1690/head
Aaron Liu 1 year ago
parent ae118c337e
commit 7cb5e68b78

@ -48,6 +48,12 @@ func Init(path string, statics fs.FS) {
model.Init() model.Init()
}, },
}, },
{
"slave",
func() {
model.InitSlaveDefaults()
},
},
{ {
"both", "both",
func() { func() {

@ -1,6 +1,7 @@
package model package model
import ( import (
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf" "github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
@ -125,3 +126,9 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{Name: "wopi_max_size", Value: "52428800", Type: "wopi"}, {Name: "wopi_max_size", Value: "52428800", Type: "wopi"},
{Name: "wopi_session_timeout", Value: "36000", Type: "wopi"}, {Name: "wopi_session_timeout", Value: "36000", Type: "wopi"},
} }
func InitSlaveDefaults() {
for _, setting := range defaultSettings {
cache.Set("setting_"+setting.Name, setting.Value, -1)
}
}

@ -464,10 +464,5 @@ func (file *File) GetPosition() string {
// `True` does not guarantee the load request will success in next step, but the client // `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. // should try to load and fallback to default placeholder in case error returned.
func (file *File) ShouldLoadThumb() bool { func (file *File) ShouldLoadThumb() bool {
switch file.GetPolicy().Type { return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable
case "local":
return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable
default:
return file.PicInfo != "" && file.PicInfo != " " && file.PicInfo != "null,null"
}
} }

@ -7,7 +7,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/cloudreve/Cloudreve/v3/pkg/cache" "github.com/cloudreve/Cloudreve/v3/pkg/cache"
@ -68,18 +67,8 @@ type PolicyOption struct {
// Set this to `true` to force the request to use path-style addressing, // Set this to `true` to force the request to use path-style addressing,
// i.e., `http://s3.amazonaws.com/BUCKET/KEY ` // i.e., `http://s3.amazonaws.com/BUCKET/KEY `
S3ForcePathStyle bool `json:"s3_path_style"` S3ForcePathStyle bool `json:"s3_path_style"`
} // File extensions that support thumbnail generation using native policy API.
ThumbExts []string `json:"thumb_exts,omitempty"`
// thumbSuffix 支持缩略图处理的文件扩展名
var thumbSuffix = map[string][]string{
"local": {},
"qiniu": {".psd", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"oss": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"cos": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"upyun": {".svg", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"s3": {},
"remote": {},
"onedrive": {"*"},
} }
func init() { func init() {
@ -192,17 +181,6 @@ func (policy *Policy) IsDirectlyPreview() bool {
return policy.Type == "local" return policy.Type == "local"
} }
// IsThumbExist 给定文件名,返回此存储策略下是否可能存在缩略图
func (policy *Policy) IsThumbExist(name string) bool {
if list, ok := thumbSuffix[policy.Type]; ok {
if len(list) == 1 && list[0] == "*" {
return true
}
return util.ContainsString(list, strings.ToLower(filepath.Ext(name)))
}
return false
}
// IsTransitUpload 返回此策略上传给定size文件时是否需要服务端中转 // IsTransitUpload 返回此策略上传给定size文件时是否需要服务端中转
func (policy *Policy) IsTransitUpload(size uint64) bool { func (policy *Policy) IsTransitUpload(size uint64) bool {
return policy.Type == "local" return policy.Type == "local"

@ -11,7 +11,8 @@ import (
) )
var ( var (
ErrorThumbNotExist = fmt.Errorf("thumb not exist") ErrorThumbNotExist = fmt.Errorf("thumb not exist")
ErrorThumbNotSupported = fmt.Errorf("thumb not supported")
) )
// Handler 存储策略适配器 // Handler 存储策略适配器
@ -28,6 +29,9 @@ type Handler interface {
// 获取缩略图可直接在ContentResponse中返回文件数据流也可指 // 获取缩略图可直接在ContentResponse中返回文件数据流也可指
// 定为重定向 // 定为重定向
// 如果缩略图不存在, 且需要 Cloudreve 代理生成并上传,应返回 ErrorThumbNotExist
// 成的缩略图文件存储规则与本机策略一致。
// 如果不支持此文件的缩略图,并且不希望后续继续请求此缩略图,应返回 ErrorThumbNotSupported
Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error)
// 获取外链/下载地址, // 获取外链/下载地址,

@ -12,6 +12,7 @@ import (
model "github.com/cloudreve/Cloudreve/v3/models" model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth" "github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache" "github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
@ -196,13 +197,18 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
// Thumb 获取文件缩略图 // Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
if file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist { // Quick check thumb existence on master.
if conf.SystemConfig.Mode == "master" && file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist {
// Tell invoker to generate a thumb // Tell invoker to generate a thumb
return nil, driver.ErrorThumbNotExist return nil, driver.ErrorThumbNotExist
} }
thumbFile, err := handler.Get(ctx, file.SourceName+model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")) thumbFile, err := handler.Get(ctx, file.SourceName+model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("thumb not exist: %w (%w)", err, driver.ErrorThumbNotExist)
}
return nil, err return nil, err
} }

@ -8,15 +8,18 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"path" "path"
"path/filepath"
"strings" "strings"
"time" "time"
model "github.com/cloudreve/Cloudreve/v3/models" model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth" "github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request" "github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer" "github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
) )
// Driver 远程存储策略适配器 // Driver 远程存储策略适配器
@ -205,8 +208,18 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er
// Thumb 获取文件缩略图 // Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extensions
supported := []string{"png", "jpg", "jpeg", "gif"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) {
return nil, driver.ErrorThumbNotSupported
}
sourcePath := base64.RawURLEncoding.EncodeToString([]byte(file.SourceName)) sourcePath := base64.RawURLEncoding.EncodeToString([]byte(file.SourceName))
thumbURL := handler.getAPIUrl("thumb") + "/" + sourcePath thumbURL := fmt.Sprintf("%s/%s/%s", handler.getAPIUrl("thumb"), sourcePath, filepath.Ext(file.Name))
ttl := model.GetIntSetting("preview_timeout", 60) ttl := model.GetIntSetting("preview_timeout", 60)
signedThumbURL, err := auth.SignURI(handler.AuthInstance, thumbURL, int64(ttl)) signedThumbURL, err := auth.SignURI(handler.AuthInstance, thumbURL, int64(ttl))
if err != nil { if err != nil {

@ -64,10 +64,6 @@ func (fs *FileSystem) AddFile(ctx context.Context, parent *model.Folder, file fs
UploadSessionID: uploadInfo.UploadSessionID, UploadSessionID: uploadInfo.UploadSessionID,
} }
if fs.Policy.IsThumbExist(uploadInfo.FileName) {
newFile.PicInfo = "1,1"
}
err = newFile.Create() err = newFile.Create()
if err != nil { if err != nil {
@ -97,9 +93,10 @@ func (fs *FileSystem) GetPhysicalFileContent(ctx context.Context, path string) (
} }
// Preview 预览文件 // Preview 预览文件
// path - 文件虚拟路径 //
// isText - 是否为文本文件,文本文件会忽略重定向,直接由 // path - 文件虚拟路径
// 服务端拉取中转给用户,故会对文件大小进行限制 // isText - 是否为文本文件,文本文件会忽略重定向,直接由
// 服务端拉取中转给用户,故会对文件大小进行限制
func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*response.ContentResponse, error) { func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*response.ContentResponse, error) {
err := fs.resetFileIDIfNotExist(ctx, id) err := fs.resetFileIDIfNotExist(ctx, id)
if err != nil { if err != nil {

@ -177,23 +177,12 @@ func GenericAfterUpdate(ctx context.Context, fs *FileSystem, newFile fsctx.FileH
// SlaveAfterUpload Slave模式下上传完成钩子 // SlaveAfterUpload Slave模式下上传完成钩子
func SlaveAfterUpload(session *serializer.UploadSession) Hook { func SlaveAfterUpload(session *serializer.UploadSession) Hook {
return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error { return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileInfo := fileHeader.Info()
// 构造一个model.File用于生成缩略图
file := model.File{
Name: fileInfo.FileName,
SourceName: fileInfo.SavePath,
}
if session.Callback == "" { if session.Callback == "" {
return nil return nil
} }
// 发送回调请求 // 发送回调请求
callbackBody := serializer.UploadCallback{ callbackBody := serializer.UploadCallback{}
PicInfo: file.PicInfo,
}
return cluster.RemoteCallback(session.Callback, callbackBody) return cluster.RemoteCallback(session.Callback, callbackBody)
} }
} }
@ -268,10 +257,6 @@ func HookPopPlaceholderToFile(picInfo string) Hook {
return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error { return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileInfo := fileHeader.Info() fileInfo := fileHeader.Info()
fileModel := fileInfo.Model.(*model.File) fileModel := fileInfo.Model.(*model.File)
if picInfo == "" && fs.Policy.IsThumbExist(fileInfo.FileName) {
picInfo = "1,1"
}
return fileModel.PopChunkToFile(fileInfo.LastModified, picInfo) return fileModel.PopChunkToFile(fileInfo.LastModified, picInfo)
} }
} }

@ -40,6 +40,9 @@ func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentR
// Regenerate thumb if the thumb is not initialized yet // Regenerate thumb if the thumb is not initialized yet
fs.GenerateThumbnail(ctx, &fs.FileTarget[0]) fs.GenerateThumbnail(ctx, &fs.FileTarget[0])
res, err = fs.Handler.Thumb(ctx, &fs.FileTarget[0]) res, err = fs.Handler.Thumb(ctx, &fs.FileTarget[0])
} else if errors.Is(err, driver.ErrorThumbNotSupported) {
// Policy handler explicitly indicates thumb not available
_ = updateThumbStatus(&fs.FileTarget[0], model.ThumbStatusNotAvailable)
} }
if err == nil && conf.SystemConfig.Mode == "master" { if err == nil && conf.SystemConfig.Mode == "master" {
@ -157,6 +160,10 @@ func updateThumbStatus(file *model.File, status string) error {
model.ThumbStatusMetadataKey: status, model.ThumbStatusMetadataKey: status,
}) })
} else { } else {
if file.MetadataSerialized == nil {
file.MetadataSerialized = map[string]string{}
}
file.MetadataSerialized[model.ThumbStatusMetadataKey] = status file.MetadataSerialized[model.ThumbStatusMetadataKey] = status
} }

@ -2,7 +2,6 @@ package filesystem
import ( import (
"context" "context"
"path/filepath"
"strings" "strings"
"github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/cloudreve/Cloudreve/v3/pkg/util"
@ -63,20 +62,5 @@ func (fs *FileSystem) ValidateExtension(ctx context.Context, fileName string) bo
return true return true
} }
return IsInExtensionList(fs.Policy.OptionsSerialized.FileType, fileName) return util.IsInExtensionList(fs.Policy.OptionsSerialized.FileType, fileName)
}
// IsInExtensionList 返回文件的扩展名是否在给定的列表范围内
func IsInExtensionList(extList []string, fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
// 无扩展名时
if len(ext) == 0 {
return false
}
if util.ContainsString(extList, ext[1:]) {
return true
}
return false
} }

@ -39,9 +39,7 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) {
var err error var err error
var img image.Image var img image.Image
switch ext[1:] { switch ext[1:] {
case "jpg": case "jpg", "jpeg":
img, err = jpeg.Decode(file)
case "jpeg":
img, err = jpeg.Decode(file) img, err = jpeg.Decode(file)
case "gif": case "gif":
img, err = gif.Decode(file) img, err = gif.Decode(file)

@ -2,6 +2,7 @@ package util
import ( import (
"math/rand" "math/rand"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -32,6 +33,21 @@ func ContainsUint(s []uint, e uint) bool {
return false return false
} }
// IsInExtensionList 返回文件的扩展名是否在给定的列表范围内
func IsInExtensionList(extList []string, fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
// 无扩展名时
if len(ext) == 0 {
return false
}
if ContainsString(extList, ext[1:]) {
return true
}
return false
}
// ContainsString 返回list中是否包含 // ContainsString 返回list中是否包含
func ContainsString(s []string, e string) bool { func ContainsString(s []string, e string) bool {
for _, a := range s { for _, a := range s {

@ -64,7 +64,7 @@ func InitSlaveRouter() *gin.Engine {
// 预览 / 外链 // 预览 / 外链
v3.GET("source/:speed/:path/:name", controllers.SlavePreview) v3.GET("source/:speed/:path/:name", controllers.SlavePreview)
// 缩略图 // 缩略图
v3.GET("thumb/:path", controllers.SlaveThumb) v3.GET("thumb/:path/:ext", controllers.SlaveThumb)
// 删除文件 // 删除文件
v3.POST("delete", controllers.SlaveDelete) v3.POST("delete", controllers.SlaveDelete)
// 列出文件 // 列出文件

@ -32,6 +32,7 @@ type SlaveDownloadService struct {
// SlaveFileService 从机单文件文件相关服务 // SlaveFileService 从机单文件文件相关服务
type SlaveFileService struct { type SlaveFileService struct {
PathEncoded string `uri:"path" binding:"required"` PathEncoded string `uri:"path" binding:"required"`
Ext string `uri:"ext"`
} }
// SlaveFilesService 从机多文件相关服务 // SlaveFilesService 从机多文件相关服务
@ -132,7 +133,7 @@ func (service *SlaveFileService) Thumb(ctx context.Context, c *gin.Context) seri
if err != nil { if err != nil {
return serializer.Err(serializer.CodeFileNotFound, "", err) return serializer.Err(serializer.CodeFileNotFound, "", err)
} }
fs.FileTarget = []model.File{{SourceName: string(fileSource), PicInfo: "1,1"}} fs.FileTarget = []model.File{{SourceName: string(fileSource), Name: fmt.Sprintf("%s.%s", fileSource, service.Ext), PicInfo: "1,1"}}
// 获取缩略图 // 获取缩略图
resp, err := fs.GetThumb(ctx, 0) resp, err := fs.GetThumb(ctx, 0)

Loading…
Cancel
Save