common: check the target write permission and quota on the server-side copy

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2026-08-12 23:33:45 +02:00
parent f0e660bef2
commit 31060cb401
2 changed files with 68 additions and 17 deletions
+31 -16
View File
@@ -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
+37 -1
View File
@@ -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)