Cloudreve/pkg/filemanager/manager/operation.go

346 lines
9.6 KiB
Go

package manager
import (
"context"
"encoding/gob"
"fmt"
"strings"
"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"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/samber/lo"
)
const (
EntityUrlCacheKeyPrefix = "entity_url_"
DownloadSentinelCachePrefix = "download_sentinel_"
)
type (
ListArgs struct {
Page int
PageSize int
PageToken string
Order string
OrderDirection string
// StreamResponseCallback is used for streamed list operation, e.g. searching files.
// Whenever a new item is found, this callback will be called with the current item and the parent item.
StreamResponseCallback func(fs.File, []fs.File)
}
EntityUrlCache struct {
Url string
BrowserDownloadDisplayName string
ExpireAt *time.Time
}
)
func init() {
gob.Register(EntityUrlCache{})
}
func (m *manager) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.File, error) {
return m.fs.Get(ctx, path, opts...)
}
func (m *manager) List(ctx context.Context, path *fs.URI, args *ListArgs) (fs.File, *fs.ListFileResult, error) {
dbfsSetting := m.settings.DBFS(ctx)
opts := []fs.Option{
fs.WithPageSize(args.PageSize),
fs.WithOrderBy(args.Order),
fs.WithOrderDirection(args.OrderDirection),
dbfs.WithFilePublicMetadata(),
dbfs.WithContextHint(),
dbfs.WithFileShareIfOwned(),
}
searchParams := path.SearchParameters()
if searchParams != nil {
if dbfsSetting.UseSSEForSearch {
opts = append(opts, dbfs.WithStreamListResponseCallback(args.StreamResponseCallback))
}
if searchParams.Category != "" {
// Overwrite search query with predefined category
category := fs.SearchCategoryFromString(searchParams.Category)
if category == setting.CategoryUnknown {
return nil, nil, fmt.Errorf("unknown category: %s", searchParams.Category)
}
path = path.SetQuery(m.settings.SearchCategoryQuery(ctx, category))
searchParams = path.SearchParameters()
}
}
if dbfsSetting.UseCursorPagination || searchParams != nil {
opts = append(opts, dbfs.WithCursorPagination(args.PageToken))
} else {
opts = append(opts, fs.WithPage(args.Page))
}
return m.fs.List(ctx, path, opts...)
}
func (m *manager) SharedAddressTranslation(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.File, *fs.URI, error) {
o := newOption()
for _, opt := range opts {
opt.Apply(o)
}
return m.fs.SharedAddressTranslation(ctx, path)
}
func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileType, opts ...fs.Option) (fs.File, error) {
o := newOption()
for _, opt := range opts {
opt.Apply(o)
}
if m.stateless {
return nil, o.Node.CreateFile(ctx, &fs.StatelessCreateFileService{
Path: path.String(),
Type: fileType,
UserID: o.StatelessUserID,
})
}
isSymbolic := false
if o.Metadata != nil {
if err := m.validateMetadata(ctx, lo.MapToSlice(o.Metadata, func(key string, value string) fs.MetadataPatch {
if key == shareRedirectMetadataKey {
isSymbolic = true
}
return fs.MetadataPatch{
Key: key,
Value: value,
}
})...); err != nil {
return nil, err
}
}
if isSymbolic {
opts = append(opts, dbfs.WithSymbolicLink())
}
return m.fs.Create(ctx, path, fileType, opts...)
}
func (m *manager) Rename(ctx context.Context, path *fs.URI, newName string) (fs.File, error) {
return m.fs.Rename(ctx, path, newName)
}
func (m *manager) MoveOrCopy(ctx context.Context, src []*fs.URI, dst *fs.URI, isCopy bool) error {
return m.fs.MoveOrCopy(ctx, src, dst, isCopy)
}
func (m *manager) SoftDelete(ctx context.Context, path ...*fs.URI) error {
return m.fs.SoftDelete(ctx, path...)
}
func (m *manager) Delete(ctx context.Context, path []*fs.URI, opts ...fs.Option) error {
o := newOption()
for _, opt := range opts {
opt.Apply(o)
}
if !o.SkipSoftDelete && !o.SysSkipSoftDelete {
return m.SoftDelete(ctx, path...)
}
staleEntities, err := m.fs.Delete(ctx, path, fs.WithUnlinkOnly(o.UnlinkOnly), fs.WithSysSkipSoftDelete(o.SysSkipSoftDelete))
if err != nil {
return err
}
m.l.Debug("New stale entities: %v", staleEntities)
// Delete stale entities
if len(staleEntities) > 0 {
t, err := newExplicitEntityRecycleTask(ctx, lo.Map(staleEntities, func(entity fs.Entity, index int) int {
return entity.ID()
}))
if err != nil {
return fmt.Errorf("failed to create explicit entity recycle task: %w", err)
}
if err := m.dep.EntityRecycleQueue(ctx).QueueTask(ctx, t); err != nil {
return fmt.Errorf("failed to queue explicit entity recycle task: %w", err)
}
}
return nil
}
func (m *manager) Walk(ctx context.Context, path *fs.URI, depth int, f fs.WalkFunc, opts ...fs.Option) error {
return m.fs.Walk(ctx, path, depth, f, opts...)
}
func (m *manager) Capacity(ctx context.Context) (*fs.Capacity, error) {
res, err := m.fs.Capacity(ctx, m.user)
if err != nil {
return nil, err
}
return res, nil
}
func (m *manager) CheckIfCapacityExceeded(ctx context.Context) error {
ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
capacity, err := m.Capacity(ctx)
if err != nil {
return fmt.Errorf("failed to get user capacity: %w", err)
}
if capacity.Used <= capacity.Total {
return nil
}
return nil
}
func (l *manager) ConfirmLock(ctx context.Context, ancestor fs.File, uri *fs.URI, token ...string) (func(), fs.LockSession, error) {
return l.fs.ConfirmLock(ctx, ancestor, uri, token...)
}
func (l *manager) Lock(ctx context.Context, d time.Duration, requester *ent.User, zeroDepth bool,
application lock.Application, uri *fs.URI, token string) (fs.LockSession, error) {
return l.fs.Lock(ctx, d, requester, zeroDepth, application, uri, token)
}
func (l *manager) Unlock(ctx context.Context, tokens ...string) error {
return l.fs.Unlock(ctx, tokens...)
}
func (l *manager) Refresh(ctx context.Context, d time.Duration, token string) (lock.LockDetails, error) {
return l.fs.Refresh(ctx, d, token)
}
func (l *manager) Restore(ctx context.Context, path ...*fs.URI) error {
return l.fs.Restore(ctx, path...)
}
func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *CreateShareArgs) (*ent.Share, error) {
file, err := l.fs.Get(ctx, path, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityShare), dbfs.WithNotRoot())
if err != nil {
return nil, serializer.NewError(serializer.CodeNotFound, "src file not found", err)
}
// Only file owner can share file
if file.OwnerID() != l.user.ID {
return nil, serializer.NewError(serializer.CodeNoPermissionErr, "permission denied", nil)
}
if file.IsSymbolic() {
return nil, serializer.NewError(serializer.CodeNoPermissionErr, "cannot share symbolic file", nil)
}
var existed *ent.Share
shareClient := l.dep.ShareClient()
if args.ExistedShareID != 0 {
loadShareCtx := context.WithValue(ctx, inventory.LoadShareFile{}, true)
existed, err = shareClient.GetByID(loadShareCtx, args.ExistedShareID)
if err != nil {
return nil, serializer.NewError(serializer.CodeNotFound, "failed to get existed share", err)
}
if existed.Edges.File.ID != file.ID() {
return nil, serializer.NewError(serializer.CodeNotFound, "share link not found", nil)
}
}
password := ""
if args.IsPrivate {
password = args.Password
if strings.TrimSpace(password) == "" {
password = util.RandString(8, util.RandomLowerCases)
}
}
props := &types.ShareProps{
ShareView: args.ShareView,
ShowReadMe: args.ShowReadMe,
}
share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{
OwnerID: file.OwnerID(),
FileID: file.ID(),
Password: password,
Expires: args.Expire,
RemainDownloads: args.RemainDownloads,
Existed: existed,
Props: props,
})
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "failed to create share", err)
}
return share, nil
}
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:
return fmt.Sprintf("%s_thumbnail", f.DisplayName())
case types.EntityTypeLivePhoto:
return fmt.Sprintf("%s_live_photo.mov", f.DisplayName())
default:
return f.Name()
}
}
func expireTimeToTTL(expireAt *time.Time) int {
if expireAt == nil {
return -1
}
return int(time.Until(*expireAt).Seconds())
}