feat(explorer): save user's view setting to server / optionally share view setting via share link (#2232)

pull/2224/merge
Aaron Liu 4 months ago
parent c13b7365b0
commit 522fcca6af

@ -1 +1 @@
Subproject commit d674a23b21bdeb0a415985d4d5dc2b2051bc80d1
Subproject commit 9f91f8c98a9e645ce49c9180af1fce75b1eb46d5

File diff suppressed because one or more lines are too long

@ -300,6 +300,7 @@ var (
{Name: "downloads", Type: field.TypeInt, Default: 0},
{Name: "expires", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
{Name: "remain_downloads", Type: field.TypeInt, Nullable: true},
{Name: "props", Type: field.TypeJSON, Nullable: true},
{Name: "file_shares", Type: field.TypeInt, Nullable: true},
{Name: "user_shares", Type: field.TypeInt, Nullable: true},
}
@ -311,13 +312,13 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "shares_files_shares",
Columns: []*schema.Column{SharesColumns[9]},
Columns: []*schema.Column{SharesColumns[10]},
RefColumns: []*schema.Column{FilesColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "shares_users_shares",
Columns: []*schema.Column{SharesColumns[10]},
Columns: []*schema.Column{SharesColumns[11]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.SetNull,
},

@ -8958,6 +8958,7 @@ type ShareMutation struct {
expires *time.Time
remain_downloads *int
addremain_downloads *int
props **types.ShareProps
clearedFields map[string]struct{}
user *int
cleareduser bool
@ -9467,6 +9468,55 @@ func (m *ShareMutation) ResetRemainDownloads() {
delete(m.clearedFields, share.FieldRemainDownloads)
}
// SetProps sets the "props" field.
func (m *ShareMutation) SetProps(tp *types.ShareProps) {
m.props = &tp
}
// Props returns the value of the "props" field in the mutation.
func (m *ShareMutation) Props() (r *types.ShareProps, exists bool) {
v := m.props
if v == nil {
return
}
return *v, true
}
// OldProps returns the old "props" field's value of the Share entity.
// If the Share 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 *ShareMutation) OldProps(ctx context.Context) (v *types.ShareProps, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldProps is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldProps requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldProps: %w", err)
}
return oldValue.Props, nil
}
// ClearProps clears the value of the "props" field.
func (m *ShareMutation) ClearProps() {
m.props = nil
m.clearedFields[share.FieldProps] = struct{}{}
}
// PropsCleared returns if the "props" field was cleared in this mutation.
func (m *ShareMutation) PropsCleared() bool {
_, ok := m.clearedFields[share.FieldProps]
return ok
}
// ResetProps resets all changes to the "props" field.
func (m *ShareMutation) ResetProps() {
m.props = nil
delete(m.clearedFields, share.FieldProps)
}
// SetUserID sets the "user" edge to the User entity by id.
func (m *ShareMutation) SetUserID(id int) {
m.user = &id
@ -9579,7 +9629,7 @@ func (m *ShareMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ShareMutation) Fields() []string {
fields := make([]string, 0, 8)
fields := make([]string, 0, 9)
if m.created_at != nil {
fields = append(fields, share.FieldCreatedAt)
}
@ -9604,6 +9654,9 @@ func (m *ShareMutation) Fields() []string {
if m.remain_downloads != nil {
fields = append(fields, share.FieldRemainDownloads)
}
if m.props != nil {
fields = append(fields, share.FieldProps)
}
return fields
}
@ -9628,6 +9681,8 @@ func (m *ShareMutation) Field(name string) (ent.Value, bool) {
return m.Expires()
case share.FieldRemainDownloads:
return m.RemainDownloads()
case share.FieldProps:
return m.Props()
}
return nil, false
}
@ -9653,6 +9708,8 @@ func (m *ShareMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldExpires(ctx)
case share.FieldRemainDownloads:
return m.OldRemainDownloads(ctx)
case share.FieldProps:
return m.OldProps(ctx)
}
return nil, fmt.Errorf("unknown Share field %s", name)
}
@ -9718,6 +9775,13 @@ func (m *ShareMutation) SetField(name string, value ent.Value) error {
}
m.SetRemainDownloads(v)
return nil
case share.FieldProps:
v, ok := value.(*types.ShareProps)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetProps(v)
return nil
}
return fmt.Errorf("unknown Share field %s", name)
}
@ -9799,6 +9863,9 @@ func (m *ShareMutation) ClearedFields() []string {
if m.FieldCleared(share.FieldRemainDownloads) {
fields = append(fields, share.FieldRemainDownloads)
}
if m.FieldCleared(share.FieldProps) {
fields = append(fields, share.FieldProps)
}
return fields
}
@ -9825,6 +9892,9 @@ func (m *ShareMutation) ClearField(name string) error {
case share.FieldRemainDownloads:
m.ClearRemainDownloads()
return nil
case share.FieldProps:
m.ClearProps()
return nil
}
return fmt.Errorf("unknown Share nullable field %s", name)
}
@ -9857,6 +9927,9 @@ func (m *ShareMutation) ResetField(name string) error {
case share.FieldRemainDownloads:
m.ResetRemainDownloads()
return nil
case share.FieldProps:
m.ResetProps()
return nil
}
return fmt.Errorf("unknown Share field %s", name)
}

@ -5,6 +5,7 @@ import (
"entgo.io/ent/dialect"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
)
// Share holds the schema definition for the Share entity.
@ -30,6 +31,7 @@ func (Share) Fields() []ent.Field {
field.Int("remain_downloads").
Nillable().
Optional(),
field.JSON("props", &types.ShareProps{}).Optional(),
}
}

@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"strings"
"time"
@ -12,6 +13,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/file"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
)
// Share is the model entity for the Share schema.
@ -35,6 +37,8 @@ type Share struct {
Expires *time.Time `json:"expires,omitempty"`
// RemainDownloads holds the value of the "remain_downloads" field.
RemainDownloads *int `json:"remain_downloads,omitempty"`
// Props holds the value of the "props" field.
Props *types.ShareProps `json:"props,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the ShareQuery when eager-loading is set.
Edges ShareEdges `json:"edges"`
@ -85,6 +89,8 @@ func (*Share) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case share.FieldProps:
values[i] = new([]byte)
case share.FieldID, share.FieldViews, share.FieldDownloads, share.FieldRemainDownloads:
values[i] = new(sql.NullInt64)
case share.FieldPassword:
@ -167,6 +173,14 @@ func (s *Share) assignValues(columns []string, values []any) error {
s.RemainDownloads = new(int)
*s.RemainDownloads = int(value.Int64)
}
case share.FieldProps:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field props", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &s.Props); err != nil {
return fmt.Errorf("unmarshal field props: %w", err)
}
}
case share.ForeignKeys[0]:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for edge-field file_shares", value)
@ -256,6 +270,9 @@ func (s *Share) String() string {
builder.WriteString("remain_downloads=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("props=")
builder.WriteString(fmt.Sprintf("%v", s.Props))
builder.WriteByte(')')
return builder.String()
}

@ -31,6 +31,8 @@ const (
FieldExpires = "expires"
// FieldRemainDownloads holds the string denoting the remain_downloads field in the database.
FieldRemainDownloads = "remain_downloads"
// FieldProps holds the string denoting the props field in the database.
FieldProps = "props"
// EdgeUser holds the string denoting the user edge name in mutations.
EdgeUser = "user"
// EdgeFile holds the string denoting the file edge name in mutations.
@ -64,6 +66,7 @@ var Columns = []string{
FieldDownloads,
FieldExpires,
FieldRemainDownloads,
FieldProps,
}
// ForeignKeys holds the SQL foreign-keys that are owned by the "shares"

@ -480,6 +480,16 @@ func RemainDownloadsNotNil() predicate.Share {
return predicate.Share(sql.FieldNotNull(FieldRemainDownloads))
}
// PropsIsNil applies the IsNil predicate on the "props" field.
func PropsIsNil() predicate.Share {
return predicate.Share(sql.FieldIsNull(FieldProps))
}
// PropsNotNil applies the NotNil predicate on the "props" field.
func PropsNotNil() predicate.Share {
return predicate.Share(sql.FieldNotNull(FieldProps))
}
// HasUser applies the HasEdge predicate on the "user" edge.
func HasUser() predicate.Share {
return predicate.Share(func(s *sql.Selector) {

@ -14,6 +14,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/file"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
)
// ShareCreate is the builder for creating a Share entity.
@ -136,6 +137,12 @@ func (sc *ShareCreate) SetNillableRemainDownloads(i *int) *ShareCreate {
return sc
}
// SetProps sets the "props" field.
func (sc *ShareCreate) SetProps(tp *types.ShareProps) *ShareCreate {
sc.mutation.SetProps(tp)
return sc
}
// SetUserID sets the "user" edge to the User entity by ID.
func (sc *ShareCreate) SetUserID(id int) *ShareCreate {
sc.mutation.SetUserID(id)
@ -316,6 +323,10 @@ func (sc *ShareCreate) createSpec() (*Share, *sqlgraph.CreateSpec) {
_spec.SetField(share.FieldRemainDownloads, field.TypeInt, value)
_node.RemainDownloads = &value
}
if value, ok := sc.mutation.Props(); ok {
_spec.SetField(share.FieldProps, field.TypeJSON, value)
_node.Props = value
}
if nodes := sc.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@ -528,6 +539,24 @@ func (u *ShareUpsert) ClearRemainDownloads() *ShareUpsert {
return u
}
// SetProps sets the "props" field.
func (u *ShareUpsert) SetProps(v *types.ShareProps) *ShareUpsert {
u.Set(share.FieldProps, v)
return u
}
// UpdateProps sets the "props" field to the value that was provided on create.
func (u *ShareUpsert) UpdateProps() *ShareUpsert {
u.SetExcluded(share.FieldProps)
return u
}
// ClearProps clears the value of the "props" field.
func (u *ShareUpsert) ClearProps() *ShareUpsert {
u.SetNull(share.FieldProps)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
@ -720,6 +749,27 @@ func (u *ShareUpsertOne) ClearRemainDownloads() *ShareUpsertOne {
})
}
// SetProps sets the "props" field.
func (u *ShareUpsertOne) SetProps(v *types.ShareProps) *ShareUpsertOne {
return u.Update(func(s *ShareUpsert) {
s.SetProps(v)
})
}
// UpdateProps sets the "props" field to the value that was provided on create.
func (u *ShareUpsertOne) UpdateProps() *ShareUpsertOne {
return u.Update(func(s *ShareUpsert) {
s.UpdateProps()
})
}
// ClearProps clears the value of the "props" field.
func (u *ShareUpsertOne) ClearProps() *ShareUpsertOne {
return u.Update(func(s *ShareUpsert) {
s.ClearProps()
})
}
// Exec executes the query.
func (u *ShareUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@ -1083,6 +1133,27 @@ func (u *ShareUpsertBulk) ClearRemainDownloads() *ShareUpsertBulk {
})
}
// SetProps sets the "props" field.
func (u *ShareUpsertBulk) SetProps(v *types.ShareProps) *ShareUpsertBulk {
return u.Update(func(s *ShareUpsert) {
s.SetProps(v)
})
}
// UpdateProps sets the "props" field to the value that was provided on create.
func (u *ShareUpsertBulk) UpdateProps() *ShareUpsertBulk {
return u.Update(func(s *ShareUpsert) {
s.UpdateProps()
})
}
// ClearProps clears the value of the "props" field.
func (u *ShareUpsertBulk) ClearProps() *ShareUpsertBulk {
return u.Update(func(s *ShareUpsert) {
s.ClearProps()
})
}
// Exec executes the query.
func (u *ShareUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

@ -15,6 +15,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
"github.com/cloudreve/Cloudreve/v4/ent/share"
"github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
)
// ShareUpdate is the builder for updating Share entities.
@ -165,6 +166,18 @@ func (su *ShareUpdate) ClearRemainDownloads() *ShareUpdate {
return su
}
// SetProps sets the "props" field.
func (su *ShareUpdate) SetProps(tp *types.ShareProps) *ShareUpdate {
su.mutation.SetProps(tp)
return su
}
// ClearProps clears the value of the "props" field.
func (su *ShareUpdate) ClearProps() *ShareUpdate {
su.mutation.ClearProps()
return su
}
// SetUserID sets the "user" edge to the User entity by ID.
func (su *ShareUpdate) SetUserID(id int) *ShareUpdate {
su.mutation.SetUserID(id)
@ -313,6 +326,12 @@ func (su *ShareUpdate) sqlSave(ctx context.Context) (n int, err error) {
if su.mutation.RemainDownloadsCleared() {
_spec.ClearField(share.FieldRemainDownloads, field.TypeInt)
}
if value, ok := su.mutation.Props(); ok {
_spec.SetField(share.FieldProps, field.TypeJSON, value)
}
if su.mutation.PropsCleared() {
_spec.ClearField(share.FieldProps, field.TypeJSON)
}
if su.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@ -526,6 +545,18 @@ func (suo *ShareUpdateOne) ClearRemainDownloads() *ShareUpdateOne {
return suo
}
// SetProps sets the "props" field.
func (suo *ShareUpdateOne) SetProps(tp *types.ShareProps) *ShareUpdateOne {
suo.mutation.SetProps(tp)
return suo
}
// ClearProps clears the value of the "props" field.
func (suo *ShareUpdateOne) ClearProps() *ShareUpdateOne {
suo.mutation.ClearProps()
return suo
}
// SetUserID sets the "user" edge to the User entity by ID.
func (suo *ShareUpdateOne) SetUserID(id int) *ShareUpdateOne {
suo.mutation.SetUserID(id)
@ -704,6 +735,12 @@ func (suo *ShareUpdateOne) sqlSave(ctx context.Context) (_node *Share, err error
if suo.mutation.RemainDownloadsCleared() {
_spec.ClearField(share.FieldRemainDownloads, field.TypeInt)
}
if value, ok := suo.mutation.Props(); ok {
_spec.SetField(share.FieldProps, field.TypeJSON, value)
}
if suo.mutation.PropsCleared() {
_spec.ClearField(share.FieldProps, field.TypeJSON)
}
if suo.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

@ -209,6 +209,8 @@ type FileClient interface {
Update(ctx context.Context, file *ent.File) (*ent.File, error)
// ListEntities lists entities
ListEntities(ctx context.Context, args *ListEntityParameters) (*ListEntityResult, error)
// UpdateProps updates props of a file
UpdateProps(ctx context.Context, file *ent.File, props *types.FileProps) (*ent.File, error)
}
func NewFileClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) FileClient {
@ -275,6 +277,17 @@ func (f *fileClient) Update(ctx context.Context, file *ent.File) (*ent.File, err
return q.Save(ctx)
}
func (f *fileClient) UpdateProps(ctx context.Context, file *ent.File, props *types.FileProps) (*ent.File, error) {
file, err := f.client.File.UpdateOne(file).
SetProps(props).
Save(ctx)
if err != nil {
return nil, err
}
return file, nil
}
func (f *fileClient) CountByTimeRange(ctx context.Context, start, end *time.Time) (int, error) {
if start == nil || end == nil {
return f.client.File.Query().Count(ctx)
@ -554,6 +567,10 @@ func (f *fileClient) Copy(ctx context.Context, files []*ent.File, dstMap map[int
stm.SetPrimaryEntity(file.PrimaryEntity)
}
if file.Props != nil && dstMap[file.FileChildren][0].OwnerID == file.OwnerID {
stm.SetProps(file.Props)
}
return stm
})

@ -3,6 +3,7 @@ package inventory
import (
"context"
"fmt"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"time"
"entgo.io/ent/dialect/sql"
@ -62,6 +63,7 @@ type (
Expires *time.Time
OwnerID int
FileID int
Props *types.ShareProps
}
ListShareArgs struct {
@ -122,6 +124,10 @@ func (c *shareClient) Upsert(ctx context.Context, params *CreateShareParams) (*e
createQuery.ClearExpires()
}
if params.Props != nil {
createQuery.SetProps(params.Props)
}
return createQuery.Save(ctx)
}
@ -138,6 +144,9 @@ func (c *shareClient) Upsert(ctx context.Context, params *CreateShareParams) (*e
if params.Expires != nil {
query.SetNillableExpires(params.Expires)
}
if params.Props != nil {
query.SetProps(params.Props)
}
return query.Save(ctx)
}

@ -14,6 +14,8 @@ type (
VersionRetentionMax int `json:"version_retention_max,omitempty"`
Pined []PinedFile `json:"pined,omitempty"`
Language string `json:"email_language,omitempty"`
DisableViewSync bool `json:"disable_view_sync,omitempty"`
FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"`
}
PinedFile struct {
@ -149,6 +151,32 @@ type (
PolicyType string
FileProps struct {
View *ExplorerView `json:"view,omitempty"`
}
ExplorerView struct {
PageSize int `json:"page_size" binding:"min=50"`
Order string `json:"order,omitempty" binding:"max=255"`
OrderDirection string `json:"order_direction,omitempty" binding:"eq=asc|eq=desc"`
View string `json:"view,omitempty" binding:"eq=list|eq=grid|eq=gallery"`
Thumbnail bool `json:"thumbnail,omitempty"`
GalleryWidth int `json:"gallery_width,omitempty" binding:"min=50,max=500"`
Columns []ListViewColumn `json:"columns,omitempty" binding:"max=1000"`
}
ListViewColumn struct {
Type int `json:"type" binding:"min=0"`
Width *int `json:"width,omitempty"`
Props *ColumTypeProps `json:"props,omitempty"`
}
ColumTypeProps struct {
MetadataKey string `json:"metadata_key,omitempty" binding:"max=255"`
}
ShareProps struct {
// Whether to share view setting from owner
ShareView bool `json:"share_view,omitempty"`
}
)

@ -35,6 +35,7 @@ const (
ContextHintTTL = 5 * 60 // 5 minutes
folderSummaryCachePrefix = "folder_summary_"
defaultPageSize = 100
)
type (
@ -119,15 +120,44 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi
searchParams := path.SearchParameters()
isSearching := searchParams != nil
parent, err := f.getFileByPath(ctx, navigator, path)
if err != nil {
return nil, nil, fmt.Errorf("Parent not exist: %w", err)
}
pageSize := 0
orderDirection := ""
orderBy := ""
view := navigator.GetView(ctx, parent)
if view != nil {
pageSize = view.PageSize
orderDirection = view.OrderDirection
orderBy = view.Order
}
if o.PageSize > 0 {
pageSize = o.PageSize
}
if o.OrderDirection != "" {
orderDirection = o.OrderDirection
}
if o.OrderBy != "" {
orderBy = o.OrderBy
}
// Validate pagination args
props := navigator.Capabilities(isSearching)
if o.PageSize > props.MaxPageSize {
o.PageSize = props.MaxPageSize
if pageSize > props.MaxPageSize {
pageSize = props.MaxPageSize
} else if pageSize == 0 {
pageSize = defaultPageSize
}
parent, err := f.getFileByPath(ctx, navigator, path)
if err != nil {
return nil, nil, fmt.Errorf("Parent not exist: %w", err)
if view != nil {
view.PageSize = pageSize
view.OrderDirection = orderDirection
view.Order = orderBy
}
var hintId *uuid.UUID
@ -155,9 +185,9 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi
children, err := navigator.Children(ctx, parent, &ListArgs{
Page: &inventory.PaginationArgs{
Page: o.FsOption.Page,
PageSize: o.PageSize,
OrderBy: o.OrderBy,
Order: inventory.OrderDirection(o.OrderDirection),
PageSize: pageSize,
OrderBy: orderBy,
Order: inventory.OrderDirection(orderDirection),
UseCursorPagination: o.useCursorPagination,
PageToken: o.pageToken,
},
@ -188,6 +218,7 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi
SingleFileView: children.SingleFileView,
Parent: parent,
StoragePolicy: storagePolicy,
View: view,
}, nil
}
@ -270,89 +301,6 @@ func (f *DBFS) CreateEntity(ctx context.Context, file fs.File, policy *ent.Stora
return fs.NewEntity(entity), nil
}
func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.MetadataPatch) error {
ae := serializer.NewAggregateError()
targets := make([]*File, 0, len(path))
for _, p := range path {
navigator, err := f.getNavigator(ctx, p, NavigatorCapabilityUpdateMetadata, NavigatorCapabilityLockFile)
if err != nil {
ae.Add(p.String(), err)
continue
}
target, err := f.getFileByPath(ctx, navigator, p)
if err != nil {
ae.Add(p.String(), fmt.Errorf("failed to get target file: %w", err))
continue
}
// Require Update permission
if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && target.OwnerID() != f.user.ID {
return fs.ErrOwnerOnly.WithError(fmt.Errorf("permission denied"))
}
if target.IsRootFolder() {
ae.Add(p.String(), fs.ErrNotSupportedAction.WithError(fmt.Errorf("cannot move root folder")))
continue
}
targets = append(targets, target)
}
if len(targets) == 0 {
return ae.Aggregate()
}
// Lock all targets
lockTargets := lo.Map(targets, func(value *File, key int) *LockByPath {
return &LockByPath{value.Uri(true), value, value.Type(), ""}
})
ls, err := f.acquireByPath(ctx, -1, f.user, true, fs.LockApp(fs.ApplicationUpdateMetadata), lockTargets...)
defer func() { _ = f.Release(ctx, ls) }()
if err != nil {
return err
}
metadataMap := make(map[string]string)
privateMap := make(map[string]bool)
deleted := make([]string, 0)
for _, meta := range metas {
if meta.Remove {
deleted = append(deleted, meta.Key)
continue
}
metadataMap[meta.Key] = meta.Value
if meta.Private {
privateMap[meta.Key] = meta.Private
}
}
fc, tx, ctx, err := inventory.WithTx(ctx, f.fileClient)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
}
for _, target := range targets {
if err := fc.UpsertMetadata(ctx, target.Model, metadataMap, privateMap); err != nil {
_ = inventory.Rollback(tx)
return fmt.Errorf("failed to upsert metadata: %w", err)
}
if len(deleted) > 0 {
if err := fc.RemoveMetadata(ctx, target.Model, deleted...); err != nil {
_ = inventory.Rollback(tx)
return fmt.Errorf("failed to remove metadata: %w", err)
}
}
}
if err := inventory.Commit(tx); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to commit metadata change", err)
}
return ae.Aggregate()
}
func (f *DBFS) SharedAddressTranslation(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.File, *fs.URI, error) {
o := newDbfsOption()
for _, opt := range opts {
@ -470,6 +418,9 @@ func (f *DBFS) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fil
target.FileExtendedInfo = extendedInfo
if target.OwnerID() == f.user.ID || f.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
target.FileExtendedInfo.Shares = target.Model.Edges.Shares
if target.Model.Props != nil {
target.FileExtendedInfo.View = target.Model.Props.View
}
}
entities := target.Entities()

@ -22,13 +22,20 @@ func init() {
gob.Register(map[int]*File{})
}
var filePool = &sync.Pool{
var (
filePool = &sync.Pool{
New: func() any {
return &File{
Children: make(map[string]*File),
}
},
}
}
defaultView = &types.ExplorerView{
PageSize: defaultPageSize,
View: "grid",
Thumbnail: true,
}
)
type (
File struct {
@ -42,6 +49,7 @@ type (
FileExtendedInfo *fs.FileExtendedInfo
FileFolderSummary *fs.FolderSummary
disableView bool
mu *sync.Mutex
}
)
@ -181,6 +189,31 @@ func (f *File) Uri(isRoot bool) *fs.URI {
return parent.Path[index].Join(elements...)
}
// View returns the view setting of the file, can be inherited from parent.
func (f *File) View() *types.ExplorerView {
// If owner has disabled view sync, return nil
owner := f.Owner()
if owner != nil && owner.Settings != nil && owner.Settings.DisableViewSync {
return nil
}
// If navigator has disabled view sync, return nil
userRoot := f.UserRoot()
if userRoot == nil || userRoot.disableView {
return nil
}
current := f
for current != nil {
if current.Model.Props != nil && current.Model.Props.View != nil {
return current.Model.Props.View
}
current = current.Parent
}
return defaultView
}
// UserRoot return the root file from user's view.
func (f *File) UserRoot() *File {
root := f

@ -106,6 +106,7 @@ func (n *myNavigator) To(ctx context.Context, path *fs.URI) (*File, error) {
rootPath := path.Root()
n.root.Path[pathIndexRoot], n.root.Path[pathIndexUser] = rootPath, rootPath
n.root.OwnerModel = targetUser
n.root.disableView = fsUid != n.user.ID
n.root.IsUserRoot = true
n.root.CapabilitiesBs = n.Capabilities(false).Capability
}
@ -178,3 +179,7 @@ func (n *myNavigator) FollowTx(ctx context.Context) (func(), error) {
func (n *myNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
return nil
}
func (n *myNavigator) GetView(ctx context.Context, file *File) *types.ExplorerView {
return file.View()
}

@ -53,6 +53,8 @@ type (
FollowTx(ctx context.Context) (func(), error)
// ExecuteHook performs custom operations before or after certain actions.
ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error
// GetView returns the view setting of the given file.
GetView(ctx context.Context, file *File) *types.ExplorerView
}
State interface{}
@ -100,6 +102,7 @@ const (
NavigatorCapability_CommunityPlacehodler8
NavigatorCapability_CommunityPlacehodler9
NavigatorCapabilityEnterFolder
NavigatorCapabilityModifyProps
searchTokenSeparator = "|"
)
@ -120,6 +123,7 @@ func init() {
NavigatorCapabilityInfo: true,
NavigatorCapabilityVersionControl: true,
NavigatorCapabilityEnterFolder: true,
NavigatorCapabilityModifyProps: true,
}, myNavigatorCapability)
boolset.Sets(map[NavigatorCapability]bool{
NavigatorCapabilityDownloadFile: true,
@ -129,6 +133,7 @@ func init() {
NavigatorCapabilityInfo: true,
NavigatorCapabilityVersionControl: true,
NavigatorCapabilityEnterFolder: true,
NavigatorCapabilityModifyProps: true,
}, shareNavigatorCapability)
boolset.Sets(map[NavigatorCapability]bool{
NavigatorCapabilityListChildren: true,

@ -0,0 +1,138 @@
package dbfs
import (
"context"
"fmt"
"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/serializer"
"github.com/samber/lo"
)
func (f *DBFS) PatchProps(ctx context.Context, uri *fs.URI, props *types.FileProps, delete bool) error {
navigator, err := f.getNavigator(ctx, uri, NavigatorCapabilityModifyProps, NavigatorCapabilityLockFile)
if err != nil {
return err
}
target, err := f.getFileByPath(ctx, navigator, uri)
if err != nil {
return fmt.Errorf("failed to get target file: %w", err)
}
if target.OwnerID() != f.user.ID && !f.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
return fs.ErrOwnerOnly.WithError(fmt.Errorf("only file owner can modify file props"))
}
// Lock target
lr := &LockByPath{target.Uri(true), target, target.Type(), ""}
ls, err := f.acquireByPath(ctx, -1, f.user, false, fs.LockApp(fs.ApplicationUpdateMetadata), lr)
defer func() { _ = f.Release(ctx, ls) }()
if err != nil {
return err
}
currentProps := target.Model.Props
if currentProps == nil {
currentProps = &types.FileProps{}
}
if props.View != nil {
if delete {
currentProps.View = nil
} else {
currentProps.View = props.View
}
}
if _, err := f.fileClient.UpdateProps(ctx, target.Model, currentProps); err != nil {
return serializer.NewError(serializer.CodeDBError, "failed to update file props", err)
}
return nil
}
func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.MetadataPatch) error {
ae := serializer.NewAggregateError()
targets := make([]*File, 0, len(path))
for _, p := range path {
navigator, err := f.getNavigator(ctx, p, NavigatorCapabilityUpdateMetadata, NavigatorCapabilityLockFile)
if err != nil {
ae.Add(p.String(), err)
continue
}
target, err := f.getFileByPath(ctx, navigator, p)
if err != nil {
ae.Add(p.String(), fmt.Errorf("failed to get target file: %w", err))
continue
}
// Require Update permission
if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && target.OwnerID() != f.user.ID {
return fs.ErrOwnerOnly.WithError(fmt.Errorf("permission denied"))
}
if target.IsRootFolder() {
ae.Add(p.String(), fs.ErrNotSupportedAction.WithError(fmt.Errorf("cannot move root folder")))
continue
}
targets = append(targets, target)
}
if len(targets) == 0 {
return ae.Aggregate()
}
// Lock all targets
lockTargets := lo.Map(targets, func(value *File, key int) *LockByPath {
return &LockByPath{value.Uri(true), value, value.Type(), ""}
})
ls, err := f.acquireByPath(ctx, -1, f.user, true, fs.LockApp(fs.ApplicationUpdateMetadata), lockTargets...)
defer func() { _ = f.Release(ctx, ls) }()
if err != nil {
return err
}
metadataMap := make(map[string]string)
privateMap := make(map[string]bool)
deleted := make([]string, 0)
for _, meta := range metas {
if meta.Remove {
deleted = append(deleted, meta.Key)
continue
}
metadataMap[meta.Key] = meta.Value
if meta.Private {
privateMap[meta.Key] = meta.Private
}
}
fc, tx, ctx, err := inventory.WithTx(ctx, f.fileClient)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
}
for _, target := range targets {
if err := fc.UpsertMetadata(ctx, target.Model, metadataMap, privateMap); err != nil {
_ = inventory.Rollback(tx)
return fmt.Errorf("failed to upsert metadata: %w", err)
}
if len(deleted) > 0 {
if err := fc.RemoveMetadata(ctx, target.Model, deleted...); err != nil {
_ = inventory.Rollback(tx)
return fmt.Errorf("failed to remove metadata: %w", err)
}
}
}
if err := inventory.Commit(tx); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to commit metadata change", err)
}
return ae.Aggregate()
}

@ -148,6 +148,7 @@ func (n *shareNavigator) Root(ctx context.Context, path *fs.URI) (*File, error)
n.shareRoot.Path[pathIndexUser] = path.Root()
n.shareRoot.OwnerModel = n.owner
n.shareRoot.IsUserRoot = true
n.shareRoot.disableView = (share.Props == nil || !share.Props.ShareView) && n.user.ID != n.owner.ID
n.shareRoot.CapabilitiesBs = n.Capabilities(false).Capability
// Check if any ancestors is deleted
@ -303,3 +304,7 @@ func (n *shareNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType,
func (n *shareNavigator) Walk(ctx context.Context, levelFiles []*File, limit, depth int, f WalkFunc) error {
return n.baseNavigator.walk(ctx, levelFiles, limit, depth, f)
}
func (n *shareNavigator) GetView(ctx context.Context, file *File) *types.ExplorerView {
return file.View()
}

@ -4,8 +4,11 @@ import (
"context"
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"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/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
@ -139,3 +142,10 @@ func (n *sharedWithMeNavigator) FollowTx(ctx context.Context) (func(), error) {
func (n *sharedWithMeNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
return nil
}
func (n *sharedWithMeNavigator) GetView(ctx context.Context, file *File) *types.ExplorerView {
if view, ok := n.user.Settings.FsViewMap[string(constants.FileSystemSharedWithMe)]; ok {
return &view
}
return defaultView
}

@ -3,8 +3,11 @@ package dbfs
import (
"context"
"fmt"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"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/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
@ -13,7 +16,26 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
)
var trashNavigatorCapability = &boolset.BooleanSet{}
var (
trashNavigatorCapability = &boolset.BooleanSet{}
defaultTrashView = &types.ExplorerView{
View: "list",
Columns: []types.ListViewColumn{
{
Type: 0,
},
{
Type: 2,
},
{
Type: 8,
},
{
Type: 7,
},
},
}
)
// NewTrashNavigator creates a navigator for user's "trash" file system.
func NewTrashNavigator(u *ent.User, fileClient inventory.FileClient, l logging.Logger, config *setting.DBFS,
@ -135,3 +157,10 @@ func (n *trashNavigator) FollowTx(ctx context.Context) (func(), error) {
func (n *trashNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
return nil
}
func (n *trashNavigator) GetView(ctx context.Context, file *File) *types.ExplorerView {
if view, ok := n.user.Settings.FsViewMap[string(constants.FileSystemTrash)]; ok {
return &view
}
return defaultTrashView
}

@ -97,6 +97,8 @@ type (
GetFileFromDirectLink(ctx context.Context, dl *ent.DirectLink) (File, error)
// TraverseFile traverses a file to its root file, return the file with linked root.
TraverseFile(ctx context.Context, fileID int) (File, error)
// PatchProps patches the props of a file.
PatchProps(ctx context.Context, uri *URI, props *types.FileProps, delete bool) error
}
UploadManager interface {
@ -165,6 +167,7 @@ type (
FolderSummary() *FolderSummary
Capabilities() *boolset.BooleanSet
IsRootFolder() bool
View() *types.ExplorerView
}
Entities []Entity
@ -187,6 +190,7 @@ type (
StorageUsed int64
Shares []*ent.Share
EntityStoragePolicies map[int]*ent.StoragePolicy
View *types.ExplorerView
}
FolderSummary struct {
@ -215,6 +219,7 @@ type (
MixedType bool
SingleFileView bool
StoragePolicy *ent.StoragePolicy
View *types.ExplorerView
}
// NavigatorProps is the properties of current filesystem.

@ -75,6 +75,8 @@ type (
CastStoragePolicyOnSlave(ctx context.Context, policy *ent.StoragePolicy) *ent.StoragePolicy
// GetStorageDriver gets storage driver for given policy
GetStorageDriver(ctx context.Context, policy *ent.StoragePolicy) (driver.Handler, error)
// PatchView patches the view setting of a file
PatchView(ctx context.Context, uri *fs.URI, view *types.ExplorerView) error
}
ShareManagement interface {
@ -111,6 +113,7 @@ type (
IsPrivate bool
RemainDownloads int
Expire *time.Time
ShareView bool
}
)

@ -6,6 +6,7 @@ import (
"fmt"
"time"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
@ -261,6 +262,10 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C
password = util.RandString(8, util.RandomLowerCases)
}
props := &types.ShareProps{
ShareView: args.ShareView,
}
share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{
OwnerID: file.OwnerID(),
FileID: file.ID(),
@ -268,6 +273,7 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C
Expires: args.Expire,
RemainDownloads: args.RemainDownloads,
Existed: existed,
Props: props,
})
if err != nil {
@ -281,6 +287,39 @@ func (m *manager) TraverseFile(ctx context.Context, fileID int) (fs.File, error)
return m.fs.TraverseFile(ctx, fileID)
}
func (m *manager) PatchView(ctx context.Context, uri *fs.URI, view *types.ExplorerView) error {
if uri.PathTrimmed() == "" && uri.FileSystem() != constants.FileSystemMy && uri.FileSystem() != constants.FileSystemShare {
if m.user.Settings.FsViewMap == nil {
m.user.Settings.FsViewMap = make(map[string]types.ExplorerView)
}
if view == nil {
delete(m.user.Settings.FsViewMap, string(uri.FileSystem()))
} else {
m.user.Settings.FsViewMap[string(uri.FileSystem())] = *view
}
if err := m.dep.UserClient().SaveSettings(ctx, m.user); err != nil {
return serializer.NewError(serializer.CodeDBError, "failed to save user settings", err)
}
return nil
}
patch := &types.FileProps{
View: view,
}
isDelete := view == nil
if isDelete {
patch.View = &types.ExplorerView{}
}
if err := m.fs.PatchProps(ctx, uri, patch, isDelete); err != nil {
return err
}
return nil
}
func getEntityDisplayName(f fs.File, e fs.Entity) string {
switch e.Type() {
case types.EntityTypeThumbnail:

@ -388,3 +388,15 @@ func DeleteVersion(c *gin.Context) {
c.JSON(200, serializer.Response{})
}
func PatchView(c *gin.Context) {
service := ParametersFromContext[*explorer.PatchViewService](c, explorer.PatchViewParameterCtx{})
err := service.Patch(c)
if err != nil {
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
}
c.JSON(200, serializer.Response{})
}

@ -701,6 +701,11 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}),
middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}),
controllers.GetSource)
// Patch view
file.PATCH("view",
controllers.FromJSON[explorer.PatchViewService](explorer.PatchViewParameterCtx{}),
controllers.PatchView,
)
}
// 分享相关

@ -120,8 +120,6 @@ func (s *GetDirectLinkService) Get(c *gin.Context) ([]DirectLinkResponse, error)
return BuildDirectLinkResponse(res), err
}
const defaultPageSize = 100
type (
// ListFileParameterCtx define key fore ListFileService
ListFileParameterCtx struct{}
@ -130,7 +128,7 @@ type (
ListFileService struct {
Uri string `uri:"uri" form:"uri" json:"uri" binding:"required"`
Page int `uri:"page" form:"page" json:"page" binding:"min=0"`
PageSize int `uri:"page_size" form:"page_size" json:"page_size" binding:"min=10"`
PageSize int `uri:"page_size" form:"page_size" json:"page_size"`
OrderBy string `uri:"order_by" form:"order_by" json:"order_by"`
OrderDirection string `uri:"order_direction" form:"order_direction" json:"order_direction"`
NextPageToken string `uri:"next_page_token" form:"next_page_token" json:"next_page_token"`
@ -150,10 +148,6 @@ func (service *ListFileService) List(c *gin.Context) (*ListResponse, error) {
}
pageSize := service.PageSize
if pageSize == 0 {
pageSize = defaultPageSize
}
streamed := false
hasher := dep.HashIDEncoder()
parent, res, err := m.List(c, uri, &manager.ListArgs{
@ -670,3 +664,29 @@ func RedirectDirectLink(c *gin.Context, name string) error {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(earliestExpire.Sub(time.Now()).Seconds())))
return nil
}
type (
PatchViewParameterCtx struct{}
PatchViewService struct {
Uri string `json:"uri" binding:"required"`
View *types.ExplorerView `json:"view"`
}
)
func (s *PatchViewService) Patch(c *gin.Context) error {
dep := dependency.FromContext(c)
user := inventory.UserFromContext(c)
m := manager.NewFileManager(dep, user)
defer m.Recycle()
uri, err := fs.NewUriFromString(s.Uri)
if err != nil {
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
}
if err := m.PatchView(c, uri, s.View); err != nil {
return err
}
return nil
}

@ -212,6 +212,7 @@ type ListResponse struct {
MixedType bool `json:"mixed_type"`
SingleFileView bool `json:"single_file_view,omitempty"`
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
View *types.ExplorerView `json:"view,omitempty"`
}
type FileResponse struct {
@ -237,6 +238,7 @@ type ExtendedInfo struct {
StorageUsed int64 `json:"storage_used"`
Shares []Share `json:"shares,omitempty"`
Entities []Entity `json:"entities,omitempty"`
View *types.ExplorerView `json:"view,omitempty"`
}
type StoragePolicy struct {
@ -274,6 +276,7 @@ type Share struct {
// Only viewable by owner
IsPrivate bool `json:"is_private,omitempty"`
Password string `json:"password,omitempty"`
ShareView bool `json:"share_view,omitempty"`
// Only viewable if explicitly unlocked by owner
SourceUri string `json:"source_uri,omitempty"`
@ -306,6 +309,7 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e
if requester.ID == owner.ID {
res.IsPrivate = s.Password != ""
res.ShareView = s.Props != nil && s.Props.ShareView
}
return &res
@ -323,6 +327,7 @@ func BuildListResponse(ctx context.Context, u *ent.User, parent fs.File, res *fs
MixedType: res.MixedType,
SingleFileView: res.SingleFileView,
StoragePolicy: BuildStoragePolicy(res.StoragePolicy, hasher),
View: res.View,
}
if !res.Parent.IsNil() {
@ -382,7 +387,7 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi
ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share {
return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true, false)
})
ext.View = extendedInfo.View
}
return ext

@ -22,6 +22,7 @@ type (
IsPrivate bool `json:"is_private"`
RemainDownloads int `json:"downloads"`
Expire int `json:"expire"`
ShareView bool `json:"share_view"`
}
ShareCreateParamCtx struct{}
)
@ -54,6 +55,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string,
RemainDownloads: service.RemainDownloads,
Expire: expires,
ExistedShareID: existed,
ShareView: service.ShareView,
})
if err != nil {
return "", err

@ -28,6 +28,7 @@ type UserSettings struct {
Paswordless bool `json:"passwordless"`
TwoFAEnabled bool `json:"two_fa_enabled"`
Passkeys []Passkey `json:"passkeys,omitempty"`
DisableViewSync bool `json:"disable_view_sync"`
}
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
@ -40,6 +41,7 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
return BuildPasskey(item)
}),
DisableViewSync: u.Settings.DisableViewSync,
}
}
@ -106,6 +108,7 @@ type User struct {
Group *Group `json:"group,omitempty"`
Pined []types.PinedFile `json:"pined,omitempty"`
Language string `json:"language,omitempty"`
DisableViewSync bool `json:"disable_view_sync,omitempty"`
}
type Group struct {
@ -161,6 +164,7 @@ func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
Group: BuildGroup(user.Edges.Group, idEncoder),
Pined: user.Settings.Pined,
Language: user.Settings.Language,
DisableViewSync: user.Settings.DisableViewSync,
}
}

@ -4,6 +4,13 @@ import (
"context"
"crypto/md5"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
@ -15,12 +22,6 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
const (
@ -219,6 +220,7 @@ type (
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=128"`
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"`
}
PatchUserSettingParamsCtx struct{}
)
@ -260,6 +262,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
saveSetting = true
}
if s.DisableViewSync != nil {
u.Settings.DisableViewSync = *s.DisableViewSync
saveSetting = true
}
if s.CurrentPassword != nil && s.NewPassword != nil {
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)

Loading…
Cancel
Save