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

This commit is contained in:
Aaron Liu
2025-06-05 10:00:37 +08:00
parent c13b7365b0
commit 522fcca6af
31 changed files with 704 additions and 158 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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)