chore: 初始化 Cloudreve 源码仓库

This commit is contained in:
2026-09-13 11:28:59 +08:00
commit 2a7b03ab1d
489 changed files with 163867 additions and 0 deletions
+360
View File
@@ -0,0 +1,360 @@
package util
import (
"context"
cryptoRand "crypto/rand"
"fmt"
"math/big"
"math/rand"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
var (
RandomVariantAll = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
RandomLowerCases = []rune("1234567890abcdefghijklmnopqrstuvwxyz")
)
// RandStringRunes 返回随机字符串
func RandStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = RandomVariantAll[rand.Intn(len(RandomVariantAll))]
}
return string(b)
}
func RandStringRunesCrypto(n int) string {
b := make([]rune, n)
for i := range b {
num, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(len(RandomVariantAll))))
if err != nil {
// fallback to math/rand on crypto failure
b[i] = RandomVariantAll[rand.Intn(len(RandomVariantAll))]
} else {
b[i] = RandomVariantAll[num.Int64()]
}
}
return string(b)
}
// RandString returns random string in given length and variant
func RandString(n int, variant []rune) string {
b := make([]rune, n)
for i := range b {
b[i] = variant[rand.Intn(len(variant))]
}
return string(b)
}
// ContainsUint 返回list中是否包含
func ContainsUint(s []uint, e uint) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// IsInExtensionList 返回文件的扩展名是否在给定的列表范围内
func IsInExtensionList(extList []string, fileName string) bool {
ext := Ext(fileName)
// 无扩展名时
if len(ext) == 0 {
return false
}
if ContainsString(extList, ext) {
return true
}
return false
}
// IsInExtensionList 返回文件的扩展名是否在给定的列表范围内
func IsInExtensionListExt(extList []string, ext string) bool {
// 无扩展名时
if len(ext) == 0 {
return false
}
if ContainsString(extList, ext) {
return true
}
return false
}
// ContainsString 返回list中是否包含
func ContainsString(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// Replace 根据替换表执行批量替换
func Replace(table map[string]string, s string) string {
for key, value := range table {
s = strings.Replace(s, key, value, -1)
}
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 求两个切片交集
func SliceIntersect(slice1, slice2 []string) []string {
m := make(map[string]int)
nn := make([]string, 0)
for _, v := range slice1 {
m[v]++
}
for _, v := range slice2 {
times, _ := m[v]
if times == 1 {
nn = append(nn, v)
}
}
return nn
}
// SliceDifference 求两个切片差集
func SliceDifference(slice1, slice2 []string) []string {
m := make(map[string]int)
nn := make([]string, 0)
inter := SliceIntersect(slice1, slice2)
for _, v := range inter {
m[v]++
}
for _, value := range slice1 {
times, _ := m[value]
if times == 0 {
nn = append(nn, value)
}
}
return nn
}
// WithValue inject key-value pair into request context.
func WithValue(c *gin.Context, key any, value any) {
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), key, value))
}
// BoolToString transform bool to string
func BoolToString(b bool) string {
if b {
return "1"
}
return "0"
}
func EncodeTimeFlowString(str string, timeNow int64) string {
timeNow = timeNow / 1000
timeDigits := []int{}
timeDigitIndex := 0
if len(str) == 0 {
return ""
}
str = fmt.Sprintf("%d|%s", timeNow, str)
res := make([]int32, 0, utf8.RuneCountInString(str))
for timeNow > 0 {
timeDigits = append(timeDigits, int(timeNow%int64(10)))
timeNow = timeNow / 10
}
add := false
for pos, rune := range str {
// take single digit with index timeDigitIndex from timeNow
newIndex := pos
if add {
newIndex = pos + timeDigits[timeDigitIndex]*timeDigitIndex
} else {
newIndex = 2*timeDigitIndex*timeDigits[timeDigitIndex] - pos
}
if newIndex < 0 {
newIndex = newIndex * -1
}
res = append(res, rune)
newIndex = newIndex % len(res)
res[newIndex], res[len(res)-1] = res[len(res)-1], res[newIndex]
add = !add
// Add timeDigitIndex by 1, but does not exceed total digits in timeNow
timeDigitIndex++
timeDigitIndex = timeDigitIndex % len(timeDigits)
}
return string(res)
}
func DecodeTimeFlowStringTime(str string, timeNow int64) string {
timeNow = timeNow / 1000
timeDigits := []int{}
if len(str) == 0 {
return ""
}
for timeNow > 0 {
timeDigits = append(timeDigits, int(timeNow%int64(10)))
timeNow = timeNow / 10
}
res := make([]int32, utf8.RuneCountInString(str))
secret := []rune(str)
add := false
if len(secret)%2 == 0 {
add = true
}
timeDigitIndex := (len(secret) - 1) % len(timeDigits)
for pos := range secret {
// take single digit with index timeDigitIndex from timeNow
newIndex := len(res) - 1 - pos
if add {
newIndex = newIndex + timeDigits[timeDigitIndex]*timeDigitIndex
} else {
newIndex = 2*timeDigitIndex*timeDigits[timeDigitIndex] - newIndex
}
if newIndex < 0 {
newIndex = newIndex * -1
}
newIndex = newIndex % len(secret)
res[len(res)-1-pos] = secret[newIndex]
secret[newIndex], secret[len(res)-1-pos] = secret[len(res)-1-pos], secret[newIndex]
secret = secret[:len(secret)-1]
add = !add
// Add timeDigitIndex by 1, but does not exceed total digits in timeNow
timeDigitIndex--
if timeDigitIndex < 0 {
timeDigitIndex = len(timeDigits) - 1
}
}
return string(res)
}
func ToPtr[T any](v T) *T {
return &v
}
+98
View File
@@ -0,0 +1,98 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestRandStringRunes(t *testing.T) {
asserts := assert.New(t)
// 0 长度字符
randStr := RandStringRunes(0)
asserts.Len(randStr, 0)
// 16 长度字符
randStr = RandStringRunes(16)
asserts.Len(randStr, 16)
// 32 长度字符
randStr = RandStringRunes(32)
asserts.Len(randStr, 32)
//相同长度字符
sameLenStr1 := RandStringRunes(32)
sameLenStr2 := RandStringRunes(32)
asserts.NotEqual(sameLenStr1, sameLenStr2)
}
func TestContainsUint(t *testing.T) {
asserts := assert.New(t)
asserts.True(ContainsUint([]uint{0, 2, 3, 65, 4}, 65))
asserts.True(ContainsUint([]uint{65}, 65))
asserts.False(ContainsUint([]uint{65}, 6))
}
func TestContainsString(t *testing.T) {
asserts := assert.New(t)
asserts.True(ContainsString([]string{"", "1"}, ""))
asserts.True(ContainsString([]string{"", "1"}, "1"))
asserts.False(ContainsString([]string{"", "1"}, " "))
}
func TestReplace(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("origin", Replace(map[string]string{
"123": "321",
}, "origin"))
asserts.Equal("321origin321", Replace(map[string]string{
"123": "321",
}, "123origin123"))
asserts.Equal("321new321", Replace(map[string]string{
"123": "321",
"origin": "new",
}, "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) {
asserts := assert.New(t)
{
s1 := []string{"1", "2", "3", "4"}
s2 := []string{"2", "4"}
asserts.Equal([]string{"1", "3"}, SliceDifference(s1, s2))
}
{
s2 := []string{"1", "2", "3", "4"}
s1 := []string{"2", "4"}
asserts.Equal([]string{}, SliceDifference(s1, s2))
}
{
s1 := []string{"1", "2", "3", "4"}
s2 := []string{"1", "2", "3", "4"}
asserts.Equal([]string{}, SliceDifference(s1, s2))
}
{
s1 := []string{"1", "2", "3", "4"}
s2 := []string{}
asserts.Equal([]string{"1", "2", "3", "4"}, SliceDifference(s1, s2))
}
}
+76
View File
@@ -0,0 +1,76 @@
package util
import (
"io"
"os"
"path/filepath"
)
// Exists reports whether the named file or directory exists.
func Exists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// CreatNestedFile 给定path创建文件,如果目录不存在就递归创建
func CreatNestedFile(path string) (*os.File, error) {
basePath := filepath.Dir(path)
if !Exists(basePath) {
err := os.MkdirAll(basePath, 0700)
if err != nil {
return nil, err
}
}
return os.Create(path)
}
// CreatNestedFolder creates a folder with the given path, if the directory does not exist,
// it will be created recursively.
func CreatNestedFolder(path string) error {
if !Exists(path) {
err := os.MkdirAll(path, 0700)
if err != nil {
return err
}
}
return nil
}
// IsEmpty 返回给定目录是否为空目录
func IsEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
}
type CallbackReader struct {
reader io.Reader
callback func(int64)
}
func NewCallbackReader(reader io.Reader, callback func(int64)) *CallbackReader {
return &CallbackReader{
reader: reader,
callback: callback,
}
}
func (r *CallbackReader) Read(p []byte) (n int, err error) {
n, err = r.reader.Read(p)
r.callback(int64(n))
return
}
+39
View File
@@ -0,0 +1,39 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestExists(t *testing.T) {
asserts := assert.New(t)
asserts.True(Exists("io_test.go"))
asserts.False(Exists("io_test.js"))
}
func TestCreatNestedFile(t *testing.T) {
asserts := assert.New(t)
// 父目录不存在
{
file, err := CreatNestedFile("test/nest.txt")
asserts.NoError(err)
asserts.NoError(file.Close())
asserts.FileExists("test/nest.txt")
}
// 父目录存在
{
file, err := CreatNestedFile("test/direct.txt")
asserts.NoError(err)
asserts.NoError(file.Close())
asserts.FileExists("test/direct.txt")
}
}
func TestIsEmpty(t *testing.T) {
asserts := assert.New(t)
asserts.False(IsEmpty(""))
asserts.False(IsEmpty("not_exist"))
}
+150
View File
@@ -0,0 +1,150 @@
package util
import (
"fmt"
"github.com/fatih/color"
"sync"
"time"
)
const (
// LevelError 错误
LevelError = iota
// LevelWarning 警告
LevelWarning
// LevelInformational 提示
LevelInformational
// LevelDebug 除错
LevelDebug
)
var GloablLogger *Logger
var Level = LevelDebug
// Logger 日志
type Logger struct {
level int
mu sync.Mutex
}
// 日志颜色
var colors = map[string]func(a ...interface{}) string{
"Warning": color.New(color.FgYellow).Add(color.Bold).SprintFunc(),
"Panic": color.New(color.BgRed).Add(color.Bold).SprintFunc(),
"Error": color.New(color.FgRed).Add(color.Bold).SprintFunc(),
"Info": color.New(color.FgCyan).Add(color.Bold).SprintFunc(),
"Debug": color.New(color.FgWhite).Add(color.Bold).SprintFunc(),
}
// 不同级别前缀与时间的间隔,保持宽度一致
var spaces = map[string]string{
"Warning": "",
"Panic": " ",
"Error": " ",
"Info": " ",
"Debug": " ",
}
// Println 打印
func (ll *Logger) Println(prefix string, msg string) {
// TODO Release时去掉
// color.NoColor = false
c := color.New()
ll.mu.Lock()
defer ll.mu.Unlock()
_, _ = c.Printf(
"%s%s %s %s\n",
colors[prefix]("["+prefix+"]"),
spaces[prefix],
time.Now().Format("2006-01-02 15:04:05"),
msg,
)
}
// Panic 极端错误
func (ll *Logger) Panic(format string, v ...interface{}) {
if LevelError > ll.level {
return
}
msg := fmt.Sprintf(format, v...)
ll.Println("Panic", msg)
panic(msg)
}
// Error 错误
func (ll *Logger) Error(format string, v ...interface{}) {
if LevelError > ll.level {
return
}
msg := fmt.Sprintf(format, v...)
ll.Println("Error", msg)
}
// Warning 警告
func (ll *Logger) Warning(format string, v ...interface{}) {
if LevelWarning > ll.level {
return
}
msg := fmt.Sprintf(format, v...)
ll.Println("Warning", msg)
}
// Info 信息
func (ll *Logger) Info(format string, v ...interface{}) {
if LevelInformational > ll.level {
return
}
msg := fmt.Sprintf(format, v...)
ll.Println("Info", msg)
}
// Debug 校验
func (ll *Logger) Debug(format string, v ...interface{}) {
if LevelDebug > ll.level {
return
}
msg := fmt.Sprintf(format, v...)
ll.Println("Debug", msg)
}
// Print GORM 的 Logger实现
//func (ll *Logger) Print(v ...interface{}) {
// if LevelDebug > ll.level {
// return
// }
// msg := fmt.Sprintf("[SQL] %s", v...)
// ll.println(msg)
//}
// BuildLogger 构建logger
func BuildLogger(level string) {
intLevel := LevelError
switch level {
case "error":
intLevel = LevelError
case "warning":
intLevel = LevelWarning
case "info":
intLevel = LevelInformational
case "debug":
intLevel = LevelDebug
}
l := Logger{
level: intLevel,
}
GloablLogger = &l
}
// Log 返回日志对象
func Log() *Logger {
if GloablLogger == nil {
l := Logger{
level: Level,
}
GloablLogger = &l
}
return GloablLogger
}
+103
View File
@@ -0,0 +1,103 @@
// +build !race
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestBuildLogger(t *testing.T) {
asserts := assert.New(t)
asserts.NotPanics(func() {
BuildLogger("error")
})
asserts.NotPanics(func() {
BuildLogger("warning")
})
asserts.NotPanics(func() {
BuildLogger("info")
})
asserts.NotPanics(func() {
BuildLogger("?")
})
asserts.NotPanics(func() {
BuildLogger("debug")
})
}
func TestLog(t *testing.T) {
asserts := assert.New(t)
asserts.NotNil(Log())
GloablLogger = nil
asserts.NotNil(Log())
}
func TestLogger_Debug(t *testing.T) {
asserts := assert.New(t)
l := Logger{
level: LevelDebug,
}
asserts.NotPanics(func() {
l.Debug("123")
})
l.level = LevelError
asserts.NotPanics(func() {
l.Debug("123")
})
}
func TestLogger_Info(t *testing.T) {
asserts := assert.New(t)
l := Logger{
level: LevelDebug,
}
asserts.NotPanics(func() {
l.Info("123")
})
l.level = LevelError
asserts.NotPanics(func() {
l.Info("123")
})
}
func TestLogger_Warning(t *testing.T) {
asserts := assert.New(t)
l := Logger{
level: LevelDebug,
}
asserts.NotPanics(func() {
l.Warning("123")
})
l.level = LevelError
asserts.NotPanics(func() {
l.Warning("123")
})
}
func TestLogger_Error(t *testing.T) {
asserts := assert.New(t)
l := Logger{
level: LevelDebug,
}
asserts.NotPanics(func() {
l.Error("123")
})
l.level = -1
asserts.NotPanics(func() {
l.Error("123")
})
}
func TestLogger_Panic(t *testing.T) {
asserts := assert.New(t)
l := Logger{
level: LevelDebug,
}
asserts.Panics(func() {
l.Panic("123")
})
l.level = -1
asserts.NotPanics(func() {
l.Error("123")
})
}
+108
View File
@@ -0,0 +1,108 @@
package util
import (
"context"
"os"
"path"
"path/filepath"
"strings"
)
const (
DataFolder = "data"
)
var UseWorkingDir = false
// DotPathToStandardPath 将","分割的路径转换为标准路径
func DotPathToStandardPath(path string) string {
return "/" + strings.Replace(path, ",", "/", -1)
}
// FillSlash 给路径补全`/`
func FillSlash(path string) string {
if path == "/" {
return path
}
return path + "/"
}
// RemoveSlash 移除路径最后的`/`
func RemoveSlash(path string) string {
if len(path) > 1 {
return strings.TrimSuffix(path, "/")
}
return path
}
// SplitPath 分割路径为列表
func SplitPath(path string) []string {
if len(path) == 0 || path[0] != '/' {
return []string{}
}
if path == "/" {
return []string{"/"}
}
pathSplit := strings.Split(path, "/")
pathSplit[0] = "/"
return pathSplit
}
// FormSlash 将path中的反斜杠'\'替换为'/'
func FormSlash(old string) string {
return path.Clean(strings.ReplaceAll(old, "\\", "/"))
}
// RelativePath 获取相对可执行文件的路径
func RelativePath(name string) string {
if UseWorkingDir {
return name
}
if filepath.IsAbs(name) {
return name
}
e, _ := os.Executable()
return filepath.Join(filepath.Dir(e), name)
}
// DataPath relative path for store persist data file
func DataPath(child string) string {
dataPath := RelativePath(DataFolder)
if !Exists(dataPath) {
os.MkdirAll(dataPath, 0700)
}
if filepath.IsAbs(child) {
return child
}
return filepath.Join(dataPath, child)
}
// MkdirIfNotExist create directory if not exist
func MkdirIfNotExist(ctx context.Context, p string) {
if !Exists(p) {
os.MkdirAll(p, 0700)
}
}
// SlashClean is equivalent to but slightly more efficient than
// path.Clean("/" + name).
func SlashClean(name string) string {
if name == "" || name[0] != '/' {
name = "/" + name
}
return path.Clean(name)
}
// Ext returns the file name extension used by path, without the dot.
func Ext(name string) string {
ext := strings.ToLower(filepath.Ext(name))
if len(ext) > 0 {
ext = ext[1:]
}
return ext
}
+36
View File
@@ -0,0 +1,36 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestDotPathToStandardPath(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("/", DotPathToStandardPath(""))
asserts.Equal("/目录", DotPathToStandardPath("目录"))
asserts.Equal("/目录/目录2", DotPathToStandardPath("目录,目录2"))
}
func TestFillSlash(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("/", FillSlash("/"))
asserts.Equal("/", FillSlash(""))
asserts.Equal("/123/", FillSlash("/123"))
}
func TestRemoveSlash(t *testing.T) {
asserts := assert.New(t)
asserts.Equal("/", RemoveSlash("/"))
asserts.Equal("/123/1236", RemoveSlash("/123/1236"))
asserts.Equal("/123/1236", RemoveSlash("/123/1236/"))
}
func TestSplitPath(t *testing.T) {
asserts := assert.New(t)
asserts.Equal([]string{}, SplitPath(""))
asserts.Equal([]string{}, SplitPath("1"))
asserts.Equal([]string{"/"}, SplitPath("/"))
asserts.Equal([]string{"/", "123", "321"}, SplitPath("/123/321"))
}
+39
View File
@@ -0,0 +1,39 @@
package util
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// SetSession 设置session
func SetSession(c *gin.Context, list map[string]interface{}) {
s := sessions.Default(c)
for key, value := range list {
s.Set(key, value)
}
err := s.Save()
if err != nil {
Log().Warning("无法设置 Session 值:%s", err)
}
}
// GetSession 获取session
func GetSession(c *gin.Context, key any) interface{} {
s := sessions.Default(c)
return s.Get(key)
}
// DeleteSession 删除session
func DeleteSession(c *gin.Context, key any) {
s := sessions.Default(c)
s.Delete(key)
s.Save()
}
// ClearSession 清空session
func ClearSession(c *gin.Context) {
s := sessions.Default(c)
s.Clear()
s.Save()
}