feat(policy): suspend storage policies like nodes (#2961)

Mirrors the existing node active/suspended model onto storage
policies. A suspended policy keeps serving existing files but rejects
new uploads.

- schema: StoragePolicy.status (active|suspended, default active)
- group default-policy pick skips suspended policies; explicit picks
  for upload/import are rejected with CodePolicyNotAllowed
- admin policy edit gets a status select; suspended policies show a
  chip on the list card
- Upsert only writes status when provided, so partial payloads keep
  the stored value
- tests: suspended group policy skipped, active returned

Authored By: TDvorak <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent ba9a2b3d57
commit 7a9b0a1de0

File diff suppressed because one or more lines are too long

@ -419,6 +419,7 @@ var (
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}}, {Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
{Name: "name", Type: field.TypeString}, {Name: "name", Type: field.TypeString},
{Name: "type", Type: field.TypeString}, {Name: "type", Type: field.TypeString},
{Name: "status", Type: field.TypeEnum, Enums: []string{"active", "suspended"}, Default: "active"},
{Name: "server", Type: field.TypeString, Nullable: true}, {Name: "server", Type: field.TypeString, Nullable: true},
{Name: "bucket_name", Type: field.TypeString, Nullable: true}, {Name: "bucket_name", Type: field.TypeString, Nullable: true},
{Name: "is_private", Type: field.TypeBool, Nullable: true}, {Name: "is_private", Type: field.TypeBool, Nullable: true},
@ -438,7 +439,7 @@ var (
ForeignKeys: []*schema.ForeignKey{ ForeignKeys: []*schema.ForeignKey{
{ {
Symbol: "storage_policies_nodes_storage_policy", Symbol: "storage_policies_nodes_storage_policy",
Columns: []*schema.Column{StoragePoliciesColumns[15]}, Columns: []*schema.Column{StoragePoliciesColumns[16]},
RefColumns: []*schema.Column{NodesColumns[0]}, RefColumns: []*schema.Column{NodesColumns[0]},
OnDelete: schema.SetNull, OnDelete: schema.SetNull,
}, },

@ -12519,6 +12519,7 @@ type StoragePolicyMutation struct {
deleted_at *time.Time deleted_at *time.Time
name *string name *string
_type *string _type *string
status *storagepolicy.Status
server *string server *string
bucket_name *string bucket_name *string
is_private *bool is_private *bool
@ -12837,6 +12838,42 @@ func (m *StoragePolicyMutation) ResetType() {
m._type = nil m._type = nil
} }
// SetStatus sets the "status" field.
func (m *StoragePolicyMutation) SetStatus(s storagepolicy.Status) {
m.status = &s
}
// Status returns the value of the "status" field in the mutation.
func (m *StoragePolicyMutation) Status() (r storagepolicy.Status, exists bool) {
v := m.status
if v == nil {
return
}
return *v, true
}
// OldStatus returns the old "status" field's value of the StoragePolicy entity.
// If the StoragePolicy object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *StoragePolicyMutation) OldStatus(ctx context.Context) (v storagepolicy.Status, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldStatus is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldStatus requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldStatus: %w", err)
}
return oldValue.Status, nil
}
// ResetStatus resets all changes to the "status" field.
func (m *StoragePolicyMutation) ResetStatus() {
m.status = nil
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (m *StoragePolicyMutation) SetServer(s string) { func (m *StoragePolicyMutation) SetServer(s string) {
m.server = &s m.server = &s
@ -13571,7 +13608,7 @@ func (m *StoragePolicyMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call // order to get all numeric fields that were incremented/decremented, call
// AddedFields(). // AddedFields().
func (m *StoragePolicyMutation) Fields() []string { func (m *StoragePolicyMutation) Fields() []string {
fields := make([]string, 0, 15) fields := make([]string, 0, 16)
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, storagepolicy.FieldCreatedAt) fields = append(fields, storagepolicy.FieldCreatedAt)
} }
@ -13587,6 +13624,9 @@ func (m *StoragePolicyMutation) Fields() []string {
if m._type != nil { if m._type != nil {
fields = append(fields, storagepolicy.FieldType) fields = append(fields, storagepolicy.FieldType)
} }
if m.status != nil {
fields = append(fields, storagepolicy.FieldStatus)
}
if m.server != nil { if m.server != nil {
fields = append(fields, storagepolicy.FieldServer) fields = append(fields, storagepolicy.FieldServer)
} }
@ -13635,6 +13675,8 @@ func (m *StoragePolicyMutation) Field(name string) (ent.Value, bool) {
return m.Name() return m.Name()
case storagepolicy.FieldType: case storagepolicy.FieldType:
return m.GetType() return m.GetType()
case storagepolicy.FieldStatus:
return m.Status()
case storagepolicy.FieldServer: case storagepolicy.FieldServer:
return m.Server() return m.Server()
case storagepolicy.FieldBucketName: case storagepolicy.FieldBucketName:
@ -13674,6 +13716,8 @@ func (m *StoragePolicyMutation) OldField(ctx context.Context, name string) (ent.
return m.OldName(ctx) return m.OldName(ctx)
case storagepolicy.FieldType: case storagepolicy.FieldType:
return m.OldType(ctx) return m.OldType(ctx)
case storagepolicy.FieldStatus:
return m.OldStatus(ctx)
case storagepolicy.FieldServer: case storagepolicy.FieldServer:
return m.OldServer(ctx) return m.OldServer(ctx)
case storagepolicy.FieldBucketName: case storagepolicy.FieldBucketName:
@ -13738,6 +13782,13 @@ func (m *StoragePolicyMutation) SetField(name string, value ent.Value) error {
} }
m.SetType(v) m.SetType(v)
return nil return nil
case storagepolicy.FieldStatus:
v, ok := value.(storagepolicy.Status)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetStatus(v)
return nil
case storagepolicy.FieldServer: case storagepolicy.FieldServer:
v, ok := value.(string) v, ok := value.(string)
if !ok { if !ok {
@ -13956,6 +14007,9 @@ func (m *StoragePolicyMutation) ResetField(name string) error {
case storagepolicy.FieldType: case storagepolicy.FieldType:
m.ResetType() m.ResetType()
return nil return nil
case storagepolicy.FieldStatus:
m.ResetStatus()
return nil
case storagepolicy.FieldServer: case storagepolicy.FieldServer:
m.ResetServer() m.ResetServer()
return nil return nil

@ -361,7 +361,7 @@ func init() {
// storagepolicy.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. // storagepolicy.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
storagepolicy.UpdateDefaultUpdatedAt = storagepolicyDescUpdatedAt.UpdateDefault.(func() time.Time) storagepolicy.UpdateDefaultUpdatedAt = storagepolicyDescUpdatedAt.UpdateDefault.(func() time.Time)
// storagepolicyDescSettings is the schema descriptor for settings field. // storagepolicyDescSettings is the schema descriptor for settings field.
storagepolicyDescSettings := storagepolicyFields[10].Descriptor() storagepolicyDescSettings := storagepolicyFields[11].Descriptor()
// storagepolicy.DefaultSettings holds the default value on creation for the settings field. // storagepolicy.DefaultSettings holds the default value on creation for the settings field.
storagepolicy.DefaultSettings = storagepolicyDescSettings.Default.(*types.PolicySetting) storagepolicy.DefaultSettings = storagepolicyDescSettings.Default.(*types.PolicySetting)
taskMixin := schema.Task{}.Mixin() taskMixin := schema.Task{}.Mixin()

@ -16,6 +16,10 @@ func (StoragePolicy) Fields() []ent.Field {
return []ent.Field{ return []ent.Field{
field.String("name"), field.String("name"),
field.String("type"), field.String("type"),
// Suspended policies keep serving reads but reject new uploads.
field.Enum("status").
Values("active", "suspended").
Default("active"),
field.String("server"). field.String("server").
Optional(), Optional(),
field.String("bucket_name"). field.String("bucket_name").

@ -30,6 +30,8 @@ type StoragePolicy struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// Type holds the value of the "type" field. // Type holds the value of the "type" field.
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
// Status holds the value of the "status" field.
Status storagepolicy.Status `json:"status,omitempty"`
// Server holds the value of the "server" field. // Server holds the value of the "server" field.
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
// BucketName holds the value of the "bucket_name" field. // BucketName holds the value of the "bucket_name" field.
@ -122,7 +124,7 @@ func (*StoragePolicy) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullBool) values[i] = new(sql.NullBool)
case storagepolicy.FieldID, storagepolicy.FieldMaxSize, storagepolicy.FieldNodeID: case storagepolicy.FieldID, storagepolicy.FieldMaxSize, storagepolicy.FieldNodeID:
values[i] = new(sql.NullInt64) values[i] = new(sql.NullInt64)
case storagepolicy.FieldName, storagepolicy.FieldType, storagepolicy.FieldServer, storagepolicy.FieldBucketName, storagepolicy.FieldAccessKey, storagepolicy.FieldSecretKey, storagepolicy.FieldDirNameRule, storagepolicy.FieldFileNameRule: case storagepolicy.FieldName, storagepolicy.FieldType, storagepolicy.FieldStatus, storagepolicy.FieldServer, storagepolicy.FieldBucketName, storagepolicy.FieldAccessKey, storagepolicy.FieldSecretKey, storagepolicy.FieldDirNameRule, storagepolicy.FieldFileNameRule:
values[i] = new(sql.NullString) values[i] = new(sql.NullString)
case storagepolicy.FieldCreatedAt, storagepolicy.FieldUpdatedAt, storagepolicy.FieldDeletedAt: case storagepolicy.FieldCreatedAt, storagepolicy.FieldUpdatedAt, storagepolicy.FieldDeletedAt:
values[i] = new(sql.NullTime) values[i] = new(sql.NullTime)
@ -178,6 +180,12 @@ func (sp *StoragePolicy) assignValues(columns []string, values []any) error {
} else if value.Valid { } else if value.Valid {
sp.Type = value.String sp.Type = value.String
} }
case storagepolicy.FieldStatus:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field status", values[i])
} else if value.Valid {
sp.Status = storagepolicy.Status(value.String)
}
case storagepolicy.FieldServer: case storagepolicy.FieldServer:
if value, ok := values[i].(*sql.NullString); !ok { if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field server", values[i]) return fmt.Errorf("unexpected type %T for field server", values[i])
@ -313,6 +321,9 @@ func (sp *StoragePolicy) String() string {
builder.WriteString("type=") builder.WriteString("type=")
builder.WriteString(sp.Type) builder.WriteString(sp.Type)
builder.WriteString(", ") builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(fmt.Sprintf("%v", sp.Status))
builder.WriteString(", ")
builder.WriteString("server=") builder.WriteString("server=")
builder.WriteString(sp.Server) builder.WriteString(sp.Server)
builder.WriteString(", ") builder.WriteString(", ")

@ -3,6 +3,7 @@
package storagepolicy package storagepolicy
import ( import (
"fmt"
"time" "time"
"entgo.io/ent" "entgo.io/ent"
@ -26,6 +27,8 @@ const (
FieldName = "name" FieldName = "name"
// FieldType holds the string denoting the type field in the database. // FieldType holds the string denoting the type field in the database.
FieldType = "type" FieldType = "type"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldServer holds the string denoting the server field in the database. // FieldServer holds the string denoting the server field in the database.
FieldServer = "server" FieldServer = "server"
// FieldBucketName holds the string denoting the bucket_name field in the database. // FieldBucketName holds the string denoting the bucket_name field in the database.
@ -94,6 +97,7 @@ var Columns = []string{
FieldDeletedAt, FieldDeletedAt,
FieldName, FieldName,
FieldType, FieldType,
FieldStatus,
FieldServer, FieldServer,
FieldBucketName, FieldBucketName,
FieldIsPrivate, FieldIsPrivate,
@ -134,6 +138,32 @@ var (
DefaultSettings *types.PolicySetting DefaultSettings *types.PolicySetting
) )
// Status defines the type for the "status" enum field.
type Status string
// StatusActive is the default value of the Status enum.
const DefaultStatus = StatusActive
// Status values.
const (
StatusActive Status = "active"
StatusSuspended Status = "suspended"
)
func (s Status) String() string {
return string(s)
}
// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
func StatusValidator(s Status) error {
switch s {
case StatusActive, StatusSuspended:
return nil
default:
return fmt.Errorf("storagepolicy: invalid enum value for status field: %q", s)
}
}
// OrderOption defines the ordering options for the StoragePolicy queries. // OrderOption defines the ordering options for the StoragePolicy queries.
type OrderOption func(*sql.Selector) type OrderOption func(*sql.Selector)
@ -167,6 +197,11 @@ func ByType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldType, opts...).ToFunc() return sql.OrderByField(FieldType, opts...).ToFunc()
} }
// ByStatus orders the results by the status field.
func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStatus, opts...).ToFunc()
}
// ByServer orders the results by the server field. // ByServer orders the results by the server field.
func ByServer(opts ...sql.OrderTermOption) OrderOption { func ByServer(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldServer, opts...).ToFunc() return sql.OrderByField(FieldServer, opts...).ToFunc()

@ -385,6 +385,26 @@ func TypeContainsFold(v string) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldContainsFold(FieldType, v)) return predicate.StoragePolicy(sql.FieldContainsFold(FieldType, v))
} }
// StatusEQ applies the EQ predicate on the "status" field.
func StatusEQ(v Status) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldEQ(FieldStatus, v))
}
// StatusNEQ applies the NEQ predicate on the "status" field.
func StatusNEQ(v Status) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldNEQ(FieldStatus, v))
}
// StatusIn applies the In predicate on the "status" field.
func StatusIn(vs ...Status) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldIn(FieldStatus, vs...))
}
// StatusNotIn applies the NotIn predicate on the "status" field.
func StatusNotIn(vs ...Status) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldNotIn(FieldStatus, vs...))
}
// ServerEQ applies the EQ predicate on the "server" field. // ServerEQ applies the EQ predicate on the "server" field.
func ServerEQ(v string) predicate.StoragePolicy { func ServerEQ(v string) predicate.StoragePolicy {
return predicate.StoragePolicy(sql.FieldEQ(FieldServer, v)) return predicate.StoragePolicy(sql.FieldEQ(FieldServer, v))

@ -81,6 +81,20 @@ func (spc *StoragePolicyCreate) SetType(s string) *StoragePolicyCreate {
return spc return spc
} }
// SetStatus sets the "status" field.
func (spc *StoragePolicyCreate) SetStatus(s storagepolicy.Status) *StoragePolicyCreate {
spc.mutation.SetStatus(s)
return spc
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (spc *StoragePolicyCreate) SetNillableStatus(s *storagepolicy.Status) *StoragePolicyCreate {
if s != nil {
spc.SetStatus(*s)
}
return spc
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (spc *StoragePolicyCreate) SetServer(s string) *StoragePolicyCreate { func (spc *StoragePolicyCreate) SetServer(s string) *StoragePolicyCreate {
spc.mutation.SetServer(s) spc.mutation.SetServer(s)
@ -314,6 +328,10 @@ func (spc *StoragePolicyCreate) defaults() error {
v := storagepolicy.DefaultUpdatedAt() v := storagepolicy.DefaultUpdatedAt()
spc.mutation.SetUpdatedAt(v) spc.mutation.SetUpdatedAt(v)
} }
if _, ok := spc.mutation.Status(); !ok {
v := storagepolicy.DefaultStatus
spc.mutation.SetStatus(v)
}
if _, ok := spc.mutation.Settings(); !ok { if _, ok := spc.mutation.Settings(); !ok {
v := storagepolicy.DefaultSettings v := storagepolicy.DefaultSettings
spc.mutation.SetSettings(v) spc.mutation.SetSettings(v)
@ -335,6 +353,14 @@ func (spc *StoragePolicyCreate) check() error {
if _, ok := spc.mutation.GetType(); !ok { if _, ok := spc.mutation.GetType(); !ok {
return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "StoragePolicy.type"`)} return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "StoragePolicy.type"`)}
} }
if _, ok := spc.mutation.Status(); !ok {
return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "StoragePolicy.status"`)}
}
if v, ok := spc.mutation.Status(); ok {
if err := storagepolicy.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "StoragePolicy.status": %w`, err)}
}
}
return nil return nil
} }
@ -389,6 +415,10 @@ func (spc *StoragePolicyCreate) createSpec() (*StoragePolicy, *sqlgraph.CreateSp
_spec.SetField(storagepolicy.FieldType, field.TypeString, value) _spec.SetField(storagepolicy.FieldType, field.TypeString, value)
_node.Type = value _node.Type = value
} }
if value, ok := spc.mutation.Status(); ok {
_spec.SetField(storagepolicy.FieldStatus, field.TypeEnum, value)
_node.Status = value
}
if value, ok := spc.mutation.Server(); ok { if value, ok := spc.mutation.Server(); ok {
_spec.SetField(storagepolicy.FieldServer, field.TypeString, value) _spec.SetField(storagepolicy.FieldServer, field.TypeString, value)
_node.Server = value _node.Server = value
@ -596,6 +626,18 @@ func (u *StoragePolicyUpsert) UpdateType() *StoragePolicyUpsert {
return u return u
} }
// SetStatus sets the "status" field.
func (u *StoragePolicyUpsert) SetStatus(v storagepolicy.Status) *StoragePolicyUpsert {
u.Set(storagepolicy.FieldStatus, v)
return u
}
// UpdateStatus sets the "status" field to the value that was provided on create.
func (u *StoragePolicyUpsert) UpdateStatus() *StoragePolicyUpsert {
u.SetExcluded(storagepolicy.FieldStatus)
return u
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (u *StoragePolicyUpsert) SetServer(v string) *StoragePolicyUpsert { func (u *StoragePolicyUpsert) SetServer(v string) *StoragePolicyUpsert {
u.Set(storagepolicy.FieldServer, v) u.Set(storagepolicy.FieldServer, v)
@ -890,6 +932,20 @@ func (u *StoragePolicyUpsertOne) UpdateType() *StoragePolicyUpsertOne {
}) })
} }
// SetStatus sets the "status" field.
func (u *StoragePolicyUpsertOne) SetStatus(v storagepolicy.Status) *StoragePolicyUpsertOne {
return u.Update(func(s *StoragePolicyUpsert) {
s.SetStatus(v)
})
}
// UpdateStatus sets the "status" field to the value that was provided on create.
func (u *StoragePolicyUpsertOne) UpdateStatus() *StoragePolicyUpsertOne {
return u.Update(func(s *StoragePolicyUpsert) {
s.UpdateStatus()
})
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (u *StoragePolicyUpsertOne) SetServer(v string) *StoragePolicyUpsertOne { func (u *StoragePolicyUpsertOne) SetServer(v string) *StoragePolicyUpsertOne {
return u.Update(func(s *StoragePolicyUpsert) { return u.Update(func(s *StoragePolicyUpsert) {
@ -1386,6 +1442,20 @@ func (u *StoragePolicyUpsertBulk) UpdateType() *StoragePolicyUpsertBulk {
}) })
} }
// SetStatus sets the "status" field.
func (u *StoragePolicyUpsertBulk) SetStatus(v storagepolicy.Status) *StoragePolicyUpsertBulk {
return u.Update(func(s *StoragePolicyUpsert) {
s.SetStatus(v)
})
}
// UpdateStatus sets the "status" field to the value that was provided on create.
func (u *StoragePolicyUpsertBulk) UpdateStatus() *StoragePolicyUpsertBulk {
return u.Update(func(s *StoragePolicyUpsert) {
s.UpdateStatus()
})
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (u *StoragePolicyUpsertBulk) SetServer(v string) *StoragePolicyUpsertBulk { func (u *StoragePolicyUpsertBulk) SetServer(v string) *StoragePolicyUpsertBulk {
return u.Update(func(s *StoragePolicyUpsert) { return u.Update(func(s *StoragePolicyUpsert) {

@ -87,6 +87,20 @@ func (spu *StoragePolicyUpdate) SetNillableType(s *string) *StoragePolicyUpdate
return spu return spu
} }
// SetStatus sets the "status" field.
func (spu *StoragePolicyUpdate) SetStatus(s storagepolicy.Status) *StoragePolicyUpdate {
spu.mutation.SetStatus(s)
return spu
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (spu *StoragePolicyUpdate) SetNillableStatus(s *storagepolicy.Status) *StoragePolicyUpdate {
if s != nil {
spu.SetStatus(*s)
}
return spu
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (spu *StoragePolicyUpdate) SetServer(s string) *StoragePolicyUpdate { func (spu *StoragePolicyUpdate) SetServer(s string) *StoragePolicyUpdate {
spu.mutation.SetServer(s) spu.mutation.SetServer(s)
@ -452,7 +466,20 @@ func (spu *StoragePolicyUpdate) defaults() error {
return nil return nil
} }
// check runs all checks and user-defined validators on the builder.
func (spu *StoragePolicyUpdate) check() error {
if v, ok := spu.mutation.Status(); ok {
if err := storagepolicy.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "StoragePolicy.status": %w`, err)}
}
}
return nil
}
func (spu *StoragePolicyUpdate) sqlSave(ctx context.Context) (n int, err error) { func (spu *StoragePolicyUpdate) sqlSave(ctx context.Context) (n int, err error) {
if err := spu.check(); err != nil {
return n, err
}
_spec := sqlgraph.NewUpdateSpec(storagepolicy.Table, storagepolicy.Columns, sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt)) _spec := sqlgraph.NewUpdateSpec(storagepolicy.Table, storagepolicy.Columns, sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt))
if ps := spu.mutation.predicates; len(ps) > 0 { if ps := spu.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) { _spec.Predicate = func(selector *sql.Selector) {
@ -476,6 +503,9 @@ func (spu *StoragePolicyUpdate) sqlSave(ctx context.Context) (n int, err error)
if value, ok := spu.mutation.GetType(); ok { if value, ok := spu.mutation.GetType(); ok {
_spec.SetField(storagepolicy.FieldType, field.TypeString, value) _spec.SetField(storagepolicy.FieldType, field.TypeString, value)
} }
if value, ok := spu.mutation.Status(); ok {
_spec.SetField(storagepolicy.FieldStatus, field.TypeEnum, value)
}
if value, ok := spu.mutation.Server(); ok { if value, ok := spu.mutation.Server(); ok {
_spec.SetField(storagepolicy.FieldServer, field.TypeString, value) _spec.SetField(storagepolicy.FieldServer, field.TypeString, value)
} }
@ -771,6 +801,20 @@ func (spuo *StoragePolicyUpdateOne) SetNillableType(s *string) *StoragePolicyUpd
return spuo return spuo
} }
// SetStatus sets the "status" field.
func (spuo *StoragePolicyUpdateOne) SetStatus(s storagepolicy.Status) *StoragePolicyUpdateOne {
spuo.mutation.SetStatus(s)
return spuo
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func (spuo *StoragePolicyUpdateOne) SetNillableStatus(s *storagepolicy.Status) *StoragePolicyUpdateOne {
if s != nil {
spuo.SetStatus(*s)
}
return spuo
}
// SetServer sets the "server" field. // SetServer sets the "server" field.
func (spuo *StoragePolicyUpdateOne) SetServer(s string) *StoragePolicyUpdateOne { func (spuo *StoragePolicyUpdateOne) SetServer(s string) *StoragePolicyUpdateOne {
spuo.mutation.SetServer(s) spuo.mutation.SetServer(s)
@ -1149,7 +1193,20 @@ func (spuo *StoragePolicyUpdateOne) defaults() error {
return nil return nil
} }
// check runs all checks and user-defined validators on the builder.
func (spuo *StoragePolicyUpdateOne) check() error {
if v, ok := spuo.mutation.Status(); ok {
if err := storagepolicy.StatusValidator(v); err != nil {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "StoragePolicy.status": %w`, err)}
}
}
return nil
}
func (spuo *StoragePolicyUpdateOne) sqlSave(ctx context.Context) (_node *StoragePolicy, err error) { func (spuo *StoragePolicyUpdateOne) sqlSave(ctx context.Context) (_node *StoragePolicy, err error) {
if err := spuo.check(); err != nil {
return _node, err
}
_spec := sqlgraph.NewUpdateSpec(storagepolicy.Table, storagepolicy.Columns, sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt)) _spec := sqlgraph.NewUpdateSpec(storagepolicy.Table, storagepolicy.Columns, sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt))
id, ok := spuo.mutation.ID() id, ok := spuo.mutation.ID()
if !ok { if !ok {
@ -1190,6 +1247,9 @@ func (spuo *StoragePolicyUpdateOne) sqlSave(ctx context.Context) (_node *Storage
if value, ok := spuo.mutation.GetType(); ok { if value, ok := spuo.mutation.GetType(); ok {
_spec.SetField(storagepolicy.FieldType, field.TypeString, value) _spec.SetField(storagepolicy.FieldType, field.TypeString, value)
} }
if value, ok := spuo.mutation.Status(); ok {
_spec.SetField(storagepolicy.FieldStatus, field.TypeEnum, value)
}
if value, ok := spuo.mutation.Server(); ok { if value, ok := spuo.mutation.Server(); ok {
_spec.SetField(storagepolicy.FieldServer, field.TypeString, value) _spec.SetField(storagepolicy.FieldServer, field.TypeString, value)
} }

@ -1176,7 +1176,9 @@
"fileEncryption": "File encryption", "fileEncryption": "File encryption",
"enableFileEncryption": "Enable file encryption", "enableFileEncryption": "Enable file encryption",
"enableFileEncryptionDes": "When enabled, file blobs will be stored encrypted. Changing this setting only affects newly added files.", "enableFileEncryptionDes": "When enabled, file blobs will be stored encrypted. Changing this setting only affects newly added files.",
"encryptedFile": "Encrypted files" "encryptedFile": "Encrypted files",
"status": "Status",
"statusDes": "Suspended policies keep serving existing files but reject new uploads."
}, },
"node": { "node": {
"slave": "slave", "slave": "slave",

@ -1176,7 +1176,9 @@
"fileEncryption": "文件加密", "fileEncryption": "文件加密",
"enableFileEncryption": "启用文件加密", "enableFileEncryption": "启用文件加密",
"enableFileEncryptionDes": "开启后,文件 Blob 会被加密存储。更改此设置只对新增的文件有效。", "enableFileEncryptionDes": "开启后,文件 Blob 会被加密存储。更改此设置只对新增的文件有效。",
"encryptedFile": "加密的文件" "encryptedFile": "加密的文件",
"status": "状态",
"statusDes": "已暂停的存储策略仍可读取已有文件,但会拒绝新的上传。"
}, },
"node": { "node": {
"slave": "从机", "slave": "从机",

@ -123,9 +123,15 @@ export interface CommonMixin {
deleted_at?: string; deleted_at?: string;
} }
export enum PolicyStatus {
active = "active",
suspended = "suspended",
}
export interface StoragePolicy extends CommonMixin { export interface StoragePolicy extends CommonMixin {
name: string; name: string;
type: PolicyType; type: PolicyType;
status?: PolicyStatus;
server?: string; server?: string;
bucket_name?: string; bucket_name?: string;
is_private?: boolean; is_private?: boolean;

@ -14,7 +14,7 @@ import { useCallback, useContext, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { createStoragePolicyCors, getOneDriveDriverRoot } from "../../../../../api/api"; import { createStoragePolicyCors, getOneDriveDriverRoot } from "../../../../../api/api";
import { StoragePolicy } from "../../../../../api/dashboard"; import { PolicyStatus, StoragePolicy } from "../../../../../api/dashboard";
import { PolicyType } from "../../../../../api/explorer"; import { PolicyType } from "../../../../../api/explorer";
import { useAppDispatch } from "../../../../../redux/hooks"; import { useAppDispatch } from "../../../../../redux/hooks";
import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar";
@ -291,6 +291,21 @@ const BasicInfoSection = () => {
<NoMarginHelperText>{t("policy.policyName")}</NoMarginHelperText> <NoMarginHelperText>{t("policy.policyName")}</NoMarginHelperText>
</FormControl> </FormControl>
</SettingForm> </SettingForm>
<SettingForm title={t("policy.status")} lgWidth={5}>
<FormControl fullWidth>
<DenseSelect
value={values.status ?? PolicyStatus.active}
onChange={(e) => setPolicy((prev) => ({ ...prev, status: e.target.value as PolicyStatus }))}
>
{Object.values(PolicyStatus).map((value) => (
<SquareMenuItem value={value} key={value}>
<ListItemText slotProps={{ primary: { variant: "body2" } }}>{t(`node.${value}`)}</ListItemText>
</SquareMenuItem>
))}
</DenseSelect>
<NoMarginHelperText>{t("policy.statusDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
{showBucket && ( {showBucket && (
<> <>
<SettingForm title={t(policyProps.bucketName ?? "")} lgWidth={5}> <SettingForm title={t(policyProps.bucketName ?? "")} lgWidth={5}>

@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { deleteStoragePolicy, getStoragePolicyDetail } from "../../../api/api"; import { deleteStoragePolicy, getStoragePolicyDetail } from "../../../api/api";
import { StoragePolicy } from "../../../api/dashboard"; import { PolicyStatus, StoragePolicy } from "../../../api/dashboard";
import { useAppDispatch } from "../../../redux/hooks"; import { useAppDispatch } from "../../../redux/hooks";
import { confirmOperation } from "../../../redux/thunks/dialog"; import { confirmOperation } from "../../../redux/thunks/dialog";
import { sizeToString } from "../../../util"; import { sizeToString } from "../../../util";
@ -125,6 +125,9 @@ const StoragePolicyCard = ({ policy, onRefresh, loading }: StoragePolicyCardProp
> >
<Typography variant="subtitle1" fontWeight={600}> <Typography variant="subtitle1" fontWeight={600}>
{policy?.name} {policy?.name}
{policy?.status == PolicyStatus.suspended && (
<SquareChip sx={{ ml: 1 }} size="small" color="warning" label={t("node.suspended")} />
)}
</Typography> </Typography>
{policy && ( {policy && (
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">

@ -125,6 +125,11 @@ func (c *storagePolicyClient) Upsert(ctx context.Context, policy *ent.StoragePol
SetSettings(policy.Settings). SetSettings(policy.Settings).
SetNillableNodeID(nodeId) SetNillableNodeID(nodeId)
// Status is optional in the update payload; empty keeps the stored value.
if policy.Status != "" {
updateQuery.SetStatus(policy.Status)
}
if policy.Type != types.PolicyTypeOd { if policy.Type != types.PolicyTypeOd {
updateQuery.SetAccessKey(policy.AccessKey) updateQuery.SetAccessKey(policy.AccessKey)
} }
@ -146,7 +151,9 @@ func (c *storagePolicyClient) GetByGroup(ctx context.Context, group *ent.Group)
val, skipCache := ctx.Value(SkipStoragePolicyCache{}).(bool) val, skipCache := ctx.Value(SkipStoragePolicyCache{}).(bool)
skipCache = skipCache && val skipCache = skipCache && val
res, err := withStoragePolicyEagerLoading(ctx, c.client.Group.QueryStoragePolicies(group)).WithNode().First(ctx) res, err := withStoragePolicyEagerLoading(ctx, c.client.Group.QueryStoragePolicies(group)).
Where(storagepolicy.StatusEQ(storagepolicy.StatusActive)).
WithNode().First(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("get storage policies: %w", err) return nil, fmt.Errorf("get storage policies: %w", err)
} }

@ -0,0 +1,42 @@
package inventory
import (
"context"
"testing"
"github.com/cloudreve/Cloudreve/v4/ent/enttest"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/stretchr/testify/require"
)
func TestGetByGroupSkipsSuspended(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("suspended group policy yields not found", func(t *testing.T) {
p := client.StoragePolicy.Create().
SetName("suspended").SetType("local").
SetStatus(storagepolicy.StatusSuspended).SaveX(ctx)
group := client.Group.Create().SetName("g1").SetPermissions(&boolset.BooleanSet{}).
SetStoragePolicies(p).SaveX(ctx)
_, err := pc.GetByGroup(ctx, group)
require.Error(t, err)
})
t.Run("active group policy returned", func(t *testing.T) {
p := client.StoragePolicy.Create().
SetName("active").SetType("local").
SetStatus(storagepolicy.StatusActive).SaveX(ctx)
group := client.Group.Create().SetName("g2").SetPermissions(&boolset.BooleanSet{}).
SetStoragePolicies(p).SaveX(ctx)
got, err := pc.GetByGroup(ctx, group)
require.NoError(t, err)
require.Equal(t, "active", got.Name)
})
}

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/storagepolicy"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
@ -138,6 +139,10 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ..
return nil, err return nil, err
} }
if policy.Status == storagepolicy.StatusSuspended {
return nil, serializer.NewError(serializer.CodePolicyNotAllowed, "Storage policy is suspended", nil)
}
// Encryption setting // Encryption setting
var ( var (
encryptMetadata *types.EncryptMetadata encryptMetadata *types.EncryptMetadata

Loading…
Cancel
Save