mirror of https://github.com/cloudreve/Cloudreve
feat(ent): migrate DB settings in patches
parent
8fe2889772
commit
fec549f5ec
|
@ -3,7 +3,7 @@ package constants
|
||||||
// These values will be injected at build time, DO NOT EDIT.
|
// These values will be injected at build time, DO NOT EDIT.
|
||||||
|
|
||||||
// BackendVersion 当前后端版本号
|
// BackendVersion 当前后端版本号
|
||||||
var BackendVersion = "4.0.0-alpha.1"
|
var BackendVersion = "4.1.0"
|
||||||
|
|
||||||
// IsPro 是否为Pro版本
|
// IsPro 是否为Pro版本
|
||||||
var IsPro = "false"
|
var IsPro = "false"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -4,6 +4,7 @@ go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.13.0
|
entgo.io/ent v0.13.0
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1
|
||||||
github.com/abslant/gzip v0.0.9
|
github.com/abslant/gzip v0.0.9
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||||
github.com/aws/aws-sdk-go v1.31.5
|
github.com/aws/aws-sdk-go v1.31.5
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -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 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.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.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.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||||
github.com/Masterminds/sprig v2.22.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=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
|
|
@ -5,20 +5,12 @@ import (
|
||||||
rawsql "database/sql"
|
rawsql "database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"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/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/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/cache"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||||
|
@ -153,302 +145,3 @@ func (d sqlite3Driver) Open(name string) (conn driver.Conn, err error) {
|
||||||
func init() {
|
func init() {
|
||||||
rawsql.Register("sqlite3", sqlite3Driver{Driver: &sqlite.Driver{}})
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
@ -178,6 +178,14 @@ type (
|
||||||
// Whether to share view setting from owner
|
// Whether to share view setting from owner
|
||||||
ShareView bool `json:"share_view,omitempty"`
|
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 (
|
const (
|
||||||
|
@ -250,3 +258,40 @@ const (
|
||||||
DownloaderProviderAria2 = DownloaderProvider("aria2")
|
DownloaderProviderAria2 = DownloaderProvider("aria2")
|
||||||
DownloaderProviderQBittorrent = DownloaderProvider("qbittorrent")
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
"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/filemanager/manager"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
"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/util"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -67,7 +67,7 @@ func ViewerSessionValidation() gin.HandlerFunc {
|
||||||
|
|
||||||
// Check if the viewer is still available
|
// Check if the viewer is still available
|
||||||
viewers := settings.FileViewers(c)
|
viewers := settings.FileViewers(c)
|
||||||
var v *setting.Viewer
|
var v *types.Viewer
|
||||||
for _, group := range viewers {
|
for _, group := range viewers {
|
||||||
for _, viewer := range group.Viewers {
|
for _, viewer := range group.Viewers {
|
||||||
if viewer.ID == session.ViewerID && !viewer.Disabled {
|
if viewer.ID == session.ViewerID && !viewer.Disabled {
|
||||||
|
|
|
@ -54,7 +54,7 @@ type (
|
||||||
// UpsertMedata update or insert metadata of given file
|
// UpsertMedata update or insert metadata of given file
|
||||||
PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error
|
PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error
|
||||||
// CreateViewerSession creates a viewer session for given file
|
// 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 traverses a file to its root file, return the file with linked root.
|
||||||
TraverseFile(ctx context.Context, fileID int) (fs.File, error)
|
TraverseFile(ctx context.Context, fileID int) (fs.File, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
"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/cloudreve/Cloudreve/v4/pkg/util"
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
)
|
)
|
||||||
|
@ -44,7 +43,7 @@ func init() {
|
||||||
gob.Register(ViewerSessionCache{})
|
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())
|
file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithNotRoot())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -88,6 +87,6 @@ func ViewerSessionFromContext(ctx context.Context) *ViewerSessionCache {
|
||||||
return ctx.Value(ViewerSessionCacheCtx{}).(*ViewerSessionCache)
|
return ctx.Value(ViewerSessionCacheCtx{}).(*ViewerSessionCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewerFromContext(ctx context.Context) *setting.Viewer {
|
func ViewerFromContext(ctx context.Context) *types.Viewer {
|
||||||
return ctx.Value(ViewerCtx{}).(*setting.Viewer)
|
return ctx.Value(ViewerCtx{}).(*types.Viewer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -169,7 +170,7 @@ type (
|
||||||
// FolderPropsCacheTTL returns the cache TTL of folder summary.
|
// FolderPropsCacheTTL returns the cache TTL of folder summary.
|
||||||
FolderPropsCacheTTL(ctx context.Context) int
|
FolderPropsCacheTTL(ctx context.Context) int
|
||||||
// FileViewers returns the file viewers settings.
|
// 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 returns the TTL of viewer session.
|
||||||
ViewerSessionTTL(ctx context.Context) int
|
ViewerSessionTTL(ctx context.Context) int
|
||||||
// MimeMapping returns the extension to MIME mapping settings.
|
// 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", "[]")
|
raw := s.getString(ctx, "file_viewers", "[]")
|
||||||
var viewers []ViewerGroup
|
var viewers []types.ViewerGroup
|
||||||
if err := json.Unmarshal([]byte(raw), &viewers); err != nil {
|
if err := json.Unmarshal([]byte(raw), &viewers); err != nil {
|
||||||
return []ViewerGroup{}
|
return []types.ViewerGroup{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewers
|
return viewers
|
||||||
|
|
|
@ -176,42 +176,6 @@ type MapSetting struct {
|
||||||
|
|
||||||
// Viewer related
|
// 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 (
|
type (
|
||||||
SearchCategory string
|
SearchCategory string
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ package wopi
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
@ -16,23 +16,23 @@ var (
|
||||||
ActionEdit = ActonType("edit")
|
ActionEdit = ActonType("edit")
|
||||||
)
|
)
|
||||||
|
|
||||||
func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) {
|
func DiscoveryXmlToViewerGroup(xmlStr string) (*types.ViewerGroup, error) {
|
||||||
var discovery WopiDiscovery
|
var discovery WopiDiscovery
|
||||||
if err := xml.Unmarshal([]byte(xmlStr), &discovery); err != nil {
|
if err := xml.Unmarshal([]byte(xmlStr), &discovery); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse WOPI discovery XML: %w", err)
|
return nil, fmt.Errorf("failed to parse WOPI discovery XML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
group := &setting.ViewerGroup{
|
group := &types.ViewerGroup{
|
||||||
Viewers: make([]setting.Viewer, 0, len(discovery.NetZone.App)),
|
Viewers: make([]types.Viewer, 0, len(discovery.NetZone.App)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, app := range discovery.NetZone.App {
|
for _, app := range discovery.NetZone.App {
|
||||||
viewer := setting.Viewer{
|
viewer := types.Viewer{
|
||||||
ID: uuid.Must(uuid.NewV4()).String(),
|
ID: uuid.Must(uuid.NewV4()).String(),
|
||||||
DisplayName: app.Name,
|
DisplayName: app.Name,
|
||||||
Type: setting.ViewerTypeWopi,
|
Type: types.ViewerTypeWopi,
|
||||||
Icon: app.FavIconUrl,
|
Icon: app.FavIconUrl,
|
||||||
WopiActions: make(map[string]map[setting.ViewerAction]string),
|
WopiActions: make(map[string]map[types.ViewerAction]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, action := range app.Action {
|
for _, action := range app.Action {
|
||||||
|
@ -41,21 +41,21 @@ func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := viewer.WopiActions[action.Ext]; !ok {
|
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) {
|
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) {
|
} 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) {
|
} 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 {
|
} else if len(viewer.WopiActions[action.Ext]) == 0 {
|
||||||
delete(viewer.WopiActions, action.Ext)
|
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
|
return key
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -56,7 +57,7 @@ const (
|
||||||
LockDuration = time.Duration(30) * time.Minute
|
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)
|
dep := dependency.FromContext(ctx)
|
||||||
base := dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(ctx))
|
base := dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(ctx))
|
||||||
hasher := dep.HashIDEncoder()
|
hasher := dep.HashIDEncoder()
|
||||||
|
@ -69,7 +70,7 @@ func GenerateWopiSrc(ctx context.Context, action setting.ViewerAction, viewer *s
|
||||||
var (
|
var (
|
||||||
src string
|
src string
|
||||||
)
|
)
|
||||||
fallbackOrder := []setting.ViewerAction{action, setting.ViewerActionView, setting.ViewerActionEdit}
|
fallbackOrder := []types.ViewerAction{action, types.ViewerActionView, types.ViewerActionEdit}
|
||||||
for _, a := range fallbackOrder {
|
for _, a := range fallbackOrder {
|
||||||
if src, ok = availableActions[a]; ok {
|
if src, ok = availableActions[a]; ok {
|
||||||
break
|
break
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"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/wopi"
|
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-mail/mail"
|
"github.com/go-mail/mail"
|
||||||
)
|
)
|
||||||
|
@ -107,7 +108,7 @@ type (
|
||||||
FetchWOPIDiscoveryParamCtx struct{}
|
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)
|
dep := dependency.FromContext(c)
|
||||||
requestClient := dep.RequestClient(request2.WithContext(c), request2.WithLogger(dep.Logger()))
|
requestClient := dep.RequestClient(request2.WithContext(c), request2.WithLogger(dep.Logger()))
|
||||||
content, err := requestClient.Request("GET", s.Endpoint, nil).CheckHTTPResponse(http.StatusOK).GetResponse()
|
content, err := requestClient.Request("GET", s.Endpoint, nil).CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||||
|
|
|
@ -3,6 +3,7 @@ package basic
|
||||||
import (
|
import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
"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/pkg/setting"
|
||||||
"github.com/cloudreve/Cloudreve/v4/service/user"
|
"github.com/cloudreve/Cloudreve/v4/service/user"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -39,7 +40,7 @@ type SiteConfig struct {
|
||||||
EmojiPreset string `json:"emoji_preset,omitempty"`
|
EmojiPreset string `json:"emoji_preset,omitempty"`
|
||||||
MapProvider setting.MapProvider `json:"map_provider,omitempty"`
|
MapProvider setting.MapProvider `json:"map_provider,omitempty"`
|
||||||
GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,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"`
|
MaxBatchSize int `json:"max_batch_size,omitempty"`
|
||||||
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||||
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||||
|
|
|
@ -21,7 +21,6 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||||
"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/wopi"
|
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -371,7 +370,7 @@ type (
|
||||||
Uri string `json:"uri" form:"uri" binding:"required"`
|
Uri string `json:"uri" form:"uri" binding:"required"`
|
||||||
Version string `json:"version" form:"version"`
|
Version string `json:"version" form:"version"`
|
||||||
ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"`
|
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{}
|
CreateViewerSessionParamCtx struct{}
|
||||||
)
|
)
|
||||||
|
@ -389,7 +388,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo
|
||||||
|
|
||||||
// Find the given viewer
|
// Find the given viewer
|
||||||
viewers := dep.SettingProvider().FileViewers(c)
|
viewers := dep.SettingProvider().FileViewers(c)
|
||||||
var targetViewer *setting.Viewer
|
var targetViewer *types.Viewer
|
||||||
for _, group := range viewers {
|
for _, group := range viewers {
|
||||||
for _, viewer := range group.Viewers {
|
for _, viewer := range group.Viewers {
|
||||||
if viewer.ID == s.ViewerID && !viewer.Disabled {
|
if viewer.ID == s.ViewerID && !viewer.Disabled {
|
||||||
|
@ -413,7 +412,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &ViewerSessionResponse{Session: viewerSession}
|
res := &ViewerSessionResponse{Session: viewerSession}
|
||||||
if targetViewer.Type == setting.ViewerTypeWopi {
|
if targetViewer.Type == types.ViewerTypeWopi {
|
||||||
// For WOPI viewer, generate WOPI src
|
// For WOPI viewer, generate WOPI src
|
||||||
wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession)
|
wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue