Skip to content

operations: make --immutable work with copyto, moveto and single file copy - fixes #4921 - #9944

Merged
ncw merged 2 commits into
rclone:masterfrom
hsdfat:fix-immutable-single-file-4921
Sep 21, 2026
Merged

ncw merged 2 commits into
rclone:masterfrom
hsdfat:fix-immutable-single-file-4921

Conversation

@hsdfat

@hsdfat hsdfat commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What does this change do?

--immutable is enforced by sync, but the single file paths through operations.moveOrCopyFile ignored it, so
these overwrote a destination which differed from the source:

  • rclone copyto and rclone moveto
  • rclone copy, rclone move and rclone sync when the source is a single file
  • the operations/copyfile and operations/movefile rc calls

This adds the same check sync makes: if a transfer is needed and the destination exists, fail with
ErrorImmutableModified as a no-retry error and leave both files alone. It sits before the --backup-dir step, so
nothing is moved into the backup directory first. An unchanged destination is still fine, and a missing one is
still copied.

It also rejects --no-check-dest with --immutable, as sync already does, since without looking at the
destination it cannot be protected.

The other callers of CopyFile/MoveFile get the same check, which only fires when --immutable is set and they
would overwrite a file with different contents:

  • bisync conflict renames and gitannex, where refusing seems right.
  • The deprecated cache backend's background upload of temporary files, which uses the global config. A mount of a
    cache remote run with --immutable would now refuse to upload over a changed file instead of overwriting it. If
    you'd rather that internal upload ignored --immutable, I can clear it there.

The second commit fixes a related hole, which sync has too: with --copy-dest, when the source matched a file in
the copy-dest directory, copyDest server-side copied it over an existing destination even if that differed, before
the immutable check was reached. It now leaves such a destination for the caller, which rejects it as for any other
modified file. It is separate so it can be dropped if you'd rather handle it differently.

Testing

  • TestCopyFileImmutable: copying an unchanged file twice succeeds; copying a modified file fails with
    ErrorImmutableModified and leaves both sides untouched; --no-check-dest is rejected.
  • TestMoveFileImmutable: moving a modified file fails and the source is not deleted.
  • TestCopyFileCopyDestImmutable and TestSyncCopyDestImmutable: a match in --copy-dest does not replace a
    different destination. Like the existing copy-dest tests these need server-side copy, so they skip on local; I ran
    them with -remote :memory:.

All four fail on master (the destination is overwritten). go build ./..., make quicktest,
go test ./fs/operations/ ./fs/sync/ (local and :memory:) and golangci-lint run ./fs/operations/ ./fs/sync/
pass.

Linked issue

Fixes #4921

Checklist

  • This change is trivial OR it has been discussed and agreed in the linked issue.
  • I have read the contribution guidelines.
  • (If I used AI tools to help write this code) I have read and understood the AI-assisted contributions guidance, and I have tested and take ownership of this change myself.
  • I have added tests for all changes in this PR if appropriate.
  • I have added documentation for the changes if appropriate. (No doc change needed - the --immutable docs already say it applies to commands which transfer files.)
  • All commit messages are in house style.
  • (Backend changes only) test_all passes for this backend - not a backend change.
  • This Pull Request is ready for review.

@hsdfat
hsdfat requested review from ncw and nielash as code owners September 18, 2026 21:03

@ncw ncw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this - it looks great!

There are a couple of misplaced comments - if you could fix these and squash them into the existing commits, leaving two commits on the branch that would be perfect.

I agree the cache backend doesn't need special handling. It only matters if someone deliberately runs with --immutable

Optionally, you could also call logger(ctx, TransferError, srcObj, dstObj, err) on the immutable error path in moveOrCopyFile, like the other error returns there, so it shows up in --combined/--error reports.

Thanks again

Comment thread fs/sync/sync_test.go Outdated
@@ -2257,6 +2257,35 @@ func TestSyncMultipleCompareDest(t *testing.T) {
}

// Test with CopyDest set

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment belongs on the test below

Comment thread fs/operations/copy_test.go Outdated
@@ -268,6 +302,34 @@ func TestCopyFileCompareDest(t *testing.T) {
}

// Test with CopyDest set

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment belongs on the test below

@hsdfat
hsdfat force-pushed the fix-immutable-single-file-4921 branch from 0333138 to 50cd74f Compare September 19, 2026 14:57
… copy - fixes rclone#4921

--immutable was only enforced by sync, so copyto, moveto, copy and move
of a single file, and the operations/copyfile and operations/movefile rc
calls overwrote a destination which differed from the source.

Fail these with the same ErrorImmutableModified as sync, and reject
--no-check-dest with --immutable as sync does, since without checking
the destination it cannot be protected.
When the source matched a file in --copy-dest, copyDest server-side
copied it over an existing destination which differed, so sync, copy
and copyto with --copy-dest could modify a file --immutable should have
protected.

Leave such a destination for the caller, which rejects it with
ErrorImmutableModified as for any other modified file.
@hsdfat
hsdfat force-pushed the fix-immutable-single-file-4921 branch from 7393f38 to 4bc9cf7 Compare September 21, 2026 06:49

@ncw ncw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :-)

@ncw
ncw merged commit dc98c21 into rclone:master Sep 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Immutable only works with syncCopyMove

2 participants