mirror of
https://github.com/rclone/rclone
synced 2024-11-29 07:55:12 +01:00
Implement --immutable option
This commit is contained in:
parent
5a3a56abd8
commit
2d8e75cab4
@ -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.
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
1
fs/fs.go
1
fs/fs.go
@ -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
|
||||||
|
@ -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")
|
||||||
|
28
fs/sync.go
28
fs/sync.go
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user