mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-11-01 00:57:15 +08:00 
			
		
		
		
	feat(ent): migrate DB settings in patches
This commit is contained in:
		| @ -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
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -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
									
									
									
									
									
								
							
							
						
						
									
										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/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= | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										416
									
								
								inventory/migration.go
									
									
									
									
									
										Normal 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
											
										
									
								
							| @ -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"` | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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) | ||||
| 	} | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| ) | ||||
|  | ||||
| @ -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 | ||||
| 		}) | ||||
|  | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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"` | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Aaron Liu
					Aaron Liu