From 969e35192a858cca379a6cca283a279fb0fc2d5a Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 24 Apr 2025 15:26:58 +0800 Subject: [PATCH] fix(remote download): improve file pre-upload validation (fix #2286) --- pkg/filemanager/fs/dbfs/upload.go | 54 ++++++++++++++++++++ pkg/filemanager/fs/fs.go | 8 +++ pkg/filemanager/manager/upload.go | 6 +++ pkg/filemanager/workflows/remote_download.go | 34 +++--------- 4 files changed, 76 insertions(+), 26 deletions(-) diff --git a/pkg/filemanager/fs/dbfs/upload.go b/pkg/filemanager/fs/dbfs/upload.go index 4e047d3..278229f 100644 --- a/pkg/filemanager/fs/dbfs/upload.go +++ b/pkg/filemanager/fs/dbfs/upload.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "path" "time" "github.com/cloudreve/Cloudreve/v4/ent" @@ -15,6 +16,59 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/util" ) +func (f *DBFS) PreValidateUpload(ctx context.Context, dst *fs.URI, files ...fs.PreValidateFile) error { + // Get navigator + navigator, err := f.getNavigator(ctx, dst, NavigatorCapabilityUploadFile, NavigatorCapabilityLockFile) + if err != nil { + return err + } + + dstFile, err := f.getFileByPath(ctx, navigator, dst) + if err != nil { + return fmt.Errorf("failed to get destination folder: %w", err) + } + + if dstFile.Type() != types.FileTypeFolder { + return fmt.Errorf("destination is not a folder") + } + + // check permission + if err := f.EvaluatePermission(ctx, dstFile, types.FilePermissionCreate, false); err != nil { + return fmt.Errorf("failed to evaluate permission: %w", err) + } + + total := int64(0) + for _, file := range files { + total += file.Size + } + + // Get parent folder storage policy and performs validation + policy, err := f.getPreferredPolicy(ctx, dstFile, 0) + if err != nil { + return err + } + + // validate upload request + for _, file := range files { + if file.OmitName { + if err := validateFileSize(file.Size, policy); err != nil { + return err + } + } else { + if err := validateNewFile(path.Base(file.Name), file.Size, policy); err != nil { + return err + } + } + } + + // Validate available capacity + if err := f.validateUserCapacity(ctx, total, dstFile.Owner()); err != nil { + return err + } + + return nil +} + func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ...fs.Option) (*fs.UploadSession, error) { // Get navigator navigator, err := f.getNavigator(ctx, req.Props.Uri, NavigatorCapabilityUploadFile, NavigatorCapabilityLockFile) diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 314354a..1962e8a 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -103,6 +103,8 @@ type ( CompleteUpload(ctx context.Context, session *UploadSession) (File, error) // CancelUploadSession cancels an upload session. Delete the placeholder file if no other entity is created. CancelUploadSession(ctx context.Context, path *URI, sessionID string, session *UploadSession) ([]Entity, error) + // PreValidateUpload pre-validates an upload request. + PreValidateUpload(ctx context.Context, dst *URI, files ...PreValidateFile) error } LockSystem interface { @@ -369,6 +371,12 @@ type ( ParentFiles []int `json:"parent_files"` PrimaryEntityParentFiles []int `json:"primary_entity_parent_files"` } + + PreValidateFile struct { + Name string + Size int64 + OmitName bool // if true, file name will not be validated + } ) const ( diff --git a/pkg/filemanager/manager/upload.go b/pkg/filemanager/manager/upload.go index 6a4a695..4eae264 100644 --- a/pkg/filemanager/manager/upload.go +++ b/pkg/filemanager/manager/upload.go @@ -38,9 +38,15 @@ type ( OnUploadFailed(ctx context.Context, session *fs.UploadSession) // Similar to CompleteUpload, but does not create actual uplaod session in storage. PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ...fs.Option) (*fs.UploadSession, error) + // PreValidateUpload pre-validates an upload request. + PreValidateUpload(ctx context.Context, dst *fs.URI, files ...fs.PreValidateFile) error } ) +func (m *manager) PreValidateUpload(ctx context.Context, dst *fs.URI, files ...fs.PreValidateFile) error { + return m.fs.PreValidateUpload(ctx, dst, files...) +} + func (m *manager) CreateUploadSession(ctx context.Context, req *fs.UploadRequest, opts ...fs.Option) (*fs.UploadCredential, error) { o := newOption() for _, opt := range opts { diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index dc7e8fe..b833ee8 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -25,7 +25,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/queue" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" - "github.com/gofrs/uuid" "github.com/samber/lo" ) @@ -537,35 +536,18 @@ func (m *RemoteDownloadTask) validateFiles(ctx context.Context, dep dependency.D return fmt.Errorf("no selected file found in download task") } - // find the first valid file - var placeholderFileName string - for _, f := range selectedFiles { - if f.Name != "" { - placeholderFileName = f.Name - break + validateArgs := lo.Map(selectedFiles, func(f downloader.TaskFile, _ int) fs.PreValidateFile { + return fs.PreValidateFile{ + Name: f.Name, + Size: f.Size, + OmitName: f.Name == "", } - } - - if placeholderFileName == "" { - // File name not available yet, generate one - m.l.Debug("File name not available yet, generate one to validate the destination") - placeholderFileName = uuid.Must(uuid.NewV4()).String() - } - - // Create a placeholder file then delete it to validate the destination - session, err := fm.PrepareUpload(ctx, &fs.UploadRequest{ - Props: &fs.UploadProps{ - Uri: dstUri.Join(path.Base(placeholderFileName)), - Size: status.Total, - UploadSessionID: uuid.Must(uuid.NewV4()).String(), - ExpireAt: time.Now().Add(time.Second * 3600), - }, }) - if err != nil { - return err + + if err := fm.PreValidateUpload(ctx, dstUri, validateArgs...); err != nil { + return fmt.Errorf("failed to pre-validate files: %w", err) } - fm.OnUploadFailed(ctx, session) return nil }