mirror of
https://github.com/cloudreve/cloudreve.git
synced 2025-12-15 10:04:01 +08:00
feat(explorer): save user's view setting to server / optionally share view setting via share link (#2232)
This commit is contained in:
2
assets
2
assets
Submodule assets updated: d674a23b21...9f91f8c98a
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
ent/share.go
17
ent/share.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
// UserSetting 用户其他配置
|
||||
type (
|
||||
UserSetting struct {
|
||||
ProfileOff bool `json:"profile_off,omitempty"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
VersionRetention bool `json:"version_retention,omitempty"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Pined []PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"email_language,omitempty"`
|
||||
ProfileOff bool `json:"profile_off,omitempty"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
VersionRetention bool `json:"version_retention,omitempty"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
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,17 +120,46 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi
|
||||
searchParams := path.SearchParameters()
|
||||
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)
|
||||
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 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
|
||||
if o.generateContextHint {
|
||||
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{
|
||||
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{
|
||||
New: func() any {
|
||||
return &File{
|
||||
Children: make(map[string]*File),
|
||||
}
|
||||
},
|
||||
}
|
||||
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,7 +49,8 @@ type (
|
||||
FileExtendedInfo *fs.FileExtendedInfo
|
||||
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...)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
138
pkg/filemanager/fs/dbfs/props.go
Normal file
138
pkg/filemanager/fs/dbfs/props.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
@@ -207,11 +207,12 @@ type ListResponse struct {
|
||||
// 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
|
||||
// as X-Cr-Context-Hint.
|
||||
ContextHint *uuid.UUID `json:"context_hint"`
|
||||
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
||||
MixedType bool `json:"mixed_type"`
|
||||
SingleFileView bool `json:"single_file_view,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
ContextHint *uuid.UUID `json:"context_hint"`
|
||||
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
||||
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 {
|
||||
@@ -233,10 +234,11 @@ type FileResponse struct {
|
||||
}
|
||||
|
||||
type ExtendedInfo struct {
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
StorageUsed int64 `json:"storage_used"`
|
||||
Shares []Share `json:"shares,omitempty"`
|
||||
Entities []Entity `json:"entities,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,17 +97,18 @@ type BuiltinLoginResponse struct {
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
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 {
|
||||
@@ -150,17 +153,18 @@ func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials
|
||||
// BuildUser 序列化用户
|
||||
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
||||
return User{
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||
Pined: user.Settings.Pined,
|
||||
Language: user.Settings.Language,
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user