Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion pkg/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,17 @@ func trimDotsForRename(paths []string) (res []string) {
}

func (fs *FileSystem) Rename(ctx meta.Context, oldpath string, newpath string, flags uint32) (err syscall.Errno) {
return fs.rename(ctx, oldpath, newpath, flags, 0, false)
}

// RenameWithInheritedMetadata commits a staged regular file using the
// destination directory's GID and default ACL inheritance rules. The metadata
// service performs the inheritance and rename in one backend transaction.
func (fs *FileSystem) RenameWithInheritedMetadata(ctx meta.Context, oldpath string, newpath string, flags uint32, mode uint16) (err syscall.Errno) {
return fs.rename(ctx, oldpath, newpath, flags, mode, true)
}

func (fs *FileSystem) rename(ctx meta.Context, oldpath string, newpath string, flags uint32, mode uint16, inheritMetadata bool) (err syscall.Errno) {
oss := trimDotsForRename(strings.Split(oldpath, "/"))
nss := trimDotsForRename(strings.Split(newpath, "/"))
var err0 syscall.Errno
Expand Down Expand Up @@ -681,7 +692,11 @@ func (fs *FileSystem) Rename(ctx meta.Context, oldpath string, newpath string, f
if err0 != 0 {
return err0
}
err = fs.m.Rename(ctx, oldfi.inode, path.Base(oldpath), newfi.inode, path.Base(newpath), flags, nil, nil)
if inheritMetadata {
err = fs.m.RenameWithInheritedMetadata(ctx, oldfi.inode, path.Base(oldpath), newfi.inode, path.Base(newpath), flags, mode, nil, nil)
} else {
err = fs.m.Rename(ctx, oldfi.inode, path.Base(oldpath), newfi.inode, path.Base(newpath), flags, nil, nil)
}
fs.InvalidateEntry(oldfi.inode, path.Base(oldpath))
fs.InvalidateEntry(newfi.inode, path.Base(newpath))
return
Expand Down
24 changes: 15 additions & 9 deletions pkg/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,14 +740,14 @@ func (n *jfsObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
return
}

eno = n.fs.Rename(mctx, tmp, dst, 0)
eno = n.fs.RenameWithInheritedMetadata(mctx, tmp, dst, 0, 0666)
if eno == syscall.ENOENT {
if err = n.mkdirAllInBucket(ctx, dstBucket, path.Dir(dst)); err != nil {
logger.Errorf("mkdirAll %s: %s", path.Dir(dst), err)
err = n.objectCommitErr(ctx, err, dstBucket, dstObject)
return
}
eno = n.fs.Rename(mctx, tmp, dst, 0)
eno = n.fs.RenameWithInheritedMetadata(mctx, tmp, dst, 0, 0666)
}
if eno != 0 {
err = n.objectCommitErr(ctx, eno, dstBucket, dstObject)
Expand Down Expand Up @@ -880,7 +880,7 @@ func (n *jfsObjects) mkdirAllUntil(ctx context.Context, p, root string) error {
return eno
}

func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, opts minio.ObjectOptions, applyObjTaggingFunc func(tmpName string)) (fi os.FileInfo, err error) {
func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, opts minio.ObjectOptions, applyObjTaggingFunc func(tmpName string), inheritMetadata bool) (fi os.FileInfo, err error) {
uuid := minio.MustGetUUID()
tmpname := n.tpath(bucket, "tmp", uuid[:subDirPrefix], uuid)
f, eno := n.fs.Create(mctx, tmpname, 0666, n.gConf.Umask)
Expand Down Expand Up @@ -937,7 +937,13 @@ func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *mi
return
}

eno = n.fs.Rename(mctx, tmpname, object, 0)
rename := n.fs.Rename
if inheritMetadata {
rename = func(ctx meta.Context, oldpath, newpath string, flags uint32) syscall.Errno {
return n.fs.RenameWithInheritedMetadata(ctx, oldpath, newpath, flags, 0666)
}
}
eno = rename(mctx, tmpname, object, 0)
if eno == syscall.ENOENT {
if strings.HasPrefix(object, sep+metaBucket+sep) {
err = n.objectCommitErr(ctx, eno, bucket, object, path.Base(path.Dir(object)))
Expand All @@ -948,7 +954,7 @@ func (n *jfsObjects) putObject(ctx context.Context, bucket, object string, r *mi
err = n.objectCommitErr(ctx, err, bucket, object)
return
}
eno = n.fs.Rename(mctx, tmpname, object, 0)
eno = rename(mctx, tmpname, object, 0)
}
if eno != 0 {
err = n.objectCommitErr(ctx, eno, bucket, object)
Expand Down Expand Up @@ -1004,7 +1010,7 @@ func (n *jfsObjects) PutObject(ctx context.Context, bucket string, object string
if err != nil {
logger.Errorf("set object metadata error, path: %s error %s", p, err)
}
}); err != nil {
}, true); err != nil {
return
}
}
Expand Down Expand Up @@ -1294,7 +1300,7 @@ func (n *jfsObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
if n.fs.SetXattr(mctx, tmpName, s3Etag, []byte(etag), 0) != 0 {
logger.Warnf("set xattr error, path: %s,xattr: %s,value: %s,flags: %d", tmpName, s3Etag, etag, 0)
}
}); err != nil {
}, false); err != nil {
err = jfsToObjectErr(ctx, err, bucket, object)
return
}
Expand Down Expand Up @@ -1406,15 +1412,15 @@ func (n *jfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
}

name := n.path(bucket, object)
eno = n.fs.Rename(mctx, tmp, name, 0)
eno = n.fs.RenameWithInheritedMetadata(mctx, tmp, name, 0, 0666)
if eno == syscall.ENOENT {
if err = n.mkdirAllInBucket(ctx, bucket, path.Dir(name)); err != nil {
logger.Errorf("mkdirAll %s: %s", path.Dir(name), err)
_ = n.fs.Delete(mctx, tmp)
err = n.objectCommitErr(ctx, err, bucket, object, uploadID)
return
}
eno = n.fs.Rename(mctx, tmp, name, 0)
eno = n.fs.RenameWithInheritedMetadata(mctx, tmp, name, 0, 0666)
}
if eno != 0 {
_ = n.fs.Delete(mctx, tmp)
Expand Down
216 changes: 215 additions & 1 deletion pkg/gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"testing"
"time"

"github.com/juicedata/juicefs/pkg/acl"
"github.com/juicedata/juicefs/pkg/chunk"
"github.com/juicedata/juicefs/pkg/fs"
"github.com/juicedata/juicefs/pkg/meta"
Expand Down Expand Up @@ -283,7 +284,7 @@ func TestObjectCommitAfterBucketDeleted(t *testing.T) {
// exercise the commit path deterministically, bypassing checkBucket
data := []byte("data")
if _, err := g1.putObject(ctx, bucket, g1.path(bucket, "obj"),
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}, func(string) {}); !errors.As(err, &minio.BucketNotFound{}) {
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}, func(string) {}, false); !errors.As(err, &minio.BucketNotFound{}) {
t.Fatalf("putObject after bucket deleted should return BucketNotFound, got %v", err)
}
if _, errno := g2.fs.Stat(mctx, g2.path(bucket)); !fs.IsNotExist(errno) {
Expand Down Expand Up @@ -843,3 +844,216 @@ func TestDeleteObjects(t *testing.T) {
}
})
}

func setupMetadataInheritanceTarget(t *testing.T, jfs *fs.FileSystem, defaultRule *acl.Rule) (string, *acl.Rule) {
t.Helper()
format := jfs.Meta().GetFormat()
format.EnableACL = true
if err := jfs.Meta().Init(&format, false); err != nil {
t.Fatalf("enable ACL support: %s", err)
}

const target = "/acl-target"
if eno := jfs.Mkdir(mctx, target, 0777, 0); eno != 0 {
t.Fatalf("mkdir target: %s", eno)
}

// Configure the target through the metadata API with an explicit root
// context. The test must also run as an unprivileged local macOS process;
// going through vfs.File.Chown would depend on the host's OS permissions.
rootCtx := meta.Background()
var targetIno meta.Ino
eno := jfs.Meta().Lookup(rootCtx, meta.RootInode, "acl-target", &targetIno, new(meta.Attr), false)
if eno != 0 {
t.Fatalf("lookup target: %s", eno)
}
if eno = jfs.Meta().SetAttr(rootCtx, targetIno, meta.SetAttrGID, 0, &meta.Attr{Gid: 2468}); eno != 0 {
t.Fatalf("set target gid: %s", eno)
}
// Chown may clear setgid, so set it afterwards.
if eno = jfs.Meta().SetAttr(rootCtx, targetIno, meta.SetAttrMode, 0, &meta.Attr{Mode: 02770}); eno != 0 {
t.Fatalf("set target mode: %s", eno)
}

if defaultRule == nil {
// Keep the resulting mode at 0600 while making the ACL extended, so the
// test checks both the mode derived from the default ACL and ACL storage.
defaultRule = &acl.Rule{
Owner: 6,
Group: 0,
Mask: 0,
Other: 0,
NamedUsers: []acl.Entry{{
Id: 1001,
Perm: 0,
}},
}
}
if eno = jfs.Meta().SetFacl(rootCtx, targetIno, acl.TypeDefault, defaultRule); eno != 0 {
t.Fatalf("set default ACL: %s", eno)
}

return target, defaultRule
}

func setupNestedMetadataInheritanceTarget(t *testing.T, jfs *fs.FileSystem) (string, *acl.Rule) {
// Nested directory creation needs execute permission. Keep this setup
// separate from the regular file case, whose ACL intentionally produces 0600.
nestedRule := &acl.Rule{
Owner: 7,
Group: 0,
Mask: 7,
Other: 0,
NamedUsers: []acl.Entry{{
Id: 1001,
Perm: 0,
}},
}
target, _ := setupMetadataInheritanceTarget(t, jfs, nestedRule)
return target, nestedRule
}

func assertInheritedMetadata(t *testing.T, jfs *fs.FileSystem, name string, wantACL *acl.Rule) {
t.Helper()
fi, eno := jfs.Stat(mctx, name)
if eno != 0 {
t.Fatalf("stat %s: %s", name, eno)
}
gotACL := &acl.Rule{}
aclErr := jfs.GetFacl(mctx, name, acl.TypeAccess, gotACL)
t.Logf("%s: mode=%#o gid=%d access_acl_err=%v access_acl=%s", name, uint32(fi.Mode().Perm()), fi.Gid(), aclErr, gotACL)

if fi.Gid() != 2468 || uint32(fi.Mode().Perm()) != uint32(wantACL.GetMode()) || aclErr != 0 || !gotACL.IsEqual(wantACL) {
t.Errorf("metadata mismatch for %s: got mode=%#o gid=%d acl_err=%v acl=%s, want mode=%#o gid=%d acl=%s",
name, uint32(fi.Mode().Perm()), fi.Gid(), aclErr, gotACL, uint32(wantACL.GetMode()), 2468, wantACL)
}
}

// TestGatewayObjectOperationsInheritDestinationMetadata verifies that every
// Gateway upload completion path applies the destination directory's POSIX
// GID and default ACL to the final object inode.
func TestGatewayObjectOperationsInheritDestinationMetadata(t *testing.T) {
// A regular PUT creates a staged inode first and commits it with rename.
t.Run("PUT", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, defaultRule := setupMetadataInheritanceTarget(t, jfs, nil)
wantACL := defaultRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")

if _, err := jfsObj.PutObject(context.Background(), bucket, "acl-target/put",
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}); err != nil {
t.Fatalf("put object: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/put", wantACL)
})

// A nested object path exercises the ENOENT retry: the first rename sees
// missing parent directories, mkdirAllInBucket creates them, and the second
// rename commits the staged file with inherited metadata.
t.Run("PUT with missing parent directories", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, nestedRule := setupNestedMetadataInheritanceTarget(t, jfs)
wantACL := nestedRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")
object := "acl-target/nested/deep/put"

if _, err := jfsObj.PutObject(context.Background(), bucket, object,
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}); err != nil {
t.Fatalf("put nested object: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/nested/deep/put", wantACL)
})

// COPY follows the same staged-file commit path as PUT and must preserve the
// destination directory's inherited metadata.
t.Run("COPY", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, defaultRule := setupMetadataInheritanceTarget(t, jfs, nil)
wantACL := defaultRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")
if _, err := jfsObj.PutObject(context.Background(), bucket, "source",
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}); err != nil {
t.Fatalf("put source object: %s", err)
}
srcInfo, err := jfsObj.GetObjectInfo(context.Background(), bucket, "source", minio.ObjectOptions{})
if err != nil {
t.Fatalf("get source object info: %s", err)
}
if _, err = jfsObj.CopyObject(context.Background(), bucket, "source", bucket,
"acl-target/copy", srcInfo, minio.ObjectOptions{}, minio.ObjectOptions{}); err != nil {
t.Fatalf("copy object: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/copy", wantACL)
})

// COPY must also retry after the destination's missing parent directories
// are created, then apply the destination directory's inherited metadata.
t.Run("COPY with missing parent directories", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, nestedRule := setupNestedMetadataInheritanceTarget(t, jfs)
wantACL := nestedRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")
if _, err := jfsObj.PutObject(context.Background(), bucket, "source",
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{}); err != nil {
t.Fatalf("put source object: %s", err)
}
srcInfo, err := jfsObj.GetObjectInfo(context.Background(), bucket, "source", minio.ObjectOptions{})
if err != nil {
t.Fatalf("get source object info: %s", err)
}
object := "acl-target/nested/deep/copy"
if _, err = jfsObj.CopyObject(context.Background(), bucket, "source", bucket,
object, srcInfo, minio.ObjectOptions{}, minio.ObjectOptions{}); err != nil {
t.Fatalf("copy nested object: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/nested/deep/copy", wantACL)
})

// Completing a multipart upload commits a previously staged inode and must
// apply inheritance at completion time, not only when the upload starts.
t.Run("multipart completion", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, defaultRule := setupMetadataInheritanceTarget(t, jfs, nil)
wantACL := defaultRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")
object := "acl-target/multipart"
uploadID, err := jfsObj.NewMultipartUpload(context.Background(), bucket, object, minio.ObjectOptions{})
if err != nil {
t.Fatalf("new multipart upload: %s", err)
}
part, err := jfsObj.PutObjectPart(context.Background(), bucket, object, uploadID, 1,
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{})
if err != nil {
t.Fatalf("put multipart part: %s", err)
}
if _, err = jfsObj.CompleteMultipartUpload(context.Background(), bucket, object, uploadID,
[]minio.CompletePart{{PartNumber: 1, ETag: part.ETag}}, minio.ObjectOptions{}); err != nil {
t.Fatalf("complete multipart upload: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/multipart", wantACL)
})

// Multipart completion must create missing destination parents before the
// retrying rename and preserve the inherited metadata on the final object.
t.Run("multipart completion with missing parent directories", func(t *testing.T) {
jfsObj, jfs, bucket := newTestGateway(t, Config{})
target, nestedRule := setupNestedMetadataInheritanceTarget(t, jfs)
wantACL := nestedRule.ChildAccessACL(0666)
data := []byte("metadata-inheritance")
object := "acl-target/nested/deep/multipart"
uploadID, err := jfsObj.NewMultipartUpload(context.Background(), bucket, object, minio.ObjectOptions{})
if err != nil {
t.Fatalf("new nested multipart upload: %s", err)
}
part, err := jfsObj.PutObjectPart(context.Background(), bucket, object, uploadID, 1,
newTestPutObjReader(t, bytes.NewReader(data), data), minio.ObjectOptions{})
if err != nil {
t.Fatalf("put nested multipart part: %s", err)
}
if _, err = jfsObj.CompleteMultipartUpload(context.Background(), bucket, object, uploadID,
[]minio.CompletePart{{PartNumber: 1, ETag: part.ETag}}, minio.ObjectOptions{}); err != nil {
t.Fatalf("complete nested multipart upload: %s", err)
}
assertInheritedMetadata(t, jfs, target+"/nested/deep/multipart", wantACL)
})
}
Loading
Loading