diff --git a/internal/fs/inode/base_dir.go b/internal/fs/inode/base_dir.go index 00dcf22977..9bd89d52c3 100644 --- a/internal/fs/inode/base_dir.go +++ b/internal/fs/inode/base_dir.go @@ -256,6 +256,11 @@ func (d *baseDirInode) ShouldInvalidateKernelListCache(ttl time.Duration) bool { // List operation is not supported for baseDirInode. func (d *baseDirInode) InvalidateKernelListCache() {} +func (d *baseDirInode) RenameFile(ctx context.Context, fileToRename *gcs.MinObject, destinationFileName string) (*gcs.Object, error) { + err := fuse.ENOSYS + return nil, err +} + func (d *baseDirInode) RenameFolder(ctx context.Context, folderName string, destinationFolderId string) (op *gcs.Folder, err error) { err = fuse.ENOSYS return diff --git a/internal/fs/inode/dir.go b/internal/fs/inode/dir.go index cdffed15a4..6d4641ee59 100644 --- a/internal/fs/inode/dir.go +++ b/internal/fs/inode/dir.go @@ -65,6 +65,9 @@ type DirInode interface { // true. LookUpChild(ctx context.Context, name string) (*Core, error) + // Rename the file. + RenameFile(ctx context.Context, fileToRename *gcs.MinObject, destinationFileName string) (*gcs.Object, error) + // Rename the directiory/folder. RenameFolder(ctx context.Context, folderName string, destinationFolderId string) (*gcs.Folder, error) @@ -1038,6 +1041,24 @@ func (d *dirInode) ShouldInvalidateKernelListCache(ttl time.Duration) bool { return cachedDuration >= ttl } +// LOCKS_REQUIRED(d) +// LOCKS_REQUIRED(parent of destinationFileName) +func (d *dirInode) RenameFile(ctx context.Context, fileToRename *gcs.MinObject, destinationFileName string) (*gcs.Object, error) { + req := &gcs.MoveObjectRequest{ + SrcName: fileToRename.Name, + DstName: destinationFileName, + SrcGeneration: fileToRename.Generation, + SrcMetaGenerationPrecondition: &fileToRename.MetaGeneration, + } + + o, err := d.bucket.MoveObject(ctx, req) + + // Invalidate the cache entry for the old object name. + d.cache.Erase(fileToRename.Name) + + return o, err +} + func (d *dirInode) RenameFolder(ctx context.Context, folderName string, destinationFolderName string) (*gcs.Folder, error) { folder, err := d.bucket.RenameFolder(ctx, folderName, destinationFolderName) if err != nil { diff --git a/internal/fs/inode/hns_dir_test.go b/internal/fs/inode/hns_dir_test.go index ec46c3202d..c613b8df5e 100644 --- a/internal/fs/inode/hns_dir_test.go +++ b/internal/fs/inode/hns_dir_test.go @@ -320,6 +320,59 @@ func (t *HNSDirTest) TestRenameFolderWithNonExistentSourceFolder() { assert.Nil(t.T(), f) } +func (t *HNSDirTest) TestRenameFileWithGivenName() { + const ( + fileName = "qux" + renameFileName = "rename" + ) + oldObjName := path.Join(dirInodeName, fileName) + newObjName := path.Join(dirInodeName, renameFileName) + var metaGeneration int64 = 0 + moveObjectReq := gcs.MoveObjectRequest{ + SrcName: oldObjName, + DstName: newObjName, + SrcGeneration: 0, + SrcMetaGenerationPrecondition: &metaGeneration, + } + oldObj := gcs.MinObject{Name: oldObjName} + newObj := gcs.Object{Name: newObjName} + t.mockBucket.On("MoveObject", t.ctx, &moveObjectReq).Return(&newObj, nil) + + // Attempt to rename the file. + f, err := t.in.RenameFile(t.ctx, &oldObj, path.Join(dirInodeName, renameFileName)) + + t.mockBucket.AssertExpectations(t.T()) + // Verify the renamed file exists. + assert.NoError(t.T(), err) + assert.Equal(t.T(), newObjName, f.Name) +} + +func (t *HNSDirTest) TestRenameFileWithNonExistentSourceFile() { + const ( + fileName = "qux" + renameFileName = "rename" + ) + oldObjName := path.Join(dirInodeName, fileName) + newObjName := path.Join(dirInodeName, renameFileName) + var metaGeneration int64 = 0 + moveObjectReq := gcs.MoveObjectRequest{ + SrcName: oldObjName, + DstName: newObjName, + SrcGeneration: 0, + SrcMetaGenerationPrecondition: &metaGeneration, + } + oldObj := gcs.MinObject{Name: oldObjName} + var notFoundErr *gcs.NotFoundError + t.mockBucket.On("MoveObject", t.ctx, &moveObjectReq).Return(nil, &gcs.NotFoundError{}) + + // Attempt to rename the file. + f, err := t.in.RenameFile(t.ctx, &oldObj, newObjName) + + t.mockBucket.AssertExpectations(t.T()) + assert.True(t.T(), errors.As(err, ¬FoundErr)) + assert.Nil(t.T(), f) +} + func (t *HNSDirTest) TestDeleteChildDir_WhenImplicitDirFlagTrueOnNonHNSBucket() { const folderName = "folder" dirName := path.Join(dirInodeName, folderName) + "/" @@ -350,6 +403,7 @@ func (t *HNSDirTest) TestDeleteChildDir_WhenImplicitDirFlagFalseAndNonHNSBucket_ assert.Equal(t.T(), metadata.Type(0), t.typeCache.Get(t.fixedTime.Now(), dirName)) assert.False(t.T(), dirIn.IsUnlinked()) } + func (t *HNSDirTest) TestDeleteChildDir_WithImplicitDirFlagFalseAndNonHNSBucket_DeleteObjectThrowAnError() { const name = "folder" dirName := path.Join(dirInodeName, name) + "/" diff --git a/internal/storage/mock/testify_mock_bucket.go b/internal/storage/mock/testify_mock_bucket.go index 417a08c8f1..46565f7092 100644 --- a/internal/storage/mock/testify_mock_bucket.go +++ b/internal/storage/mock/testify_mock_bucket.go @@ -98,7 +98,10 @@ func (m *TestifyMockBucket) DeleteObject(ctx context.Context, req *gcs.DeleteObj func (m *TestifyMockBucket) MoveObject(ctx context.Context, req *gcs.MoveObjectRequest) (*gcs.Object, error) { args := m.Called(ctx, req) - return args.Get(0).(*gcs.Object), args.Error(1) + if args.Get(0) != nil { + return args.Get(0).(*gcs.Object), nil + } + return nil, args.Error(1) } func (m *TestifyMockBucket) DeleteFolder(ctx context.Context, folderName string) error {