diff --git a/ROADMAP.md b/ROADMAP.md index 4af72514..eef7834e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -176,8 +176,8 @@ Order = user-visible value first; each ships with backend + UI + tests. - [ ] Paid shares — `share.price_points` + gate page + purchase/resume-ticket flow; needs B.4 credits first 2. **Storage policy advanced** — multiple policies per group, per-directory binding, load-balancer policy, file migration (fixes #3518, #2961, #2262). See §1.3a. - [x] PR #175 — resumable admin relocation task (entities or whole-policy scope), encryption-aware re-wrap, admin UI + per-policy migrate action (#9, #125, #136) - - [ ] Group→policies M:N join + group-editor multi-select; per-directory policy picker in FM toolbar; `StoragePolicyTypeLB` weighted children - - [ ] User-facing relocate — extend #175 path to FM context menu, restricted to group-allowed policies + - [x] Group→policies M:N (`allowed_policies` edge, empty = legacy single) + group-editor multi-select; per-directory `sys:preferred_policy` metadata marker with nearest-ancestor precedence (invalid marker cuts inheritance); user `preferred_policy` setting applied in own tree only; `load_balance` policy type with weighted children resolved before drivers (#182, fixes #2961) + - [x] User-facing relocate — `POST /file/relocate` from FM More-actions dialog, entity expansion + same-policy skip + dedup, restricted to group-allowed policies (#182, fixes #2262) 3. **SSO** — generic OIDC, Logto, multi-account switching, sign-up email filtering (fixes #3464, #3056, #3505). See §1.3a. - [x] PR #141 — inbound OIDC consumer (auth-code + nonce, JWKS-verified RS256 id_tokens, userinfo fallback, auto-provisioning, one-time ticket handoff, SSRF-validated endpoints, redacted secret); covers Keycloak/Authentik/Logto/generic IdPs; sign-up email domain filtering (whitelist/blacklist + sub-address block) enforced at registration and SSO provisioning; multi-account lands via existing session `upsert` (fixes #3464, #3056) - [ ] Multi-account switcher UI — N-token session store, avatar-menu switch + signed-out badges (frontend-only, backend already supports) diff --git a/ent/client.go b/ent/client.go index 1cf7f5b1..0e654a2f 100644 --- a/ent/client.go +++ b/ent/client.go @@ -1552,6 +1552,22 @@ func (c *GroupClient) QueryStoragePolicies(gr *Group) *StoragePolicyQuery { return query } +// QueryAllowedPolicies queries the allowed_policies edge of a Group. +func (c *GroupClient) QueryAllowedPolicies(gr *Group) *StoragePolicyQuery { + query := (&StoragePolicyClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := gr.ID + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, id), + sqlgraph.To(storagepolicy.Table, storagepolicy.FieldID), + sqlgraph.Edge(sqlgraph.M2M, false, group.AllowedPoliciesTable, group.AllowedPoliciesPrimaryKey...), + ) + fromV = sqlgraph.Neighbors(gr.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *GroupClient) Hooks() []Hook { hooks := c.hooks.Group @@ -2943,6 +2959,22 @@ func (c *StoragePolicyClient) QueryEntities(sp *StoragePolicy) *EntityQuery { return query } +// QueryAllowedGroups queries the allowed_groups edge of a StoragePolicy. +func (c *StoragePolicyClient) QueryAllowedGroups(sp *StoragePolicy) *GroupQuery { + query := (&GroupClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := sp.ID + step := sqlgraph.NewStep( + sqlgraph.From(storagepolicy.Table, storagepolicy.FieldID, id), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, storagepolicy.AllowedGroupsTable, storagepolicy.AllowedGroupsPrimaryKey...), + ) + fromV = sqlgraph.Neighbors(sp.driver.Dialect(), step) + return fromV, nil + } + return query +} + // QueryNode queries the node edge of a StoragePolicy. func (c *StoragePolicyClient) QueryNode(sp *StoragePolicy) *NodeQuery { query := (&NodeClient{config: c.config}).Query() diff --git a/ent/group.go b/ent/group.go index 80882cb5..72aa292d 100644 --- a/ent/group.go +++ b/ent/group.go @@ -51,9 +51,11 @@ type GroupEdges struct { Users []*User `json:"users,omitempty"` // StoragePolicies holds the value of the storage_policies edge. StoragePolicies *StoragePolicy `json:"storage_policies,omitempty"` + // AllowedPolicies holds the value of the allowed_policies edge. + AllowedPolicies []*StoragePolicy `json:"allowed_policies,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [2]bool + loadedTypes [3]bool } // UsersOrErr returns the Users value or an error if the edge @@ -78,6 +80,15 @@ func (e GroupEdges) StoragePoliciesOrErr() (*StoragePolicy, error) { return nil, &NotLoadedError{edge: "storage_policies"} } +// AllowedPoliciesOrErr returns the AllowedPolicies value or an error if the edge +// was not loaded in eager-loading. +func (e GroupEdges) AllowedPoliciesOrErr() ([]*StoragePolicy, error) { + if e.loadedTypes[2] { + return e.AllowedPolicies, nil + } + return nil, &NotLoadedError{edge: "allowed_policies"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*Group) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) @@ -194,6 +205,11 @@ func (gr *Group) QueryStoragePolicies() *StoragePolicyQuery { return NewGroupClient(gr.config).QueryStoragePolicies(gr) } +// QueryAllowedPolicies queries the "allowed_policies" edge of the Group entity. +func (gr *Group) QueryAllowedPolicies() *StoragePolicyQuery { + return NewGroupClient(gr.config).QueryAllowedPolicies(gr) +} + // Update returns a builder for updating this Group. // Note that you need to call Group.Unwrap() before calling this method if this Group // was returned from a transaction, and the transaction was committed or rolled back. @@ -261,5 +277,11 @@ func (e *Group) SetStoragePolicies(v *StoragePolicy) { e.Edges.loadedTypes[1] = true } +// SetAllowedPolicies manually set the edge as loaded state. +func (e *Group) SetAllowedPolicies(v []*StoragePolicy) { + e.Edges.AllowedPolicies = v + e.Edges.loadedTypes[2] = true +} + // Groups is a parsable slice of Group. type Groups []*Group diff --git a/ent/group/group.go b/ent/group/group.go index 7ceee545..aeb0dadd 100644 --- a/ent/group/group.go +++ b/ent/group/group.go @@ -38,6 +38,8 @@ const ( EdgeUsers = "users" // EdgeStoragePolicies holds the string denoting the storage_policies edge name in mutations. EdgeStoragePolicies = "storage_policies" + // EdgeAllowedPolicies holds the string denoting the allowed_policies edge name in mutations. + EdgeAllowedPolicies = "allowed_policies" // Table holds the table name of the group in the database. Table = "groups" // UsersTable is the table that holds the users relation/edge. @@ -54,6 +56,11 @@ const ( StoragePoliciesInverseTable = "storage_policies" // StoragePoliciesColumn is the table column denoting the storage_policies relation/edge. StoragePoliciesColumn = "storage_policy_id" + // AllowedPoliciesTable is the table that holds the allowed_policies relation/edge. The primary key declared below. + AllowedPoliciesTable = "group_allowed_policies" + // AllowedPoliciesInverseTable is the table name for the StoragePolicy entity. + // It exists in this package in order to avoid circular dependency with the "storagepolicy" package. + AllowedPoliciesInverseTable = "storage_policies" ) // Columns holds all SQL columns for group fields. @@ -70,6 +77,12 @@ var Columns = []string{ FieldStoragePolicyID, } +var ( + // AllowedPoliciesPrimaryKey and AllowedPoliciesColumn2 are the table columns denoting the + // primary key for the allowed_policies relation (M2M). + AllowedPoliciesPrimaryKey = []string{"group_id", "storage_policy_id"} +) + // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { for i := range Columns { @@ -161,6 +174,20 @@ func ByStoragePoliciesField(field string, opts ...sql.OrderTermOption) OrderOpti sqlgraph.OrderByNeighborTerms(s, newStoragePoliciesStep(), sql.OrderByField(field, opts...)) } } + +// ByAllowedPoliciesCount orders the results by allowed_policies count. +func ByAllowedPoliciesCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newAllowedPoliciesStep(), opts...) + } +} + +// ByAllowedPolicies orders the results by allowed_policies terms. +func ByAllowedPolicies(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newAllowedPoliciesStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} func newUsersStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), @@ -175,3 +202,10 @@ func newStoragePoliciesStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.M2O, true, StoragePoliciesTable, StoragePoliciesColumn), ) } +func newAllowedPoliciesStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(AllowedPoliciesInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2M, false, AllowedPoliciesTable, AllowedPoliciesPrimaryKey...), + ) +} diff --git a/ent/group/where.go b/ent/group/where.go index 4c414ff5..db466140 100644 --- a/ent/group/where.go +++ b/ent/group/where.go @@ -517,6 +517,29 @@ func HasStoragePoliciesWith(preds ...predicate.StoragePolicy) predicate.Group { }) } +// HasAllowedPolicies applies the HasEdge predicate on the "allowed_policies" edge. +func HasAllowedPolicies() predicate.Group { + return predicate.Group(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2M, false, AllowedPoliciesTable, AllowedPoliciesPrimaryKey...), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasAllowedPoliciesWith applies the HasEdge predicate on the "allowed_policies" edge with a given conditions (other predicates). +func HasAllowedPoliciesWith(preds ...predicate.StoragePolicy) predicate.Group { + return predicate.Group(func(s *sql.Selector) { + step := newAllowedPoliciesStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Group) predicate.Group { return predicate.Group(sql.AndPredicates(predicates...)) diff --git a/ent/group_create.go b/ent/group_create.go index e11afdd6..9887b73a 100644 --- a/ent/group_create.go +++ b/ent/group_create.go @@ -162,6 +162,21 @@ func (gc *GroupCreate) SetStoragePolicies(s *StoragePolicy) *GroupCreate { return gc.SetStoragePoliciesID(s.ID) } +// AddAllowedPolicyIDs adds the "allowed_policies" edge to the StoragePolicy entity by IDs. +func (gc *GroupCreate) AddAllowedPolicyIDs(ids ...int) *GroupCreate { + gc.mutation.AddAllowedPolicyIDs(ids...) + return gc +} + +// AddAllowedPolicies adds the "allowed_policies" edges to the StoragePolicy entity. +func (gc *GroupCreate) AddAllowedPolicies(s ...*StoragePolicy) *GroupCreate { + ids := make([]int, len(s)) + for i := range s { + ids[i] = s[i].ID + } + return gc.AddAllowedPolicyIDs(ids...) +} + // Mutation returns the GroupMutation object of the builder. func (gc *GroupCreate) Mutation() *GroupMutation { return gc.mutation @@ -333,6 +348,22 @@ func (gc *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _node.StoragePolicyID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } + if nodes := gc.mutation.AllowedPoliciesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/ent/group_query.go b/ent/group_query.go index 27d4ac91..78c723fe 100644 --- a/ent/group_query.go +++ b/ent/group_query.go @@ -26,6 +26,7 @@ type GroupQuery struct { predicates []predicate.Group withUsers *UserQuery withStoragePolicies *StoragePolicyQuery + withAllowedPolicies *StoragePolicyQuery // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -106,6 +107,28 @@ func (gq *GroupQuery) QueryStoragePolicies() *StoragePolicyQuery { return query } +// QueryAllowedPolicies chains the current query on the "allowed_policies" edge. +func (gq *GroupQuery) QueryAllowedPolicies() *StoragePolicyQuery { + query := (&StoragePolicyClient{config: gq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := gq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := gq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(group.Table, group.FieldID, selector), + sqlgraph.To(storagepolicy.Table, storagepolicy.FieldID), + sqlgraph.Edge(sqlgraph.M2M, false, group.AllowedPoliciesTable, group.AllowedPoliciesPrimaryKey...), + ) + fromU = sqlgraph.SetNeighbors(gq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first Group entity from the query. // Returns a *NotFoundError when no Group was found. func (gq *GroupQuery) First(ctx context.Context) (*Group, error) { @@ -300,6 +323,7 @@ func (gq *GroupQuery) Clone() *GroupQuery { predicates: append([]predicate.Group{}, gq.predicates...), withUsers: gq.withUsers.Clone(), withStoragePolicies: gq.withStoragePolicies.Clone(), + withAllowedPolicies: gq.withAllowedPolicies.Clone(), // clone intermediate query. sql: gq.sql.Clone(), path: gq.path, @@ -328,6 +352,17 @@ func (gq *GroupQuery) WithStoragePolicies(opts ...func(*StoragePolicyQuery)) *Gr return gq } +// WithAllowedPolicies tells the query-builder to eager-load the nodes that are connected to +// the "allowed_policies" edge. The optional arguments are used to configure the query builder of the edge. +func (gq *GroupQuery) WithAllowedPolicies(opts ...func(*StoragePolicyQuery)) *GroupQuery { + query := (&StoragePolicyClient{config: gq.config}).Query() + for _, opt := range opts { + opt(query) + } + gq.withAllowedPolicies = query + return gq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -406,9 +441,10 @@ func (gq *GroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Group, var ( nodes = []*Group{} _spec = gq.querySpec() - loadedTypes = [2]bool{ + loadedTypes = [3]bool{ gq.withUsers != nil, gq.withStoragePolicies != nil, + gq.withAllowedPolicies != nil, } ) _spec.ScanValues = func(columns []string) ([]any, error) { @@ -442,6 +478,13 @@ func (gq *GroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Group, return nil, err } } + if query := gq.withAllowedPolicies; query != nil { + if err := gq.loadAllowedPolicies(ctx, query, nodes, + func(n *Group) { n.Edges.AllowedPolicies = []*StoragePolicy{} }, + func(n *Group, e *StoragePolicy) { n.Edges.AllowedPolicies = append(n.Edges.AllowedPolicies, e) }); err != nil { + return nil, err + } + } return nodes, nil } @@ -504,6 +547,67 @@ func (gq *GroupQuery) loadStoragePolicies(ctx context.Context, query *StoragePol } return nil } +func (gq *GroupQuery) loadAllowedPolicies(ctx context.Context, query *StoragePolicyQuery, nodes []*Group, init func(*Group), assign func(*Group, *StoragePolicy)) error { + edgeIDs := make([]driver.Value, len(nodes)) + byID := make(map[int]*Group) + nids := make(map[int]map[*Group]struct{}) + for i, node := range nodes { + edgeIDs[i] = node.ID + byID[node.ID] = node + if init != nil { + init(node) + } + } + query.Where(func(s *sql.Selector) { + joinT := sql.Table(group.AllowedPoliciesTable) + s.Join(joinT).On(s.C(storagepolicy.FieldID), joinT.C(group.AllowedPoliciesPrimaryKey[1])) + s.Where(sql.InValues(joinT.C(group.AllowedPoliciesPrimaryKey[0]), edgeIDs...)) + columns := s.SelectedColumns() + s.Select(joinT.C(group.AllowedPoliciesPrimaryKey[0])) + s.AppendSelect(columns...) + s.SetDistinct(false) + }) + if err := query.prepareQuery(ctx); err != nil { + return err + } + qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) { + assign := spec.Assign + values := spec.ScanValues + spec.ScanValues = func(columns []string) ([]any, error) { + values, err := values(columns[1:]) + if err != nil { + return nil, err + } + return append([]any{new(sql.NullInt64)}, values...), nil + } + spec.Assign = func(columns []string, values []any) error { + outValue := int(values[0].(*sql.NullInt64).Int64) + inValue := int(values[1].(*sql.NullInt64).Int64) + if nids[inValue] == nil { + nids[inValue] = map[*Group]struct{}{byID[outValue]: {}} + return assign(columns[1:], values[1:]) + } + nids[inValue][byID[outValue]] = struct{}{} + return nil + } + }) + }) + neighbors, err := withInterceptors[[]*StoragePolicy](ctx, query, qr, query.inters) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nids[n.ID] + if !ok { + return fmt.Errorf(`unexpected "allowed_policies" node returned %v`, n.ID) + } + for kn := range nodes { + assign(kn, n) + } + } + return nil +} func (gq *GroupQuery) sqlCount(ctx context.Context) (int, error) { _spec := gq.querySpec() diff --git a/ent/group_update.go b/ent/group_update.go index ae734535..211caff2 100644 --- a/ent/group_update.go +++ b/ent/group_update.go @@ -198,6 +198,21 @@ func (gu *GroupUpdate) SetStoragePolicies(s *StoragePolicy) *GroupUpdate { return gu.SetStoragePoliciesID(s.ID) } +// AddAllowedPolicyIDs adds the "allowed_policies" edge to the StoragePolicy entity by IDs. +func (gu *GroupUpdate) AddAllowedPolicyIDs(ids ...int) *GroupUpdate { + gu.mutation.AddAllowedPolicyIDs(ids...) + return gu +} + +// AddAllowedPolicies adds the "allowed_policies" edges to the StoragePolicy entity. +func (gu *GroupUpdate) AddAllowedPolicies(s ...*StoragePolicy) *GroupUpdate { + ids := make([]int, len(s)) + for i := range s { + ids[i] = s[i].ID + } + return gu.AddAllowedPolicyIDs(ids...) +} + // Mutation returns the GroupMutation object of the builder. func (gu *GroupUpdate) Mutation() *GroupMutation { return gu.mutation @@ -230,6 +245,27 @@ func (gu *GroupUpdate) ClearStoragePolicies() *GroupUpdate { return gu } +// ClearAllowedPolicies clears all "allowed_policies" edges to the StoragePolicy entity. +func (gu *GroupUpdate) ClearAllowedPolicies() *GroupUpdate { + gu.mutation.ClearAllowedPolicies() + return gu +} + +// RemoveAllowedPolicyIDs removes the "allowed_policies" edge to StoragePolicy entities by IDs. +func (gu *GroupUpdate) RemoveAllowedPolicyIDs(ids ...int) *GroupUpdate { + gu.mutation.RemoveAllowedPolicyIDs(ids...) + return gu +} + +// RemoveAllowedPolicies removes "allowed_policies" edges to StoragePolicy entities. +func (gu *GroupUpdate) RemoveAllowedPolicies(s ...*StoragePolicy) *GroupUpdate { + ids := make([]int, len(s)) + for i := range s { + ids[i] = s[i].ID + } + return gu.RemoveAllowedPolicyIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (gu *GroupUpdate) Save(ctx context.Context) (int, error) { if err := gu.defaults(); err != nil { @@ -394,6 +430,51 @@ func (gu *GroupUpdate) sqlSave(ctx context.Context) (n int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if gu.mutation.AllowedPoliciesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gu.mutation.RemovedAllowedPoliciesIDs(); len(nodes) > 0 && !gu.mutation.AllowedPoliciesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := gu.mutation.AllowedPoliciesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if n, err = sqlgraph.UpdateNodes(ctx, gu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{group.Label} @@ -580,6 +661,21 @@ func (guo *GroupUpdateOne) SetStoragePolicies(s *StoragePolicy) *GroupUpdateOne return guo.SetStoragePoliciesID(s.ID) } +// AddAllowedPolicyIDs adds the "allowed_policies" edge to the StoragePolicy entity by IDs. +func (guo *GroupUpdateOne) AddAllowedPolicyIDs(ids ...int) *GroupUpdateOne { + guo.mutation.AddAllowedPolicyIDs(ids...) + return guo +} + +// AddAllowedPolicies adds the "allowed_policies" edges to the StoragePolicy entity. +func (guo *GroupUpdateOne) AddAllowedPolicies(s ...*StoragePolicy) *GroupUpdateOne { + ids := make([]int, len(s)) + for i := range s { + ids[i] = s[i].ID + } + return guo.AddAllowedPolicyIDs(ids...) +} + // Mutation returns the GroupMutation object of the builder. func (guo *GroupUpdateOne) Mutation() *GroupMutation { return guo.mutation @@ -612,6 +708,27 @@ func (guo *GroupUpdateOne) ClearStoragePolicies() *GroupUpdateOne { return guo } +// ClearAllowedPolicies clears all "allowed_policies" edges to the StoragePolicy entity. +func (guo *GroupUpdateOne) ClearAllowedPolicies() *GroupUpdateOne { + guo.mutation.ClearAllowedPolicies() + return guo +} + +// RemoveAllowedPolicyIDs removes the "allowed_policies" edge to StoragePolicy entities by IDs. +func (guo *GroupUpdateOne) RemoveAllowedPolicyIDs(ids ...int) *GroupUpdateOne { + guo.mutation.RemoveAllowedPolicyIDs(ids...) + return guo +} + +// RemoveAllowedPolicies removes "allowed_policies" edges to StoragePolicy entities. +func (guo *GroupUpdateOne) RemoveAllowedPolicies(s ...*StoragePolicy) *GroupUpdateOne { + ids := make([]int, len(s)) + for i := range s { + ids[i] = s[i].ID + } + return guo.RemoveAllowedPolicyIDs(ids...) +} + // Where appends a list predicates to the GroupUpdate builder. func (guo *GroupUpdateOne) Where(ps ...predicate.Group) *GroupUpdateOne { guo.mutation.Where(ps...) @@ -806,6 +923,51 @@ func (guo *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if guo.mutation.AllowedPoliciesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := guo.mutation.RemovedAllowedPoliciesIDs(); len(nodes) > 0 && !guo.mutation.AllowedPoliciesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := guo.mutation.AllowedPoliciesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: false, + Table: group.AllowedPoliciesTable, + Columns: group.AllowedPoliciesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &Group{config: guo.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/ent/internal/schema.go b/ent/internal/schema.go index 9971a43b..fd5dd85b 100644 --- a/ent/internal/schema.go +++ b/ent/internal/schema.go @@ -6,4 +6,4 @@ // Package internal holds a loadable version of the latest schema. package internal -const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"AclEntry\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"acl_entries\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subject_type\",\"type\":{\"Type\":6,\"Ident\":\"aclentry.SubjectType\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"user\",\"V\":\"user\"},{\"N\":\"group\",\"V\":\"group\"},{\"N\":\"anonymous\",\"V\":\"anonymous\"},{\"N\":\"everyone\",\"V\":\"everyone\"}],\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subject_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"subject_type\",\"subject_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityProps\",\"Ident\":\"types.EntityProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"storage_key\":\"recycle_options\",\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"acl_entries\",\"type\":\"AclEntry\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"created_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}]},{\"name\":\"FsEvent\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_fsevent\",\"ref_name\":\"fsevents\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"event\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subscriber\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_fsevent\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"InvitationCode\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"code\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":64,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_uses\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"used_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"OAuthClient\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"grants\",\"type\":\"OAuthGrant\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"guid\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"validators\":1,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"homepage_url\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2048,\"optional\":true,\"validators\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"redirect_uris\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"scopes\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.OAuthClientProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"OAuthClientProps\",\"Ident\":\"types.OAuthClientProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_enabled\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":true,\"default_kind\":1,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"OAuthGrant\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"oauth_grants\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"client\",\"type\":\"OAuthClient\",\"field\":\"client_id\",\"ref_name\":\"grants\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"client_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"scopes\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"last_used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"client_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"storagepolicy.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":12,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"creator_ip\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":45,\"optional\":true,\"validators\":1,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"hidden\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"fsevents\",\"type\":\"FsEvent\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"oauth_grants\",\"type\":\"OAuthGrant\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"ban_expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"ban_reason\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"last_login\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0},\"comment\":\"Time of the last successful sign-in\"},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" +const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"AclEntry\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"acl_entries\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subject_type\",\"type\":{\"Type\":6,\"Ident\":\"aclentry.SubjectType\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"user\",\"V\":\"user\"},{\"N\":\"group\",\"V\":\"group\"},{\"N\":\"anonymous\",\"V\":\"anonymous\"},{\"N\":\"everyone\",\"V\":\"everyone\"}],\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subject_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"subject_type\",\"subject_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityProps\",\"Ident\":\"types.EntityProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"storage_key\":\"recycle_options\",\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"acl_entries\",\"type\":\"AclEntry\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"created_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}]},{\"name\":\"FsEvent\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_fsevent\",\"ref_name\":\"fsevents\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"event\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"subscriber\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_fsevent\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true},{\"name\":\"allowed_policies\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"InvitationCode\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"code\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":64,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_uses\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"used_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"OAuthClient\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"grants\",\"type\":\"OAuthGrant\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"guid\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":255,\"validators\":1,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"homepage_url\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2048,\"optional\":true,\"validators\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"redirect_uris\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"scopes\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.OAuthClientProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"OAuthClientProps\",\"Ident\":\"types.OAuthClientProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_enabled\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":true,\"default_kind\":1,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"OAuthGrant\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"oauth_grants\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"client\",\"type\":\"OAuthClient\",\"field\":\"client_id\",\"ref_name\":\"grants\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"client_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"scopes\",\"type\":{\"Type\":3,\"Ident\":\"[]string\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":true,\"RType\":{\"Name\":\"\",\"Ident\":\"[]string\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":{}}},\"default\":true,\"default_value\":[],\"default_kind\":23,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"last_used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"client_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"allowed_groups\",\"type\":\"Group\",\"ref_name\":\"allowed_policies\",\"inverse\":true},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"storagepolicy.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":12,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"creator_ip\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":45,\"optional\":true,\"validators\":1,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"hidden\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"fsevents\",\"type\":\"FsEvent\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"oauth_grants\",\"type\":\"OAuthGrant\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"ban_expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"ban_reason\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"last_login\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0},\"comment\":\"Time of the last successful sign-in\"},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index a5b568e2..8ec2676f 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -582,6 +582,31 @@ var ( }, }, } + // GroupAllowedPoliciesColumns holds the columns for the "group_allowed_policies" table. + GroupAllowedPoliciesColumns = []*schema.Column{ + {Name: "group_id", Type: field.TypeInt}, + {Name: "storage_policy_id", Type: field.TypeInt}, + } + // GroupAllowedPoliciesTable holds the schema information for the "group_allowed_policies" table. + GroupAllowedPoliciesTable = &schema.Table{ + Name: "group_allowed_policies", + Columns: GroupAllowedPoliciesColumns, + PrimaryKey: []*schema.Column{GroupAllowedPoliciesColumns[0], GroupAllowedPoliciesColumns[1]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "group_allowed_policies_group_id", + Columns: []*schema.Column{GroupAllowedPoliciesColumns[0]}, + RefColumns: []*schema.Column{GroupsColumns[0]}, + OnDelete: schema.Cascade, + }, + { + Symbol: "group_allowed_policies_storage_policy_id", + Columns: []*schema.Column{GroupAllowedPoliciesColumns[1]}, + RefColumns: []*schema.Column{StoragePoliciesColumns[0]}, + OnDelete: schema.Cascade, + }, + }, + } // Tables holds all the tables in the schema. Tables = []*schema.Table{ ACLEntriesTable, @@ -603,6 +628,7 @@ var ( TasksTable, UsersTable, FileEntitiesTable, + GroupAllowedPoliciesTable, } ) @@ -628,4 +654,6 @@ func init() { UsersTable.ForeignKeys[0].RefTable = GroupsTable FileEntitiesTable.ForeignKeys[0].RefTable = FilesTable FileEntitiesTable.ForeignKeys[1].RefTable = EntitiesTable + GroupAllowedPoliciesTable.ForeignKeys[0].RefTable = GroupsTable + GroupAllowedPoliciesTable.ForeignKeys[1].RefTable = StoragePoliciesTable } diff --git a/ent/mutation.go b/ent/mutation.go index b1c7953e..01267fba 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -6205,6 +6205,9 @@ type GroupMutation struct { clearedusers bool storage_policies *int clearedstorage_policies bool + allowed_policies map[int]struct{} + removedallowed_policies map[int]struct{} + clearedallowed_policies bool done bool oldValue func(context.Context) (*Group, error) predicates []predicate.Group @@ -6833,6 +6836,60 @@ func (m *GroupMutation) ResetStoragePolicies() { m.clearedstorage_policies = false } +// AddAllowedPolicyIDs adds the "allowed_policies" edge to the StoragePolicy entity by ids. +func (m *GroupMutation) AddAllowedPolicyIDs(ids ...int) { + if m.allowed_policies == nil { + m.allowed_policies = make(map[int]struct{}) + } + for i := range ids { + m.allowed_policies[ids[i]] = struct{}{} + } +} + +// ClearAllowedPolicies clears the "allowed_policies" edge to the StoragePolicy entity. +func (m *GroupMutation) ClearAllowedPolicies() { + m.clearedallowed_policies = true +} + +// AllowedPoliciesCleared reports if the "allowed_policies" edge to the StoragePolicy entity was cleared. +func (m *GroupMutation) AllowedPoliciesCleared() bool { + return m.clearedallowed_policies +} + +// RemoveAllowedPolicyIDs removes the "allowed_policies" edge to the StoragePolicy entity by IDs. +func (m *GroupMutation) RemoveAllowedPolicyIDs(ids ...int) { + if m.removedallowed_policies == nil { + m.removedallowed_policies = make(map[int]struct{}) + } + for i := range ids { + delete(m.allowed_policies, ids[i]) + m.removedallowed_policies[ids[i]] = struct{}{} + } +} + +// RemovedAllowedPolicies returns the removed IDs of the "allowed_policies" edge to the StoragePolicy entity. +func (m *GroupMutation) RemovedAllowedPoliciesIDs() (ids []int) { + for id := range m.removedallowed_policies { + ids = append(ids, id) + } + return +} + +// AllowedPoliciesIDs returns the "allowed_policies" edge IDs in the mutation. +func (m *GroupMutation) AllowedPoliciesIDs() (ids []int) { + for id := range m.allowed_policies { + ids = append(ids, id) + } + return +} + +// ResetAllowedPolicies resets all changes to the "allowed_policies" edge. +func (m *GroupMutation) ResetAllowedPolicies() { + m.allowed_policies = nil + m.clearedallowed_policies = false + m.removedallowed_policies = nil +} + // Where appends a list predicates to the GroupMutation builder. func (m *GroupMutation) Where(ps ...predicate.Group) { m.predicates = append(m.predicates, ps...) @@ -7162,13 +7219,16 @@ func (m *GroupMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *GroupMutation) AddedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.users != nil { edges = append(edges, group.EdgeUsers) } if m.storage_policies != nil { edges = append(edges, group.EdgeStoragePolicies) } + if m.allowed_policies != nil { + edges = append(edges, group.EdgeAllowedPolicies) + } return edges } @@ -7186,16 +7246,25 @@ func (m *GroupMutation) AddedIDs(name string) []ent.Value { if id := m.storage_policies; id != nil { return []ent.Value{*id} } + case group.EdgeAllowedPolicies: + ids := make([]ent.Value, 0, len(m.allowed_policies)) + for id := range m.allowed_policies { + ids = append(ids, id) + } + return ids } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *GroupMutation) RemovedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.removedusers != nil { edges = append(edges, group.EdgeUsers) } + if m.removedallowed_policies != nil { + edges = append(edges, group.EdgeAllowedPolicies) + } return edges } @@ -7209,19 +7278,28 @@ func (m *GroupMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case group.EdgeAllowedPolicies: + ids := make([]ent.Value, 0, len(m.removedallowed_policies)) + for id := range m.removedallowed_policies { + ids = append(ids, id) + } + return ids } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *GroupMutation) ClearedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.clearedusers { edges = append(edges, group.EdgeUsers) } if m.clearedstorage_policies { edges = append(edges, group.EdgeStoragePolicies) } + if m.clearedallowed_policies { + edges = append(edges, group.EdgeAllowedPolicies) + } return edges } @@ -7233,6 +7311,8 @@ func (m *GroupMutation) EdgeCleared(name string) bool { return m.clearedusers case group.EdgeStoragePolicies: return m.clearedstorage_policies + case group.EdgeAllowedPolicies: + return m.clearedallowed_policies } return false } @@ -7258,6 +7338,9 @@ func (m *GroupMutation) ResetEdge(name string) error { case group.EdgeStoragePolicies: m.ResetStoragePolicies() return nil + case group.EdgeAllowedPolicies: + m.ResetAllowedPolicies() + return nil } return fmt.Errorf("unknown Group edge %s", name) } @@ -14227,40 +14310,43 @@ func (m *ShareMutation) ResetEdge(name string) error { // StoragePolicyMutation represents an operation that mutates the StoragePolicy nodes in the graph. type StoragePolicyMutation struct { config - op Op - typ string - id *int - created_at *time.Time - updated_at *time.Time - deleted_at *time.Time - name *string - _type *string - status *storagepolicy.Status - server *string - bucket_name *string - is_private *bool - access_key *string - secret_key *string - max_size *int64 - addmax_size *int64 - dir_name_rule *string - file_name_rule *string - settings **types.PolicySetting - clearedFields map[string]struct{} - groups map[int]struct{} - removedgroups map[int]struct{} - clearedgroups bool - files map[int]struct{} - removedfiles map[int]struct{} - clearedfiles bool - entities map[int]struct{} - removedentities map[int]struct{} - clearedentities bool - node *int - clearednode bool - done bool - oldValue func(context.Context) (*StoragePolicy, error) - predicates []predicate.StoragePolicy + op Op + typ string + id *int + created_at *time.Time + updated_at *time.Time + deleted_at *time.Time + name *string + _type *string + status *storagepolicy.Status + server *string + bucket_name *string + is_private *bool + access_key *string + secret_key *string + max_size *int64 + addmax_size *int64 + dir_name_rule *string + file_name_rule *string + settings **types.PolicySetting + clearedFields map[string]struct{} + groups map[int]struct{} + removedgroups map[int]struct{} + clearedgroups bool + files map[int]struct{} + removedfiles map[int]struct{} + clearedfiles bool + entities map[int]struct{} + removedentities map[int]struct{} + clearedentities bool + allowed_groups map[int]struct{} + removedallowed_groups map[int]struct{} + clearedallowed_groups bool + node *int + clearednode bool + done bool + oldValue func(context.Context) (*StoragePolicy, error) + predicates []predicate.StoragePolicy } var _ ent.Mutation = (*StoragePolicyMutation)(nil) @@ -15263,6 +15349,60 @@ func (m *StoragePolicyMutation) ResetEntities() { m.removedentities = nil } +// AddAllowedGroupIDs adds the "allowed_groups" edge to the Group entity by ids. +func (m *StoragePolicyMutation) AddAllowedGroupIDs(ids ...int) { + if m.allowed_groups == nil { + m.allowed_groups = make(map[int]struct{}) + } + for i := range ids { + m.allowed_groups[ids[i]] = struct{}{} + } +} + +// ClearAllowedGroups clears the "allowed_groups" edge to the Group entity. +func (m *StoragePolicyMutation) ClearAllowedGroups() { + m.clearedallowed_groups = true +} + +// AllowedGroupsCleared reports if the "allowed_groups" edge to the Group entity was cleared. +func (m *StoragePolicyMutation) AllowedGroupsCleared() bool { + return m.clearedallowed_groups +} + +// RemoveAllowedGroupIDs removes the "allowed_groups" edge to the Group entity by IDs. +func (m *StoragePolicyMutation) RemoveAllowedGroupIDs(ids ...int) { + if m.removedallowed_groups == nil { + m.removedallowed_groups = make(map[int]struct{}) + } + for i := range ids { + delete(m.allowed_groups, ids[i]) + m.removedallowed_groups[ids[i]] = struct{}{} + } +} + +// RemovedAllowedGroups returns the removed IDs of the "allowed_groups" edge to the Group entity. +func (m *StoragePolicyMutation) RemovedAllowedGroupsIDs() (ids []int) { + for id := range m.removedallowed_groups { + ids = append(ids, id) + } + return +} + +// AllowedGroupsIDs returns the "allowed_groups" edge IDs in the mutation. +func (m *StoragePolicyMutation) AllowedGroupsIDs() (ids []int) { + for id := range m.allowed_groups { + ids = append(ids, id) + } + return +} + +// ResetAllowedGroups resets all changes to the "allowed_groups" edge. +func (m *StoragePolicyMutation) ResetAllowedGroups() { + m.allowed_groups = nil + m.clearedallowed_groups = false + m.removedallowed_groups = nil +} + // ClearNode clears the "node" edge to the Node entity. func (m *StoragePolicyMutation) ClearNode() { m.clearednode = true @@ -15762,7 +15902,7 @@ func (m *StoragePolicyMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *StoragePolicyMutation) AddedEdges() []string { - edges := make([]string, 0, 4) + edges := make([]string, 0, 5) if m.groups != nil { edges = append(edges, storagepolicy.EdgeGroups) } @@ -15772,6 +15912,9 @@ func (m *StoragePolicyMutation) AddedEdges() []string { if m.entities != nil { edges = append(edges, storagepolicy.EdgeEntities) } + if m.allowed_groups != nil { + edges = append(edges, storagepolicy.EdgeAllowedGroups) + } if m.node != nil { edges = append(edges, storagepolicy.EdgeNode) } @@ -15800,6 +15943,12 @@ func (m *StoragePolicyMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case storagepolicy.EdgeAllowedGroups: + ids := make([]ent.Value, 0, len(m.allowed_groups)) + for id := range m.allowed_groups { + ids = append(ids, id) + } + return ids case storagepolicy.EdgeNode: if id := m.node; id != nil { return []ent.Value{*id} @@ -15810,7 +15959,7 @@ func (m *StoragePolicyMutation) AddedIDs(name string) []ent.Value { // RemovedEdges returns all edge names that were removed in this mutation. func (m *StoragePolicyMutation) RemovedEdges() []string { - edges := make([]string, 0, 4) + edges := make([]string, 0, 5) if m.removedgroups != nil { edges = append(edges, storagepolicy.EdgeGroups) } @@ -15820,6 +15969,9 @@ func (m *StoragePolicyMutation) RemovedEdges() []string { if m.removedentities != nil { edges = append(edges, storagepolicy.EdgeEntities) } + if m.removedallowed_groups != nil { + edges = append(edges, storagepolicy.EdgeAllowedGroups) + } return edges } @@ -15845,13 +15997,19 @@ func (m *StoragePolicyMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case storagepolicy.EdgeAllowedGroups: + ids := make([]ent.Value, 0, len(m.removedallowed_groups)) + for id := range m.removedallowed_groups { + ids = append(ids, id) + } + return ids } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *StoragePolicyMutation) ClearedEdges() []string { - edges := make([]string, 0, 4) + edges := make([]string, 0, 5) if m.clearedgroups { edges = append(edges, storagepolicy.EdgeGroups) } @@ -15861,6 +16019,9 @@ func (m *StoragePolicyMutation) ClearedEdges() []string { if m.clearedentities { edges = append(edges, storagepolicy.EdgeEntities) } + if m.clearedallowed_groups { + edges = append(edges, storagepolicy.EdgeAllowedGroups) + } if m.clearednode { edges = append(edges, storagepolicy.EdgeNode) } @@ -15877,6 +16038,8 @@ func (m *StoragePolicyMutation) EdgeCleared(name string) bool { return m.clearedfiles case storagepolicy.EdgeEntities: return m.clearedentities + case storagepolicy.EdgeAllowedGroups: + return m.clearedallowed_groups case storagepolicy.EdgeNode: return m.clearednode } @@ -15907,6 +16070,9 @@ func (m *StoragePolicyMutation) ResetEdge(name string) error { case storagepolicy.EdgeEntities: m.ResetEntities() return nil + case storagepolicy.EdgeAllowedGroups: + m.ResetAllowedGroups() + return nil case storagepolicy.EdgeNode: m.ResetNode() return nil diff --git a/ent/schema/group.go b/ent/schema/group.go index ca1613b8..1262062e 100644 --- a/ent/schema/group.go +++ b/ent/schema/group.go @@ -41,5 +41,8 @@ func (Group) Edges() []ent.Edge { Ref("groups"). Field("storage_policy_id"). Unique(), + // allowed_policies is the pool of storage policies members may pick + // from. Empty means the single storage_policies default applies. + edge.To("allowed_policies", StoragePolicy.Type), } } diff --git a/ent/schema/policy.go b/ent/schema/policy.go index 6b24a261..2ebf9201 100644 --- a/ent/schema/policy.go +++ b/ent/schema/policy.go @@ -54,6 +54,8 @@ func (StoragePolicy) Edges() []ent.Edge { edge.To("groups", Group.Type), edge.To("files", File.Type), edge.To("entities", Entity.Type), + edge.From("allowed_groups", Group.Type). + Ref("allowed_policies"), edge.From("node", Node.Type). Ref("storage_policy"). Field("node_id"). diff --git a/ent/storagepolicy.go b/ent/storagepolicy.go index 7209dcb7..c0cb3be5 100644 --- a/ent/storagepolicy.go +++ b/ent/storagepolicy.go @@ -66,11 +66,13 @@ type StoragePolicyEdges struct { Files []*File `json:"files,omitempty"` // Entities holds the value of the entities edge. Entities []*Entity `json:"entities,omitempty"` + // AllowedGroups holds the value of the allowed_groups edge. + AllowedGroups []*Group `json:"allowed_groups,omitempty"` // Node holds the value of the node edge. Node *Node `json:"node,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [4]bool + loadedTypes [5]bool } // GroupsOrErr returns the Groups value or an error if the edge @@ -100,10 +102,19 @@ func (e StoragePolicyEdges) EntitiesOrErr() ([]*Entity, error) { return nil, &NotLoadedError{edge: "entities"} } +// AllowedGroupsOrErr returns the AllowedGroups value or an error if the edge +// was not loaded in eager-loading. +func (e StoragePolicyEdges) AllowedGroupsOrErr() ([]*Group, error) { + if e.loadedTypes[3] { + return e.AllowedGroups, nil + } + return nil, &NotLoadedError{edge: "allowed_groups"} +} + // NodeOrErr returns the Node value or an error if the edge // was not loaded in eager-loading, or loaded but was not found. func (e StoragePolicyEdges) NodeOrErr() (*Node, error) { - if e.loadedTypes[3] { + if e.loadedTypes[4] { if e.Node == nil { // Edge was loaded but was not found. return nil, &NotFoundError{label: node.Label} @@ -276,6 +287,11 @@ func (sp *StoragePolicy) QueryEntities() *EntityQuery { return NewStoragePolicyClient(sp.config).QueryEntities(sp) } +// QueryAllowedGroups queries the "allowed_groups" edge of the StoragePolicy entity. +func (sp *StoragePolicy) QueryAllowedGroups() *GroupQuery { + return NewStoragePolicyClient(sp.config).QueryAllowedGroups(sp) +} + // QueryNode queries the "node" edge of the StoragePolicy entity. func (sp *StoragePolicy) QueryNode() *NodeQuery { return NewStoragePolicyClient(sp.config).QueryNode(sp) @@ -375,10 +391,16 @@ func (e *StoragePolicy) SetEntities(v []*Entity) { e.Edges.loadedTypes[2] = true } +// SetAllowedGroups manually set the edge as loaded state. +func (e *StoragePolicy) SetAllowedGroups(v []*Group) { + e.Edges.AllowedGroups = v + e.Edges.loadedTypes[3] = true +} + // SetNode manually set the edge as loaded state. func (e *StoragePolicy) SetNode(v *Node) { e.Edges.Node = v - e.Edges.loadedTypes[3] = true + e.Edges.loadedTypes[4] = true } // StoragePolicies is a parsable slice of StoragePolicy. diff --git a/ent/storagepolicy/storagepolicy.go b/ent/storagepolicy/storagepolicy.go index 91a4d021..6bd299eb 100644 --- a/ent/storagepolicy/storagepolicy.go +++ b/ent/storagepolicy/storagepolicy.go @@ -55,6 +55,8 @@ const ( EdgeFiles = "files" // EdgeEntities holds the string denoting the entities edge name in mutations. EdgeEntities = "entities" + // EdgeAllowedGroups holds the string denoting the allowed_groups edge name in mutations. + EdgeAllowedGroups = "allowed_groups" // EdgeNode holds the string denoting the node edge name in mutations. EdgeNode = "node" // Table holds the table name of the storagepolicy in the database. @@ -80,6 +82,11 @@ const ( EntitiesInverseTable = "entities" // EntitiesColumn is the table column denoting the entities relation/edge. EntitiesColumn = "storage_policy_entities" + // AllowedGroupsTable is the table that holds the allowed_groups relation/edge. The primary key declared below. + AllowedGroupsTable = "group_allowed_policies" + // AllowedGroupsInverseTable is the table name for the Group entity. + // It exists in this package in order to avoid circular dependency with the "group" package. + AllowedGroupsInverseTable = "groups" // NodeTable is the table that holds the node relation/edge. NodeTable = "storage_policies" // NodeInverseTable is the table name for the Node entity. @@ -110,6 +117,12 @@ var Columns = []string{ FieldNodeID, } +var ( + // AllowedGroupsPrimaryKey and AllowedGroupsColumn2 are the table columns denoting the + // primary key for the allowed_groups relation (M2M). + AllowedGroupsPrimaryKey = []string{"group_id", "storage_policy_id"} +) + // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { for i := range Columns { @@ -289,6 +302,20 @@ func ByEntities(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { } } +// ByAllowedGroupsCount orders the results by allowed_groups count. +func ByAllowedGroupsCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newAllowedGroupsStep(), opts...) + } +} + +// ByAllowedGroups orders the results by allowed_groups terms. +func ByAllowedGroups(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newAllowedGroupsStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} + // ByNodeField orders the results by node field. func ByNodeField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -316,6 +343,13 @@ func newEntitiesStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.O2M, false, EntitiesTable, EntitiesColumn), ) } +func newAllowedGroupsStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(AllowedGroupsInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, AllowedGroupsTable, AllowedGroupsPrimaryKey...), + ) +} func newNodeStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/ent/storagepolicy/where.go b/ent/storagepolicy/where.go index d73e63ee..35e43c4a 100644 --- a/ent/storagepolicy/where.go +++ b/ent/storagepolicy/where.go @@ -1034,6 +1034,29 @@ func HasEntitiesWith(preds ...predicate.Entity) predicate.StoragePolicy { }) } +// HasAllowedGroups applies the HasEdge predicate on the "allowed_groups" edge. +func HasAllowedGroups() predicate.StoragePolicy { + return predicate.StoragePolicy(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, AllowedGroupsTable, AllowedGroupsPrimaryKey...), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasAllowedGroupsWith applies the HasEdge predicate on the "allowed_groups" edge with a given conditions (other predicates). +func HasAllowedGroupsWith(preds ...predicate.Group) predicate.StoragePolicy { + return predicate.StoragePolicy(func(s *sql.Selector) { + step := newAllowedGroupsStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // HasNode applies the HasEdge predicate on the "node" edge. func HasNode() predicate.StoragePolicy { return predicate.StoragePolicy(func(s *sql.Selector) { diff --git a/ent/storagepolicy_create.go b/ent/storagepolicy_create.go index 9581c8b8..5ee6d4d5 100644 --- a/ent/storagepolicy_create.go +++ b/ent/storagepolicy_create.go @@ -272,6 +272,21 @@ func (spc *StoragePolicyCreate) AddEntities(e ...*Entity) *StoragePolicyCreate { return spc.AddEntityIDs(ids...) } +// AddAllowedGroupIDs adds the "allowed_groups" edge to the Group entity by IDs. +func (spc *StoragePolicyCreate) AddAllowedGroupIDs(ids ...int) *StoragePolicyCreate { + spc.mutation.AddAllowedGroupIDs(ids...) + return spc +} + +// AddAllowedGroups adds the "allowed_groups" edges to the Group entity. +func (spc *StoragePolicyCreate) AddAllowedGroups(g ...*Group) *StoragePolicyCreate { + ids := make([]int, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return spc.AddAllowedGroupIDs(ids...) +} + // SetNode sets the "node" edge to the Node entity. func (spc *StoragePolicyCreate) SetNode(n *Node) *StoragePolicyCreate { return spc.SetNodeID(n.ID) @@ -503,6 +518,22 @@ func (spc *StoragePolicyCreate) createSpec() (*StoragePolicy, *sqlgraph.CreateSp } _spec.Edges = append(_spec.Edges, edge) } + if nodes := spc.mutation.AllowedGroupsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } if nodes := spc.mutation.NodeIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/ent/storagepolicy_query.go b/ent/storagepolicy_query.go index 4ee8142b..031c64e3 100644 --- a/ent/storagepolicy_query.go +++ b/ent/storagepolicy_query.go @@ -22,14 +22,15 @@ import ( // StoragePolicyQuery is the builder for querying StoragePolicy entities. type StoragePolicyQuery struct { config - ctx *QueryContext - order []storagepolicy.OrderOption - inters []Interceptor - predicates []predicate.StoragePolicy - withGroups *GroupQuery - withFiles *FileQuery - withEntities *EntityQuery - withNode *NodeQuery + ctx *QueryContext + order []storagepolicy.OrderOption + inters []Interceptor + predicates []predicate.StoragePolicy + withGroups *GroupQuery + withFiles *FileQuery + withEntities *EntityQuery + withAllowedGroups *GroupQuery + withNode *NodeQuery // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -132,6 +133,28 @@ func (spq *StoragePolicyQuery) QueryEntities() *EntityQuery { return query } +// QueryAllowedGroups chains the current query on the "allowed_groups" edge. +func (spq *StoragePolicyQuery) QueryAllowedGroups() *GroupQuery { + query := (&GroupClient{config: spq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := spq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := spq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(storagepolicy.Table, storagepolicy.FieldID, selector), + sqlgraph.To(group.Table, group.FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, storagepolicy.AllowedGroupsTable, storagepolicy.AllowedGroupsPrimaryKey...), + ) + fromU = sqlgraph.SetNeighbors(spq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // QueryNode chains the current query on the "node" edge. func (spq *StoragePolicyQuery) QueryNode() *NodeQuery { query := (&NodeClient{config: spq.config}).Query() @@ -341,15 +364,16 @@ func (spq *StoragePolicyQuery) Clone() *StoragePolicyQuery { return nil } return &StoragePolicyQuery{ - config: spq.config, - ctx: spq.ctx.Clone(), - order: append([]storagepolicy.OrderOption{}, spq.order...), - inters: append([]Interceptor{}, spq.inters...), - predicates: append([]predicate.StoragePolicy{}, spq.predicates...), - withGroups: spq.withGroups.Clone(), - withFiles: spq.withFiles.Clone(), - withEntities: spq.withEntities.Clone(), - withNode: spq.withNode.Clone(), + config: spq.config, + ctx: spq.ctx.Clone(), + order: append([]storagepolicy.OrderOption{}, spq.order...), + inters: append([]Interceptor{}, spq.inters...), + predicates: append([]predicate.StoragePolicy{}, spq.predicates...), + withGroups: spq.withGroups.Clone(), + withFiles: spq.withFiles.Clone(), + withEntities: spq.withEntities.Clone(), + withAllowedGroups: spq.withAllowedGroups.Clone(), + withNode: spq.withNode.Clone(), // clone intermediate query. sql: spq.sql.Clone(), path: spq.path, @@ -389,6 +413,17 @@ func (spq *StoragePolicyQuery) WithEntities(opts ...func(*EntityQuery)) *Storage return spq } +// WithAllowedGroups tells the query-builder to eager-load the nodes that are connected to +// the "allowed_groups" edge. The optional arguments are used to configure the query builder of the edge. +func (spq *StoragePolicyQuery) WithAllowedGroups(opts ...func(*GroupQuery)) *StoragePolicyQuery { + query := (&GroupClient{config: spq.config}).Query() + for _, opt := range opts { + opt(query) + } + spq.withAllowedGroups = query + return spq +} + // WithNode tells the query-builder to eager-load the nodes that are connected to // the "node" edge. The optional arguments are used to configure the query builder of the edge. func (spq *StoragePolicyQuery) WithNode(opts ...func(*NodeQuery)) *StoragePolicyQuery { @@ -478,10 +513,11 @@ func (spq *StoragePolicyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ( var ( nodes = []*StoragePolicy{} _spec = spq.querySpec() - loadedTypes = [4]bool{ + loadedTypes = [5]bool{ spq.withGroups != nil, spq.withFiles != nil, spq.withEntities != nil, + spq.withAllowedGroups != nil, spq.withNode != nil, } ) @@ -524,6 +560,13 @@ func (spq *StoragePolicyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ( return nil, err } } + if query := spq.withAllowedGroups; query != nil { + if err := spq.loadAllowedGroups(ctx, query, nodes, + func(n *StoragePolicy) { n.Edges.AllowedGroups = []*Group{} }, + func(n *StoragePolicy, e *Group) { n.Edges.AllowedGroups = append(n.Edges.AllowedGroups, e) }); err != nil { + return nil, err + } + } if query := spq.withNode; query != nil { if err := spq.loadNode(ctx, query, nodes, nil, func(n *StoragePolicy, e *Node) { n.Edges.Node = e }); err != nil { @@ -623,6 +666,67 @@ func (spq *StoragePolicyQuery) loadEntities(ctx context.Context, query *EntityQu } return nil } +func (spq *StoragePolicyQuery) loadAllowedGroups(ctx context.Context, query *GroupQuery, nodes []*StoragePolicy, init func(*StoragePolicy), assign func(*StoragePolicy, *Group)) error { + edgeIDs := make([]driver.Value, len(nodes)) + byID := make(map[int]*StoragePolicy) + nids := make(map[int]map[*StoragePolicy]struct{}) + for i, node := range nodes { + edgeIDs[i] = node.ID + byID[node.ID] = node + if init != nil { + init(node) + } + } + query.Where(func(s *sql.Selector) { + joinT := sql.Table(storagepolicy.AllowedGroupsTable) + s.Join(joinT).On(s.C(group.FieldID), joinT.C(storagepolicy.AllowedGroupsPrimaryKey[0])) + s.Where(sql.InValues(joinT.C(storagepolicy.AllowedGroupsPrimaryKey[1]), edgeIDs...)) + columns := s.SelectedColumns() + s.Select(joinT.C(storagepolicy.AllowedGroupsPrimaryKey[1])) + s.AppendSelect(columns...) + s.SetDistinct(false) + }) + if err := query.prepareQuery(ctx); err != nil { + return err + } + qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) { + assign := spec.Assign + values := spec.ScanValues + spec.ScanValues = func(columns []string) ([]any, error) { + values, err := values(columns[1:]) + if err != nil { + return nil, err + } + return append([]any{new(sql.NullInt64)}, values...), nil + } + spec.Assign = func(columns []string, values []any) error { + outValue := int(values[0].(*sql.NullInt64).Int64) + inValue := int(values[1].(*sql.NullInt64).Int64) + if nids[inValue] == nil { + nids[inValue] = map[*StoragePolicy]struct{}{byID[outValue]: {}} + return assign(columns[1:], values[1:]) + } + nids[inValue][byID[outValue]] = struct{}{} + return nil + } + }) + }) + neighbors, err := withInterceptors[[]*Group](ctx, query, qr, query.inters) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nids[n.ID] + if !ok { + return fmt.Errorf(`unexpected "allowed_groups" node returned %v`, n.ID) + } + for kn := range nodes { + assign(kn, n) + } + } + return nil +} func (spq *StoragePolicyQuery) loadNode(ctx context.Context, query *NodeQuery, nodes []*StoragePolicy, init func(*StoragePolicy), assign func(*StoragePolicy, *Node)) error { ids := make([]int, 0, len(nodes)) nodeids := make(map[int][]*StoragePolicy) diff --git a/ent/storagepolicy_update.go b/ent/storagepolicy_update.go index d0af7053..5a6ff297 100644 --- a/ent/storagepolicy_update.go +++ b/ent/storagepolicy_update.go @@ -345,6 +345,21 @@ func (spu *StoragePolicyUpdate) AddEntities(e ...*Entity) *StoragePolicyUpdate { return spu.AddEntityIDs(ids...) } +// AddAllowedGroupIDs adds the "allowed_groups" edge to the Group entity by IDs. +func (spu *StoragePolicyUpdate) AddAllowedGroupIDs(ids ...int) *StoragePolicyUpdate { + spu.mutation.AddAllowedGroupIDs(ids...) + return spu +} + +// AddAllowedGroups adds the "allowed_groups" edges to the Group entity. +func (spu *StoragePolicyUpdate) AddAllowedGroups(g ...*Group) *StoragePolicyUpdate { + ids := make([]int, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return spu.AddAllowedGroupIDs(ids...) +} + // SetNode sets the "node" edge to the Node entity. func (spu *StoragePolicyUpdate) SetNode(n *Node) *StoragePolicyUpdate { return spu.SetNodeID(n.ID) @@ -418,6 +433,27 @@ func (spu *StoragePolicyUpdate) RemoveEntities(e ...*Entity) *StoragePolicyUpdat return spu.RemoveEntityIDs(ids...) } +// ClearAllowedGroups clears all "allowed_groups" edges to the Group entity. +func (spu *StoragePolicyUpdate) ClearAllowedGroups() *StoragePolicyUpdate { + spu.mutation.ClearAllowedGroups() + return spu +} + +// RemoveAllowedGroupIDs removes the "allowed_groups" edge to Group entities by IDs. +func (spu *StoragePolicyUpdate) RemoveAllowedGroupIDs(ids ...int) *StoragePolicyUpdate { + spu.mutation.RemoveAllowedGroupIDs(ids...) + return spu +} + +// RemoveAllowedGroups removes "allowed_groups" edges to Group entities. +func (spu *StoragePolicyUpdate) RemoveAllowedGroups(g ...*Group) *StoragePolicyUpdate { + ids := make([]int, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return spu.RemoveAllowedGroupIDs(ids...) +} + // ClearNode clears the "node" edge to the Node entity. func (spu *StoragePolicyUpdate) ClearNode() *StoragePolicyUpdate { spu.mutation.ClearNode() @@ -698,6 +734,51 @@ func (spu *StoragePolicyUpdate) sqlSave(ctx context.Context) (n int, err error) } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if spu.mutation.AllowedGroupsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := spu.mutation.RemovedAllowedGroupsIDs(); len(nodes) > 0 && !spu.mutation.AllowedGroupsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := spu.mutation.AllowedGroupsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if spu.mutation.NodeCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -1059,6 +1140,21 @@ func (spuo *StoragePolicyUpdateOne) AddEntities(e ...*Entity) *StoragePolicyUpda return spuo.AddEntityIDs(ids...) } +// AddAllowedGroupIDs adds the "allowed_groups" edge to the Group entity by IDs. +func (spuo *StoragePolicyUpdateOne) AddAllowedGroupIDs(ids ...int) *StoragePolicyUpdateOne { + spuo.mutation.AddAllowedGroupIDs(ids...) + return spuo +} + +// AddAllowedGroups adds the "allowed_groups" edges to the Group entity. +func (spuo *StoragePolicyUpdateOne) AddAllowedGroups(g ...*Group) *StoragePolicyUpdateOne { + ids := make([]int, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return spuo.AddAllowedGroupIDs(ids...) +} + // SetNode sets the "node" edge to the Node entity. func (spuo *StoragePolicyUpdateOne) SetNode(n *Node) *StoragePolicyUpdateOne { return spuo.SetNodeID(n.ID) @@ -1132,6 +1228,27 @@ func (spuo *StoragePolicyUpdateOne) RemoveEntities(e ...*Entity) *StoragePolicyU return spuo.RemoveEntityIDs(ids...) } +// ClearAllowedGroups clears all "allowed_groups" edges to the Group entity. +func (spuo *StoragePolicyUpdateOne) ClearAllowedGroups() *StoragePolicyUpdateOne { + spuo.mutation.ClearAllowedGroups() + return spuo +} + +// RemoveAllowedGroupIDs removes the "allowed_groups" edge to Group entities by IDs. +func (spuo *StoragePolicyUpdateOne) RemoveAllowedGroupIDs(ids ...int) *StoragePolicyUpdateOne { + spuo.mutation.RemoveAllowedGroupIDs(ids...) + return spuo +} + +// RemoveAllowedGroups removes "allowed_groups" edges to Group entities. +func (spuo *StoragePolicyUpdateOne) RemoveAllowedGroups(g ...*Group) *StoragePolicyUpdateOne { + ids := make([]int, len(g)) + for i := range g { + ids[i] = g[i].ID + } + return spuo.RemoveAllowedGroupIDs(ids...) +} + // ClearNode clears the "node" edge to the Node entity. func (spuo *StoragePolicyUpdateOne) ClearNode() *StoragePolicyUpdateOne { spuo.mutation.ClearNode() @@ -1442,6 +1559,51 @@ func (spuo *StoragePolicyUpdateOne) sqlSave(ctx context.Context) (_node *Storage } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if spuo.mutation.AllowedGroupsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := spuo.mutation.RemovedAllowedGroupsIDs(); len(nodes) > 0 && !spuo.mutation.AllowedGroupsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := spuo.mutation.AllowedGroupsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: storagepolicy.AllowedGroupsTable, + Columns: storagepolicy.AllowedGroupsPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if spuo.mutation.NodeCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/frontend/public/locales/en-US/application.json b/frontend/public/locales/en-US/application.json index b6ecd211..c8a95579 100644 --- a/frontend/public/locales/en-US/application.json +++ b/frontend/public/locales/en-US/application.json @@ -488,7 +488,16 @@ "ftsIndexed": "Indexed", "ftsNotIndexed": "Not indexed", "lineEnding": "Line ending", - "downloadAll": "Download all" + "downloadAll": "Download all", + "dirPolicyTitle": "Storage Policy for Uploads", + "dirPolicyDes": "New files uploaded into \"{{name}}\" will use the selected storage policy.", + "relocateTitle": "Relocate Storage", + "relocateDes": "Move the physical content of \"{{name}}\" to the selected storage policy.", + "relocateSubmitted": "Relocation task submitted", + "relocateStart": "Relocate", + "policySaved": "Storage policy updated", + "policyInherit": "Inherit (use parent or group default)", + "policyGroupDefault": "Group default" }, "modals": { "includePasswordInShareLink": "Include password in share link", @@ -1056,7 +1065,12 @@ "shareDefaultPrivate": "Default share privacy", "shareDefaultPrivateDes": "New shares default to private (password protected).", "sharePrivateOn": "Private", - "sharePrivateOff": "Public" + "sharePrivateOff": "Public", + "preferredPolicy": "Preferred storage policy", + "preferredPolicyDes": "New uploads in your own files use this storage policy unless a folder overrides it.", + "policySaved": "Storage policy updated", + "policyInherit": "Inherit (group default)", + "policyGroupDefault": "Group default" }, "vas": { "points": "Points", diff --git a/frontend/public/locales/en-US/dashboard.json b/frontend/public/locales/en-US/dashboard.json index 7847aeb0..eefba071 100644 --- a/frontend/public/locales/en-US/dashboard.json +++ b/frontend/public/locales/en-US/dashboard.json @@ -1190,7 +1190,14 @@ "enableFileEncryptionDes": "When enabled, file blobs will be stored encrypted. Changing this setting only affects newly added files.", "encryptedFile": "Encrypted files", "status": "Status", - "statusDes": "Suspended policies keep serving existing files but reject new uploads." + "statusDes": "Suspended policies keep serving existing files but reject new uploads.", + "loadBalance": "Load Balance", + "lbChildren": "Child storage policies", + "lbChildPolicy": "Storage policy", + "lbWeight": "Weight", + "lbAddChild": "Add child policy", + "lbChildrenDes": "New uploads are distributed across child policies proportionally to their weight.", + "lbWizardDes": "A load-balance policy distributes uploads across weighted child policies. Children are configured after creation." }, "node": { "slave": "slave", @@ -1373,7 +1380,9 @@ "notCopy": "Not copy", "confirmDelete": "Are you sure you want to delete group {{group}}?", "new": "New group", - "editGroup": "Edit {{group}}" + "editGroup": "Edit {{group}}", + "switchablePolicies": "Switchable storage policies", + "switchablePoliciesDes": "Policies members of this group can freely switch to. The default policy above is always available." }, "user": { "createdAt": "Created at", @@ -1446,8 +1455,7 @@ "invitationCodeCopy": "Copy", "invitationCodeDefaultGroup": "Default", "noInvitationCodes": "No invitation codes yet.", - "loadMore": "Load more", - "createdAt": "Created at" + "loadMore": "Load more" }, "file": { "deleteXFiles": "Delete {{num}} files", diff --git a/frontend/public/locales/zh-CN/application.json b/frontend/public/locales/zh-CN/application.json index 6ab60abc..197ce275 100644 --- a/frontend/public/locales/zh-CN/application.json +++ b/frontend/public/locales/zh-CN/application.json @@ -488,7 +488,16 @@ "ftsIndexed": "已索引", "ftsNotIndexed": "未索引", "lineEnding": "换行符", - "downloadAll": "全部下载" + "downloadAll": "全部下载", + "dirPolicyTitle": "上传存储策略", + "dirPolicyDes": "上传到「{{name}}」的新文件将使用所选的存储策略。", + "relocateTitle": "迁移存储", + "relocateDes": "将「{{name}}」的物理内容迁移到所选存储策略。", + "relocateSubmitted": "迁移任务已提交", + "relocateStart": "迁移", + "policySaved": "存储策略已更新", + "policyInherit": "继承(使用父目录或用户组默认)", + "policyGroupDefault": "用户组默认" }, "modals": { "includePasswordInShareLink": "在链接中包含密码", @@ -1056,7 +1065,12 @@ "shareDefaultPrivate": "默认分享隐私", "shareDefaultPrivateDes": "新建分享默认启用私密分享(密码保护)。", "sharePrivateOn": "私密", - "sharePrivateOff": "公开" + "sharePrivateOff": "公开", + "preferredPolicy": "首选存储策略", + "preferredPolicyDes": "在您自己的文件中上传新文件时使用此存储策略,除非某个目录单独指定。", + "policySaved": "存储策略已更新", + "policyInherit": "继承(用户组默认)", + "policyGroupDefault": "用户组默认" }, "vas": { "points": "积分", diff --git a/frontend/public/locales/zh-CN/dashboard.json b/frontend/public/locales/zh-CN/dashboard.json index 735d6670..1a896ff5 100644 --- a/frontend/public/locales/zh-CN/dashboard.json +++ b/frontend/public/locales/zh-CN/dashboard.json @@ -1190,7 +1190,14 @@ "enableFileEncryptionDes": "开启后,文件 Blob 会被加密存储。更改此设置只对新增的文件有效。", "encryptedFile": "加密的文件", "status": "状态", - "statusDes": "已暂停的存储策略仍可读取已有文件,但会拒绝新的上传。" + "statusDes": "已暂停的存储策略仍可读取已有文件,但会拒绝新的上传。", + "loadBalance": "负载均衡", + "lbChildren": "子存储策略", + "lbChildPolicy": "存储策略", + "lbWeight": "权重", + "lbAddChild": "添加子策略", + "lbChildrenDes": "新上传将按权重比例分配到各子存储策略。", + "lbWizardDes": "负载均衡策略按权重将上传分配到子策略,创建后再配置子策略。" }, "node": { "slave": "从机", @@ -1373,10 +1380,12 @@ "notCopy": "不复制", "confirmDelete": "确认要删除用户组 {{group}}?", "new": "新建用户组", - "editGroup": "编辑 {{group}}" + "editGroup": "编辑 {{group}}", + "switchablePolicies": "可切换存储策略", + "switchablePoliciesDes": "该组成员可以自由切换使用的存储策略。上方默认策略始终可用。" }, "user": { - "createdAt": "创建日期", + "createdAt": "创建时间", "originUserGroup": "原始用户组", "originUserGroupDes": "用户在购买用户组前所属的用户组,当前用户组到期后会回退到此用户组。", "noOriginUserGroup": "无", @@ -1446,8 +1455,7 @@ "invitationCodeCopy": "复制", "invitationCodeDefaultGroup": "默认组", "noInvitationCodes": "暂无邀请码。", - "loadMore": "加载更多", - "createdAt": "创建时间" + "loadMore": "加载更多" }, "file": { "deleteXFiles": "删除 {{num}} 个文件", diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a42dc246..f6ade666 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -54,6 +54,8 @@ import { DeleteFileService, DeleteUploadSessionService, DirectLink, + FileRelocateResponse, + FileRelocateService, FileResponse, FileThumbResponse, FileUpdateService, @@ -67,9 +69,11 @@ import { PatchMetadataService, PatchViewSyncService, PinFileService, + PreferredPolicyService, RenameFileService, Share, ShareCreateService, + StoragePolicyBrief, UnlockFileService, UploadCredential, UploadSessionRequest, @@ -526,6 +530,56 @@ export function sendMetadataPatch(req: PatchMetadataService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/policy", + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function setPreferredPolicy(req: PreferredPolicyService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/policy", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function relocateToPolicy(req: FileRelocateService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/relocate", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + export function getSearchUser(keyword: string): ThunkResponse { return async (dispatch, _getState) => { return await dispatch( diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts index 02c73547..57cb51ca 100644 --- a/frontend/src/api/dashboard.ts +++ b/frontend/src/api/dashboard.ts @@ -47,6 +47,7 @@ export interface GroupEnt extends CommonMixin { permissions?: string; edges: { storage_policies?: StoragePolicy; + allowed_policies?: StoragePolicy[]; }; total_users?: number; settings?: GroupSetting; @@ -263,6 +264,12 @@ export interface PolicySetting { qiniu_upload_cdn?: boolean; chunk_concurrency?: number; encryption?: boolean; + lb_policies?: LBPolicyRef[]; +} + +export interface LBPolicyRef { + policy: number; + weight?: number; } export interface User extends CommonMixin { diff --git a/frontend/src/api/explorer.ts b/frontend/src/api/explorer.ts index b85f4d35..add3c502 100644 --- a/frontend/src/api/explorer.ts +++ b/frontend/src/api/explorer.ts @@ -176,6 +176,7 @@ export const Metadata = { restore_uri: "sys:restore_uri", expected_collect_time: "sys:expected_collect_time", full_text_index: "sys:fulltext_index", + preferred_policy: "sys:preferred_policy", // Exif gps_lng: "exif:longitude", @@ -641,3 +642,26 @@ export interface FullTextSearchResult { file: FileResponse; content: string; } + +// StoragePolicyBrief is the user-facing view of a storage policy the current +// group is allowed to use. +export interface StoragePolicyBrief { + id: string; + name: string; + type: string; + is_default?: boolean; +} + +export interface PreferredPolicyService { + uri: string; + policy: string; +} + +export interface FileRelocateService { + uri: string; + policy: string; +} + +export interface FileRelocateResponse { + id: string; +} diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 7b5c7a96..47f3bfe4 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -146,6 +146,7 @@ export interface UserSettings { share_default_private?: boolean; preferred_viewers?: Record; trash_retention?: number; + preferred_policy?: string; oauth_grants?: OAuthGrant[]; } @@ -175,6 +176,8 @@ export interface PatchUserSetting { preferred_viewers?: Record; // Trash retention override in seconds; 0 inherits the group setting. trash_retention?: number; + // Preferred storage policy hash ID; "" inherits the group default. + preferred_policy?: string; } export interface PasskeyCredentialOption { diff --git a/frontend/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx b/frontend/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx index 8ef1a904..36278e62 100644 --- a/frontend/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx +++ b/frontend/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx @@ -11,6 +11,7 @@ import SettingForm from "../../../Pages/Setting/SettingForm"; import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; import { AnonymousGroupID } from "../GroupRow"; import { GroupSettingContext } from "./GroupSettingWrapper"; +import PolicyMultiSelectionInput from "./PolicyMultiSelectionInput"; import PolicySelectionInput from "./PolicySelectionInput"; const BasicInfoSection = () => { const { t } = useTranslation("dashboard"); @@ -37,6 +38,16 @@ const BasicInfoSection = () => { [setGroup], ); + const onAllowedPoliciesChange = useCallback( + (value: number[]) => { + setGroup((p: GroupEnt) => ({ + ...p, + edges: { ...p.edges, allowed_policies: value.map((id) => ({ id }) as StoragePolicy) }, + })); + }, + [setGroup], + ); + const onMaxStorageChange = useCallback( (size: number) => { setGroup((p: GroupEnt) => ({ @@ -108,6 +119,13 @@ const BasicInfoSection = () => { {t("group.availablePolicyDesPro")} + + p.id)} + onChange={onAllowedPoliciesChange} + /> + {t("group.switchablePoliciesDes")} + { storage_policies: { id: group.edges.storage_policies?.id ?? 0, } as StoragePolicy, + allowed_policies: (group.edges.allowed_policies ?? []).map((p) => ({ id: p.id }) as StoragePolicy), }, }; }; const GroupSettingWrapper = ({ groupID, children, onGroupChange }: GroupSettingWrapperProps) => { const dispatch = useAppDispatch(); - const { t } = useTranslation("dashboard"); const [values, setValues] = useState({ ...defaultGroup, }); diff --git a/frontend/src/component/Admin/Group/EditGroup/PolicyMultiSelectionInput.tsx b/frontend/src/component/Admin/Group/EditGroup/PolicyMultiSelectionInput.tsx new file mode 100644 index 00000000..b07c101a --- /dev/null +++ b/frontend/src/component/Admin/Group/EditGroup/PolicyMultiSelectionInput.tsx @@ -0,0 +1,82 @@ +import { Box, Checkbox, FormControl, ListItemText, SelectChangeEvent } from "@mui/material"; +import { useEffect, useState } from "react"; +import { getStoragePolicyList } from "../../../../api/api"; +import { StoragePolicy } from "../../../../api/dashboard"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { DenseSelect, SquareChip } from "../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu"; + +export interface PolicyMultiSelectionInputProps { + value: number[]; + onChange: (value: number[]) => void; +} + +// PolicyMultiSelectionInput picks the pool of storage policies members of a +// group may freely switch between. The group's default policy is always +// available implicitly and need not be selected here. +const PolicyMultiSelectionInput = ({ value, onChange }: PolicyMultiSelectionInputProps) => { + const dispatch = useAppDispatch(); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(false); + const [policyMap, setPolicyMap] = useState>({}); + + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value: v }, + } = event; + onChange(typeof v === "string" ? v.split(",").map((x) => parseInt(x)) : (v as number[])); + }; + + useEffect(() => { + setLoading(true); + dispatch(getStoragePolicyList({ page: 1, page_size: 1000, order_by: "id", order_direction: "asc" })) + .then((res) => { + setPolicies(res.policies); + setPolicyMap( + res.policies.reduce( + (acc, policy) => { + acc[policy.id] = policy; + return acc; + }, + {} as Record, + ), + ); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( + + ( + + {(selected as number[]).map((id) => ( + + ))} + + )} + > + {policies.map((p) => ( + + -1} /> + + + ))} + + + ); +}; + +export default PolicyMultiSelectionInput; diff --git a/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/LoadBalanceSection.tsx b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/LoadBalanceSection.tsx new file mode 100644 index 00000000..1e7d348c --- /dev/null +++ b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/LoadBalanceSection.tsx @@ -0,0 +1,160 @@ +import { + Box, + FormControl, + IconButton, + SelectChangeEvent, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getStoragePolicyList } from "../../../../../api/api"; +import { LBPolicyRef, StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu"; +import Dismiss from "../../../../Icons/Dismiss"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; + +// LoadBalanceSection edits the weighted child policies of a load_balance +// policy. Only concrete (non-load-balance) policies can be children. +const LoadBalanceSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + const dispatch = useAppDispatch(); + const [policies, setPolicies] = useState([]); + + const isLB = values.type === PolicyType.load_balance; + const refs = values.settings?.lb_policies ?? []; + + useEffect(() => { + if (!isLB) { + return; + } + dispatch(getStoragePolicyList({ page: 1, page_size: 1000, order_by: "id", order_direction: "asc" })).then( + (res) => { + setPolicies(res.policies.filter((p) => p.type !== PolicyType.load_balance && p.id !== values.id)); + }, + ); + }, [isLB]); + + const setRefs = useCallback( + (next: LBPolicyRef[]) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, lb_policies: next }, + })); + }, + [setPolicy], + ); + + const addChild = useCallback(() => { + const used = new Set(refs.map((r) => r.policy)); + const candidate = policies.find((p) => !used.has(p.id)); + if (!candidate) { + return; + } + setRefs([...refs, { policy: candidate.id, weight: 1 }]); + }, [refs, policies, setRefs]); + + const onChildChange = useCallback( + (index: number, e: SelectChangeEvent) => { + const next = refs.slice(); + next[index] = { ...next[index], policy: e.target.value as number }; + setRefs(next); + }, + [refs, setRefs], + ); + + const onWeightChange = useCallback( + (index: number, v: string) => { + const w = parseInt(v); + const next = refs.slice(); + next[index] = { ...next[index], weight: isNaN(w) ? undefined : Math.max(1, w) }; + setRefs(next); + }, + [refs, setRefs], + ); + + const removeChild = useCallback( + (index: number) => { + setRefs(refs.filter((_, i) => i !== index)); + }, + [refs, setRefs], + ); + + if (!isLB) { + return null; + } + + return ( + + + {t("policy.loadBalance")} + + + + + + + {t("policy.lbChildPolicy")} + {t("policy.lbWeight")} + + + + + {refs.map((r, i) => ( + + + + onChildChange(i, e)}> + {policies.map((p) => ( + xi !== i && x.policy === p.id)} + > + {p.name} + + ))} + + + + + onWeightChange(i, e.target.value)} + /> + + + removeChild(i)}> + + + + + ))} + +
+ + = policies.length}> + {t("policy.lbAddChild")} + + + {t("policy.lbChildrenDes")} +
+
+
+ ); +}; + +export default LoadBalanceSection; diff --git a/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts index 993283e6..cecb8464 100644 --- a/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts +++ b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts @@ -1,4 +1,5 @@ export { default as BasicInfoSection } from "./BasicInfoSection"; +export { default as LoadBalanceSection } from "./LoadBalanceSection"; export { default as DownloadSection } from "./DownloadSection"; export { default as EncryptionSection } from "./EncryptionSection"; export * from "./magicVars"; diff --git a/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx index b703b9a8..e701ff2b 100644 --- a/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx +++ b/frontend/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx @@ -6,6 +6,7 @@ import { BasicInfoSection, DownloadSection, EncryptionSection, + LoadBalanceSection, MediaMetadataSection, StorageAndUploadSection, ThumbnailsSection, @@ -29,6 +30,7 @@ const StoragePolicyForm = () => { )} + diff --git a/frontend/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx b/frontend/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx index e5fff526..15cb23ad 100644 --- a/frontend/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx +++ b/frontend/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx @@ -21,6 +21,7 @@ import AddWizardDialog, { AddWizardProps } from "./AddWizardDialog"; import SelectProvider from "./SelectProvider"; import StoragePolicyCard from "./StoragePolicyCard"; import CosWizard from "./Wizards/COS/CosWizard"; +import LBWizard from "./Wizards/LB/LBWizard"; import LocalWizard from "./Wizards/Local/LocalWizard"; import ObsWizard from "./Wizards/OBS/ObsWizard"; import OneDriveWizard from "./Wizards/OneDrive/OneDriveWizard"; @@ -77,6 +78,7 @@ export const PolicyPropsMap: Record = { name: "policy.load_balance", img: "/static/img/lb.svg", wizardSize: "sm", + wizard: LBWizard, }, [PolicyType.remote]: { name: "policy.remote", diff --git a/frontend/src/component/Admin/StoragePolicy/Wizards/LB/LBWizard.tsx b/frontend/src/component/Admin/StoragePolicy/Wizards/LB/LBWizard.tsx new file mode 100644 index 00000000..aacaa4de --- /dev/null +++ b/frontend/src/component/Admin/StoragePolicy/Wizards/LB/LBWizard.tsx @@ -0,0 +1,52 @@ +import { Button } from "@mui/material"; +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; + +// LBWizard creates a bare load_balance policy; weighted children are bound on +// the policy edit page. +const LBWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + name: "", + type: PolicyType.load_balance, + settings: { + lb_policies: [], + }, + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.lbWizardDes")} + + +
+ ); +}; + +export default LBWizard; diff --git a/frontend/src/component/FileManager/ContextMenu/MoreMenuItems.tsx b/frontend/src/component/FileManager/ContextMenu/MoreMenuItems.tsx index 848e94c2..3f18f75d 100644 --- a/frontend/src/component/FileManager/ContextMenu/MoreMenuItems.tsx +++ b/frontend/src/component/FileManager/ContextMenu/MoreMenuItems.tsx @@ -7,12 +7,15 @@ import { setCreateArchiveDialog, setDirectLinkManagementDialog, setManageShareDialog, + setStoragePolicyDialog, setVersionControlDialog, } from "../../../redux/globalStateSlice.ts"; import { useAppDispatch } from "../../../redux/hooks.ts"; import { resetThumbnails } from "../../../redux/thunks/file.ts"; import Archive from "../../Icons/Archive.tsx"; +import BoxMultiple from "../../Icons/BoxMultiple.tsx"; import BranchForkLink from "../../Icons/BranchForkLink.tsx"; +import CloudArrowIUp from "../../Icons/CloudArrowIUp.tsx"; import HistoryOutlined from "../../Icons/HistoryOutlined.tsx"; import ImageArrowCounterclockwise from "../../Icons/ImageAarowCounterclockwise.tsx"; import LinkSetting from "../../Icons/LinkSetting.tsx"; @@ -25,7 +28,7 @@ const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const onClick = useCallback( - (f: () => any) => () => { + (f: () => void) => () => { f(); if (rootPopupState) { rootPopupState.close(); @@ -126,6 +129,42 @@ const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { {t("application:fileManager.createArchive")} )} + {displayOpt.showDirPolicy && ( + + dispatch( + setStoragePolicyDialog({ + open: true, + mode: "dir", + file: targets[0], + }), + ), + )} + > + + + + {t("application:fileManager.dirPolicyTitle")} + + )} + {displayOpt.showRelocate && ( + + dispatch( + setStoragePolicyDialog({ + open: true, + mode: "relocate", + file: targets[0], + }), + ), + )} + > + + + + {t("application:fileManager.relocateTitle")} + + )} {displayOpt.showResetThumb && ( dispatch(resetThumbnails(targets)))}> diff --git a/frontend/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts b/frontend/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts index 0fde3ef8..3572cd40 100644 --- a/frontend/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts +++ b/frontend/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts @@ -103,6 +103,8 @@ export interface DisplayOption { showAcl?: boolean; showCreateArchive?: boolean; showResetThumb?: boolean; + showDirPolicy?: boolean; + showRelocate?: boolean; andCapability?: Boolset; orCapability?: Boolset; @@ -320,6 +322,15 @@ export const getActionOpt = ( display.orCapability && display.hasReadable && canManageVersion(targets[0], display.orCapability); + // Storage-policy actions apply only to files the user owns; share visitors + // cannot re-home entities they do not own. + display.showDirPolicy = + targets.length == 1 && + !!currentUser && + targets[0].owned && + display.hasFolder && + !display.hasTrashFile; + display.showRelocate = targets.length == 1 && !!currentUser && targets[0].owned && !display.hasTrashFile; display.showManageShares = targets.length == 1 && targets[0].shared && diff --git a/frontend/src/component/FileManager/Dialogs/Dialogs.tsx b/frontend/src/component/FileManager/Dialogs/Dialogs.tsx index 79a2e6b7..208de619 100644 --- a/frontend/src/component/FileManager/Dialogs/Dialogs.tsx +++ b/frontend/src/component/FileManager/Dialogs/Dialogs.tsx @@ -9,6 +9,7 @@ import ChangeIcon from "./ChangeIcon.tsx"; import ShareDialog from "./Share/ShareDialog.tsx"; import VersionControl from "./VersionControl.tsx"; import ManageShares from "./Share/ManageShares.tsx"; +import StoragePolicy from "./StoragePolicy.tsx"; import AclDialog from "./Acl/AclDialog.tsx"; import StaleVersionConfirm from "./StaleVersionConfirm.tsx"; import SaveAs from "./SaveAs.tsx"; @@ -31,7 +32,6 @@ import CreateArchive from "./CreateArchive.tsx"; import ExtractArchive from "./ExtractArchive.tsx"; import CreateRemoteDownload from "./CreateRemoteDownload.tsx"; import AdvanceSearch from "../Search/AdvanceSearch/AdvanceSearch.tsx"; -import React from "react"; import ColumnSetting from "../Explorer/ListView/ColumnSetting.tsx"; import DirectLinks from "./DirectLinks/DirectLinks.tsx"; import DirectLinksControl from "./DirectLinksControl.tsx"; @@ -83,6 +83,7 @@ const Dialogs = () => { {directLink != undefined && } {excalidrawViewer != undefined && } {directLinkManagement != undefined && } + {archivePreview != undefined && } {desktopMountSetup != undefined && } diff --git a/frontend/src/component/FileManager/Dialogs/StoragePolicy.tsx b/frontend/src/component/FileManager/Dialogs/StoragePolicy.tsx new file mode 100644 index 00000000..823abd04 --- /dev/null +++ b/frontend/src/component/FileManager/Dialogs/StoragePolicy.tsx @@ -0,0 +1,117 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControl, + ListItemText, + SelectChangeEvent, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getAllowedPolicies, relocateToPolicy, setPreferredPolicy } from "../../../api/api"; +import { Metadata, StoragePolicyBrief } from "../../../api/explorer"; +import { closeStoragePolicyDialog } from "../../../redux/globalStateSlice"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { refreshFileList } from "../../../redux/thunks/filemanager"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar"; +import { DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../ContextMenu/ContextMenu"; +import { FileManagerIndex } from "../FileManager"; + +// StoragePolicyDialog switches the storage policy for either a folder's +// preferred upload policy ("dir") or a file/folder's physical entities +// ("relocate"), choosing from the group's allowed set. +const StoragePolicyDialog = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const open = useAppSelector((state) => state.globalState.storagePolicyDialogOpen); + const mode = useAppSelector((state) => state.globalState.storagePolicyDialogMode); + const file = useAppSelector((state) => state.globalState.storagePolicyDialogFile); + + const [policies, setPolicies] = useState([]); + const [value, setValue] = useState(""); + const [loading, setLoading] = useState(false); + + const isDir = mode === "dir"; + const current = file?.metadata?.[Metadata.preferred_policy] ?? ""; + + useEffect(() => { + if (!open) { + return; + } + setValue(isDir ? current : ""); + dispatch(getAllowedPolicies()).then((res) => setPolicies(res ?? [])); + }, [open]); + + const onClose = () => dispatch(closeStoragePolicyDialog()); + + const onChange = (e: SelectChangeEvent) => { + setValue(e.target.value as string); + }; + + const onSubmit = () => { + if (!file) { + return; + } + setLoading(true); + const uri = file.path; + const req = isDir + ? dispatch(setPreferredPolicy({ uri, policy: value })).then(() => { + enqueueSnackbar(t("fileManager.policySaved"), { variant: "success", action: DefaultCloseAction }); + }) + : dispatch(relocateToPolicy({ uri, policy: value })).then(() => { + enqueueSnackbar(t("fileManager.relocateSubmitted"), { variant: "success", action: DefaultCloseAction }); + }); + req.then(() => { + onClose(); + dispatch(refreshFileList(FileManagerIndex.main)); + }).finally(() => setLoading(false)); + }; + + return ( + + {isDir ? t("fileManager.dirPolicyTitle") : t("fileManager.relocateTitle")} + + + {isDir + ? t("fileManager.dirPolicyDes", { name: file?.name ?? "" }) + : t("fileManager.relocateDes", { name: file?.name ?? "" })} + + + + {isDir && ( + + + + )} + {policies.map((p) => ( + + + + ))} + + + + + + + + + ); +}; + +export default StoragePolicyDialog; diff --git a/frontend/src/component/Pages/Setting/StorageSetting.tsx b/frontend/src/component/Pages/Setting/StorageSetting.tsx index 505a4fdb..7782d8ef 100644 --- a/frontend/src/component/Pages/Setting/StorageSetting.tsx +++ b/frontend/src/component/Pages/Setting/StorageSetting.tsx @@ -1,12 +1,19 @@ -import { Box, Stack, styled, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Box, FormControl, ListItemText, SelectChangeEvent, Stack, styled, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useSnackbar } from "notistack"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import { getAllowedPolicies, sendUpdateUserSetting } from "../../../api/api.ts"; +import { StoragePolicyBrief } from "../../../api/explorer.ts"; import { Capacity, UserSettings } from "../../../api/user.ts"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; import { updateUserCapacity } from "../../../redux/thunks/filemanager.ts"; import { loadSiteConfig } from "../../../redux/thunks/site.ts"; import { sizeToString } from "../../../util"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; +import { DenseSelect } from "../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import { NoMarginHelperText } from "../../Admin/Settings/Settings.tsx"; import SettingForm from "./SettingForm.tsx"; export const StorageBar = styled(Box)(({ theme }) => ({ @@ -108,12 +115,28 @@ const StorageSetting = ({ setting }: StorageSettingProps) => { used: 0, base: 0, }); + const [policies, setPolicies] = useState([]); + const [preferred, setPreferred] = useState(setting.preferred_policy ?? ""); + const [saving, setSaving] = useState(false); + const { enqueueSnackbar } = useSnackbar(); useEffect(() => { dispatch(updateUserCapacity(0)); dispatch(loadSiteConfig("vas")); + dispatch(getAllowedPolicies()).then((res) => setPolicies(res ?? [])); }, []); + const onPolicyChange = (e: SelectChangeEvent) => { + const v = e.target.value as string; + setPreferred(v); + setSaving(true); + dispatch(sendUpdateUserSetting({ preferred_policy: v })) + .then(() => { + enqueueSnackbar(t("setting.policySaved"), { variant: "success", action: DefaultCloseAction }); + }) + .finally(() => setSaving(false)); + }; + return ( @@ -121,6 +144,26 @@ const StorageSetting = ({ setting }: StorageSettingProps) => { + {policies.length > 1 && ( + + + + + + + {policies.map((p) => ( + + + + ))} + + {t("setting.preferredPolicyDes")} + + + )} ); }; diff --git a/frontend/src/redux/globalStateSlice.ts b/frontend/src/redux/globalStateSlice.ts index 92a15c5a..bb48de4a 100644 --- a/frontend/src/redux/globalStateSlice.ts +++ b/frontend/src/redux/globalStateSlice.ts @@ -222,6 +222,12 @@ export interface GlobalStateSlice { directLinkManagementDialogFile?: FileResponse; directLinkHighlight?: string; + // Storage policy dialog ("dir" sets the folder's preferred upload policy, + // "relocate" moves the target's entities to another policy) + storagePolicyDialogOpen?: boolean; + storagePolicyDialogMode?: "dir" | "relocate"; + storagePolicyDialogFile?: FileResponse; + // Desktop mount setup dialog desktopMountSetupDialogOpen?: boolean; desktopMountSetupState?: DesktopCallbackState; @@ -362,6 +368,19 @@ export const globalStateSlice = createSlice({ state.directLinkManagementDialogFile = undefined; state.directLinkHighlight = undefined; }, + setStoragePolicyDialog: ( + state, + action: PayloadAction<{ open: boolean; mode?: "dir" | "relocate"; file?: FileResponse }>, + ) => { + state.storagePolicyDialogOpen = action.payload.open; + state.storagePolicyDialogMode = action.payload.mode; + state.storagePolicyDialogFile = action.payload.file; + }, + closeStoragePolicyDialog: (state) => { + state.storagePolicyDialogOpen = false; + state.storagePolicyDialogMode = undefined; + state.storagePolicyDialogFile = undefined; + }, setMobileDrawerOpen: (state, action: PayloadAction) => { state.mobileDrawerOpen = action.payload; }, @@ -966,6 +985,8 @@ export const { closeExcalidrawViewer, setDirectLinkManagementDialog, closeDirectLinkManagementDialog, + setStoragePolicyDialog, + closeStoragePolicyDialog, setShareReadmeDetect, closeShareReadme, setShareReadmeOpen, diff --git a/inventory/group.go b/inventory/group.go index 9c8f8fac..87147319 100644 --- a/inventory/group.go +++ b/inventory/group.go @@ -8,11 +8,16 @@ import ( "github.com/cloudreve/Cloudreve/v4/ent/group" "github.com/cloudreve/Cloudreve/v4/pkg/cache" "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/samber/lo" ) type ( // Ctx keys for eager loading options. LoadGroupPolicy struct{} + + // LoadGroupAllowedPolicies eagerly loads the group's allowed_policies M2M + // edge. + LoadGroupAllowedPolicies struct{} ) const ( @@ -77,6 +82,10 @@ func (c *groupClient) ListAll(ctx context.Context) ([]*ent.Group, error) { } func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, error) { + allowedIDs := lo.Map(group.Edges.AllowedPolicies, func(p *ent.StoragePolicy, _ int) int { + return p.ID + }) + if group.ID == 0 { stm := c.client.Group.Create(). SetName(group.Name). @@ -88,6 +97,9 @@ func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, if group.Edges.StoragePolicies != nil && group.Edges.StoragePolicies.ID > 0 { stm.SetStoragePolicyID(group.Edges.StoragePolicies.ID) } + if len(allowedIDs) > 0 { + stm.AddAllowedPolicyIDs(allowedIDs...) + } return stm.Save(ctx) } @@ -98,11 +110,15 @@ func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, SetSpeedLimit(group.SpeedLimit). SetPermissions(group.Permissions). SetSettings(group.Settings). - ClearStoragePolicies() + ClearStoragePolicies(). + ClearAllowedPolicies() if group.Edges.StoragePolicies != nil && group.Edges.StoragePolicies.ID > 0 { stm.SetStoragePolicyID(group.Edges.StoragePolicies.ID) } + if len(allowedIDs) > 0 { + stm.AddAllowedPolicyIDs(allowedIDs...) + } res, err := stm.Save(ctx) if err != nil { @@ -173,5 +189,8 @@ func withGroupEagerLoading(ctx context.Context, q *ent.GroupQuery) *ent.GroupQue withStoragePolicyEagerLoading(ctx, spq) }) } + if _, ok := ctx.Value(LoadGroupAllowedPolicies{}).(bool); ok { + q.WithAllowedPolicies() + } return q } diff --git a/inventory/policy.go b/inventory/policy.go index 9efc14c4..1b25f726 100644 --- a/inventory/policy.go +++ b/inventory/policy.go @@ -4,12 +4,15 @@ import ( "context" "encoding/gob" "fmt" + "math/rand" + "sort" "strconv" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/cache" + "github.com/samber/lo" ) const ( @@ -30,6 +33,15 @@ type ( TxOperator // GetByGroup returns the storage policies of the group. GetByGroup(ctx context.Context, group *ent.Group) (*ent.StoragePolicy, error) + // ListByGroup returns every active storage policy usable by the + // group: the allowed_policies set plus the legacy single-policy + // default. Returns the set in stable id order. + ListByGroup(ctx context.Context, group *ent.Group) ([]*ent.StoragePolicy, error) + // ResolveLoadBalance picks a concrete child of a load_balance policy + // by its configured weights. Suspended, missing, and nested + // load_balance children are skipped; the pick is weighted over the + // remaining set. + ResolveLoadBalance(ctx context.Context, policy *ent.StoragePolicy) (*ent.StoragePolicy, error) // GetPolicyByID returns the storage policy by id. GetPolicyByID(ctx context.Context, id int) (*ent.StoragePolicy, error) // UpdateAccessKey updates the access key of the storage policy. It also clear related cache in KV. @@ -161,6 +173,68 @@ func (c *storagePolicyClient) GetByGroup(ctx context.Context, group *ent.Group) return res, nil } +func (c *storagePolicyClient) ListByGroup(ctx context.Context, group *ent.Group) ([]*ent.StoragePolicy, error) { + allowed, err := withStoragePolicyEagerLoading(ctx, c.client.Group.QueryAllowedPolicies(group)). + Where(storagepolicy.StatusEQ(storagepolicy.StatusActive)). + All(ctx) + if err != nil { + return nil, fmt.Errorf("list allowed storage policies: %w", err) + } + + // The legacy storage_policy_id default is always part of the allowed set. + if group.Edges.StoragePolicies != nil { + def := group.Edges.StoragePolicies + if !lo.ContainsBy(allowed, func(p *ent.StoragePolicy) bool { return p.ID == def.ID }) { + if def.Status == storagepolicy.StatusActive { + allowed = append(allowed, def) + } + } + } else if group.StoragePolicyID > 0 { + if def, err := c.GetPolicyByID(ctx, group.StoragePolicyID); err == nil && def.Status == storagepolicy.StatusActive { + if !lo.ContainsBy(allowed, func(p *ent.StoragePolicy) bool { return p.ID == def.ID }) { + allowed = append(allowed, def) + } + } + } + + sort.Slice(allowed, func(i, j int) bool { return allowed[i].ID < allowed[j].ID }) + return allowed, nil +} + +func (c *storagePolicyClient) ResolveLoadBalance(ctx context.Context, policy *ent.StoragePolicy) (*ent.StoragePolicy, error) { + refs := policy.Settings.LBPolicies + if len(refs) == 0 { + return nil, fmt.Errorf("load-balance policy %d has no child policies", policy.ID) + } + + children := make([]*ent.StoragePolicy, 0, len(refs)) + weights := make([]int, 0, len(refs)) + for _, ref := range refs { + child, err := c.GetPolicyByID(ctx, ref.PolicyID) + if err != nil || child.Status != storagepolicy.StatusActive || child.Type == types.PolicyTypeLoadBalance { + continue + } + children = append(children, child) + weights = append(weights, max(ref.Weight, 1)) + } + if len(children) == 0 { + return nil, fmt.Errorf("load-balance policy %d has no usable child policies", policy.ID) + } + + total := 0 + for _, w := range weights { + total += w + } + n := rand.Intn(total) + for i, w := range weights { + if n < w { + return children[i], nil + } + n -= w + } + return children[len(children)-1], nil +} + // GetPolicyByID returns the storage policy by id. func (c *storagePolicyClient) GetPolicyByID(ctx context.Context, id int) (*ent.StoragePolicy, error) { val, skipCache := ctx.Value(SkipStoragePolicyCache{}).(bool) diff --git a/inventory/policy_allowed_test.go b/inventory/policy_allowed_test.go new file mode 100644 index 00000000..ab1a5efe --- /dev/null +++ b/inventory/policy_allowed_test.go @@ -0,0 +1,130 @@ +package inventory + +import ( + "context" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/ent/enttest" + "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/stretchr/testify/require" +) + +func TestListByGroup(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:"+t.Name()+"?mode=memory&cache=shared") + t.Cleanup(func() { require.NoError(t, client.Close()) }) + ctx := context.Background() + + pc := NewStoragePolicyClient(client, nil) + mk := func(name string, status storagepolicy.Status) *ent.StoragePolicy { + return client.StoragePolicy.Create(). + SetName(name).SetType("local").SetStatus(status).SaveX(ctx) + } + + t.Run("legacy default included when no M2M rows", func(t *testing.T) { + def := mk("def", storagepolicy.StatusActive) + g := client.Group.Create().SetName("g1").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).SaveX(ctx) + + got, err := pc.ListByGroup(ctx, g) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, def.ID, got[0].ID) + }) + + t.Run("allowed set union with default, active only, ordered by id", func(t *testing.T) { + def := mk("def", storagepolicy.StatusActive) + a := mk("a", storagepolicy.StatusActive) + b := mk("b", storagepolicy.StatusActive) + susp := mk("susp", storagepolicy.StatusSuspended) + g := client.Group.Create().SetName("g2").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).AddAllowedPolicies(a, b, susp).SaveX(ctx) + + got, err := pc.ListByGroup(ctx, g) + require.NoError(t, err) + require.Len(t, got, 3) + ids := []int{got[0].ID, got[1].ID, got[2].ID} + require.IsIncreasing(t, ids) + require.ElementsMatch(t, []int{a.ID, b.ID, def.ID}, ids) + }) + + t.Run("suspended default not included", func(t *testing.T) { + def := mk("def", storagepolicy.StatusSuspended) + a := mk("a", storagepolicy.StatusActive) + g := client.Group.Create().SetName("g3").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).AddAllowedPolicies(a).SaveX(ctx) + + got, err := pc.ListByGroup(ctx, g) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, a.ID, got[0].ID) + }) + + t.Run("no usable policy returns empty", func(t *testing.T) { + def := mk("def", storagepolicy.StatusSuspended) + g := client.Group.Create().SetName("g4").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).SaveX(ctx) + + got, err := pc.ListByGroup(ctx, g) + require.NoError(t, err) + require.Empty(t, got) + }) +} + +func TestResolveLoadBalance(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:"+t.Name()+"?mode=memory&cache=shared") + t.Cleanup(func() { require.NoError(t, client.Close()) }) + ctx := context.Background() + + pc := NewStoragePolicyClient(client, nil) + + t.Run("skips suspended, missing and nested children", func(t *testing.T) { + nested := client.StoragePolicy.Create().SetName("nested").SetType(types.PolicyTypeLoadBalance). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + susp := client.StoragePolicy.Create().SetName("susp").SetType("local"). + SetStatus(storagepolicy.StatusSuspended).SaveX(ctx) + ok := client.StoragePolicy.Create().SetName("ok").SetType("local"). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + lb := client.StoragePolicy.Create().SetName("lb").SetType(types.PolicyTypeLoadBalance). + SetStatus(storagepolicy.StatusActive). + SetSettings(&types.PolicySetting{LBPolicies: []types.LBPolicyRef{ + {PolicyID: nested.ID, Weight: 10}, + {PolicyID: susp.ID, Weight: 10}, + {PolicyID: 99999, Weight: 10}, + {PolicyID: ok.ID, Weight: 10}, + }}).SaveX(ctx) + + for i := 0; i < 20; i++ { + got, err := pc.ResolveLoadBalance(ctx, lb) + require.NoError(t, err) + require.Equal(t, ok.ID, got.ID) + } + }) + + t.Run("weight of zero treated as one", func(t *testing.T) { + zero := client.StoragePolicy.Create().SetName("zero").SetType("local"). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + lb := client.StoragePolicy.Create().SetName("lb").SetType(types.PolicyTypeLoadBalance). + SetStatus(storagepolicy.StatusActive). + SetSettings(&types.PolicySetting{LBPolicies: []types.LBPolicyRef{ + {PolicyID: zero.ID, Weight: 0}, + }}).SaveX(ctx) + + got, err := pc.ResolveLoadBalance(ctx, lb) + require.NoError(t, err) + require.Equal(t, zero.ID, got.ID) + }) + + t.Run("no usable children errors", func(t *testing.T) { + lb := client.StoragePolicy.Create().SetName("lb").SetType(types.PolicyTypeLoadBalance). + SetStatus(storagepolicy.StatusActive). + SetSettings(&types.PolicySetting{LBPolicies: []types.LBPolicyRef{ + {PolicyID: 424242, Weight: 5}, + }}).SaveX(ctx) + + _, err := pc.ResolveLoadBalance(ctx, lb) + require.Error(t, err) + }) +} diff --git a/inventory/types/types.go b/inventory/types/types.go index 225fdce1..72add853 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -28,6 +28,16 @@ type ( // 0 means inherit the group setting. Applied when a file is moved to // trash — already-trashed files keep their original expiry. TrashRetention int `json:"trash_retention,omitempty"` + // PreferredPolicy is the user's default storage policy, chosen from + // the policies allowed for their group. 0 means the group default. + PreferredPolicy int `json:"preferred_policy,omitempty"` + } + + // LBPolicyRef binds a child storage policy to a load_balance policy with + // a selection weight. Weight 0 counts as 1. + LBPolicyRef struct { + PolicyID int `json:"policy"` + Weight int `json:"weight,omitempty"` } ShareLinksInProfileLevel string @@ -48,9 +58,9 @@ type ( Aria2TaskLimit int `json:"aria2_task_limit,omitempty"` // Aria2MaxFileSize caps the total byte size of a single download task. Aria2MaxFileSize int64 `json:"aria2_max_file_size,omitempty"` - MaxWalkedFiles int `json:"max_walked_files,omitempty"` - TrashRetention int `json:"trash_retention,omitempty"` - RedirectedSource bool `json:"redirected_source,omitempty"` + MaxWalkedFiles int `json:"max_walked_files,omitempty"` + TrashRetention int `json:"trash_retention,omitempty"` + RedirectedSource bool `json:"redirected_source,omitempty"` // LoginIPWhitelist restricts sign-in to the given IPs/CIDR ranges. // Empty means no restriction. LoginIPWhitelist []string `json:"login_ip_whitelist,omitempty"` @@ -122,6 +132,9 @@ type ( StreamSaver bool `json:"stream_saver,omitempty"` // UseCname whether to use CNAME for endpoint (OSS). UseCname bool `json:"use_cname,omitempty"` + // LBPolicies binds weighted child policies to a load_balance policy. + // Children must be concrete (non-load_balance) policies. + LBPolicies []LBPolicyRef `json:"lb_policies,omitempty"` // CDN domain does not need to be signed. SourceAuth bool `json:"source_auth,omitempty"` // QiniuUploadCdn whether to use CDN for Qiniu upload. @@ -419,6 +432,10 @@ const ( PolicyTypeOd = "onedrive" PolicyTypeRemote = "remote" PolicyTypeObs = "obs" + // PolicyTypeLoadBalance distributes uploads across weighted child + // policies. It is resolved to a concrete child policy before use and + // never reaches a storage driver. + PolicyTypeLoadBalance = "load_balance" ) const ( diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index b3109613..ed249c31 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -735,18 +735,82 @@ func (f *DBFS) generateEncryptMetadata(ctx context.Context, uploadRequest *fs.Up // getPreferredPolicy tries to get the preferred storage policy for the given file. func (f *DBFS) getPreferredPolicy(ctx context.Context, file *File) (*ent.StoragePolicy, error) { - ownerGroup := file.Owner().Edges.Group + owner := file.Owner() + ownerGroup := owner.Edges.Group if ownerGroup == nil { return nil, fmt.Errorf("owner group not loaded") } sc, _ := inventory.InheritTx(ctx, f.storagePolicyClient) - groupPolicy, err := sc.GetByGroup(ctx, ownerGroup) + allowed, err := sc.ListByGroup(ctx, ownerGroup) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get available storage policies", err) } + if len(allowed) == 0 { + return nil, serializer.NewError(serializer.CodeDBError, "No active storage policy available for the group", nil) + } + + candidate := f.pickPolicy(ctx, file, owner, allowed) + + // A load_balance policy resolves to one of its weighted children before + // any storage driver sees it. + if candidate.Type == types.PolicyTypeLoadBalance { + child, err := sc.ResolveLoadBalance(ctx, candidate) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to resolve load-balanced storage policy", err) + } + candidate = child + } + + return candidate, nil +} + +// pickPolicy chooses among the group's allowed policies: the nearest ancestor +// directory carrying a preferred-policy marker wins; next the owner's own +// preferred_policy setting (applied only in their own tree); finally the +// group default, or the first allowed policy when none is configured. +func (f *DBFS) pickPolicy(ctx context.Context, file *File, owner *ent.User, allowed []*ent.StoragePolicy) *ent.StoragePolicy { + inAllowed := func(id int) *ent.StoragePolicy { + for _, p := range allowed { + if p.ID == id { + return p + } + } + return nil + } - return groupPolicy, nil + // The nearest ancestor directory with a preference wins; if its choice is + // no longer usable, the preference is ignored rather than inherited from + // a further ancestor. Ancestor metadata may not be eager-loaded, so each + // level is loaded lazily until a marker is found. + for _, ancestor := range file.AncestorsChain() { + if _, err := ancestor.Model.Edges.MetadataOrErr(); err != nil { + if err := f.fileClient.QueryMetadata(ctx, ancestor.Model); err != nil { + continue + } + } + raw := ancestor.Metadata()[MetadataPreferredPolicy] + if raw == "" { + continue + } + if id, err := f.hasher.Decode(raw, hashid.PolicyID); err == nil { + if p := inAllowed(id); p != nil { + return p + } + } + break + } + + if f.user.ID == owner.ID && owner.Settings != nil && owner.Settings.PreferredPolicy > 0 { + if p := inAllowed(owner.Settings.PreferredPolicy); p != nil { + return p + } + } + + if p := inAllowed(owner.Edges.Group.StoragePolicyID); p != nil { + return p + } + return allowed[0] } func (f *DBFS) getFileByPath(ctx context.Context, navigator Navigator, path *fs.URI) (*File, error) { diff --git a/pkg/filemanager/fs/dbfs/file.go b/pkg/filemanager/fs/dbfs/file.go index 762c3dfd..54d307dc 100644 --- a/pkg/filemanager/fs/dbfs/file.go +++ b/pkg/filemanager/fs/dbfs/file.go @@ -65,6 +65,9 @@ const ( MetadataRestoreUri = MetadataSysPrefix + "restore_uri" MetadataExpectedCollectTime = MetadataSysPrefix + "expected_collect_time" MetadataSharedOwner = MetadataSysPrefix + "shared_owner" + // MetadataPreferredPolicy stores the hashid-encoded storage policy a + // directory prefers for new uploads inside it. + MetadataPreferredPolicy = MetadataSysPrefix + "preferred_policy" ThumbMetadataPrefix = "thumb:" ThumbDisabledKey = ThumbMetadataPrefix + "disabled" diff --git a/pkg/filemanager/fs/dbfs/pick_policy_test.go b/pkg/filemanager/fs/dbfs/pick_policy_test.go new file mode 100644 index 00000000..f663d30c --- /dev/null +++ b/pkg/filemanager/fs/dbfs/pick_policy_test.go @@ -0,0 +1,138 @@ +package dbfs + +import ( + "context" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/ent/enttest" + entfile "github.com/cloudreve/Cloudreve/v4/ent/file" + entmetadata "github.com/cloudreve/Cloudreve/v4/ent/metadata" + "github.com/cloudreve/Cloudreve/v4/ent/schema" + "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" + "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/cloudreve/Cloudreve/v4/pkg/hashid" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/stretchr/testify/require" +) + +// pickPolicyFixture builds an owner in a group whose allowed set is +// [a, b] with `def` as the legacy default, and a dir chain root/dir/target. +func pickPolicyFixture(t *testing.T, client *ent.Client) (*ent.Group, *ent.StoragePolicy, *ent.StoragePolicy, *ent.StoragePolicy, *ent.User, *ent.File, *ent.File) { + t.Helper() + ctx := context.Background() + + mk := func(name string) *ent.StoragePolicy { + return client.StoragePolicy.Create().SetName(name).SetType("local"). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + } + def := mk("def") + a := mk("a") + b := mk("b") + + group := client.Group.Create().SetName("g").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).AddAllowedPolicies(a, b).SaveX(ctx) + owner := client.User.Create().SetEmail("o@example.com").SetNick("o"). + SetGroup(group).SetSettings(&types.UserSetting{PreferredPolicy: b.ID}).SaveX(ctx) + + root := client.File.Create().SetName(inventory.RootFolderName). + SetType(int(types.FileTypeFolder)).SetOwner(owner).SaveX(ctx) + dir := client.File.Create().SetName("dir").SetType(int(types.FileTypeFolder)). + SetOwner(owner).SetParent(root).SaveX(ctx) + return group, def, a, b, owner, root, dir +} + +func pickPolicyDBFS(t *testing.T, client *ent.Client, user *ent.User) *DBFS { + t.Helper() + hasher, err := hashid.New("pick-test-salt") + require.NoError(t, err) + return &DBFS{ + user: user, + fileClient: inventory.NewFileClient(client, conf.SQLiteDB, hasher), + storagePolicyClient: inventory.NewStoragePolicyClient(client, nil), + hasher: hasher, + l: logging.NewConsoleLogger(logging.LevelError), + } +} + +func wrapChain(models ...*ent.File) *File { + var parent *File + for _, m := range models { + parent = &File{Model: m, Parent: parent} + } + return parent +} + +func setPreferredMarker(t *testing.T, client *ent.Client, f *ent.File, hasher hashid.Encoder, policyID int) { + t.Helper() + ctx := schema.SkipSoftDelete(context.Background()) + client.Metadata.Delete().Where(entmetadata.Name(MetadataPreferredPolicy), + entmetadata.HasFileWith(entfile.ID(f.ID))).ExecX(ctx) + client.Metadata.Create().SetFile(f).SetName(MetadataPreferredPolicy). + SetValue(hashid.EncodePolicyID(hasher, policyID)).SaveX(ctx) +} + +// freshChain reloads the file rows so previously lazy-loaded metadata edges +// do not leak between subtests. +func freshChain(t *testing.T, client *ent.Client, ids ...int) *File { + t.Helper() + models := make([]*ent.File, len(ids)) + for i, id := range ids { + models[i] = client.File.GetX(context.Background(), id) + } + return wrapChain(models...) +} + +func TestPickPolicyPrecedence(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:"+t.Name()+"?mode=memory&cache=shared") + t.Cleanup(func() { require.NoError(t, client.Close()) }) + ctx := context.Background() + + group, def, a, b, owner, root, dir := pickPolicyFixture(t, client) + owner.SetGroup(group) + allowed := []*ent.StoragePolicy{a, b, def} + + f := pickPolicyDBFS(t, client, owner) + + t.Run("directory marker wins over user preference", func(t *testing.T) { + setPreferredMarker(t, client, dir, f.hasher, a.ID) + got := f.pickPolicy(ctx, freshChain(t, client, root.ID, dir.ID), owner, allowed) + require.Equal(t, a.ID, got.ID) + }) + + t.Run("user preference wins over group default", func(t *testing.T) { + client.Metadata.Delete().ExecX(schema.SkipSoftDelete(ctx)) + got := f.pickPolicy(ctx, freshChain(t, client, root.ID, dir.ID), owner, allowed) + require.Equal(t, b.ID, got.ID) + }) + + t.Run("invalid marker stops ancestor inheritance", func(t *testing.T) { + // Root carries a valid marker, but the nearer dir marker points at a + // policy outside the allowed set — must not fall through to root's. + outside := client.StoragePolicy.Create().SetName("outside").SetType("local"). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + setPreferredMarker(t, client, root, f.hasher, a.ID) + setPreferredMarker(t, client, dir, f.hasher, outside.ID) + got := f.pickPolicy(ctx, freshChain(t, client, root.ID, dir.ID), owner, allowed) + require.Equal(t, b.ID, got.ID) + }) + + t.Run("other user's tree ignores user preference", func(t *testing.T) { + other := client.User.Create().SetEmail("x@example.com").SetNick("x").SetGroup(group).SaveX(ctx) + otherFs := pickPolicyDBFS(t, client, other) + got := otherFs.pickPolicy(ctx, freshChain(t, client, root.ID, dir.ID), owner, allowed) + require.Equal(t, def.ID, got.ID) + }) + + t.Run("group default applies without preferences", func(t *testing.T) { + ownerNoPref := client.User.Create().SetEmail("np@example.com").SetNick("np"). + SetGroup(group).SetSettings(&types.UserSetting{}).SaveX(ctx) + ownerNoPref.SetGroup(group) + fsNoPref := pickPolicyDBFS(t, client, ownerNoPref) + got := fsNoPref.pickPolicy(ctx, freshChain(t, client, root.ID, dir.ID), ownerNoPref, allowed) + require.Equal(t, def.ID, got.ID) + }) +} diff --git a/routers/controllers/file.go b/routers/controllers/file.go index a164efac..3472acc4 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -480,3 +480,42 @@ func FulltextSearch(c *gin.Context) { Data: resp, }) } + +// ListStoragePolicies lists storage policies available to the current user's group. +func ListStoragePolicies(c *gin.Context) { + service := ParametersFromContext[*explorer.AllowedPolicyService](c, explorer.AllowedPolicyParamCtx{}) + resp, err := service.Get(c) + if respondErr(c, err) { + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} + +// UpdatePreferredPolicy sets the preferred storage policy for a directory. +func UpdatePreferredPolicy(c *gin.Context) { + service := ParametersFromContext[*explorer.PreferredPolicyService](c, explorer.PreferredPolicyParamCtx{}) + resp, err := service.Update(c) + if respondErr(c, err) { + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} + +// RelocatePolicy queues a task to relocate files to a different storage policy. +func RelocatePolicy(c *gin.Context) { + service := ParametersFromContext[*explorer.FileRelocateService](c, explorer.FileRelocateParamCtx{}) + resp, err := service.Create(c) + if respondErr(c, err) { + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} diff --git a/routers/router.go b/routers/router.go index ddd2216a..789dc7d7 100644 --- a/routers/router.go +++ b/routers/router.go @@ -750,6 +750,23 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { middleware.ValidateBatchFileCount(dep, explorer.PatchMetadataParameterCtx{}), controllers.PatchMetadata, ) + // List storage policies available to the current group + file.GET("policy", + controllers.FromQuery[explorer.AllowedPolicyService](explorer.AllowedPolicyParamCtx{}), + controllers.ListStoragePolicies, + ) + // Set preferred storage policy for a directory + file.PUT("policy", + middleware.RequiredScopes(types.ScopeFilesWrite), + controllers.FromJSON[explorer.PreferredPolicyService](explorer.PreferredPolicyParamCtx{}), + controllers.UpdatePreferredPolicy, + ) + // Relocate files to a different storage policy + file.POST("relocate", + middleware.RequiredScopes(types.ScopeFilesWrite), + controllers.FromJSON[explorer.FileRelocateService](explorer.FileRelocateParamCtx{}), + controllers.RelocatePolicy, + ) // Upload related upload := file.Group("upload", middleware.RequiredScopes(types.ScopeFilesWrite)) { diff --git a/service/admin/group.go b/service/admin/group.go index 3b2eddc1..6e3b089a 100644 --- a/service/admin/group.go +++ b/service/admin/group.go @@ -124,6 +124,7 @@ func (s *SingleGroupService) Get(c *gin.Context) (*GetGroupResponse, error) { groupClient := dep.GroupClient() ctx := context.WithValue(c, inventory.LoadGroupPolicy{}, true) + ctx = context.WithValue(ctx, inventory.LoadGroupAllowedPolicies{}, true) group, err := groupClient.GetByID(ctx, s.ID) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get group", err) diff --git a/service/explorer/policy.go b/service/explorer/policy.go new file mode 100644 index 00000000..7e187830 --- /dev/null +++ b/service/explorer/policy.go @@ -0,0 +1,238 @@ +package explorer + +import ( + "fmt" + + "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/inventory" + "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/filemanager/manager" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/workflows" + "github.com/cloudreve/Cloudreve/v4/pkg/hashid" + "github.com/cloudreve/Cloudreve/v4/pkg/serializer" + "github.com/gin-gonic/gin" + "github.com/samber/lo" +) + +// StoragePolicyBrief is the user-facing view of an allowed storage policy. +type StoragePolicyBrief struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"is_default,omitempty"` +} + +type ( + // AllowedPolicyService lists the storage policies the caller's group may use. + AllowedPolicyService struct{} + AllowedPolicyParamCtx struct{} + + // PreferredPolicyService sets or clears a directory's preferred storage + // policy. New uploads inside the directory use it. + PreferredPolicyService struct { + Uri string `json:"uri" binding:"required"` + Policy string `json:"policy"` + } + PreferredPolicyParamCtx struct{} + + // FileRelocateService moves a file's or folder's entities to another + // storage policy via the resumable relocation task. + FileRelocateService struct { + Uri string `json:"uri" binding:"required"` + Policy string `json:"policy" binding:"required"` + } + FileRelocateParamCtx struct{} + + // FileRelocateResponse carries the created relocation task id. + FileRelocateResponse struct { + ID string `json:"id"` + } +) + +// allowedGroupPolicies resolves the group's usable policy set and maps each +// to a brief. The group's configured default is marked. +func allowedGroupPolicies(c *gin.Context, group *ent.Group, dep dependency.Dep) ([]*ent.StoragePolicy, []*StoragePolicyBrief, error) { + allowed, err := dep.StoragePolicyClient().ListByGroup(c, group) + if err != nil { + return nil, nil, serializer.NewError(serializer.CodeDBError, "Failed to list storage policies", err) + } + if len(allowed) == 0 { + return nil, nil, serializer.NewError(serializer.CodeNoPermissionErr, "No storage policy is available for your group", nil) + } + + hasher := dep.HashIDEncoder() + briefs := lo.Map(allowed, func(p *ent.StoragePolicy, _ int) *StoragePolicyBrief { + return &StoragePolicyBrief{ + ID: hashid.EncodePolicyID(hasher, p.ID), + Name: p.Name, + Type: p.Type, + IsDefault: p.ID == group.StoragePolicyID, + } + }) + return allowed, briefs, nil +} + +func (s *AllowedPolicyService) Get(c *gin.Context) ([]*StoragePolicyBrief, error) { + user := inventory.UserFromContext(c) + if user.Edges.Group == nil { + return nil, serializer.NewError(serializer.CodeNoPermissionErr, "Group not loaded", nil) + } + _, briefs, err := allowedGroupPolicies(c, user.Edges.Group, dependency.FromContext(c)) + return briefs, err +} + +// decodeAllowedPolicy validates a hashid-encoded policy id against the +// group's allowed set and returns the matching policy. +func decodeAllowedPolicy(c *gin.Context, dep dependency.Dep, group *ent.Group, raw string) (*ent.StoragePolicy, error) { + id, err := dep.HashIDEncoder().Decode(raw, hashid.PolicyID) + if err != nil { + return nil, serializer.NewError(serializer.CodeParamErr, "Invalid storage policy", err) + } + + allowed, err := dep.StoragePolicyClient().ListByGroup(c, group) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to list storage policies", err) + } + for _, p := range allowed { + if p.ID == id { + return p, nil + } + } + return nil, serializer.NewError(serializer.CodeNoPermissionErr, "Storage policy is not available for your group", nil) +} + +// ownedPolicyFile resolves the target and enforces ownership, returning the +// fs file for metadata access. +func ownedPolicyFile(c *gin.Context, dep dependency.Dep, uriRaw string) (*dbfs.File, error) { + user := inventory.UserFromContext(c) + uri, err := fs.NewUriFromString(uriRaw) + if err != nil { + return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err) + } + + m := manager.NewFileManager(dep, user) + defer m.Recycle() + + f, err := m.Get(c, uri, dbfs.WithFileEntities()) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + file, ok := f.(*dbfs.File) + if !ok { + return nil, serializer.NewError(serializer.CodeParamErr, "Unsupported file system", nil) + } + if file.OwnerID() != user.ID { + return nil, serializer.NewError(serializer.CodeNoPermissionErr, "Only the owner can manage storage policies", nil) + } + return file, nil +} + +func (s *PreferredPolicyService) Update(c *gin.Context) (*StoragePolicyBrief, error) { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + + file, err := ownedPolicyFile(c, dep, s.Uri) + if err != nil { + return nil, err + } + if file.Type() != types.FileTypeFolder { + return nil, serializer.NewError(serializer.CodeParamErr, "Preferred storage policy applies to folders only", nil) + } + + var policy *ent.StoragePolicy + if s.Policy != "" { + policy, err = decodeAllowedPolicy(c, dep, user.Edges.Group, s.Policy) + if err != nil { + return nil, err + } + } + + fc := dep.FileClient() + if policy == nil { + err = fc.RemoveMetadata(c, file.Model, dbfs.MetadataPreferredPolicy) + } else { + err = fc.UpsertMetadata(c, file.Model, map[string]string{ + dbfs.MetadataPreferredPolicy: hashid.EncodePolicyID(dep.HashIDEncoder(), policy.ID), + }, map[string]bool{dbfs.MetadataPreferredPolicy: true}) + } + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to update metadata", err) + } + + if policy == nil { + return nil, nil + } + return &StoragePolicyBrief{ + ID: hashid.EncodePolicyID(dep.HashIDEncoder(), policy.ID), + Name: policy.Name, + Type: policy.Type, + }, nil +} + +func (s *FileRelocateService) Create(c *gin.Context) (*FileRelocateResponse, error) { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + + policy, err := decodeAllowedPolicy(c, dep, user.Edges.Group, s.Policy) + if err != nil { + return nil, err + } + // A load-balance target resolves to one concrete child at enqueue time. + if policy.Type == types.PolicyTypeLoadBalance { + policy, err = dep.StoragePolicyClient().ResolveLoadBalance(c, policy) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to resolve load-balanced storage policy", err) + } + } + + file, err := ownedPolicyFile(c, dep, s.Uri) + if err != nil { + return nil, err + } + + // Depth is a countdown in the walk implementation; a large value walks + // the whole subtree. Entities already on the destination are skipped by + // the task, so they are filtered out of the request entirely. + pending := lo.Filter(file.Model.Edges.Entities, func(e *ent.Entity, _ int) bool { + return e.StoragePolicyEntities != policy.ID + }) + entityIDs := lo.Map(pending, func(e *ent.Entity, _ int) int { return e.ID }) + if file.Type() == types.FileTypeFolder { + uri, _ := fs.NewUriFromString(s.Uri) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + err = m.Walk(c, uri, 1<<30, func(f fs.File, _ int) error { + dbFile, ok := f.(*dbfs.File) + if !ok { + return nil + } + for _, e := range dbFile.Model.Edges.Entities { + if e.StoragePolicyEntities != policy.ID { + entityIDs = append(entityIDs, e.ID) + } + } + return nil + }, dbfs.WithFileEntities()) + if err != nil { + return nil, fmt.Errorf("failed to walk folder: %w", err) + } + } + + entityIDs = lo.Uniq(entityIDs) + if len(entityIDs) == 0 { + return nil, serializer.NewError(serializer.CodeParamErr, "Nothing to relocate", nil) + } + + t, err := workflows.NewRelocateTask(c, entityIDs, policy.ID) + if err != nil { + return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create relocation task", err) + } + if err := dep.IoIntenseQueue(c).QueueTask(c, t); err != nil { + return nil, fmt.Errorf("failed to submit task: %w", err) + } + + return &FileRelocateResponse{ID: hashid.EncodeTaskID(dep.HashIDEncoder(), t.ID())}, nil +} diff --git a/service/explorer/policy_test.go b/service/explorer/policy_test.go new file mode 100644 index 00000000..34c75a6f --- /dev/null +++ b/service/explorer/policy_test.go @@ -0,0 +1,78 @@ +package explorer + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/ent/enttest" + "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" + "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/cloudreve/Cloudreve/v4/pkg/hashid" + "github.com/cloudreve/Cloudreve/v4/pkg/util" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +// policyDepStub exposes only the dependencies decodeAllowedPolicy touches. +type policyDepStub struct { + dependency.Dep + policyClient inventory.StoragePolicyClient + hasher hashid.Encoder +} + +func (d *policyDepStub) StoragePolicyClient() inventory.StoragePolicyClient { return d.policyClient } +func (d *policyDepStub) HashIDEncoder() hashid.Encoder { return d.hasher } + +func TestDecodeAllowedPolicy(t *testing.T) { + gin.SetMode(gin.TestMode) + client := enttest.Open(t, "sqlite3", "file:"+t.Name()+"?mode=memory&cache=shared") + t.Cleanup(func() { require.NoError(t, client.Close()) }) + ctx := context.Background() + + mk := func(name string) *ent.StoragePolicy { + return client.StoragePolicy.Create().SetName(name).SetType("local"). + SetStatus(storagepolicy.StatusActive).SaveX(ctx) + } + def := mk("def") + a := mk("a") + outside := mk("outside") + + group := client.Group.Create().SetName("g").SetPermissions(&boolset.BooleanSet{}). + SetStoragePolicies(def).AddAllowedPolicies(a).SaveX(ctx) + + hasher, err := hashid.New("policy-test-salt") + require.NoError(t, err) + dep := &policyDepStub{policyClient: inventory.NewStoragePolicyClient(client, nil), hasher: hasher} + + engine := gin.New() + engine.ContextWithFallback = true + c := gin.CreateTestContextOnly(httptest.NewRecorder(), engine) + c.Request = httptest.NewRequest("GET", "/", nil) + util.WithValue(c, dependency.DepCtx{}, dep) + + t.Run("allowed policy decodes", func(t *testing.T) { + got, err := decodeAllowedPolicy(c, dep, group, hashid.EncodePolicyID(hasher, a.ID)) + require.NoError(t, err) + require.Equal(t, a.ID, got.ID) + }) + + t.Run("legacy default policy decodes", func(t *testing.T) { + got, err := decodeAllowedPolicy(c, dep, group, hashid.EncodePolicyID(hasher, def.ID)) + require.NoError(t, err) + require.Equal(t, def.ID, got.ID) + }) + + t.Run("policy outside the group set is rejected", func(t *testing.T) { + _, err := decodeAllowedPolicy(c, dep, group, hashid.EncodePolicyID(hasher, outside.ID)) + require.Error(t, err) + }) + + t.Run("invalid hashid is rejected", func(t *testing.T) { + _, err := decodeAllowedPolicy(c, dep, group, "!!!") + require.Error(t, err) + }) +} diff --git a/service/user/response.go b/service/user/response.go index d2c053a9..415d114f 100644 --- a/service/user/response.go +++ b/service/user/response.go @@ -35,6 +35,7 @@ type UserSettings struct { ShareDefaultPrivate *bool `json:"share_default_private,omitempty"` PreferredViewers map[string]string `json:"preferred_viewers,omitempty"` TrashRetention int `json:"trash_retention,omitempty"` + PreferredPolicy string `json:"preferred_policy,omitempty"` OAuthGrants []OauthGrant `json:"oauth_grants,omitempty"` } diff --git a/service/user/setting.go b/service/user/setting.go index 293282e9..591a74cb 100644 --- a/service/user/setting.go +++ b/service/user/setting.go @@ -25,6 +25,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gin-gonic/gin" "github.com/pquerna/otp/totp" + "github.com/samber/lo" ) const ( @@ -138,7 +139,11 @@ func GetUserSettings(c *gin.Context) (*UserSettings, error) { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user OAuth grants", err) } - return BuildUserSettings(u, passkeys, dep.UAParser(), grants), nil + res := BuildUserSettings(u, passkeys, dep.UAParser(), grants) + if u.Settings.PreferredPolicy > 0 { + res.PreferredPolicy = hashid.EncodePolicyID(dep.HashIDEncoder(), u.Settings.PreferredPolicy) + } + return res, nil // 用户组有效期 @@ -239,6 +244,9 @@ type ( // TrashRetention overrides group trash retention, in seconds. // 0 clears the override. Capped at ~10 years. TrashRetention *int `json:"trash_retention" binding:"omitempty,min=0,max=315360000"` + // PreferredPolicy selects the user's default storage policy from the + // group's allowed set, hashid-encoded. "" clears the preference. + PreferredPolicy *string `json:"preferred_policy" binding:"omitempty"` } PatchUserSettingParamsCtx struct{} ) @@ -342,6 +350,26 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error { saveSetting = true } + if s.PreferredPolicy != nil { + if *s.PreferredPolicy == "" { + u.Settings.PreferredPolicy = 0 + } else { + pid, err := dep.HashIDEncoder().Decode(*s.PreferredPolicy, hashid.PolicyID) + if err != nil { + return serializer.NewError(serializer.CodeParamErr, "Invalid storage policy", err) + } + allowed, err := dep.StoragePolicyClient().ListByGroup(c, u.Edges.Group) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "Failed to list storage policies", err) + } + if !lo.ContainsBy(allowed, func(p *ent.StoragePolicy) bool { return p.ID == pid }) { + return serializer.NewError(serializer.CodeNoPermissionErr, "Storage policy is not available for your group", nil) + } + u.Settings.PreferredPolicy = pid + } + saveSetting = true + } + if s.CurrentPassword != nil && s.NewPassword != nil { if err := auth.CheckScope(c, types.ScopeUserSecurityInfoWrite); err != nil { return err