1
mirror of https://github.com/rclone/rclone synced 2024-11-29 07:55:12 +01:00

Implement --immutable option

This commit is contained in:
Jacob McNamee 2017-09-02 01:29:01 -07:00 committed by Nick Craig-Wood
parent 5a3a56abd8
commit 2d8e75cab4
6 changed files with 81 additions and 13 deletions

View File

@ -433,6 +433,26 @@ Normally rclone would skip any files that have the same
modification time and are the same size (or have the same checksum if modification time and are the same size (or have the same checksum if
using `--checksum`). using `--checksum`).
### --immutable ###
Treat source and destination files as immutable and disallow
modification.
With this option set, files will be created and deleted as requested,
but existing files will never be updated. If an existing file does
not match between the source and destination, rclone will give the error
`Source and destination exist but do not match: immutable file modified`.
Note that only commands which transfer files (e.g. `sync`, `copy`,
`move`) are affected by this behavior, and only modification is
disallowed. Files may still be deleted explicitly (e.g. `delete`,
`purge`) or implicitly (e.g. `sync`, `move`). Use `copy --immutable`
if it is desired to avoid deletion as well as modification.
This can be useful as an additional layer of protection for immutable
or append-only data sets (notably backup archives), where modification
implies corruption and should not be propagated.
### --log-file=FILE ### ### --log-file=FILE ###
Log all of rclone's output to FILE. This is not active by default. Log all of rclone's output to FILE. This is not active by default.

View File

@ -102,6 +102,7 @@ var (
bindAddr = StringP("bind", "", "", "Local address to bind to for outgoing connections, IPv4, IPv6 or name.") bindAddr = StringP("bind", "", "", "Local address to bind to for outgoing connections, IPv4, IPv6 or name.")
disableFeatures = StringP("disable", "", "", "Disable a comma separated list of features. Use help to see a list.") disableFeatures = StringP("disable", "", "", "Disable a comma separated list of features. Use help to see a list.")
userAgent = StringP("user-agent", "", "rclone/"+Version, "Set the user-agent to a specified string. The default is rclone/ version") userAgent = StringP("user-agent", "", "rclone/"+Version, "Set the user-agent to a specified string. The default is rclone/ version")
immutable = BoolP("immutable", "", false, "Do not modify files. Fail if existing files have been modified.")
streamingUploadCutoff = SizeSuffix(100 * 1024) streamingUploadCutoff = SizeSuffix(100 * 1024)
logLevel = LogLevelNotice logLevel = LogLevelNotice
statsLogLevel = LogLevelInfo statsLogLevel = LogLevelInfo
@ -240,6 +241,7 @@ type ConfigInfo struct {
TPSLimitBurst int TPSLimitBurst int
BindAddr net.IP BindAddr net.IP
DisableFeatures []string DisableFeatures []string
Immutable bool
StreamingUploadCutoff SizeSuffix StreamingUploadCutoff SizeSuffix
} }
@ -379,6 +381,7 @@ func LoadConfig() {
Config.UseListR = *useListR Config.UseListR = *useListR
Config.TPSLimit = *tpsLimit Config.TPSLimit = *tpsLimit
Config.TPSLimitBurst = *tpsLimitBurst Config.TPSLimitBurst = *tpsLimitBurst
Config.Immutable = *immutable
Config.BufferSize = bufferSize Config.BufferSize = bufferSize
Config.StreamingUploadCutoff = streamingUploadCutoff Config.StreamingUploadCutoff = streamingUploadCutoff

View File

@ -50,6 +50,7 @@ var (
ErrorNotDeletingDirs = errors.New("not deleting directories as there were IO errors") ErrorNotDeletingDirs = errors.New("not deleting directories as there were IO errors")
ErrorCantMoveOverlapping = errors.New("can't move files on overlapping remotes") ErrorCantMoveOverlapping = errors.New("can't move files on overlapping remotes")
ErrorDirectoryNotEmpty = errors.New("directory not empty") ErrorDirectoryNotEmpty = errors.New("directory not empty")
ErrorImmutableModified = errors.New("immutable file modified")
) )
// RegInfo provides information about a filesystem // RegInfo provides information about a filesystem

View File

@ -180,8 +180,13 @@ func equal(src ObjectInfo, dst Object, sizeOnly, checkSum bool) bool {
if Config.DryRun { if Config.DryRun {
Logf(src, "Not updating modification time as --dry-run") Logf(src, "Not updating modification time as --dry-run")
} else { } else {
// Size and hash the same but mtime different so update the // Size and hash the same but mtime different
// mtime of the dst object here // Error if objects are treated as immutable
if Config.Immutable {
Errorf(dst, "Timestamp mismatch between immutable objects")
return false
}
// Update the mtime of the dst object here
err := dst.SetModTime(srcModTime) err := dst.SetModTime(srcModTime)
if err == ErrorCantSetModTime { if err == ErrorCantSetModTime {
Debugf(dst, "src and dst identical but can't set mod time without re-uploading") Debugf(dst, "src and dst identical but can't set mod time without re-uploading")

View File

@ -264,20 +264,26 @@ func (s *syncCopyMove) pairChecker(in ObjectPairChan, out ObjectPairChan, wg *sy
// Check to see if can store this // Check to see if can store this
if src.Storable() { if src.Storable() {
if NeedTransfer(pair.dst, pair.src) { if NeedTransfer(pair.dst, pair.src) {
// If destination already exists, then we must move it into --backup-dir if required // If files are treated as immutable, fail if destination exists and does not match
if pair.dst != nil && s.backupDir != nil { if Config.Immutable && pair.dst != nil {
remoteWithSuffix := pair.dst.Remote() + s.suffix Errorf(pair.dst, "Source and destination exist but do not match: immutable file modified")
overwritten, _ := s.backupDir.NewObject(remoteWithSuffix) s.processError(ErrorImmutableModified)
err := Move(s.backupDir, overwritten, remoteWithSuffix, pair.dst) } else {
if err != nil { // If destination already exists, then we must move it into --backup-dir if required
s.processError(err) if pair.dst != nil && s.backupDir != nil {
remoteWithSuffix := pair.dst.Remote() + s.suffix
overwritten, _ := s.backupDir.NewObject(remoteWithSuffix)
err := Move(s.backupDir, overwritten, remoteWithSuffix, pair.dst)
if err != nil {
s.processError(err)
} else {
// If successful zero out the dst as it is no longer there and copy the file
pair.dst = nil
out <- pair
}
} else { } else {
// If successful zero out the dst as it is no longer there and copy the file
pair.dst = nil
out <- pair out <- pair
} }
} else {
out <- pair
} }
} else { } else {
// If moving need to delete the files we don't need to copy // If moving need to delete the files we don't need to copy

View File

@ -996,3 +996,36 @@ func TestSyncUTFNorm(t *testing.T) {
file1.Path = file2.Path file1.Path = file2.Path
fstest.CheckItems(t, r.fremote, file1) fstest.CheckItems(t, r.fremote, file1)
} }
// Test --immutable
func TestSyncImmutable(t *testing.T) {
r := NewRun(t)
defer r.Finalise()
fs.Config.Immutable = true
defer func() { fs.Config.Immutable = false }()
// Create file on source
file1 := r.WriteFile("existing", "potato", t1)
fstest.CheckItems(t, r.flocal, file1)
fstest.CheckItems(t, r.fremote)
// Should succeed
fs.Stats.ResetCounters()
err := fs.Sync(r.fremote, r.flocal)
require.NoError(t, err)
fstest.CheckItems(t, r.flocal, file1)
fstest.CheckItems(t, r.fremote, file1)
// Modify file data and timestamp on source
file2 := r.WriteFile("existing", "tomato", t2)
fstest.CheckItems(t, r.flocal, file2)
fstest.CheckItems(t, r.fremote, file1)
// Should fail with ErrorImmutableModified and not modify local or remote files
fs.Stats.ResetCounters()
err = fs.Sync(r.fremote, r.flocal)
assert.EqualError(t, err, fs.ErrorImmutableModified.Error())
fstest.CheckItems(t, r.flocal, file2)
fstest.CheckItems(t, r.fremote, file1)
}