From 31060cb401fa41cd20dd0ee6d291d2ef0823c31a Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Wed, 12 Aug 2026 22:12:17 +0200 Subject: [PATCH] common: check the target write permission and quota on the server-side copy Signed-off-by: Nicola Murino --- internal/common/connection.go | 47 ++++++++++++++++++++---------- internal/common/connection_test.go | 38 +++++++++++++++++++++++- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/internal/common/connection.go b/internal/common/connection.go index 20604cde..89acf63f 100644 --- a/internal/common/connection.go +++ b/internal/common/connection.go @@ -675,7 +675,9 @@ func (c *BaseConnection) checkCopyPermissions(virtualSourcePath, virtualTargetPa return c.checkCopyTargetPattern(virtualTargetPath) } -func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo) error { +// copyFile copies a regular file. dstInfo describes the target as the caller +// resolved it, it is nil if the target does not exist. +func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcInfo, dstInfo os.FileInfo) error { if err := c.checkCopyPermissions(virtualSourcePath, virtualTargetPath); err != nil { return err } @@ -685,19 +687,32 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s return err } if copier, ok := fs.(vfs.FsFileCopier); ok { - _, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSourcePath) - if err != nil { + // A target that exists but is not a regular file is left to the + // streaming path, which reports the appropriate error. + if dstInfo == nil || dstInfo.Mode().IsRegular() { + numFiles := 1 + var truncatedSize int64 + if dstInfo != nil { + numFiles = 0 + truncatedSize = dstInfo.Size() + } + if err := checkWriterPermsAndQuota(c, virtualTargetPath, numFiles, srcInfo.Size(), truncatedSize); err != nil { + return err + } + _, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSourcePath) + if err != nil { + return err + } + startTime := time.Now() + numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcInfo) + elapsed := time.Since(startTime).Nanoseconds() / 1000000 + updateUserQuotaAfterFileWrite(c, virtualTargetPath, numFiles, sizeDiff) + logger.CommandLog(copyLogSender, fsSourcePath, fsTargetPath, virtualSourcePath, virtualTargetPath, + c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", srcInfo.Size(), + c.localAddr, c.remoteAddr, elapsed) + ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcInfo.Size(), err, elapsed, nil) //nolint:errcheck return err } - startTime := time.Now() - numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcInfo) - elapsed := time.Since(startTime).Nanoseconds() / 1000000 - updateUserQuotaAfterFileWrite(c, virtualTargetPath, numFiles, sizeDiff) - logger.CommandLog(copyLogSender, fsSourcePath, fsTargetPath, virtualSourcePath, virtualTargetPath, - c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", srcInfo.Size(), - c.localAddr, c.remoteAddr, elapsed) - ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcInfo.Size(), err, elapsed, nil) //nolint:errcheck - return err } } @@ -720,7 +735,7 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s err, operationCopy, startTime) } -func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo, +func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath string, srcInfo, dstInfo os.FileInfo, createTargetDir bool, recursion int, ) error { if srcInfo.IsDir() { @@ -759,7 +774,7 @@ func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath st return nil } - return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo) + return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo, dstInfo) } func (c *BaseConnection) recursiveCopyEntries(virtualSourcePath, virtualTargetPath string, entries []os.FileInfo, recursion int) error { @@ -779,7 +794,7 @@ func (c *BaseConnection) recursiveCopyEntries(virtualSourcePath, virtualTargetPa if err := c.checkCopy(info, targetInfo, sourcePath, targetPath); err != nil { return err } - if err := c.doRecursiveCopy(sourcePath, targetPath, info, true, recursion); err != nil { + if err := c.doRecursiveCopy(sourcePath, targetPath, info, targetInfo, true, recursion); err != nil { if c.IsNotExistError(err) { c.Log(logger.LevelInfo, "skipping copy for source path %q: %v", sourcePath, err) continue @@ -853,7 +868,7 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error defer close(done) go keepConnectionAlive(c, done, 2*time.Minute) - return c.doRecursiveCopy(virtualSourcePath, destPath, srcInfo, createTargetDir, 0) + return c.doRecursiveCopy(virtualSourcePath, destPath, srcInfo, dstInfo, createTargetDir, 0) } // Rename renames (moves) virtualSourcePath to virtualTargetPath diff --git a/internal/common/connection_test.go b/internal/common/connection_test.go index bd829ee2..7ae49bbb 100644 --- a/internal/common/connection_test.go +++ b/internal/common/connection_test.go @@ -680,7 +680,7 @@ func TestErrorResolvePath(t *testing.T) { assert.Error(t, err) err = conn.doRecursiveRemove(nil, "/fspath", "/vpath", vfs.NewFileInfo("vpath", true, 0, time.Now(), false), 2000) assert.Error(t, err, util.ErrRecursionTooDeep) - err = conn.doRecursiveCopy("/src", "/dst", vfs.NewFileInfo("src", true, 0, time.Now(), false), false, 2000) + err = conn.doRecursiveCopy("/src", "/dst", vfs.NewFileInfo("src", true, 0, time.Now(), false), nil, false, 2000) assert.Error(t, err, util.ErrRecursionTooDeep) err = conn.checkCopy(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/source", "/target") assert.Error(t, err) @@ -1477,6 +1477,42 @@ func TestHiddenVirtualFolderStat(t *testing.T) { assert.NoError(t, err) } +func TestServerSideCopyWriteChecks(t *testing.T) { + newConn := func(perms []string) *BaseConnection { + user := dataprovider.User{ + BaseUser: sdk.BaseUser{ + Username: userTestUsername, + HomeDir: filepath.Clean(os.TempDir()), + Permissions: map[string][]string{"/": perms}, + }, + FsConfig: vfs.Filesystem{ + Provider: sdk.S3FilesystemProvider, + S3Config: vfs.S3FsConfig{ + BaseS3FsConfig: sdk.BaseS3FsConfig{ + Bucket: "buck", + Region: "us-east-1", + AccessKey: "key", + }, + AccessSecret: kms.NewPlainSecret("s3secret"), + }, + }, + } + return NewBaseConnection(xid.New().String(), ProtocolHTTP, "", "", user) + } + srcInfo := vfs.NewFileInfo("a.txt", false, 100, time.Now(), false) + dstInfo := vfs.NewFileInfo("b.txt", false, 10, time.Now(), false) + + // the target exists: the overwrite permission is required, upload is not enough + conn := newConn([]string{dataprovider.PermListItems, dataprovider.PermDownload, + dataprovider.PermUpload, dataprovider.PermCopy}) + assert.ErrorIs(t, conn.copyFile("/a.txt", "/b.txt", srcInfo, dstInfo), os.ErrPermission) + + // the target does not exist: the upload permission is required, overwrite is not enough + conn = newConn([]string{dataprovider.PermListItems, dataprovider.PermDownload, + dataprovider.PermOverwrite, dataprovider.PermCopy}) + assert.ErrorIs(t, conn.copyFile("/a.txt", "/b.txt", srcInfo, nil), os.ErrPermission) +} + func TestRecursiveRenameHiddenEntry(t *testing.T) { homeDir := filepath.Join(os.TempDir(), "renamewalk") err := os.RemoveAll(homeDir)