refactor(fs): move ReplaceMagicVar to fs with named props; drop dead helpers

ReplaceMagicVar only had filemanager callers and took 9 positional
params (three same-typed strings, two adjacent bools). It now lives in
pkg/filemanager/fs taking a named MagicVarProps. BuildRegexp,
BuildConcat and the asymmetric EncodeTimeFlowString pair had no
production callers — deleted along with their tests.

Authored By: TDvorak <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 26fcb1b2eb
commit 053274afef

@ -22,7 +22,6 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/samber/lo" "github.com/samber/lo"
"math" "math"
@ -855,7 +854,14 @@ func (f *DBFS) navigatorId(path *fs.URI) string {
func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string { func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string {
currentTime := time.Now() currentTime := time.Now()
dynamicReplace := func(rule string, pathAvailable bool) string { 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(), "") return fs.ReplaceMagicVar(rule, fs.MagicVarProps{
FsSeparator: fs.Separator,
PathAvailable: pathAvailable,
Time: currentTime,
UserID: user.ID,
OriginName: req.Props.Uri.Name(),
OriginPath: req.Props.Uri.Dir(),
})
} }
dirRule := policy.DirNameRule dirRule := policy.DirNameRule

@ -178,7 +178,16 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ..
if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable { if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable {
req.Props.SavePath = generateSavePath(policy, req, f.user) req.Props.SavePath = generateSavePath(policy, req, f.user)
if isThumbnailAndPolicyNotAvailable { if isThumbnailAndPolicyNotAvailable {
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)) req.Props.SavePath = path.Clean(fs.ReplaceMagicVar(f.settingClient.ThumbEntitySuffix(ctx), fs.MagicVarProps{
FsSeparator: fs.Separator,
PathAvailable: true,
BlobAvailable: true,
Time: time.Now(),
UserID: f.user.ID,
OriginName: req.Props.Uri.Name(),
OriginPath: req.Props.Uri.Path(),
CompleteBlobPath: req.Props.SavePath,
}))
} }
} }

@ -0,0 +1,101 @@
package fs
import (
"math/rand"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gofrs/uuid"
)
// MagicVarProps carries the context available to magic-variable expansion in
// path templates.
type MagicVarProps struct {
FsSeparator string
PathAvailable bool
BlobAvailable bool
Time time.Time
UserID int
OriginName string
OriginPath string
CompleteBlobPath string
}
var magicVarRe = regexp.MustCompile(`\{[^{}]+\}`)
// ReplaceMagicVar expands {var} placeholders in rawString using p.
func ReplaceMagicVar(rawString string, p MagicVarProps) string {
return magicVarRe.ReplaceAllStringFunc(rawString, func(match string) string {
switch match {
case "{randomkey16}":
return util.RandStringRunes(16)
case "{randomkey8}":
return util.RandStringRunes(8)
case "{timestamp}":
return strconv.FormatInt(p.Time.Unix(), 10)
case "{timestamp_nano}":
return strconv.FormatInt(p.Time.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(p.UserID)
case "{datetime}":
return p.Time.Format("20060102150405")
case "{date}":
return p.Time.Format("20060102")
case "{year}":
return p.Time.Format("2006")
case "{month}":
return p.Time.Format("01")
case "{day}":
return p.Time.Format("02")
case "{hour}":
return p.Time.Format("15")
case "{minute}":
return p.Time.Format("04")
case "{second}":
return p.Time.Format("05")
case "{uuid}":
return uuid.Must(uuid.NewV4()).String()
case "{ext}":
return filepath.Ext(p.OriginName)
case "{originname}":
return p.OriginName
case "{originname_without_ext}":
return strings.TrimSuffix(p.OriginName, filepath.Ext(p.OriginName))
case "{path}":
if p.PathAvailable {
return p.OriginPath + p.FsSeparator
}
return match
case "{blob_name}":
if p.BlobAvailable {
return filepath.Base(p.CompleteBlobPath)
}
return match
case "{blob_name_without_ext}":
if p.BlobAvailable {
return strings.TrimSuffix(filepath.Base(p.CompleteBlobPath), filepath.Ext(p.CompleteBlobPath))
}
return match
case "{blob_path}":
if p.BlobAvailable {
return path.Dir(p.CompleteBlobPath) + p.FsSeparator
}
return match
default:
return match
}
})
}

@ -0,0 +1,48 @@
package fs
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestReplaceMagicVar(t *testing.T) {
a := assert.New(t)
now := time.Date(2024, 3, 5, 6, 7, 8, 0, time.UTC)
props := MagicVarProps{
FsSeparator: "/",
PathAvailable: true,
BlobAvailable: true,
Time: now,
UserID: 42,
OriginName: "photo.jpg",
OriginPath: "album/2024",
CompleteBlobPath: "blobs/abc123.bin",
}
a.Equal("u42", ReplaceMagicVar("u{uid}", props))
a.Equal("20240305", ReplaceMagicVar("{date}", props))
a.Equal("20240305060708", ReplaceMagicVar("{datetime}", props))
a.Equal("2024/03/05", ReplaceMagicVar("{year}/{month}/{day}", props))
a.Equal("photo.jpg", ReplaceMagicVar("{originname}", props))
a.Equal("photo", ReplaceMagicVar("{originname_without_ext}", props))
a.Equal(".jpg", ReplaceMagicVar("{ext}", props))
a.Equal("album/2024/", ReplaceMagicVar("{path}", props))
a.Equal("abc123.bin", ReplaceMagicVar("{blob_name}", props))
a.Equal("abc123", ReplaceMagicVar("{blob_name_without_ext}", props))
a.Equal("blobs/", ReplaceMagicVar("{blob_path}", props))
a.Equal("{unknown}", ReplaceMagicVar("{unknown}", props))
// Unavailable context leaves the placeholder untouched.
noPath := props
noPath.PathAvailable = false
a.Equal("{path}", ReplaceMagicVar("{path}", noPath))
noBlob := props
noBlob.BlobAvailable = false
a.Equal("{blob_name}", ReplaceMagicVar("{blob_name}", noBlob))
// Random vars produce non-empty output.
a.Len(ReplaceMagicVar("{randomkey8}", props), 8)
a.Len(ReplaceMagicVar("{uuid}", props), 36)
}

@ -184,9 +184,18 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es
entityType := types.EntityTypeThumbnail entityType := types.EntityTypeThumbnail
req := &fs.UploadRequest{ req := &fs.UploadRequest{
Props: &fs.UploadProps{ Props: &fs.UploadProps{
Uri: uri, Uri: uri,
Size: fileInfo.Size(), Size: fileInfo.Size(),
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())), SavePath: path.Clean(fs.ReplaceMagicVar(m.settings.ThumbEntitySuffix(ctx), fs.MagicVarProps{
FsSeparator: fs.Separator,
PathAvailable: true,
BlobAvailable: true,
Time: time.Now(),
UserID: m.user.ID,
OriginName: uri.Name(),
OriginPath: uri.Path(),
CompleteBlobPath: es.Entity().Source(),
})),
MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"), MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"),
EntityType: &entityType, EntityType: &entityType,
}, },

@ -5,15 +5,10 @@ import (
cryptoRand "crypto/rand" cryptoRand "crypto/rand"
"math/big" "math/big"
"math/rand" "math/rand"
"path"
"path/filepath"
"regexp"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
) )
func init() { func init() {
@ -114,102 +109,6 @@ func Replace(table map[string]string, s string) string {
return s 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 path.Dir(completeBlobPath) + fsSeparator
}
return match
default:
return match
}
})
}
// BuildRegexp 构建用于SQL查询用的多条件正则
func BuildRegexp(search []string, prefix, suffix, condition string) string {
var res string
for key, value := range search {
res += prefix + regexp.QuoteMeta(value) + suffix
if key < len(search)-1 {
res += condition
}
}
return res
}
// BuildConcat 根据数据库类型构建字符串连接表达式
func BuildConcat(str1, str2 string, DBType string) string {
switch DBType {
case "mysql":
return "CONCAT(" + str1 + "," + str2 + ")"
default:
return str1 + "||" + str2
}
}
// SliceIntersect 求两个切片交集 // SliceIntersect 求两个切片交集
func SliceIntersect(slice1, slice2 []string) []string { func SliceIntersect(slice1, slice2 []string) []string {
m := make(map[string]int) m := make(map[string]int)

@ -56,19 +56,6 @@ func TestReplace(t *testing.T) {
}, "123origin123")) }, "123origin123"))
} }
func TestBuildRegexp(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("^/dir/", BuildRegexp([]string{"/dir"}, "^", "/", "|"))
asserts.Equal("^/dir/|^/dir/di\\*r/", BuildRegexp([]string{"/dir", "/dir/di*r"}, "^", "/", "|"))
}
func TestBuildConcat(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("CONCAT(1,2)", BuildConcat("1", "2", "mysql"))
asserts.Equal("1||2", BuildConcat("1", "2", "sqlite"))
}
func TestSliceDifference(t *testing.T) { func TestSliceDifference(t *testing.T) {
asserts := assert.New(t) asserts := assert.New(t)

Loading…
Cancel
Save