mirror of
https://github.com/cloudreve/Cloudreve.git
synced 2025-12-26 00:12:50 +00:00
feat(explorer): save user's view setting to server / optionally share view setting via share link (#2232)
This commit is contained in:
parent
c13b7365b0
commit
522fcca6af
2
assets
2
assets
|
|
@ -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: "downloads", Type: field.TypeInt, Default: 0},
|
||||||
{Name: "expires", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
|
{Name: "expires", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
|
||||||
{Name: "remain_downloads", Type: field.TypeInt, Nullable: true},
|
{Name: "remain_downloads", Type: field.TypeInt, Nullable: true},
|
||||||
|
{Name: "props", Type: field.TypeJSON, Nullable: true},
|
||||||
{Name: "file_shares", Type: field.TypeInt, Nullable: true},
|
{Name: "file_shares", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "user_shares", Type: field.TypeInt, Nullable: true},
|
{Name: "user_shares", Type: field.TypeInt, Nullable: true},
|
||||||
}
|
}
|
||||||
|
|
@ -311,13 +312,13 @@ var (
|
||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "shares_files_shares",
|
Symbol: "shares_files_shares",
|
||||||
Columns: []*schema.Column{SharesColumns[9]},
|
Columns: []*schema.Column{SharesColumns[10]},
|
||||||
RefColumns: []*schema.Column{FilesColumns[0]},
|
RefColumns: []*schema.Column{FilesColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "shares_users_shares",
|
Symbol: "shares_users_shares",
|
||||||
Columns: []*schema.Column{SharesColumns[10]},
|
Columns: []*schema.Column{SharesColumns[11]},
|
||||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8958,6 +8958,7 @@ type ShareMutation struct {
|
||||||
expires *time.Time
|
expires *time.Time
|
||||||
remain_downloads *int
|
remain_downloads *int
|
||||||
addremain_downloads *int
|
addremain_downloads *int
|
||||||
|
props **types.ShareProps
|
||||||
clearedFields map[string]struct{}
|
clearedFields map[string]struct{}
|
||||||
user *int
|
user *int
|
||||||
cleareduser bool
|
cleareduser bool
|
||||||
|
|
@ -9467,6 +9468,55 @@ func (m *ShareMutation) ResetRemainDownloads() {
|
||||||
delete(m.clearedFields, share.FieldRemainDownloads)
|
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.
|
// SetUserID sets the "user" edge to the User entity by id.
|
||||||
func (m *ShareMutation) SetUserID(id int) {
|
func (m *ShareMutation) SetUserID(id int) {
|
||||||
m.user = &id
|
m.user = &id
|
||||||
|
|
@ -9579,7 +9629,7 @@ func (m *ShareMutation) 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 *ShareMutation) Fields() []string {
|
func (m *ShareMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 8)
|
fields := make([]string, 0, 9)
|
||||||
if m.created_at != nil {
|
if m.created_at != nil {
|
||||||
fields = append(fields, share.FieldCreatedAt)
|
fields = append(fields, share.FieldCreatedAt)
|
||||||
}
|
}
|
||||||
|
|
@ -9604,6 +9654,9 @@ func (m *ShareMutation) Fields() []string {
|
||||||
if m.remain_downloads != nil {
|
if m.remain_downloads != nil {
|
||||||
fields = append(fields, share.FieldRemainDownloads)
|
fields = append(fields, share.FieldRemainDownloads)
|
||||||
}
|
}
|
||||||
|
if m.props != nil {
|
||||||
|
fields = append(fields, share.FieldProps)
|
||||||
|
}
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9628,6 +9681,8 @@ func (m *ShareMutation) Field(name string) (ent.Value, bool) {
|
||||||
return m.Expires()
|
return m.Expires()
|
||||||
case share.FieldRemainDownloads:
|
case share.FieldRemainDownloads:
|
||||||
return m.RemainDownloads()
|
return m.RemainDownloads()
|
||||||
|
case share.FieldProps:
|
||||||
|
return m.Props()
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
@ -9653,6 +9708,8 @@ func (m *ShareMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||||
return m.OldExpires(ctx)
|
return m.OldExpires(ctx)
|
||||||
case share.FieldRemainDownloads:
|
case share.FieldRemainDownloads:
|
||||||
return m.OldRemainDownloads(ctx)
|
return m.OldRemainDownloads(ctx)
|
||||||
|
case share.FieldProps:
|
||||||
|
return m.OldProps(ctx)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown Share field %s", name)
|
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)
|
m.SetRemainDownloads(v)
|
||||||
return nil
|
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)
|
return fmt.Errorf("unknown Share field %s", name)
|
||||||
}
|
}
|
||||||
|
|
@ -9799,6 +9863,9 @@ func (m *ShareMutation) ClearedFields() []string {
|
||||||
if m.FieldCleared(share.FieldRemainDownloads) {
|
if m.FieldCleared(share.FieldRemainDownloads) {
|
||||||
fields = append(fields, share.FieldRemainDownloads)
|
fields = append(fields, share.FieldRemainDownloads)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(share.FieldProps) {
|
||||||
|
fields = append(fields, share.FieldProps)
|
||||||
|
}
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9825,6 +9892,9 @@ func (m *ShareMutation) ClearField(name string) error {
|
||||||
case share.FieldRemainDownloads:
|
case share.FieldRemainDownloads:
|
||||||
m.ClearRemainDownloads()
|
m.ClearRemainDownloads()
|
||||||
return nil
|
return nil
|
||||||
|
case share.FieldProps:
|
||||||
|
m.ClearProps()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown Share nullable field %s", name)
|
return fmt.Errorf("unknown Share nullable field %s", name)
|
||||||
}
|
}
|
||||||
|
|
@ -9857,6 +9927,9 @@ func (m *ShareMutation) ResetField(name string) error {
|
||||||
case share.FieldRemainDownloads:
|
case share.FieldRemainDownloads:
|
||||||
m.ResetRemainDownloads()
|
m.ResetRemainDownloads()
|
||||||
return nil
|
return nil
|
||||||
|
case share.FieldProps:
|
||||||
|
m.ResetProps()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown Share field %s", name)
|
return fmt.Errorf("unknown Share field %s", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"entgo.io/ent/dialect"
|
"entgo.io/ent/dialect"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Share holds the schema definition for the Share entity.
|
// Share holds the schema definition for the Share entity.
|
||||||
|
|
@ -30,6 +31,7 @@ func (Share) Fields() []ent.Field {
|
||||||
field.Int("remain_downloads").
|
field.Int("remain_downloads").
|
||||||
Nillable().
|
Nillable().
|
||||||
Optional(),
|
Optional(),
|
||||||
|
field.JSON("props", &types.ShareProps{}).Optional(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
ent/share.go
17
ent/share.go
|
|
@ -3,6 +3,7 @@
|
||||||
package ent
|
package ent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -12,6 +13,7 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/file"
|
"github.com/cloudreve/Cloudreve/v4/ent/file"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Share is the model entity for the Share schema.
|
// Share is the model entity for the Share schema.
|
||||||
|
|
@ -35,6 +37,8 @@ type Share struct {
|
||||||
Expires *time.Time `json:"expires,omitempty"`
|
Expires *time.Time `json:"expires,omitempty"`
|
||||||
// RemainDownloads holds the value of the "remain_downloads" field.
|
// RemainDownloads holds the value of the "remain_downloads" field.
|
||||||
RemainDownloads *int `json:"remain_downloads,omitempty"`
|
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.
|
// Edges holds the relations/edges for other nodes in the graph.
|
||||||
// The values are being populated by the ShareQuery when eager-loading is set.
|
// The values are being populated by the ShareQuery when eager-loading is set.
|
||||||
Edges ShareEdges `json:"edges"`
|
Edges ShareEdges `json:"edges"`
|
||||||
|
|
@ -85,6 +89,8 @@ func (*Share) scanValues(columns []string) ([]any, error) {
|
||||||
values := make([]any, len(columns))
|
values := make([]any, len(columns))
|
||||||
for i := range columns {
|
for i := range columns {
|
||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
|
case share.FieldProps:
|
||||||
|
values[i] = new([]byte)
|
||||||
case share.FieldID, share.FieldViews, share.FieldDownloads, share.FieldRemainDownloads:
|
case share.FieldID, share.FieldViews, share.FieldDownloads, share.FieldRemainDownloads:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case share.FieldPassword:
|
case share.FieldPassword:
|
||||||
|
|
@ -167,6 +173,14 @@ func (s *Share) assignValues(columns []string, values []any) error {
|
||||||
s.RemainDownloads = new(int)
|
s.RemainDownloads = new(int)
|
||||||
*s.RemainDownloads = int(value.Int64)
|
*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]:
|
case share.ForeignKeys[0]:
|
||||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for edge-field file_shares", value)
|
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("remain_downloads=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
}
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("props=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", s.Props))
|
||||||
builder.WriteByte(')')
|
builder.WriteByte(')')
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ const (
|
||||||
FieldExpires = "expires"
|
FieldExpires = "expires"
|
||||||
// FieldRemainDownloads holds the string denoting the remain_downloads field in the database.
|
// FieldRemainDownloads holds the string denoting the remain_downloads field in the database.
|
||||||
FieldRemainDownloads = "remain_downloads"
|
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 holds the string denoting the user edge name in mutations.
|
||||||
EdgeUser = "user"
|
EdgeUser = "user"
|
||||||
// EdgeFile holds the string denoting the file edge name in mutations.
|
// EdgeFile holds the string denoting the file edge name in mutations.
|
||||||
|
|
@ -64,6 +66,7 @@ var Columns = []string{
|
||||||
FieldDownloads,
|
FieldDownloads,
|
||||||
FieldExpires,
|
FieldExpires,
|
||||||
FieldRemainDownloads,
|
FieldRemainDownloads,
|
||||||
|
FieldProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForeignKeys holds the SQL foreign-keys that are owned by the "shares"
|
// 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))
|
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.
|
// HasUser applies the HasEdge predicate on the "user" edge.
|
||||||
func HasUser() predicate.Share {
|
func HasUser() predicate.Share {
|
||||||
return predicate.Share(func(s *sql.Selector) {
|
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/file"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShareCreate is the builder for creating a Share entity.
|
// ShareCreate is the builder for creating a Share entity.
|
||||||
|
|
@ -136,6 +137,12 @@ func (sc *ShareCreate) SetNillableRemainDownloads(i *int) *ShareCreate {
|
||||||
return sc
|
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.
|
// SetUserID sets the "user" edge to the User entity by ID.
|
||||||
func (sc *ShareCreate) SetUserID(id int) *ShareCreate {
|
func (sc *ShareCreate) SetUserID(id int) *ShareCreate {
|
||||||
sc.mutation.SetUserID(id)
|
sc.mutation.SetUserID(id)
|
||||||
|
|
@ -316,6 +323,10 @@ func (sc *ShareCreate) createSpec() (*Share, *sqlgraph.CreateSpec) {
|
||||||
_spec.SetField(share.FieldRemainDownloads, field.TypeInt, value)
|
_spec.SetField(share.FieldRemainDownloads, field.TypeInt, value)
|
||||||
_node.RemainDownloads = &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 {
|
if nodes := sc.mutation.UserIDs(); len(nodes) > 0 {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
|
|
@ -528,6 +539,24 @@ func (u *ShareUpsert) ClearRemainDownloads() *ShareUpsert {
|
||||||
return u
|
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.
|
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||||
// Using this option is equivalent to using:
|
// 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.
|
// Exec executes the query.
|
||||||
func (u *ShareUpsertOne) Exec(ctx context.Context) error {
|
func (u *ShareUpsertOne) Exec(ctx context.Context) error {
|
||||||
if len(u.create.conflict) == 0 {
|
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.
|
// Exec executes the query.
|
||||||
func (u *ShareUpsertBulk) Exec(ctx context.Context) error {
|
func (u *ShareUpsertBulk) Exec(ctx context.Context) error {
|
||||||
if u.create.err != nil {
|
if u.create.err != nil {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
|
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
"github.com/cloudreve/Cloudreve/v4/ent/share"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShareUpdate is the builder for updating Share entities.
|
// ShareUpdate is the builder for updating Share entities.
|
||||||
|
|
@ -165,6 +166,18 @@ func (su *ShareUpdate) ClearRemainDownloads() *ShareUpdate {
|
||||||
return su
|
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.
|
// SetUserID sets the "user" edge to the User entity by ID.
|
||||||
func (su *ShareUpdate) SetUserID(id int) *ShareUpdate {
|
func (su *ShareUpdate) SetUserID(id int) *ShareUpdate {
|
||||||
su.mutation.SetUserID(id)
|
su.mutation.SetUserID(id)
|
||||||
|
|
@ -313,6 +326,12 @@ func (su *ShareUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||||
if su.mutation.RemainDownloadsCleared() {
|
if su.mutation.RemainDownloadsCleared() {
|
||||||
_spec.ClearField(share.FieldRemainDownloads, field.TypeInt)
|
_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() {
|
if su.mutation.UserCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
|
|
@ -526,6 +545,18 @@ func (suo *ShareUpdateOne) ClearRemainDownloads() *ShareUpdateOne {
|
||||||
return suo
|
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.
|
// SetUserID sets the "user" edge to the User entity by ID.
|
||||||
func (suo *ShareUpdateOne) SetUserID(id int) *ShareUpdateOne {
|
func (suo *ShareUpdateOne) SetUserID(id int) *ShareUpdateOne {
|
||||||
suo.mutation.SetUserID(id)
|
suo.mutation.SetUserID(id)
|
||||||
|
|
@ -704,6 +735,12 @@ func (suo *ShareUpdateOne) sqlSave(ctx context.Context) (_node *Share, err error
|
||||||
if suo.mutation.RemainDownloadsCleared() {
|
if suo.mutation.RemainDownloadsCleared() {
|
||||||
_spec.ClearField(share.FieldRemainDownloads, field.TypeInt)
|
_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() {
|
if suo.mutation.UserCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,8 @@ type FileClient interface {
|
||||||
Update(ctx context.Context, file *ent.File) (*ent.File, error)
|
Update(ctx context.Context, file *ent.File) (*ent.File, error)
|
||||||
// ListEntities lists entities
|
// ListEntities lists entities
|
||||||
ListEntities(ctx context.Context, args *ListEntityParameters) (*ListEntityResult, error)
|
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 {
|
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)
|
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) {
|
func (f *fileClient) CountByTimeRange(ctx context.Context, start, end *time.Time) (int, error) {
|
||||||
if start == nil || end == nil {
|
if start == nil || end == nil {
|
||||||
return f.client.File.Query().Count(ctx)
|
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)
|
stm.SetPrimaryEntity(file.PrimaryEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if file.Props != nil && dstMap[file.FileChildren][0].OwnerID == file.OwnerID {
|
||||||
|
stm.SetProps(file.Props)
|
||||||
|
}
|
||||||
|
|
||||||
return stm
|
return stm
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package inventory
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"entgo.io/ent/dialect/sql"
|
"entgo.io/ent/dialect/sql"
|
||||||
|
|
@ -62,6 +63,7 @@ type (
|
||||||
Expires *time.Time
|
Expires *time.Time
|
||||||
OwnerID int
|
OwnerID int
|
||||||
FileID int
|
FileID int
|
||||||
|
Props *types.ShareProps
|
||||||
}
|
}
|
||||||
|
|
||||||
ListShareArgs struct {
|
ListShareArgs struct {
|
||||||
|
|
@ -122,6 +124,10 @@ func (c *shareClient) Upsert(ctx context.Context, params *CreateShareParams) (*e
|
||||||
createQuery.ClearExpires()
|
createQuery.ClearExpires()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Props != nil {
|
||||||
|
createQuery.SetProps(params.Props)
|
||||||
|
}
|
||||||
|
|
||||||
return createQuery.Save(ctx)
|
return createQuery.Save(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,6 +144,9 @@ func (c *shareClient) Upsert(ctx context.Context, params *CreateShareParams) (*e
|
||||||
if params.Expires != nil {
|
if params.Expires != nil {
|
||||||
query.SetNillableExpires(params.Expires)
|
query.SetNillableExpires(params.Expires)
|
||||||
}
|
}
|
||||||
|
if params.Props != nil {
|
||||||
|
query.SetProps(params.Props)
|
||||||
|
}
|
||||||
|
|
||||||
return query.Save(ctx)
|
return query.Save(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,15 @@ import (
|
||||||
// UserSetting 用户其他配置
|
// UserSetting 用户其他配置
|
||||||
type (
|
type (
|
||||||
UserSetting struct {
|
UserSetting struct {
|
||||||
ProfileOff bool `json:"profile_off,omitempty"`
|
ProfileOff bool `json:"profile_off,omitempty"`
|
||||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||||
VersionRetention bool `json:"version_retention,omitempty"`
|
VersionRetention bool `json:"version_retention,omitempty"`
|
||||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||||
Pined []PinedFile `json:"pined,omitempty"`
|
Pined []PinedFile `json:"pined,omitempty"`
|
||||||
Language string `json:"email_language,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 {
|
PinedFile struct {
|
||||||
|
|
@ -149,6 +151,32 @@ type (
|
||||||
PolicyType string
|
PolicyType string
|
||||||
|
|
||||||
FileProps struct {
|
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
|
ContextHintTTL = 5 * 60 // 5 minutes
|
||||||
|
|
||||||
folderSummaryCachePrefix = "folder_summary_"
|
folderSummaryCachePrefix = "folder_summary_"
|
||||||
|
defaultPageSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -119,17 +120,46 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi
|
||||||
searchParams := path.SearchParameters()
|
searchParams := path.SearchParameters()
|
||||||
isSearching := searchParams != nil
|
isSearching := searchParams != nil
|
||||||
|
|
||||||
// Validate pagination args
|
|
||||||
props := navigator.Capabilities(isSearching)
|
|
||||||
if o.PageSize > props.MaxPageSize {
|
|
||||||
o.PageSize = props.MaxPageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
parent, err := f.getFileByPath(ctx, navigator, path)
|
parent, err := f.getFileByPath(ctx, navigator, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("Parent not exist: %w", err)
|
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 pageSize > props.MaxPageSize {
|
||||||
|
pageSize = props.MaxPageSize
|
||||||
|
} else if pageSize == 0 {
|
||||||
|
pageSize = defaultPageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if view != nil {
|
||||||
|
view.PageSize = pageSize
|
||||||
|
view.OrderDirection = orderDirection
|
||||||
|
view.Order = orderBy
|
||||||
|
}
|
||||||
|
|
||||||
var hintId *uuid.UUID
|
var hintId *uuid.UUID
|
||||||
if o.generateContextHint {
|
if o.generateContextHint {
|
||||||
newHintId := uuid.Must(uuid.NewV4())
|
newHintId := uuid.Must(uuid.NewV4())
|
||||||
|
|
@ -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{
|
children, err := navigator.Children(ctx, parent, &ListArgs{
|
||||||
Page: &inventory.PaginationArgs{
|
Page: &inventory.PaginationArgs{
|
||||||
Page: o.FsOption.Page,
|
Page: o.FsOption.Page,
|
||||||
PageSize: o.PageSize,
|
PageSize: pageSize,
|
||||||
OrderBy: o.OrderBy,
|
OrderBy: orderBy,
|
||||||
Order: inventory.OrderDirection(o.OrderDirection),
|
Order: inventory.OrderDirection(orderDirection),
|
||||||
UseCursorPagination: o.useCursorPagination,
|
UseCursorPagination: o.useCursorPagination,
|
||||||
PageToken: o.pageToken,
|
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,
|
SingleFileView: children.SingleFileView,
|
||||||
Parent: parent,
|
Parent: parent,
|
||||||
StoragePolicy: storagePolicy,
|
StoragePolicy: storagePolicy,
|
||||||
|
View: view,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,89 +301,6 @@ func (f *DBFS) CreateEntity(ctx context.Context, file fs.File, policy *ent.Stora
|
||||||
return fs.NewEntity(entity), nil
|
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) {
|
func (f *DBFS) SharedAddressTranslation(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.File, *fs.URI, error) {
|
||||||
o := newDbfsOption()
|
o := newDbfsOption()
|
||||||
for _, opt := range opts {
|
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
|
target.FileExtendedInfo = extendedInfo
|
||||||
if target.OwnerID() == f.user.ID || f.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
if target.OwnerID() == f.user.ID || f.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
||||||
target.FileExtendedInfo.Shares = target.Model.Edges.Shares
|
target.FileExtendedInfo.Shares = target.Model.Edges.Shares
|
||||||
|
if target.Model.Props != nil {
|
||||||
|
target.FileExtendedInfo.View = target.Model.Props.View
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entities := target.Entities()
|
entities := target.Entities()
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,20 @@ func init() {
|
||||||
gob.Register(map[int]*File{})
|
gob.Register(map[int]*File{})
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePool = &sync.Pool{
|
var (
|
||||||
New: func() any {
|
filePool = &sync.Pool{
|
||||||
return &File{
|
New: func() any {
|
||||||
Children: make(map[string]*File),
|
return &File{
|
||||||
}
|
Children: make(map[string]*File),
|
||||||
},
|
}
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
defaultView = &types.ExplorerView{
|
||||||
|
PageSize: defaultPageSize,
|
||||||
|
View: "grid",
|
||||||
|
Thumbnail: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
File struct {
|
File struct {
|
||||||
|
|
@ -42,7 +49,8 @@ type (
|
||||||
FileExtendedInfo *fs.FileExtendedInfo
|
FileExtendedInfo *fs.FileExtendedInfo
|
||||||
FileFolderSummary *fs.FolderSummary
|
FileFolderSummary *fs.FolderSummary
|
||||||
|
|
||||||
mu *sync.Mutex
|
disableView bool
|
||||||
|
mu *sync.Mutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -181,6 +189,31 @@ func (f *File) Uri(isRoot bool) *fs.URI {
|
||||||
return parent.Path[index].Join(elements...)
|
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.
|
// UserRoot return the root file from user's view.
|
||||||
func (f *File) UserRoot() *File {
|
func (f *File) UserRoot() *File {
|
||||||
root := f
|
root := f
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ func (n *myNavigator) To(ctx context.Context, path *fs.URI) (*File, error) {
|
||||||
rootPath := path.Root()
|
rootPath := path.Root()
|
||||||
n.root.Path[pathIndexRoot], n.root.Path[pathIndexUser] = rootPath, rootPath
|
n.root.Path[pathIndexRoot], n.root.Path[pathIndexUser] = rootPath, rootPath
|
||||||
n.root.OwnerModel = targetUser
|
n.root.OwnerModel = targetUser
|
||||||
|
n.root.disableView = fsUid != n.user.ID
|
||||||
n.root.IsUserRoot = true
|
n.root.IsUserRoot = true
|
||||||
n.root.CapabilitiesBs = n.Capabilities(false).Capability
|
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 {
|
func (n *myNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
|
||||||
return nil
|
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)
|
FollowTx(ctx context.Context) (func(), error)
|
||||||
// ExecuteHook performs custom operations before or after certain actions.
|
// ExecuteHook performs custom operations before or after certain actions.
|
||||||
ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error
|
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{}
|
State interface{}
|
||||||
|
|
@ -100,6 +102,7 @@ const (
|
||||||
NavigatorCapability_CommunityPlacehodler8
|
NavigatorCapability_CommunityPlacehodler8
|
||||||
NavigatorCapability_CommunityPlacehodler9
|
NavigatorCapability_CommunityPlacehodler9
|
||||||
NavigatorCapabilityEnterFolder
|
NavigatorCapabilityEnterFolder
|
||||||
|
NavigatorCapabilityModifyProps
|
||||||
|
|
||||||
searchTokenSeparator = "|"
|
searchTokenSeparator = "|"
|
||||||
)
|
)
|
||||||
|
|
@ -120,6 +123,7 @@ func init() {
|
||||||
NavigatorCapabilityInfo: true,
|
NavigatorCapabilityInfo: true,
|
||||||
NavigatorCapabilityVersionControl: true,
|
NavigatorCapabilityVersionControl: true,
|
||||||
NavigatorCapabilityEnterFolder: true,
|
NavigatorCapabilityEnterFolder: true,
|
||||||
|
NavigatorCapabilityModifyProps: true,
|
||||||
}, myNavigatorCapability)
|
}, myNavigatorCapability)
|
||||||
boolset.Sets(map[NavigatorCapability]bool{
|
boolset.Sets(map[NavigatorCapability]bool{
|
||||||
NavigatorCapabilityDownloadFile: true,
|
NavigatorCapabilityDownloadFile: true,
|
||||||
|
|
@ -129,6 +133,7 @@ func init() {
|
||||||
NavigatorCapabilityInfo: true,
|
NavigatorCapabilityInfo: true,
|
||||||
NavigatorCapabilityVersionControl: true,
|
NavigatorCapabilityVersionControl: true,
|
||||||
NavigatorCapabilityEnterFolder: true,
|
NavigatorCapabilityEnterFolder: true,
|
||||||
|
NavigatorCapabilityModifyProps: true,
|
||||||
}, shareNavigatorCapability)
|
}, shareNavigatorCapability)
|
||||||
boolset.Sets(map[NavigatorCapability]bool{
|
boolset.Sets(map[NavigatorCapability]bool{
|
||||||
NavigatorCapabilityListChildren: true,
|
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.Path[pathIndexUser] = path.Root()
|
||||||
n.shareRoot.OwnerModel = n.owner
|
n.shareRoot.OwnerModel = n.owner
|
||||||
n.shareRoot.IsUserRoot = true
|
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
|
n.shareRoot.CapabilitiesBs = n.Capabilities(false).Capability
|
||||||
|
|
||||||
// Check if any ancestors is deleted
|
// 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 {
|
func (n *shareNavigator) Walk(ctx context.Context, levelFiles []*File, limit, depth int, f WalkFunc) error {
|
||||||
return n.baseNavigator.walk(ctx, levelFiles, limit, depth, f)
|
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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
"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 {
|
func (n *sharedWithMeNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
|
||||||
return nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||||
|
|
@ -13,7 +16,26 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
"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.
|
// NewTrashNavigator creates a navigator for user's "trash" file system.
|
||||||
func NewTrashNavigator(u *ent.User, fileClient inventory.FileClient, l logging.Logger, config *setting.DBFS,
|
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 {
|
func (n *trashNavigator) ExecuteHook(ctx context.Context, hookType fs.HookType, file *File) error {
|
||||||
return nil
|
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)
|
GetFileFromDirectLink(ctx context.Context, dl *ent.DirectLink) (File, error)
|
||||||
// TraverseFile traverses a file to its root file, return the file with linked root.
|
// TraverseFile traverses a file to its root file, return the file with linked root.
|
||||||
TraverseFile(ctx context.Context, fileID int) (File, error)
|
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 {
|
UploadManager interface {
|
||||||
|
|
@ -165,6 +167,7 @@ type (
|
||||||
FolderSummary() *FolderSummary
|
FolderSummary() *FolderSummary
|
||||||
Capabilities() *boolset.BooleanSet
|
Capabilities() *boolset.BooleanSet
|
||||||
IsRootFolder() bool
|
IsRootFolder() bool
|
||||||
|
View() *types.ExplorerView
|
||||||
}
|
}
|
||||||
|
|
||||||
Entities []Entity
|
Entities []Entity
|
||||||
|
|
@ -187,6 +190,7 @@ type (
|
||||||
StorageUsed int64
|
StorageUsed int64
|
||||||
Shares []*ent.Share
|
Shares []*ent.Share
|
||||||
EntityStoragePolicies map[int]*ent.StoragePolicy
|
EntityStoragePolicies map[int]*ent.StoragePolicy
|
||||||
|
View *types.ExplorerView
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderSummary struct {
|
FolderSummary struct {
|
||||||
|
|
@ -215,6 +219,7 @@ type (
|
||||||
MixedType bool
|
MixedType bool
|
||||||
SingleFileView bool
|
SingleFileView bool
|
||||||
StoragePolicy *ent.StoragePolicy
|
StoragePolicy *ent.StoragePolicy
|
||||||
|
View *types.ExplorerView
|
||||||
}
|
}
|
||||||
|
|
||||||
// NavigatorProps is the properties of current filesystem.
|
// NavigatorProps is the properties of current filesystem.
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ type (
|
||||||
CastStoragePolicyOnSlave(ctx context.Context, policy *ent.StoragePolicy) *ent.StoragePolicy
|
CastStoragePolicyOnSlave(ctx context.Context, policy *ent.StoragePolicy) *ent.StoragePolicy
|
||||||
// GetStorageDriver gets storage driver for given policy
|
// GetStorageDriver gets storage driver for given policy
|
||||||
GetStorageDriver(ctx context.Context, policy *ent.StoragePolicy) (driver.Handler, error)
|
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 {
|
ShareManagement interface {
|
||||||
|
|
@ -111,6 +113,7 @@ type (
|
||||||
IsPrivate bool
|
IsPrivate bool
|
||||||
RemainDownloads int
|
RemainDownloads int
|
||||||
Expire *time.Time
|
Expire *time.Time
|
||||||
|
ShareView bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
"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"
|
||||||
|
|
@ -261,6 +262,10 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C
|
||||||
password = util.RandString(8, util.RandomLowerCases)
|
password = util.RandString(8, util.RandomLowerCases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props := &types.ShareProps{
|
||||||
|
ShareView: args.ShareView,
|
||||||
|
}
|
||||||
|
|
||||||
share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{
|
share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{
|
||||||
OwnerID: file.OwnerID(),
|
OwnerID: file.OwnerID(),
|
||||||
FileID: file.ID(),
|
FileID: file.ID(),
|
||||||
|
|
@ -268,6 +273,7 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C
|
||||||
Expires: args.Expire,
|
Expires: args.Expire,
|
||||||
RemainDownloads: args.RemainDownloads,
|
RemainDownloads: args.RemainDownloads,
|
||||||
Existed: existed,
|
Existed: existed,
|
||||||
|
Props: props,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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 {
|
func getEntityDisplayName(f fs.File, e fs.Entity) string {
|
||||||
switch e.Type() {
|
switch e.Type() {
|
||||||
case types.EntityTypeThumbnail:
|
case types.EntityTypeThumbnail:
|
||||||
|
|
|
||||||
|
|
@ -388,3 +388,15 @@ func DeleteVersion(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(200, serializer.Response{})
|
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{}),
|
controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}),
|
||||||
middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}),
|
middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}),
|
||||||
controllers.GetSource)
|
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
|
return BuildDirectLinkResponse(res), err
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPageSize = 100
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ListFileParameterCtx define key fore ListFileService
|
// ListFileParameterCtx define key fore ListFileService
|
||||||
ListFileParameterCtx struct{}
|
ListFileParameterCtx struct{}
|
||||||
|
|
@ -130,7 +128,7 @@ type (
|
||||||
ListFileService struct {
|
ListFileService struct {
|
||||||
Uri string `uri:"uri" form:"uri" json:"uri" binding:"required"`
|
Uri string `uri:"uri" form:"uri" json:"uri" binding:"required"`
|
||||||
Page int `uri:"page" form:"page" json:"page" binding:"min=0"`
|
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"`
|
OrderBy string `uri:"order_by" form:"order_by" json:"order_by"`
|
||||||
OrderDirection string `uri:"order_direction" form:"order_direction" json:"order_direction"`
|
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"`
|
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
|
pageSize := service.PageSize
|
||||||
if pageSize == 0 {
|
|
||||||
pageSize = defaultPageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
streamed := false
|
streamed := false
|
||||||
hasher := dep.HashIDEncoder()
|
hasher := dep.HashIDEncoder()
|
||||||
parent, res, err := m.List(c, uri, &manager.ListArgs{
|
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())))
|
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(earliestExpire.Sub(time.Now()).Seconds())))
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,11 +207,12 @@ type ListResponse struct {
|
||||||
// It persists some intermedia state so that the following request don't need to query database again.
|
// It persists some intermedia state so that the following request don't need to query database again.
|
||||||
// All the operations under this directory that supports context hint should carry this value in header
|
// All the operations under this directory that supports context hint should carry this value in header
|
||||||
// as X-Cr-Context-Hint.
|
// as X-Cr-Context-Hint.
|
||||||
ContextHint *uuid.UUID `json:"context_hint"`
|
ContextHint *uuid.UUID `json:"context_hint"`
|
||||||
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
||||||
MixedType bool `json:"mixed_type"`
|
MixedType bool `json:"mixed_type"`
|
||||||
SingleFileView bool `json:"single_file_view,omitempty"`
|
SingleFileView bool `json:"single_file_view,omitempty"`
|
||||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||||
|
View *types.ExplorerView `json:"view,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileResponse struct {
|
type FileResponse struct {
|
||||||
|
|
@ -233,10 +234,11 @@ type FileResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtendedInfo struct {
|
type ExtendedInfo struct {
|
||||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||||
StorageUsed int64 `json:"storage_used"`
|
StorageUsed int64 `json:"storage_used"`
|
||||||
Shares []Share `json:"shares,omitempty"`
|
Shares []Share `json:"shares,omitempty"`
|
||||||
Entities []Entity `json:"entities,omitempty"`
|
Entities []Entity `json:"entities,omitempty"`
|
||||||
|
View *types.ExplorerView `json:"view,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoragePolicy struct {
|
type StoragePolicy struct {
|
||||||
|
|
@ -274,6 +276,7 @@ type Share struct {
|
||||||
// Only viewable by owner
|
// Only viewable by owner
|
||||||
IsPrivate bool `json:"is_private,omitempty"`
|
IsPrivate bool `json:"is_private,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
ShareView bool `json:"share_view,omitempty"`
|
||||||
|
|
||||||
// Only viewable if explicitly unlocked by owner
|
// Only viewable if explicitly unlocked by owner
|
||||||
SourceUri string `json:"source_uri,omitempty"`
|
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 {
|
if requester.ID == owner.ID {
|
||||||
res.IsPrivate = s.Password != ""
|
res.IsPrivate = s.Password != ""
|
||||||
|
res.ShareView = s.Props != nil && s.Props.ShareView
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res
|
return &res
|
||||||
|
|
@ -323,6 +327,7 @@ func BuildListResponse(ctx context.Context, u *ent.User, parent fs.File, res *fs
|
||||||
MixedType: res.MixedType,
|
MixedType: res.MixedType,
|
||||||
SingleFileView: res.SingleFileView,
|
SingleFileView: res.SingleFileView,
|
||||||
StoragePolicy: BuildStoragePolicy(res.StoragePolicy, hasher),
|
StoragePolicy: BuildStoragePolicy(res.StoragePolicy, hasher),
|
||||||
|
View: res.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.Parent.IsNil() {
|
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 {
|
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)
|
return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true, false)
|
||||||
})
|
})
|
||||||
|
ext.View = extendedInfo.View
|
||||||
}
|
}
|
||||||
|
|
||||||
return ext
|
return ext
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type (
|
||||||
IsPrivate bool `json:"is_private"`
|
IsPrivate bool `json:"is_private"`
|
||||||
RemainDownloads int `json:"downloads"`
|
RemainDownloads int `json:"downloads"`
|
||||||
Expire int `json:"expire"`
|
Expire int `json:"expire"`
|
||||||
|
ShareView bool `json:"share_view"`
|
||||||
}
|
}
|
||||||
ShareCreateParamCtx struct{}
|
ShareCreateParamCtx struct{}
|
||||||
)
|
)
|
||||||
|
|
@ -54,6 +55,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string,
|
||||||
RemainDownloads: service.RemainDownloads,
|
RemainDownloads: service.RemainDownloads,
|
||||||
Expire: expires,
|
Expire: expires,
|
||||||
ExistedShareID: existed,
|
ExistedShareID: existed,
|
||||||
|
ShareView: service.ShareView,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type UserSettings struct {
|
||||||
Paswordless bool `json:"passwordless"`
|
Paswordless bool `json:"passwordless"`
|
||||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||||
|
DisableViewSync bool `json:"disable_view_sync"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
|
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 {
|
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
|
||||||
return BuildPasskey(item)
|
return BuildPasskey(item)
|
||||||
}),
|
}),
|
||||||
|
DisableViewSync: u.Settings.DisableViewSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,17 +97,18 @@ type BuiltinLoginResponse struct {
|
||||||
|
|
||||||
// User 用户序列化器
|
// User 用户序列化器
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Status user.Status `json:"status,omitempty"`
|
Status user.Status `json:"status,omitempty"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||||
Anonymous bool `json:"anonymous,omitempty"`
|
Anonymous bool `json:"anonymous,omitempty"`
|
||||||
Group *Group `json:"group,omitempty"`
|
Group *Group `json:"group,omitempty"`
|
||||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
|
DisableViewSync bool `json:"disable_view_sync,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
|
@ -150,17 +153,18 @@ func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials
|
||||||
// BuildUser 序列化用户
|
// BuildUser 序列化用户
|
||||||
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
||||||
return User{
|
return User{
|
||||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Nickname: user.Nick,
|
Nickname: user.Nick,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
Avatar: user.Avatar,
|
Avatar: user.Avatar,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
PreferredTheme: user.Settings.PreferredTheme,
|
PreferredTheme: user.Settings.PreferredTheme,
|
||||||
Anonymous: user.ID == 0,
|
Anonymous: user.ID == 0,
|
||||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||||
Pined: user.Settings.Pined,
|
Pined: user.Settings.Pined,
|
||||||
Language: user.Settings.Language,
|
Language: user.Settings.Language,
|
||||||
|
DisableViewSync: user.Settings.DisableViewSync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||||
|
|
@ -15,12 +22,6 @@ import (
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -219,6 +220,7 @@ type (
|
||||||
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=128"`
|
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=128"`
|
||||||
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
|
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
|
||||||
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
|
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
|
||||||
|
DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"`
|
||||||
}
|
}
|
||||||
PatchUserSettingParamsCtx struct{}
|
PatchUserSettingParamsCtx struct{}
|
||||||
)
|
)
|
||||||
|
|
@ -260,6 +262,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
|
||||||
saveSetting = true
|
saveSetting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.DisableViewSync != nil {
|
||||||
|
u.Settings.DisableViewSync = *s.DisableViewSync
|
||||||
|
saveSetting = true
|
||||||
|
}
|
||||||
|
|
||||||
if s.CurrentPassword != nil && s.NewPassword != nil {
|
if s.CurrentPassword != nil && s.NewPassword != nil {
|
||||||
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
|
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
|
||||||
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
|
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue