feat: add option to disable copy-file-range for multipart uploads

The standard io.Copy() will attempt to use copy_file_range when available.
However, this can cause problems with certain filesystems. Add an option
that will prevent io.Copy() from being able to use copy_file_range to force
a standard data copy between file descriptors when needed for completing
multipart uploads.
This commit is contained in:
Jakob van Santen
2026-03-20 08:23:01 -07:00
committed by GitHub
parent ae411fa3c1
commit 56cb36d45a
2 changed files with 60 additions and 31 deletions
+36 -15
View File
@@ -81,6 +81,12 @@ type Posix struct {
// there are different filesystems mounted below the bucket level.
forceNoTmpFile bool
// forceNoCopyFileRange is a flag to disable the use of io.Copy to
// reassemble multipart upload parts, which uses copy_file_range on
// linux. This is needed for cases where a filesystem that does not
// support copy_file_range is mounted over NFSv4.2.
forceNoCopyFileRange bool
// enable posix level bucket name validations, not needed if the
// frontend handlers are already validating bucket names
validateBucketName bool
@@ -150,6 +156,8 @@ type PosixOpts struct {
// ForceNoTmpFile disables the use of O_TMPFILE even if the filesystem
// supports it
ForceNoTmpFile bool
// ForceNoCopyFileRange disables the use of io.Copy for multipart uploads parts
ForceNoCopyFileRange bool
// ValidateBucketNames enables minimal bucket name validation to prevent
// incorrect access to the filesystem. This is only needed if the
// frontend is not already validating bucket names.
@@ -212,19 +220,20 @@ func New(rootdir string, meta meta.MetadataStorer, opts PosixOpts) (*Posix, erro
}
return &Posix{
meta: meta,
rootfd: f,
rootdir: rootdir,
euid: os.Geteuid(),
egid: os.Getegid(),
chownuid: opts.ChownUID,
chowngid: opts.ChownGID,
bucketlinks: opts.BucketLinks,
versioningDir: versioningdirAbs,
newDirPerm: opts.NewDirPerm,
forceNoTmpFile: opts.ForceNoTmpFile,
validateBucketName: opts.ValidateBucketNames,
actionLimiter: semaphore.NewWeighted(int64(concurrencyOrDefault(opts.Concurrency))),
meta: meta,
rootfd: f,
rootdir: rootdir,
euid: os.Geteuid(),
egid: os.Getegid(),
chownuid: opts.ChownUID,
chowngid: opts.ChownGID,
bucketlinks: opts.BucketLinks,
versioningDir: versioningdirAbs,
newDirPerm: opts.NewDirPerm,
forceNoTmpFile: opts.ForceNoTmpFile,
forceNoCopyFileRange: opts.ForceNoCopyFileRange,
validateBucketName: opts.ValidateBucketNames,
actionLimiter: semaphore.NewWeighted(int64(concurrencyOrDefault(opts.Concurrency))),
}, nil
}
@@ -1610,6 +1619,10 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM
return p.CompleteMultipartUploadWithCopy(ctx, input, nil)
}
type onlyRead struct {
io.Reader
}
func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.CompleteMultipartUploadInput, customMove func(from *os.File, to *os.File) error) (s3response.CompleteMultipartUploadResult, string, error) {
acct, ok := ctx.Value("account").(auth.Account)
if !ok {
@@ -1819,10 +1832,18 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C
bucket, object, err)
fw := f.File()
fw.Seek(0, io.SeekEnd)
_, err = io.Copy(fw, pf)
if p.forceNoCopyFileRange {
_, err = io.Copy(fw, &onlyRead{pf})
} else {
_, err = io.Copy(fw, pf)
}
}
} else {
_, err = io.Copy(f.File(), pf)
if p.forceNoCopyFileRange {
_, err = io.Copy(f.File(), &onlyRead{pf})
} else {
_, err = io.Copy(f.File(), pf)
}
}
pf.Close()
if err != nil {
+24 -16
View File
@@ -25,14 +25,15 @@ import (
)
var (
chownuid, chowngid bool
bucketlinks bool
versioningDir string
dirPerms uint
sidecar string
nometa bool
forceNoTmpFile bool
actionsConcurrency int
chownuid, chowngid bool
bucketlinks bool
versioningDir string
dirPerms uint
sidecar string
nometa bool
forceNoTmpFile bool
forceNoCopyFileRange bool
actionsConcurrency int
)
func posixCommand() *cli.Command {
@@ -108,6 +109,12 @@ will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`,
EnvVars: []string{"VGW_DISABLE_OTMP"},
Destination: &forceNoTmpFile,
},
&cli.BoolFlag{
Name: "disable-copy-file-range",
Usage: "explicitly copy multipart upload parts instead of using copy_file_range (which may hang with some NFS servers)",
EnvVars: []string{"VGW_DISABLE_COPY_FILE_RANGE"},
Destination: &forceNoCopyFileRange,
},
},
}
}
@@ -132,14 +139,15 @@ func runPosix(ctx *cli.Context) error {
}
opts := posix.PosixOpts{
ChownUID: chownuid,
ChownGID: chowngid,
BucketLinks: bucketlinks,
VersioningDir: versioningDir,
NewDirPerm: fs.FileMode(dirPerms),
ForceNoTmpFile: forceNoTmpFile,
ValidateBucketNames: disableStrictBucketNames,
Concurrency: actionsConcurrency,
ChownUID: chownuid,
ChownGID: chowngid,
BucketLinks: bucketlinks,
VersioningDir: versioningDir,
NewDirPerm: fs.FileMode(dirPerms),
ForceNoTmpFile: forceNoTmpFile,
ForceNoCopyFileRange: forceNoCopyFileRange,
ValidateBucketNames: disableStrictBucketNames,
Concurrency: actionsConcurrency,
}
var ms meta.MetadataStorer