Browse Source
Use filer-side copy for mounted whole-file copy_file_range (#8747)
Use filer-side copy for mounted whole-file copy_file_range (#8747)
* Optimize mounted whole-file copy_file_range * Address mounted copy review feedback * Harden mounted copy fast path --------- Co-authored-by: Copilot <copilot@github.com>pull/8711/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1292 additions and 60 deletions
-
60weed/command/mount_std.go
-
17weed/filer/copy_params.go
-
55weed/mount/weedfs.go
-
355weed/mount/weedfs_file_copy_range.go
-
355weed/mount/weedfs_file_copy_range_test.go
-
4weed/server/filer_server.go
-
283weed/server/filer_server_handlers_copy.go
-
223weed/server/filer_server_handlers_copy_test.go
@ -0,0 +1,17 @@ |
|||
package filer |
|||
|
|||
const ( |
|||
CopyQueryParamFrom = "cp.from" |
|||
CopyQueryParamOverwrite = "overwrite" |
|||
CopyQueryParamDataOnly = "dataOnly" |
|||
CopyQueryParamRequestID = "copy.requestId" |
|||
CopyQueryParamSourceInode = "copy.srcInode" |
|||
CopyQueryParamSourceMtime = "copy.srcMtime" |
|||
CopyQueryParamSourceSize = "copy.srcSize" |
|||
CopyQueryParamDestinationInode = "copy.dstInode" |
|||
CopyQueryParamDestinationMtime = "copy.dstMtime" |
|||
CopyQueryParamDestinationSize = "copy.dstSize" |
|||
|
|||
CopyResponseHeaderCommitted = "X-SeaweedFS-Copy-Committed" |
|||
CopyResponseHeaderRequestID = "X-SeaweedFS-Copy-Request-ID" |
|||
) |
|||
@ -0,0 +1,355 @@ |
|||
package mount |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"path/filepath" |
|||
"testing" |
|||
|
|||
"github.com/seaweedfs/go-fuse/v2/fuse" |
|||
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/util" |
|||
) |
|||
|
|||
func TestWholeFileServerCopyCandidate(t *testing.T) { |
|||
wfs := newCopyRangeTestWFS() |
|||
|
|||
srcPath := util.FullPath("/src.txt") |
|||
dstPath := util.FullPath("/dst.txt") |
|||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true) |
|||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true) |
|||
|
|||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{ |
|||
Name: "src.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
FileSize: 5, |
|||
Inode: srcInode, |
|||
}, |
|||
Content: []byte("hello"), |
|||
}) |
|||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{ |
|||
Name: "dst.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
Inode: dstInode, |
|||
}, |
|||
}) |
|||
|
|||
srcHandle.RememberPath(srcPath) |
|||
dstHandle.RememberPath(dstPath) |
|||
|
|||
in := &fuse.CopyFileRangeIn{ |
|||
FhIn: uint64(srcHandle.fh), |
|||
FhOut: uint64(dstHandle.fh), |
|||
OffIn: 0, |
|||
OffOut: 0, |
|||
Len: 8, |
|||
} |
|||
|
|||
copyRequest, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in) |
|||
if !ok { |
|||
t.Fatal("expected whole-file server copy candidate") |
|||
} |
|||
if copyRequest.srcPath != srcPath { |
|||
t.Fatalf("source path = %q, want %q", copyRequest.srcPath, srcPath) |
|||
} |
|||
if copyRequest.dstPath != dstPath { |
|||
t.Fatalf("destination path = %q, want %q", copyRequest.dstPath, dstPath) |
|||
} |
|||
if copyRequest.sourceSize != 5 { |
|||
t.Fatalf("source size = %d, want 5", copyRequest.sourceSize) |
|||
} |
|||
if copyRequest.srcInode == 0 || copyRequest.dstInode == 0 { |
|||
t.Fatalf("expected inode preconditions, got src=%d dst=%d", copyRequest.srcInode, copyRequest.dstInode) |
|||
} |
|||
|
|||
srcHandle.dirtyMetadata = true |
|||
if _, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in); ok { |
|||
t.Fatal("dirty source handle should disable whole-file server copy") |
|||
} |
|||
srcHandle.dirtyMetadata = false |
|||
|
|||
in.Len = 4 |
|||
if _, ok := wholeFileServerCopyCandidate(srcHandle, dstHandle, in); ok { |
|||
t.Fatal("short copy request should disable whole-file server copy") |
|||
} |
|||
} |
|||
|
|||
func TestCopyFileRangeUsesServerSideWholeFileCopy(t *testing.T) { |
|||
wfs := newCopyRangeTestWFS() |
|||
|
|||
srcPath := util.FullPath("/src.txt") |
|||
dstPath := util.FullPath("/dst.txt") |
|||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true) |
|||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true) |
|||
|
|||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{ |
|||
Name: "src.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
FileSize: 5, |
|||
Inode: srcInode, |
|||
}, |
|||
Content: []byte("hello"), |
|||
}) |
|||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{ |
|||
Name: "dst.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
Inode: dstInode, |
|||
}, |
|||
}) |
|||
|
|||
srcHandle.RememberPath(srcPath) |
|||
dstHandle.RememberPath(dstPath) |
|||
|
|||
originalCopy := performServerSideWholeFileCopy |
|||
defer func() { |
|||
performServerSideWholeFileCopy = originalCopy |
|||
}() |
|||
|
|||
var called bool |
|||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) { |
|||
called = true |
|||
if gotWFS != wfs { |
|||
t.Fatalf("wfs = %p, want %p", gotWFS, wfs) |
|||
} |
|||
if copyRequest.srcPath != srcPath { |
|||
t.Fatalf("source path = %q, want %q", copyRequest.srcPath, srcPath) |
|||
} |
|||
if copyRequest.dstPath != dstPath { |
|||
t.Fatalf("destination path = %q, want %q", copyRequest.dstPath, dstPath) |
|||
} |
|||
return &filer_pb.Entry{ |
|||
Name: "dst.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
FileSize: 5, |
|||
Mime: "text/plain; charset=utf-8", |
|||
}, |
|||
Content: []byte("hello"), |
|||
}, serverSideWholeFileCopyCommitted, nil |
|||
} |
|||
|
|||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{ |
|||
FhIn: uint64(srcHandle.fh), |
|||
FhOut: uint64(dstHandle.fh), |
|||
OffIn: 0, |
|||
OffOut: 0, |
|||
Len: 8, |
|||
}) |
|||
if status != fuse.OK { |
|||
t.Fatalf("CopyFileRange status = %v, want OK", status) |
|||
} |
|||
if written != 5 { |
|||
t.Fatalf("CopyFileRange wrote %d bytes, want 5", written) |
|||
} |
|||
if !called { |
|||
t.Fatal("expected server-side whole-file copy path to be used") |
|||
} |
|||
|
|||
gotEntry := dstHandle.GetEntry().GetEntry() |
|||
if gotEntry.Attributes == nil || gotEntry.Attributes.FileSize != 5 { |
|||
t.Fatalf("destination size = %v, want 5", gotEntry.GetAttributes().GetFileSize()) |
|||
} |
|||
if string(gotEntry.Content) != "hello" { |
|||
t.Fatalf("destination content = %q, want %q", string(gotEntry.Content), "hello") |
|||
} |
|||
if dstHandle.dirtyMetadata { |
|||
t.Fatal("server-side whole-file copy should leave destination handle clean") |
|||
} |
|||
} |
|||
|
|||
func TestCopyFileRangeDoesNotFallbackAfterCommittedServerCopyRefreshFailure(t *testing.T) { |
|||
wfs := newCopyRangeTestWFSWithMetaCache(t) |
|||
|
|||
srcPath := util.FullPath("/src.txt") |
|||
dstPath := util.FullPath("/dst.txt") |
|||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true) |
|||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true) |
|||
|
|||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{ |
|||
Name: "src.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
FileSize: 5, |
|||
Mime: "text/plain; charset=utf-8", |
|||
Md5: []byte("abcde"), |
|||
Inode: srcInode, |
|||
}, |
|||
Content: []byte("hello"), |
|||
}) |
|||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{ |
|||
Name: "dst.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100600, |
|||
Inode: dstInode, |
|||
}, |
|||
}) |
|||
|
|||
srcHandle.RememberPath(srcPath) |
|||
dstHandle.RememberPath(dstPath) |
|||
|
|||
originalCopy := performServerSideWholeFileCopy |
|||
defer func() { |
|||
performServerSideWholeFileCopy = originalCopy |
|||
}() |
|||
|
|||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) { |
|||
if gotWFS != wfs || copyRequest.srcPath != srcPath || copyRequest.dstPath != dstPath { |
|||
t.Fatalf("unexpected server-side copy call: wfs=%p src=%q dst=%q", gotWFS, copyRequest.srcPath, copyRequest.dstPath) |
|||
} |
|||
return nil, serverSideWholeFileCopyCommitted, errors.New("reload copied entry: transient filer read failure") |
|||
} |
|||
|
|||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{ |
|||
FhIn: uint64(srcHandle.fh), |
|||
FhOut: uint64(dstHandle.fh), |
|||
OffIn: 0, |
|||
OffOut: 0, |
|||
Len: 8, |
|||
}) |
|||
if status != fuse.OK { |
|||
t.Fatalf("CopyFileRange status = %v, want OK", status) |
|||
} |
|||
if written != 5 { |
|||
t.Fatalf("CopyFileRange wrote %d bytes, want 5", written) |
|||
} |
|||
if dstHandle.dirtyMetadata { |
|||
t.Fatal("committed server-side copy should not fall back to dirty-page copy") |
|||
} |
|||
|
|||
gotEntry := dstHandle.GetEntry().GetEntry() |
|||
if gotEntry.GetAttributes().GetFileSize() != 5 { |
|||
t.Fatalf("destination size = %d, want 5", gotEntry.GetAttributes().GetFileSize()) |
|||
} |
|||
if gotEntry.GetAttributes().GetFileMode() != 0100600 { |
|||
t.Fatalf("destination mode = %#o, want %#o", gotEntry.GetAttributes().GetFileMode(), uint32(0100600)) |
|||
} |
|||
if string(gotEntry.GetContent()) != "hello" { |
|||
t.Fatalf("destination content = %q, want %q", string(gotEntry.GetContent()), "hello") |
|||
} |
|||
|
|||
cachedEntry, err := wfs.metaCache.FindEntry(context.Background(), dstPath) |
|||
if err != nil { |
|||
t.Fatalf("metaCache find entry: %v", err) |
|||
} |
|||
if cachedEntry.FileSize != 5 { |
|||
t.Fatalf("metaCache destination size = %d, want 5", cachedEntry.FileSize) |
|||
} |
|||
if cachedEntry.Mime != "text/plain; charset=utf-8" { |
|||
t.Fatalf("metaCache destination mime = %q, want %q", cachedEntry.Mime, "text/plain; charset=utf-8") |
|||
} |
|||
} |
|||
|
|||
func TestCopyFileRangeReturnsEIOForAmbiguousServerSideCopy(t *testing.T) { |
|||
wfs := newCopyRangeTestWFS() |
|||
|
|||
srcPath := util.FullPath("/src.txt") |
|||
dstPath := util.FullPath("/dst.txt") |
|||
srcInode := wfs.inodeToPath.Lookup(srcPath, 1, false, false, 0, true) |
|||
dstInode := wfs.inodeToPath.Lookup(dstPath, 1, false, false, 0, true) |
|||
|
|||
srcHandle := wfs.fhMap.AcquireFileHandle(wfs, srcInode, &filer_pb.Entry{ |
|||
Name: "src.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100644, |
|||
FileSize: 5, |
|||
Inode: srcInode, |
|||
}, |
|||
Content: []byte("hello"), |
|||
}) |
|||
dstHandle := wfs.fhMap.AcquireFileHandle(wfs, dstInode, &filer_pb.Entry{ |
|||
Name: "dst.txt", |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: 0100600, |
|||
Inode: dstInode, |
|||
}, |
|||
}) |
|||
|
|||
srcHandle.RememberPath(srcPath) |
|||
dstHandle.RememberPath(dstPath) |
|||
|
|||
originalCopy := performServerSideWholeFileCopy |
|||
defer func() { |
|||
performServerSideWholeFileCopy = originalCopy |
|||
}() |
|||
|
|||
performServerSideWholeFileCopy = func(cancel <-chan struct{}, gotWFS *WFS, copyRequest wholeFileServerCopyRequest) (*filer_pb.Entry, serverSideWholeFileCopyOutcome, error) { |
|||
if gotWFS != wfs || copyRequest.srcPath != srcPath || copyRequest.dstPath != dstPath { |
|||
t.Fatalf("unexpected server-side copy call: wfs=%p src=%q dst=%q", gotWFS, copyRequest.srcPath, copyRequest.dstPath) |
|||
} |
|||
return nil, serverSideWholeFileCopyAmbiguous, errors.New("transport timeout after request dispatch") |
|||
} |
|||
|
|||
written, status := wfs.CopyFileRange(make(chan struct{}), &fuse.CopyFileRangeIn{ |
|||
FhIn: uint64(srcHandle.fh), |
|||
FhOut: uint64(dstHandle.fh), |
|||
OffIn: 0, |
|||
OffOut: 0, |
|||
Len: 8, |
|||
}) |
|||
if status != fuse.EIO { |
|||
t.Fatalf("CopyFileRange status = %v, want EIO", status) |
|||
} |
|||
if written != 0 { |
|||
t.Fatalf("CopyFileRange wrote %d bytes, want 0", written) |
|||
} |
|||
if dstHandle.dirtyMetadata { |
|||
t.Fatal("ambiguous server-side copy should not fall back to dirty-page copy") |
|||
} |
|||
if dstHandle.GetEntry().GetEntry().GetAttributes().GetFileSize() != 0 { |
|||
t.Fatalf("destination size = %d, want 0", dstHandle.GetEntry().GetEntry().GetAttributes().GetFileSize()) |
|||
} |
|||
} |
|||
|
|||
func newCopyRangeTestWFS() *WFS { |
|||
wfs := &WFS{ |
|||
option: &Option{ |
|||
ChunkSizeLimit: 1024, |
|||
ConcurrentReaders: 1, |
|||
VolumeServerAccess: "filerProxy", |
|||
FilerAddresses: []pb.ServerAddress{"127.0.0.1:8888"}, |
|||
}, |
|||
inodeToPath: NewInodeToPath(util.FullPath("/"), 0), |
|||
fhMap: NewFileHandleToInode(), |
|||
fhLockTable: util.NewLockTable[FileHandleId](), |
|||
} |
|||
wfs.copyBufferPool.New = func() any { |
|||
return make([]byte, 1024) |
|||
} |
|||
return wfs |
|||
} |
|||
|
|||
func newCopyRangeTestWFSWithMetaCache(t *testing.T) *WFS { |
|||
t.Helper() |
|||
|
|||
wfs := newCopyRangeTestWFS() |
|||
root := util.FullPath("/") |
|||
wfs.inodeToPath.MarkChildrenCached(root) |
|||
uidGidMapper, err := meta_cache.NewUidGidMapper("", "") |
|||
if err != nil { |
|||
t.Fatalf("create uid/gid mapper: %v", err) |
|||
} |
|||
wfs.metaCache = meta_cache.NewMetaCache( |
|||
filepath.Join(t.TempDir(), "meta"), |
|||
uidGidMapper, |
|||
root, |
|||
func(path util.FullPath) { |
|||
wfs.inodeToPath.MarkChildrenCached(path) |
|||
}, |
|||
func(path util.FullPath) bool { |
|||
return wfs.inodeToPath.IsChildrenCached(path) |
|||
}, |
|||
func(util.FullPath, *filer_pb.Entry) {}, |
|||
nil, |
|||
) |
|||
t.Cleanup(func() { |
|||
wfs.metaCache.Shutdown() |
|||
}) |
|||
|
|||
return wfs |
|||
} |
|||
@ -0,0 +1,223 @@ |
|||
package weed_server |
|||
|
|||
import ( |
|||
"context" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/filer" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/util" |
|||
) |
|||
|
|||
func TestCopyEntryRefreshesDestinationTimestamps(t *testing.T) { |
|||
fs := &FilerServer{} |
|||
|
|||
oldTime := time.Unix(123, 0) |
|||
srcEntry := &filer.Entry{ |
|||
FullPath: util.FullPath("/src.txt"), |
|||
Attr: filer.Attr{ |
|||
Mtime: oldTime, |
|||
Crtime: oldTime, |
|||
}, |
|||
Content: []byte("hello"), |
|||
} |
|||
|
|||
before := time.Now().Add(-time.Second) |
|||
copied, err := fs.copyEntry(context.Background(), srcEntry, util.FullPath("/dst.txt"), nil) |
|||
after := time.Now().Add(time.Second) |
|||
if err != nil { |
|||
t.Fatalf("copyEntry: %v", err) |
|||
} |
|||
|
|||
if copied.Crtime.Before(before) || copied.Crtime.After(after) { |
|||
t.Fatalf("copied Crtime = %v, want between %v and %v", copied.Crtime, before, after) |
|||
} |
|||
if copied.Mtime.Before(before) || copied.Mtime.After(after) { |
|||
t.Fatalf("copied Mtime = %v, want between %v and %v", copied.Mtime, before, after) |
|||
} |
|||
if copied.Crtime.Equal(oldTime) || copied.Mtime.Equal(oldTime) { |
|||
t.Fatalf("destination timestamps should differ from source timestamps: src=%v copied=(%v,%v)", oldTime, copied.Crtime, copied.Mtime) |
|||
} |
|||
} |
|||
|
|||
func TestPreserveDestinationMetadataForDataCopy(t *testing.T) { |
|||
dstTime := time.Unix(100, 0) |
|||
srcTime := time.Unix(200, 0) |
|||
|
|||
dstEntry := &filer.Entry{ |
|||
FullPath: util.FullPath("/dst.txt"), |
|||
Attr: filer.Attr{ |
|||
Mtime: dstTime, |
|||
Crtime: dstTime, |
|||
Mode: 0100600, |
|||
Uid: 101, |
|||
Gid: 202, |
|||
TtlSec: 17, |
|||
UserName: "dst-user", |
|||
GroupNames: []string{"dst-group"}, |
|||
SymlinkTarget: "", |
|||
Rdev: 9, |
|||
Inode: 1234, |
|||
}, |
|||
Extended: map[string][]byte{ |
|||
"user.color": []byte("blue"), |
|||
}, |
|||
Remote: &filer_pb.RemoteEntry{ |
|||
StorageName: "remote-store", |
|||
RemoteETag: "remote-etag", |
|||
RemoteSize: 7, |
|||
}, |
|||
Quota: 77, |
|||
WORMEnforcedAtTsNs: 88, |
|||
HardLinkId: filer.HardLinkId([]byte("hard-link")), |
|||
HardLinkCounter: 3, |
|||
} |
|||
copiedEntry := &filer.Entry{ |
|||
FullPath: util.FullPath("/dst.txt"), |
|||
Attr: filer.Attr{ |
|||
Mtime: srcTime, |
|||
Crtime: srcTime, |
|||
Mode: 0100644, |
|||
Uid: 11, |
|||
Gid: 22, |
|||
Mime: "text/plain", |
|||
Md5: []byte("source-md5"), |
|||
FileSize: 5, |
|||
UserName: "src-user", |
|||
GroupNames: []string{"src-group"}, |
|||
}, |
|||
Content: []byte("hello"), |
|||
Extended: map[string][]byte{ |
|||
"user.color": []byte("red"), |
|||
}, |
|||
Quota: 5, |
|||
} |
|||
|
|||
before := time.Now().Add(-time.Second) |
|||
preserveDestinationMetadataForDataCopy(dstEntry, copiedEntry) |
|||
after := time.Now().Add(time.Second) |
|||
|
|||
if copiedEntry.Mode != dstEntry.Mode || copiedEntry.Uid != dstEntry.Uid || copiedEntry.Gid != dstEntry.Gid { |
|||
t.Fatalf("destination ownership/mode not preserved: got mode=%#o uid=%d gid=%d", copiedEntry.Mode, copiedEntry.Uid, copiedEntry.Gid) |
|||
} |
|||
if copiedEntry.Crtime != dstEntry.Crtime || copiedEntry.Inode != dstEntry.Inode { |
|||
t.Fatalf("destination identity not preserved: got crtime=%v inode=%d", copiedEntry.Crtime, copiedEntry.Inode) |
|||
} |
|||
if copiedEntry.Mtime.Before(before) || copiedEntry.Mtime.After(after) { |
|||
t.Fatalf("destination mtime = %v, want between %v and %v", copiedEntry.Mtime, before, after) |
|||
} |
|||
if copiedEntry.FileSize != 5 || copiedEntry.Mime != "text/plain" || string(copiedEntry.Md5) != "source-md5" { |
|||
t.Fatalf("copied data attributes changed unexpectedly: size=%d mime=%q md5=%q", copiedEntry.FileSize, copiedEntry.Mime, string(copiedEntry.Md5)) |
|||
} |
|||
if string(copiedEntry.Extended["user.color"]) != "blue" { |
|||
t.Fatalf("destination xattrs not preserved: got %q", string(copiedEntry.Extended["user.color"])) |
|||
} |
|||
if copiedEntry.Remote == nil || copiedEntry.Remote.StorageName != "remote-store" { |
|||
t.Fatalf("destination remote metadata not preserved: %+v", copiedEntry.Remote) |
|||
} |
|||
if copiedEntry.Quota != 77 || copiedEntry.WORMEnforcedAtTsNs != 88 { |
|||
t.Fatalf("destination quota/WORM not preserved: quota=%d worm=%d", copiedEntry.Quota, copiedEntry.WORMEnforcedAtTsNs) |
|||
} |
|||
if string(copiedEntry.HardLinkId) != "hard-link" || copiedEntry.HardLinkCounter != 3 { |
|||
t.Fatalf("destination hard-link metadata not preserved: id=%q count=%d", string(copiedEntry.HardLinkId), copiedEntry.HardLinkCounter) |
|||
} |
|||
|
|||
dstEntry.GroupNames[0] = "mutated" |
|||
dstEntry.Extended["user.color"][0] = 'g' |
|||
dstEntry.Remote.StorageName = "mutated-remote" |
|||
if copiedEntry.GroupNames[0] != "dst-group" { |
|||
t.Fatalf("group names should be cloned, got %q", copiedEntry.GroupNames[0]) |
|||
} |
|||
if string(copiedEntry.Extended["user.color"]) != "blue" { |
|||
t.Fatalf("extended metadata should be cloned, got %q", string(copiedEntry.Extended["user.color"])) |
|||
} |
|||
if copiedEntry.Remote.StorageName != "remote-store" { |
|||
t.Fatalf("remote metadata should be cloned, got %q", copiedEntry.Remote.StorageName) |
|||
} |
|||
} |
|||
|
|||
func TestValidateCopySourcePreconditions(t *testing.T) { |
|||
srcInode := uint64(101) |
|||
srcMtime := int64(200) |
|||
srcSize := int64(5) |
|||
preconditions := copyRequestPreconditions{ |
|||
srcInode: &srcInode, |
|||
srcMtime: &srcMtime, |
|||
srcSize: &srcSize, |
|||
} |
|||
|
|||
srcEntry := &filer.Entry{ |
|||
FullPath: util.FullPath("/src.txt"), |
|||
Attr: filer.Attr{ |
|||
Inode: srcInode, |
|||
Mtime: time.Unix(srcMtime, 0), |
|||
FileSize: uint64(srcSize), |
|||
}, |
|||
Content: []byte("hello"), |
|||
} |
|||
|
|||
if err := validateCopySourcePreconditions(preconditions, srcEntry); err != nil { |
|||
t.Fatalf("validate source preconditions: %v", err) |
|||
} |
|||
|
|||
changedSize := int64(6) |
|||
preconditions.srcSize = &changedSize |
|||
if err := validateCopySourcePreconditions(preconditions, srcEntry); err == nil { |
|||
t.Fatal("expected source size mismatch to fail") |
|||
} |
|||
} |
|||
|
|||
func TestValidateCopyDestinationPreconditions(t *testing.T) { |
|||
dstInode := uint64(202) |
|||
dstMtime := int64(300) |
|||
dstSize := int64(0) |
|||
preconditions := copyRequestPreconditions{ |
|||
dstInode: &dstInode, |
|||
dstMtime: &dstMtime, |
|||
dstSize: &dstSize, |
|||
} |
|||
|
|||
dstEntry := &filer.Entry{ |
|||
FullPath: util.FullPath("/dst.txt"), |
|||
Attr: filer.Attr{ |
|||
Inode: dstInode, |
|||
Mtime: time.Unix(dstMtime, 0), |
|||
FileSize: uint64(dstSize), |
|||
}, |
|||
} |
|||
|
|||
if err := validateCopyDestinationPreconditions(preconditions, dstEntry); err != nil { |
|||
t.Fatalf("validate destination preconditions: %v", err) |
|||
} |
|||
|
|||
if err := validateCopyDestinationPreconditions(preconditions, nil); err == nil { |
|||
t.Fatal("expected missing destination to fail") |
|||
} |
|||
} |
|||
|
|||
func TestRecentCopyRequestDeduplicatesByRequestID(t *testing.T) { |
|||
fs := &FilerServer{ |
|||
recentCopyRequests: make(map[string]recentCopyRequest), |
|||
} |
|||
|
|||
requestID := "copy-req" |
|||
fingerprint := "src|dst|1" |
|||
fs.rememberRecentCopyRequest(requestID, fingerprint) |
|||
|
|||
handled, err := fs.handleRecentCopyRequest(requestID, fingerprint) |
|||
if err != nil { |
|||
t.Fatalf("handle recent copy request: %v", err) |
|||
} |
|||
if !handled { |
|||
t.Fatal("expected recent copy request to be recognized") |
|||
} |
|||
|
|||
handled, err = fs.handleRecentCopyRequest(requestID, "different") |
|||
if err == nil { |
|||
t.Fatal("expected reused request id with different fingerprint to fail") |
|||
} |
|||
if handled { |
|||
t.Fatal("reused request id with different fingerprint should not be treated as handled") |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue