feat(ent): migrate DB settings in patches

pull/2557/head
Aaron Liu 2025-06-22 10:31:33 +08:00
parent 8fe2889772
commit fec549f5ec
17 changed files with 762 additions and 377 deletions

View File

@ -3,7 +3,7 @@ package constants
// These values will be injected at build time, DO NOT EDIT.
// BackendVersion 当前后端版本号
var BackendVersion = "4.0.0-alpha.1"
var BackendVersion = "4.1.0"
// IsPro 是否为Pro版本
var IsPro = "false"

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.23.0
require (
entgo.io/ent v0.13.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/abslant/gzip v0.0.9
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go v1.31.5

2
go.sum
View File

@ -76,6 +76,8 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=

View File

@ -5,20 +5,12 @@ import (
rawsql "database/sql"
"database/sql/driver"
"fmt"
"os"
"time"
"entgo.io/ent/dialect/sql"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/group"
"github.com/cloudreve/Cloudreve/v4/ent/node"
_ "github.com/cloudreve/Cloudreve/v4/ent/runtime"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/inventory/debug"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
@ -153,302 +145,3 @@ func (d sqlite3Driver) Open(name string) (conn driver.Conn, err error) {
func init() {
rawsql.Register("sqlite3", sqlite3Driver{Driver: &sqlite.Driver{}})
}
// needMigration exams if required schema version is satisfied.
func needMigration(client *ent.Client, ctx context.Context, requiredDbVersion string) bool {
c, _ := client.Setting.Query().Where(setting.NameEQ(DBVersionPrefix + requiredDbVersion)).Count(ctx)
return c == 0
}
func migrate(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver, requiredDbVersion string) error {
l.Info("Start initializing database schema...")
l.Info("Creating basic table schema...")
if err := client.Schema.Create(ctx); err != nil {
return fmt.Errorf("Failed creating schema resources: %w", err)
}
migrateDefaultSettings(l, client, ctx, kv)
if err := migrateDefaultStoragePolicy(l, client, ctx); err != nil {
return fmt.Errorf("failed migrating default storage policy: %w", err)
}
if err := migrateSysGroups(l, client, ctx); err != nil {
return fmt.Errorf("failed migrating default storage policy: %w", err)
}
client.Setting.Create().SetName(DBVersionPrefix + requiredDbVersion).SetValue("installed").Save(ctx)
return nil
}
func migrateDefaultSettings(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver) {
// clean kv cache
if err := kv.DeleteAll(); err != nil {
l.Warning("Failed to remove all KV entries while schema migration: %s", err)
}
// List existing settings into a map
existingSettings := make(map[string]struct{})
settings, err := client.Setting.Query().All(ctx)
if err != nil {
l.Warning("Failed to query existing settings: %s", err)
}
for _, s := range settings {
existingSettings[s.Name] = struct{}{}
}
l.Info("Insert default settings...")
for k, v := range DefaultSettings {
if _, ok := existingSettings[k]; ok {
l.Debug("Skip inserting setting %s, already exists.", k)
continue
}
if override, ok := os.LookupEnv(EnvDefaultOverwritePrefix + k); ok {
l.Info("Override default setting %q with env value %q", k, override)
v = override
}
client.Setting.Create().SetName(k).SetValue(v).SaveX(ctx)
}
}
func migrateDefaultStoragePolicy(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.StoragePolicy.Query().Where(storagepolicy.ID(1)).First(ctx); err == nil {
l.Info("Default storage policy (ID=1) already exists, skip migrating.")
return nil
}
l.Info("Insert default storage policy...")
if _, err := client.StoragePolicy.Create().
SetName("Default storage policy").
SetType(types.PolicyTypeLocal).
SetDirNameRule(util.DataPath("uploads/{uid}/{path}")).
SetFileNameRule("{uid}_{randomkey8}_{originname}").
SetSettings(&types.PolicySetting{
ChunkSize: 25 << 20, // 25MB
PreAllocate: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default storage policy: %w", err)
}
return nil
}
func migrateSysGroups(l logging.Logger, client *ent.Client, ctx context.Context) error {
if err := migrateAdminGroup(l, client, ctx); err != nil {
return err
}
if err := migrateUserGroup(l, client, ctx); err != nil {
return err
}
if err := migrateAnonymousGroup(l, client, ctx); err != nil {
return err
}
if err := migrateMasterNode(l, client, ctx); err != nil {
return err
}
return nil
}
func migrateAdminGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(1)).First(ctx); err == nil {
l.Info("Default admin group (ID=1) already exists, skip migrating.")
return nil
}
l.Info("Insert default admin group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionIsAdmin: true,
types.GroupPermissionShare: true,
types.GroupPermissionWebDAV: true,
types.GroupPermissionWebDAVProxy: true,
types.GroupPermissionArchiveDownload: true,
types.GroupPermissionArchiveTask: true,
types.GroupPermissionShareDownload: true,
types.GroupPermissionRemoteDownload: true,
types.GroupPermissionRedirectedSource: true,
types.GroupPermissionAdvanceDelete: true,
types.GroupPermissionIgnoreFileOwnership: true,
// TODO: review default permission
}, permissions)
if _, err := client.Group.Create().
SetName("Admin").
SetStoragePoliciesID(1).
SetMaxStorage(1 * constants.TB). // 1 TB default storage
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
SourceBatchSize: 1000,
Aria2BatchSize: 50,
MaxWalkedFiles: 100000,
TrashRetention: 7 * 24 * 3600,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default admin group: %w", err)
}
return nil
}
func migrateUserGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(2)).First(ctx); err == nil {
l.Info("Default user group (ID=2) already exists, skip migrating.")
return nil
}
l.Info("Insert default user group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionShare: true,
types.GroupPermissionShareDownload: true,
types.GroupPermissionRedirectedSource: true,
}, permissions)
if _, err := client.Group.Create().
SetName("User").
SetStoragePoliciesID(1).
SetMaxStorage(1 * constants.GB). // 1 GB default storage
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
SourceBatchSize: 10,
Aria2BatchSize: 1,
MaxWalkedFiles: 100000,
TrashRetention: 7 * 24 * 3600,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default user group: %w", err)
}
return nil
}
func migrateAnonymousGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(AnonymousGroupID)).First(ctx); err == nil {
l.Info("Default anonymous group (ID=3) already exists, skip migrating.")
return nil
}
l.Info("Insert default anonymous group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionIsAnonymous: true,
types.GroupPermissionShareDownload: true,
}, permissions)
if _, err := client.Group.Create().
SetName("Anonymous").
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
MaxWalkedFiles: 100000,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default anonymous group: %w", err)
}
return nil
}
func migrateMasterNode(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Node.Query().Where(node.TypeEQ(node.TypeMaster)).First(ctx); err == nil {
l.Info("Default master node already exists, skip migrating.")
return nil
}
capabilities := &boolset.BooleanSet{}
boolset.Sets(map[types.NodeCapability]bool{
types.NodeCapabilityCreateArchive: true,
types.NodeCapabilityExtractArchive: true,
types.NodeCapabilityRemoteDownload: true,
}, capabilities)
stm := client.Node.Create().
SetType(node.TypeMaster).
SetCapabilities(capabilities).
SetName("Master").
SetSettings(&types.NodeSetting{
Provider: types.DownloaderProviderAria2,
}).
SetStatus(node.StatusActive)
_, enableAria2 := os.LookupEnv(EnvEnableAria2)
if enableAria2 {
l.Info("Aria2 is override as enabled.")
stm.SetSettings(&types.NodeSetting{
Provider: types.DownloaderProviderAria2,
Aria2Setting: &types.Aria2Setting{
Server: "http://127.0.0.1:6800/jsonrpc",
},
})
}
l.Info("Insert default master node...")
if _, err := stm.Save(ctx); err != nil {
return fmt.Errorf("failed to create default master node: %w", err)
}
return nil
}
func createMockData(client *ent.Client, ctx context.Context) {
//userCount := 100
//folderCount := 10000
//fileCount := 25000
//
//// create users
//pwdDigest, _ := digestPassword("52121225")
//userCreates := make([]*ent.UserCreate, userCount)
//for i := 0; i < userCount; i++ {
// nick := uuid.Must(uuid.NewV4()).String()
// userCreates[i] = client.User.Create().
// SetEmail(nick + "@cloudreve.org").
// SetNick(nick).
// SetPassword(pwdDigest).
// SetStatus(user.StatusActive).
// SetGroupID(1)
//}
//users, err := client.User.CreateBulk(userCreates...).Save(ctx)
//if err != nil {
// panic(err)
//}
//
//// Create root folder
//rootFolderCreates := make([]*ent.FileCreate, userCount)
//folderIds := make([][]int, 0, folderCount*userCount+userCount)
//for i, user := range users {
// rootFolderCreates[i] = client.File.Create().
// SetName(RootFolderName).
// SetOwnerID(user.ID).
// SetType(int(FileTypeFolder))
//}
//rootFolders, err := client.File.CreateBulk(rootFolderCreates...).Save(ctx)
//for _, rootFolders := range rootFolders {
// folderIds = append(folderIds, []int{rootFolders.ID, rootFolders.OwnerID})
//}
//if err != nil {
// panic(err)
//}
//
//// create random folder
//for i := 0; i < folderCount*userCount; i++ {
// parent := lo.Sample(folderIds)
// res := client.File.Create().
// SetName(uuid.Must(uuid.NewV4()).String()).
// SetType(int(FileTypeFolder)).
// SetOwnerID(parent[1]).
// SetFileChildren(parent[0]).
// SaveX(ctx)
// folderIds = append(folderIds, []int{res.ID, res.OwnerID})
//}
for i := 0; i < 255; i++ {
fmt.Printf("%d/", i)
}
}

416
inventory/migration.go Normal file
View File

@ -0,0 +1,416 @@
package inventory
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/group"
"github.com/cloudreve/Cloudreve/v4/ent/node"
"github.com/cloudreve/Cloudreve/v4/ent/setting"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/samber/lo"
)
// needMigration exams if required schema version is satisfied.
func needMigration(client *ent.Client, ctx context.Context, requiredDbVersion string) bool {
c, _ := client.Setting.Query().Where(setting.NameEQ(DBVersionPrefix + requiredDbVersion)).Count(ctx)
return c == 0
}
func migrate(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver, requiredDbVersion string) error {
l.Info("Start initializing database schema...")
l.Info("Creating basic table schema...")
if err := client.Schema.Create(ctx); err != nil {
return fmt.Errorf("Failed creating schema resources: %w", err)
}
migrateDefaultSettings(l, client, ctx, kv)
if err := migrateDefaultStoragePolicy(l, client, ctx); err != nil {
return fmt.Errorf("failed migrating default storage policy: %w", err)
}
if err := migrateSysGroups(l, client, ctx); err != nil {
return fmt.Errorf("failed migrating default storage policy: %w", err)
}
if err := applyPatches(l, client, ctx, requiredDbVersion); err != nil {
return fmt.Errorf("failed applying schema patches: %w", err)
}
client.Setting.Create().SetName(DBVersionPrefix + requiredDbVersion).SetValue("installed").Save(ctx)
return nil
}
func migrateDefaultSettings(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver) {
// clean kv cache
if err := kv.DeleteAll(); err != nil {
l.Warning("Failed to remove all KV entries while schema migration: %s", err)
}
// List existing settings into a map
existingSettings := make(map[string]struct{})
settings, err := client.Setting.Query().All(ctx)
if err != nil {
l.Warning("Failed to query existing settings: %s", err)
}
for _, s := range settings {
existingSettings[s.Name] = struct{}{}
}
l.Info("Insert default settings...")
for k, v := range DefaultSettings {
if _, ok := existingSettings[k]; ok {
l.Debug("Skip inserting setting %s, already exists.", k)
continue
}
if override, ok := os.LookupEnv(EnvDefaultOverwritePrefix + k); ok {
l.Info("Override default setting %q with env value %q", k, override)
v = override
}
client.Setting.Create().SetName(k).SetValue(v).SaveX(ctx)
}
}
func migrateDefaultStoragePolicy(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.StoragePolicy.Query().Where(storagepolicy.ID(1)).First(ctx); err == nil {
l.Info("Default storage policy (ID=1) already exists, skip migrating.")
return nil
}
l.Info("Insert default storage policy...")
if _, err := client.StoragePolicy.Create().
SetName("Default storage policy").
SetType(types.PolicyTypeLocal).
SetDirNameRule(util.DataPath("uploads/{uid}/{path}")).
SetFileNameRule("{uid}_{randomkey8}_{originname}").
SetSettings(&types.PolicySetting{
ChunkSize: 25 << 20, // 25MB
PreAllocate: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default storage policy: %w", err)
}
return nil
}
func migrateSysGroups(l logging.Logger, client *ent.Client, ctx context.Context) error {
if err := migrateAdminGroup(l, client, ctx); err != nil {
return err
}
if err := migrateUserGroup(l, client, ctx); err != nil {
return err
}
if err := migrateAnonymousGroup(l, client, ctx); err != nil {
return err
}
if err := migrateMasterNode(l, client, ctx); err != nil {
return err
}
return nil
}
func migrateAdminGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(1)).First(ctx); err == nil {
l.Info("Default admin group (ID=1) already exists, skip migrating.")
return nil
}
l.Info("Insert default admin group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionIsAdmin: true,
types.GroupPermissionShare: true,
types.GroupPermissionWebDAV: true,
types.GroupPermissionWebDAVProxy: true,
types.GroupPermissionArchiveDownload: true,
types.GroupPermissionArchiveTask: true,
types.GroupPermissionShareDownload: true,
types.GroupPermissionRemoteDownload: true,
types.GroupPermissionRedirectedSource: true,
types.GroupPermissionAdvanceDelete: true,
types.GroupPermissionIgnoreFileOwnership: true,
// TODO: review default permission
}, permissions)
if _, err := client.Group.Create().
SetName("Admin").
SetStoragePoliciesID(1).
SetMaxStorage(1 * constants.TB). // 1 TB default storage
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
SourceBatchSize: 1000,
Aria2BatchSize: 50,
MaxWalkedFiles: 100000,
TrashRetention: 7 * 24 * 3600,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default admin group: %w", err)
}
return nil
}
func migrateUserGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(2)).First(ctx); err == nil {
l.Info("Default user group (ID=2) already exists, skip migrating.")
return nil
}
l.Info("Insert default user group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionShare: true,
types.GroupPermissionShareDownload: true,
types.GroupPermissionRedirectedSource: true,
}, permissions)
if _, err := client.Group.Create().
SetName("User").
SetStoragePoliciesID(1).
SetMaxStorage(1 * constants.GB). // 1 GB default storage
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
SourceBatchSize: 10,
Aria2BatchSize: 1,
MaxWalkedFiles: 100000,
TrashRetention: 7 * 24 * 3600,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default user group: %w", err)
}
return nil
}
func migrateAnonymousGroup(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Group.Query().Where(group.ID(AnonymousGroupID)).First(ctx); err == nil {
l.Info("Default anonymous group (ID=3) already exists, skip migrating.")
return nil
}
l.Info("Insert default anonymous group...")
permissions := &boolset.BooleanSet{}
boolset.Sets(map[types.GroupPermission]bool{
types.GroupPermissionIsAnonymous: true,
types.GroupPermissionShareDownload: true,
}, permissions)
if _, err := client.Group.Create().
SetName("Anonymous").
SetPermissions(permissions).
SetSettings(&types.GroupSetting{
MaxWalkedFiles: 100000,
RedirectedSource: true,
}).
Save(ctx); err != nil {
return fmt.Errorf("failed to create default anonymous group: %w", err)
}
return nil
}
func migrateMasterNode(l logging.Logger, client *ent.Client, ctx context.Context) error {
if _, err := client.Node.Query().Where(node.TypeEQ(node.TypeMaster)).First(ctx); err == nil {
l.Info("Default master node already exists, skip migrating.")
return nil
}
capabilities := &boolset.BooleanSet{}
boolset.Sets(map[types.NodeCapability]bool{
types.NodeCapabilityCreateArchive: true,
types.NodeCapabilityExtractArchive: true,
types.NodeCapabilityRemoteDownload: true,
}, capabilities)
stm := client.Node.Create().
SetType(node.TypeMaster).
SetCapabilities(capabilities).
SetName("Master").
SetSettings(&types.NodeSetting{
Provider: types.DownloaderProviderAria2,
}).
SetStatus(node.StatusActive)
_, enableAria2 := os.LookupEnv(EnvEnableAria2)
if enableAria2 {
l.Info("Aria2 is override as enabled.")
stm.SetSettings(&types.NodeSetting{
Provider: types.DownloaderProviderAria2,
Aria2Setting: &types.Aria2Setting{
Server: "http://127.0.0.1:6800/jsonrpc",
},
})
}
l.Info("Insert default master node...")
if _, err := stm.Save(ctx); err != nil {
return fmt.Errorf("failed to create default master node: %w", err)
}
return nil
}
type (
PatchFunc func(l logging.Logger, client *ent.Client, ctx context.Context) error
Patch struct {
Name string
EndVersion string
Func PatchFunc
}
)
var patches = []Patch{
{
Name: "apply_default_excalidraw_viewer",
EndVersion: "4.1.0",
Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error {
// 1. Apply excalidraw file icons
// 1.1 Check if it's already applied
iconSetting, err := client.Setting.Query().Where(setting.Name("explorer_icons")).First(ctx)
if err != nil {
return fmt.Errorf("failed to query explorer_icons setting: %w", err)
}
var icons []types.FileTypeIconSetting
if err := json.Unmarshal([]byte(iconSetting.Value), &icons); err != nil {
return fmt.Errorf("failed to unmarshal explorer_icons setting: %w", err)
}
iconExisted := false
for _, icon := range icons {
if lo.Contains(icon.Exts, "excalidraw") {
iconExisted = true
break
}
}
// 1.2 If not existed, add it
if !iconExisted {
// Found existing excalidraw icon default setting
var defaultExcalidrawIcon types.FileTypeIconSetting
for _, icon := range defaultIcons {
if lo.Contains(icon.Exts, "excalidraw") {
defaultExcalidrawIcon = icon
break
}
}
icons = append(icons, defaultExcalidrawIcon)
newIconSetting, err := json.Marshal(icons)
if err != nil {
return fmt.Errorf("failed to marshal explorer_icons setting: %w", err)
}
if _, err := client.Setting.UpdateOne(iconSetting).SetValue(string(newIconSetting)).Save(ctx); err != nil {
return fmt.Errorf("failed to update explorer_icons setting: %w", err)
}
}
// 2. Apply default file viewers
// 2.1 Check if it's already applied
fileViewersSetting, err := client.Setting.Query().Where(setting.Name("file_viewers")).First(ctx)
if err != nil {
return fmt.Errorf("failed to query file_viewers setting: %w", err)
}
var fileViewers []types.ViewerGroup
if err := json.Unmarshal([]byte(fileViewersSetting.Value), &fileViewers); err != nil {
return fmt.Errorf("failed to unmarshal file_viewers setting: %w", err)
}
fileViewerExisted := false
for _, viewer := range fileViewers[0].Viewers {
if viewer.ID == "excalidraw" {
fileViewerExisted = true
break
}
}
// 2.2 If not existed, add it
if !fileViewerExisted {
// Found existing excalidraw viewer default setting
var defaultExcalidrawViewer types.Viewer
for _, viewer := range defaultFileViewers[0].Viewers {
if viewer.ID == "excalidraw" {
defaultExcalidrawViewer = viewer
break
}
}
fileViewers[0].Viewers = append(fileViewers[0].Viewers, defaultExcalidrawViewer)
newFileViewersSetting, err := json.Marshal(fileViewers)
if err != nil {
return fmt.Errorf("failed to marshal file_viewers setting: %w", err)
}
if _, err := client.Setting.UpdateOne(fileViewersSetting).SetValue(string(newFileViewersSetting)).Save(ctx); err != nil {
return fmt.Errorf("failed to update file_viewers setting: %w", err)
}
}
return nil
},
},
}
func applyPatches(l logging.Logger, client *ent.Client, ctx context.Context, requiredDbVersion string) error {
allVersionMarks, err := client.Setting.Query().Where(setting.NameHasPrefix(DBVersionPrefix)).All(ctx)
if err != nil {
return err
}
requiredDbVersion = strings.TrimSuffix(requiredDbVersion, "-pro")
// Find the latest applied version
var latestAppliedVersion *semver.Version
for _, v := range allVersionMarks {
v.Name = strings.TrimSuffix(v.Name, "-pro")
version, err := semver.NewVersion(strings.TrimPrefix(v.Name, DBVersionPrefix))
if err != nil {
l.Warning("Failed to parse past version %s: %s", v.Name, err)
continue
}
if latestAppliedVersion == nil || version.Compare(latestAppliedVersion) > 0 {
latestAppliedVersion = version
}
}
requiredVersion, err := semver.NewVersion(requiredDbVersion)
if err != nil {
return fmt.Errorf("failed to parse required version %s: %w", requiredDbVersion, err)
}
if latestAppliedVersion == nil || requiredVersion.Compare(requiredVersion) > 0 {
latestAppliedVersion = requiredVersion
}
for _, patch := range patches {
if latestAppliedVersion.Compare(semver.MustParse(patch.EndVersion)) < 0 {
l.Info("Applying schema patch %s...", patch.Name)
if err := patch.Func(l, client, ctx); err != nil {
return err
}
}
}
return nil
}

File diff suppressed because one or more lines are too long

View File

@ -178,6 +178,14 @@ type (
// Whether to share view setting from owner
ShareView bool `json:"share_view,omitempty"`
}
FileTypeIconSetting struct {
Exts []string `json:"exts"`
Icon string `json:"icon,omitempty"`
Color string `json:"color,omitempty"`
ColorDark string `json:"color_dark,omitempty"`
Img string `json:"img,omitempty"`
}
)
const (
@ -250,3 +258,40 @@ const (
DownloaderProviderAria2 = DownloaderProvider("aria2")
DownloaderProviderQBittorrent = DownloaderProvider("qbittorrent")
)
type (
ViewerAction string
ViewerType string
)
const (
ViewerActionView = "view"
ViewerActionEdit = "edit"
ViewerTypeBuiltin = "builtin"
ViewerTypeWopi = "wopi"
ViewerTypeCustom = "custom"
)
type Viewer struct {
ID string `json:"id"`
Type ViewerType `json:"type"`
DisplayName string `json:"display_name"`
Exts []string `json:"exts"`
Url string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"`
Props map[string]string `json:"props,omitempty"`
MaxSize int64 `json:"max_size,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Templates []NewFileTemplate `json:"templates,omitempty"`
}
type ViewerGroup struct {
Viewers []Viewer `json:"viewers"`
}
type NewFileTemplate struct {
Ext string `json:"ext"`
DisplayName string `json:"display_name"`
}

View File

@ -2,9 +2,9 @@ package middleware
import (
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
"github.com/gin-gonic/gin"
@ -67,7 +67,7 @@ func ViewerSessionValidation() gin.HandlerFunc {
// Check if the viewer is still available
viewers := settings.FileViewers(c)
var v *setting.Viewer
var v *types.Viewer
for _, group := range viewers {
for _, viewer := range group.Viewers {
if viewer.ID == session.ViewerID && !viewer.Disabled {

View File

@ -54,7 +54,7 @@ type (
// UpsertMedata update or insert metadata of given file
PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error
// CreateViewerSession creates a viewer session for given file
CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *setting.Viewer) (*ViewerSession, error)
CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *types.Viewer) (*ViewerSession, error)
// TraverseFile traverses a file to its root file, return the file with linked root.
TraverseFile(ctx context.Context, fileID int) (fs.File, error)
}

View File

@ -9,7 +9,6 @@ import (
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gofrs/uuid"
)
@ -44,7 +43,7 @@ func init() {
gob.Register(ViewerSessionCache{})
}
func (m *manager) CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *setting.Viewer) (*ViewerSession, error) {
func (m *manager) CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *types.Viewer) (*ViewerSession, error) {
file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithNotRoot())
if err != nil {
return nil, err
@ -88,6 +87,6 @@ func ViewerSessionFromContext(ctx context.Context) *ViewerSessionCache {
return ctx.Value(ViewerSessionCacheCtx{}).(*ViewerSessionCache)
}
func ViewerFromContext(ctx context.Context) *setting.Viewer {
return ctx.Value(ViewerCtx{}).(*setting.Viewer)
func ViewerFromContext(ctx context.Context) *types.Viewer {
return ctx.Value(ViewerCtx{}).(*types.Viewer)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"net/url"
"strconv"
"strings"
@ -169,7 +170,7 @@ type (
// FolderPropsCacheTTL returns the cache TTL of folder summary.
FolderPropsCacheTTL(ctx context.Context) int
// FileViewers returns the file viewers settings.
FileViewers(ctx context.Context) []ViewerGroup
FileViewers(ctx context.Context) []types.ViewerGroup
// ViewerSessionTTL returns the TTL of viewer session.
ViewerSessionTTL(ctx context.Context) int
// MimeMapping returns the extension to MIME mapping settings.
@ -232,11 +233,11 @@ func (s *settingProvider) Avatar(ctx context.Context) *Avatar {
}
}
func (s *settingProvider) FileViewers(ctx context.Context) []ViewerGroup {
func (s *settingProvider) FileViewers(ctx context.Context) []types.ViewerGroup {
raw := s.getString(ctx, "file_viewers", "[]")
var viewers []ViewerGroup
var viewers []types.ViewerGroup
if err := json.Unmarshal([]byte(raw), &viewers); err != nil {
return []ViewerGroup{}
return []types.ViewerGroup{}
}
return viewers

View File

@ -176,42 +176,6 @@ type MapSetting struct {
// Viewer related
type (
ViewerAction string
ViewerType string
)
const (
ViewerActionView = "view"
ViewerActionEdit = "edit"
ViewerTypeBuiltin = "builtin"
ViewerTypeWopi = "wopi"
)
type Viewer struct {
ID string `json:"id"`
Type ViewerType `json:"type"`
DisplayName string `json:"display_name"`
Exts []string `json:"exts"`
Url string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"`
Props map[string]string `json:"props,omitempty"`
MaxSize int64 `json:"max_size,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Templates []NewFileTemplate `json:"templates,omitempty"`
}
type ViewerGroup struct {
Viewers []Viewer `json:"viewers"`
}
type NewFileTemplate struct {
Ext string `json:"ext"`
DisplayName string `json:"display_name"`
}
type (
SearchCategory string
)

View File

@ -3,7 +3,7 @@ package wopi
import (
"encoding/xml"
"fmt"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/gofrs/uuid"
"github.com/samber/lo"
)
@ -16,23 +16,23 @@ var (
ActionEdit = ActonType("edit")
)
func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) {
func DiscoveryXmlToViewerGroup(xmlStr string) (*types.ViewerGroup, error) {
var discovery WopiDiscovery
if err := xml.Unmarshal([]byte(xmlStr), &discovery); err != nil {
return nil, fmt.Errorf("failed to parse WOPI discovery XML: %w", err)
}
group := &setting.ViewerGroup{
Viewers: make([]setting.Viewer, 0, len(discovery.NetZone.App)),
group := &types.ViewerGroup{
Viewers: make([]types.Viewer, 0, len(discovery.NetZone.App)),
}
for _, app := range discovery.NetZone.App {
viewer := setting.Viewer{
viewer := types.Viewer{
ID: uuid.Must(uuid.NewV4()).String(),
DisplayName: app.Name,
Type: setting.ViewerTypeWopi,
Type: types.ViewerTypeWopi,
Icon: app.FavIconUrl,
WopiActions: make(map[string]map[setting.ViewerAction]string),
WopiActions: make(map[string]map[types.ViewerAction]string),
}
for _, action := range app.Action {
@ -41,21 +41,21 @@ func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) {
}
if _, ok := viewer.WopiActions[action.Ext]; !ok {
viewer.WopiActions[action.Ext] = make(map[setting.ViewerAction]string)
viewer.WopiActions[action.Ext] = make(map[types.ViewerAction]string)
}
if action.Name == string(ActionPreview) {
viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc
viewer.WopiActions[action.Ext][types.ViewerActionView] = action.Urlsrc
} else if action.Name == string(ActionPreviewFallback) {
viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc
viewer.WopiActions[action.Ext][types.ViewerActionView] = action.Urlsrc
} else if action.Name == string(ActionEdit) {
viewer.WopiActions[action.Ext][setting.ViewerActionEdit] = action.Urlsrc
viewer.WopiActions[action.Ext][types.ViewerActionEdit] = action.Urlsrc
} else if len(viewer.WopiActions[action.Ext]) == 0 {
delete(viewer.WopiActions, action.Ext)
}
}
viewer.Exts = lo.MapToSlice(viewer.WopiActions, func(key string, value map[setting.ViewerAction]string) string {
viewer.Exts = lo.MapToSlice(viewer.WopiActions, func(key string, value map[types.ViewerAction]string) string {
return key
})

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"net/url"
"strings"
"time"
@ -56,7 +57,7 @@ const (
LockDuration = time.Duration(30) * time.Minute
)
func GenerateWopiSrc(ctx context.Context, action setting.ViewerAction, viewer *setting.Viewer, viewerSession *manager.ViewerSession) (*url.URL, error) {
func GenerateWopiSrc(ctx context.Context, action types.ViewerAction, viewer *types.Viewer, viewerSession *manager.ViewerSession) (*url.URL, error) {
dep := dependency.FromContext(ctx)
base := dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(ctx))
hasher := dep.HashIDEncoder()
@ -69,7 +70,7 @@ func GenerateWopiSrc(ctx context.Context, action setting.ViewerAction, viewer *s
var (
src string
)
fallbackOrder := []setting.ViewerAction{action, setting.ViewerActionView, setting.ViewerActionEdit}
fallbackOrder := []types.ViewerAction{action, types.ViewerActionView, types.ViewerActionEdit}
for _, a := range fallbackOrder {
if src, ok = availableActions[a]; ok {
break

View File

@ -12,6 +12,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/gin-gonic/gin"
"github.com/go-mail/mail"
)
@ -107,7 +108,7 @@ type (
FetchWOPIDiscoveryParamCtx struct{}
)
func (s *FetchWOPIDiscoveryService) Fetch(c *gin.Context) (*setting.ViewerGroup, error) {
func (s *FetchWOPIDiscoveryService) Fetch(c *gin.Context) (*types.ViewerGroup, error) {
dep := dependency.FromContext(c)
requestClient := dep.RequestClient(request2.WithContext(c), request2.WithLogger(dep.Logger()))
content, err := requestClient.Request("GET", s.Endpoint, nil).CheckHTTPResponse(http.StatusOK).GetResponse()

View File

@ -3,6 +3,7 @@ package basic
import (
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/service/user"
"github.com/gin-gonic/gin"
@ -39,7 +40,7 @@ type SiteConfig struct {
EmojiPreset string `json:"emoji_preset,omitempty"`
MapProvider setting.MapProvider `json:"map_provider,omitempty"`
GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"`
FileViewers []setting.ViewerGroup `json:"file_viewers,omitempty"`
FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"`
MaxBatchSize int `json:"max_batch_size,omitempty"`
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
ThumbnailHeight int `json:"thumbnail_height,omitempty"`

View File

@ -21,7 +21,6 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
"github.com/gin-gonic/gin"
)
@ -371,7 +370,7 @@ type (
Uri string `json:"uri" form:"uri" binding:"required"`
Version string `json:"version" form:"version"`
ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"`
PreferredAction setting.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"`
PreferredAction types.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"`
}
CreateViewerSessionParamCtx struct{}
)
@ -389,7 +388,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo
// Find the given viewer
viewers := dep.SettingProvider().FileViewers(c)
var targetViewer *setting.Viewer
var targetViewer *types.Viewer
for _, group := range viewers {
for _, viewer := range group.Viewers {
if viewer.ID == s.ViewerID && !viewer.Disabled {
@ -413,7 +412,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo
}
res := &ViewerSessionResponse{Session: viewerSession}
if targetViewer.Type == setting.ViewerTypeWopi {
if targetViewer.Type == types.ViewerTypeWopi {
// For WOPI viewer, generate WOPI src
wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession)
if err != nil {