serve s3: allow copying directory markers

CopyObject with a directory marker such as "dir/" as its source failed
with InvalidArgument, although HeadObject and GetObject found it.

It now copies the marker, making the directory of another marker or an
empty object.

See #10082
This commit is contained in:
Nick Craig-Wood committed 2026-10-29 11:01:30 +00:00
1 parent 617476d0e7
commit 79c19dc1d3
3 files changed
+32 -5

No files matched your search

+9 -2
View File
@@ -824,15 +824,22 @@ func (b *s3Backend) CopyObject(ctx context.Context, srcBucket, srcKey, dstBucket
if err != nil {
return result, err
}
fp, err := bucketObjectPath(srcBucket, srcKey)
fp, isMarker, err := bucketDirMarkerPath(srcBucket, srcKey)
if !isMarker {
fp, err = bucketObjectPath(srcBucket, srcKey)
}
if err != nil {
return result, err
}
cStat, err := _vfs.Stat(fp)
if err != nil || !cStat.IsFile() {
if err != nil || cStat.IsDir() != isMarker {
return result, gofakes3.KeyNotFound(srcKey)
}
if srcBucket == dstBucket && srcKey == dstKey {
if isMarker {
// Directory markers have no metadata to replace
return result, nil
}
return result, b.tenant(ctx).storeMeta(_vfs, fp, meta)
}
+19
View File
@@ -408,6 +408,25 @@ func TestDirMarkers(t *testing.T) {
assert.True(t, gofakes3.HasErrorCode(err, gofakes3.ErrNoSuchKey), "%s: %v", key, err)
}
// Copying a marker makes the directory of another marker, or an
// empty object
_, err = b.CopyObject(ctx, "bucket", "dir/sub/", "bucket", "copy/", map[string]string{})
require.NoError(t, err)
assert.True(t, isDir("copy"))
_, err = b.CopyObject(ctx, "bucket", "dir/sub/", "bucket", "empty.txt", map[string]string{})
require.NoError(t, err)
obj, err = b.HeadObject(ctx, "bucket", "empty.txt")
require.NoError(t, err)
assert.Equal(t, int64(0), obj.Size)
_, err = b.CopyObject(ctx, "bucket", "dir/", "bucket", "dir/", map[string]string{})
require.NoError(t, err)
// A copy needs the "/" for a directory and none for an object
for _, key := range []string{"dir", "empty.txt/", "missing/"} {
_, err = b.CopyObject(ctx, "bucket", key, "bucket", "other.txt", map[string]string{})
assert.True(t, gofakes3.HasErrorCode(err, gofakes3.ErrNoSuchKey), "%s: %v", key, err)
}
// Deleting a marker removes its directory only if empty
require.NoError(t, put("dir/sub/file.txt", "data"))
require.NoError(t, b.deleteObject(ctx, "bucket", "dir/sub/"))
+4 -3
View File
@@ -493,9 +493,10 @@ A key ending in `/`, such as `dir/`, is a directory marker, standing
for the directory `dir`. Putting an empty directory marker creates the
directory (one with data is rejected with `400 Bad Request`), and
deleting it removes the directory if it is empty. Directory markers
are found by `HeadObject` and `GetObject` for any directory, but only
the markers of empty directories are listed as objects, as other
directories may not have been made by putting a marker.
are found by `HeadObject`, `GetObject` and `CopyObject` for any
directory, but only the markers of empty directories are listed as
objects, as other directories may not have been made by putting a
marker.
As keys are file paths, a key can't be both an object and a directory
holding other objects, for example `a/b` and `a/b/c`. Writing an