diff --git a/inventory/migration.go b/inventory/migration.go index 7ba8aea2..12fa1623 100644 --- a/inventory/migration.go +++ b/inventory/migration.go @@ -477,6 +477,23 @@ var patches = []Patch{ return fmt.Errorf("failed to update mail_reset_template setting: %w", err) } + return nil + }, + }, + { + Name: "apply_thumb_path_magic_var", + EndVersion: "4.10.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + thumbSuffixSetting, err := client.Setting.Query().Where(setting.Name("thumb_entity_suffix")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query thumb_entity_suffix setting: %w", err) + } + + newThumbSuffix := fmt.Sprintf("{blob_path}/{blob_name}%s", thumbSuffixSetting.Value) + if _, err := client.Setting.UpdateOne(thumbSuffixSetting).SetValue(newThumbSuffix).Save(ctx); err != nil { + return fmt.Errorf("failed to update thumb_entity_suffix setting: %w", err) + } + return nil }, }, diff --git a/inventory/setting.go b/inventory/setting.go index 8ba62d24..44f935de 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -555,7 +555,7 @@ var DefaultSettings = map[string]string{ "captcha_cap_asset_server": "jsdelivr", "thumb_width": "400", "thumb_height": "300", - "thumb_entity_suffix": "._thumb", + "thumb_entity_suffix": "{blob_path}/{blob_name}._thumb", "thumb_slave_sidecar_suffix": "._thumb_sidecar", "thumb_encode_method": "png", "thumb_gc_after_gen": "0", diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index 7f346f53..ce63ed4e 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -4,12 +4,8 @@ import ( "context" "errors" "fmt" - "math/rand" "path" "path/filepath" - "regexp" - "strconv" - "strings" "sync" "time" @@ -126,7 +122,7 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi parent, err := f.getFileByPath(ctx, navigator, path) if err != nil { - return nil, nil, fmt.Errorf("Parent not exist: %w", err) + return nil, nil, fmt.Errorf("parent not exist: %w", err) } pageSize := 0 @@ -787,71 +783,16 @@ func (f *DBFS) navigatorId(path *fs.URI) string { // generateSavePath generates the physical save path for the upload request. func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string { currentTime := time.Now() - originName := req.Props.Uri.Name() - - dynamicReplace := func(regPattern string, rule string, pathAvailable bool) string { - re := regexp.MustCompile(regPattern) - return re.ReplaceAllStringFunc(rule, func(match string) string { - switch match { - case "{timestamp}": - return strconv.FormatInt(currentTime.Unix(), 10) - case "{timestamp_nano}": - return strconv.FormatInt(currentTime.UnixNano(), 10) - case "{datetime}": - return currentTime.Format("20060102150405") - case "{date}": - return currentTime.Format("20060102") - case "{year}": - return currentTime.Format("2006") - case "{month}": - return currentTime.Format("01") - case "{day}": - return currentTime.Format("02") - case "{hour}": - return currentTime.Format("15") - case "{minute}": - return currentTime.Format("04") - case "{second}": - return currentTime.Format("05") - case "{uid}": - return strconv.Itoa(user.ID) - case "{randomkey16}": - return util.RandStringRunes(16) - case "{randomkey8}": - return util.RandStringRunes(8) - case "{randomnum8}": - return strconv.Itoa(rand.Intn(8)) - case "{randomnum4}": - return strconv.Itoa(rand.Intn(4)) - case "{randomnum3}": - return strconv.Itoa(rand.Intn(3)) - case "{randomnum2}": - return strconv.Itoa(rand.Intn(2)) - case "{uuid}": - return uuid.Must(uuid.NewV4()).String() - case "{path}": - if pathAvailable { - return req.Props.Uri.Dir() + fs.Separator - } - return match - case "{originname}": - return originName - case "{ext}": - return filepath.Ext(originName) - case "{originname_without_ext}": - return strings.TrimSuffix(originName, filepath.Ext(originName)) - default: - return match - } - }) + dynamicReplace := func(rule string, pathAvailable bool) string { + return util.ReplaceMagicVar(rule, fs.Separator, pathAvailable, false, currentTime, user.ID, req.Props.Uri.Name(), req.Props.Uri.Dir(), "") } dirRule := policy.DirNameRule dirRule = filepath.ToSlash(dirRule) - dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule, true) + dirRule = dynamicReplace(dirRule, true) nameRule := policy.FileNameRule - nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule, false) + nameRule = dynamicReplace(nameRule, false) return path.Join(path.Clean(dirRule), nameRule) } diff --git a/pkg/filemanager/fs/dbfs/upload.go b/pkg/filemanager/fs/dbfs/upload.go index 986d2a35..a50ae1d6 100644 --- a/pkg/filemanager/fs/dbfs/upload.go +++ b/pkg/filemanager/fs/dbfs/upload.go @@ -160,7 +160,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable { req.Props.SavePath = generateSavePath(policy, req, f.user) if isThumbnailAndPolicyNotAvailable { - req.Props.SavePath = req.Props.SavePath + f.settingClient.ThumbEntitySuffix(ctx) + req.Props.SavePath = path.Clean(util.ReplaceMagicVar(f.settingClient.ThumbEntitySuffix(ctx), fs.Separator, true, true, time.Now(), f.user.ID, req.Props.Uri.Name(), req.Props.Uri.Path(), req.Props.SavePath)) } } diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 3d71afcd..1b979382 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" - "github.com/cloudreve/Cloudreve/v4/pkg/thumb" "os" + "path" "runtime" "time" @@ -18,6 +18,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/queue" + "github.com/cloudreve/Cloudreve/v4/pkg/thumb" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/samber/lo" ) @@ -185,7 +186,7 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es Props: &fs.UploadProps{ Uri: uri, Size: fileInfo.Size(), - SavePath: es.Entity().Source() + m.settings.ThumbEntitySuffix(ctx), + SavePath: path.Clean(util.ReplaceMagicVar(m.settings.ThumbEntitySuffix(ctx), fs.Separator, true, true, time.Now(), m.user.ID, uri.Name(), uri.Path(), es.Entity().Source())), MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"), EntityType: &entityType, }, diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 0cf77f00..7c156638 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -510,7 +510,7 @@ func (s *settingProvider) ThumbEncode(ctx context.Context) *ThumbEncode { } func (s *settingProvider) ThumbEntitySuffix(ctx context.Context) string { - return s.getString(ctx, "thumb_entity_suffix", "._thumb") + return s.getString(ctx, "thumb_entity_suffix", "{blob_path}/{blob_name}._thumb") } func (s *settingProvider) ThumbSlaveSidecarSuffix(ctx context.Context) string { diff --git a/pkg/util/common.go b/pkg/util/common.go index a93472dd..db7035ed 100644 --- a/pkg/util/common.go +++ b/pkg/util/common.go @@ -3,12 +3,16 @@ package util import ( "context" "fmt" - "github.com/gin-gonic/gin" "math/rand" + "path/filepath" "regexp" + "strconv" "strings" "time" "unicode/utf8" + + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" ) func init() { @@ -95,6 +99,80 @@ func Replace(table map[string]string, s string) string { return s } +// ReplaceMagicVar 动态替换字符串中的魔法变量 +func ReplaceMagicVar(rawString string, fsSeparator string, pathAvailable bool, blobAvailable bool, + timeConst time.Time, userId int, originName string, originPath string, completeBlobPath string) string { + re := regexp.MustCompile(`\{[^{}]+\}`) + return re.ReplaceAllStringFunc(rawString, func(match string) string { + switch match { + case "{randomkey16}": + return RandStringRunes(16) + case "{randomkey8}": + return RandStringRunes(8) + case "{timestamp}": + return strconv.FormatInt(timeConst.Unix(), 10) + case "{timestamp_nano}": + return strconv.FormatInt(timeConst.UnixNano(), 10) + case "{randomnum2}": + return strconv.Itoa(rand.Intn(2)) + case "{randomnum3}": + return strconv.Itoa(rand.Intn(3)) + case "{randomnum4}": + return strconv.Itoa(rand.Intn(4)) + case "{randomnum8}": + return strconv.Itoa(rand.Intn(8)) + case "{uid}": + return strconv.Itoa(userId) + case "{datetime}": + return timeConst.Format("20060102150405") + case "{date}": + return timeConst.Format("20060102") + case "{year}": + return timeConst.Format("2006") + case "{month}": + return timeConst.Format("01") + case "{day}": + return timeConst.Format("02") + case "{hour}": + return timeConst.Format("15") + case "{minute}": + return timeConst.Format("04") + case "{second}": + return timeConst.Format("05") + case "{uuid}": + return uuid.Must(uuid.NewV4()).String() + case "{ext}": + return filepath.Ext(originName) + case "{originname}": + return originName + case "{originname_without_ext}": + return strings.TrimSuffix(originName, filepath.Ext(originName)) + case "{path}": + if pathAvailable { + return originPath + fsSeparator + } + return match + case "{blob_name}": + if blobAvailable { + return filepath.Base(completeBlobPath) + } + return match + case "{blob_name_without_ext}": + if blobAvailable { + return strings.TrimSuffix(filepath.Base(completeBlobPath), filepath.Ext(completeBlobPath)) + } + return match + case "{blob_path}": + if blobAvailable { + return filepath.Dir(completeBlobPath) + fsSeparator + } + return match + default: + return match + } + }) +} + // BuildRegexp 构建用于SQL查询用的多条件正则 func BuildRegexp(search []string, prefix, suffix, condition string) string { var res string