diff --git a/duplicacy/duplicacy_main.go b/duplicacy/duplicacy_main.go index c54cf8c..719ea48 100644 --- a/duplicacy/duplicacy_main.go +++ b/duplicacy/duplicacy_main.go @@ -513,6 +513,8 @@ func setPreference(context *cli.Context) { if triBool.IsSet() { newPreference.DoNotSavePassword = triBool.IsTrue() } + + newPreference.NobackupFile = context.String("nobackup-file") key := context.String("key") value := context.String("value") @@ -694,7 +696,7 @@ func backupRepository(context *cli.Context) { dryRun := context.Bool("dry-run") uploadRateLimit := context.Int("limit-rate") storage.SetRateLimits(0, uploadRateLimit) - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) @@ -783,7 +785,7 @@ func restoreRepository(context *cli.Context) { duplicacy.LOG_DEBUG("REGEX_DEBUG", "There are %d compiled regular expressions stored", len(duplicacy.RegexMap)) storage.SetRateLimits(context.Int("limit-rate"), 0) - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) @@ -823,7 +825,7 @@ func listSnapshots(context *cli.Context) { tag := context.String("t") revisions := getRevisions(context) - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) id := preference.SnapshotID @@ -871,7 +873,7 @@ func checkSnapshots(context *cli.Context) { tag := context.String("t") revisions := getRevisions(context) - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) id := preference.SnapshotID @@ -926,7 +928,7 @@ func printFile(context *cli.Context) { snapshotID = context.String("id") } - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) @@ -982,11 +984,11 @@ func diff(context *cli.Context) { } compareByHash := context.Bool("hash") - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) - backupManager.SnapshotManager.Diff(repository, snapshotID, revisions, path, compareByHash) + backupManager.SnapshotManager.Diff(repository, snapshotID, revisions, path, compareByHash, preference.NobackupFile) runScript(context, preference.Name, "post") } @@ -1025,7 +1027,7 @@ func showHistory(context *cli.Context) { revisions := getRevisions(context) showLocalHash := context.Bool("hash") - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) @@ -1083,7 +1085,7 @@ func pruneSnapshots(context *cli.Context) { os.Exit(ArgumentExitCode) } - backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password) + backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) backupManager.SetupSnapshotCache(preference.Name) @@ -1123,7 +1125,7 @@ func copySnapshots(context *cli.Context) { sourcePassword = duplicacy.GetPassword(*source, "password", "Enter source storage password:", false, false) } - sourceManager := duplicacy.CreateBackupManager(source.SnapshotID, sourceStorage, repository, sourcePassword) + sourceManager := duplicacy.CreateBackupManager(source.SnapshotID, sourceStorage, repository, sourcePassword, source.NobackupFile) sourceManager.SetupSnapshotCache(source.Name) duplicacy.SavePassword(*source, "password", sourcePassword) @@ -1156,7 +1158,7 @@ func copySnapshots(context *cli.Context) { destinationStorage.SetRateLimits(0, context.Int("upload-limit-rate")) destinationManager := duplicacy.CreateBackupManager(destination.SnapshotID, destinationStorage, repository, - destinationPassword) + destinationPassword, destination.NobackupFile) duplicacy.SavePassword(*destination, "password", destinationPassword) destinationManager.SetupSnapshotCache(destination.Name) @@ -1722,6 +1724,12 @@ func main() { Value: &TriBool{}, Arg: "true", }, + cli.StringFlag{ + Name: "nobackup-file", + Usage: "Directories containing a file with this name will not be backed up", + Argument: "", + Value: "", + }, cli.StringFlag{ Name: "key", Usage: "add a key/password whose value is supplied by the -value option", diff --git a/src/duplicacy_backupmanager.go b/src/duplicacy_backupmanager.go index b8a59bc..2671c19 100644 --- a/src/duplicacy_backupmanager.go +++ b/src/duplicacy_backupmanager.go @@ -33,6 +33,8 @@ type BackupManager struct { snapshotCache *FileStorage // for copies of chunks needed by snapshots config *Config // contains a number of options + + nobackupFile string // don't backup directory when this file name is found } func (manager *BackupManager) SetDryRun(dryRun bool) { @@ -42,7 +44,7 @@ func (manager *BackupManager) SetDryRun(dryRun bool) { // CreateBackupManager creates a backup manager using the specified 'storage'. 'snapshotID' is a unique id to // identify snapshots created for this repository. 'top' is the top directory of the repository. 'password' is the // master key which can be nil if encryption is not enabled. -func CreateBackupManager(snapshotID string, storage Storage, top string, password string) *BackupManager { +func CreateBackupManager(snapshotID string, storage Storage, top string, password string, nobackupFile string) *BackupManager { config, _, err := DownloadConfig(storage, password) if err != nil { @@ -63,6 +65,8 @@ func CreateBackupManager(snapshotID string, storage Storage, top string, passwor SnapshotManager: snapshotManager, config: config, + + nobackupFile: nobackupFile, } if IsDebugging() { @@ -184,7 +188,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta defer DeleteShadowCopy() LOG_INFO("BACKUP_INDEXING", "Indexing %s", top) - localSnapshot, skippedDirectories, skippedFiles, err := CreateSnapshotFromDirectory(manager.snapshotID, shadowTop) + localSnapshot, skippedDirectories, skippedFiles, err := CreateSnapshotFromDirectory(manager.snapshotID, shadowTop, manager.nobackupFile) if err != nil { LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err) return false @@ -752,7 +756,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu remoteSnapshot := manager.SnapshotManager.DownloadSnapshot(manager.snapshotID, revision) manager.SnapshotManager.DownloadSnapshotContents(remoteSnapshot, patterns, true) - localSnapshot, _, _, err := CreateSnapshotFromDirectory(manager.snapshotID, top) + localSnapshot, _, _, err := CreateSnapshotFromDirectory(manager.snapshotID, top, manager.nobackupFile) if err != nil { LOG_ERROR("SNAPSHOT_LIST", "Failed to list the repository: %v", err) return false diff --git a/src/duplicacy_backupmanager_test.go b/src/duplicacy_backupmanager_test.go index e463e93..80b6ecc 100644 --- a/src/duplicacy_backupmanager_test.go +++ b/src/duplicacy_backupmanager_test.go @@ -239,7 +239,7 @@ func TestBackupManager(t *testing.T) { time.Sleep(time.Duration(delay) * time.Second) SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy") - backupManager := CreateBackupManager("host1", storage, testDir, password) + backupManager := CreateBackupManager("host1", storage, testDir, password, "") backupManager.SetupSnapshotCache("default") SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy") diff --git a/src/duplicacy_entry.go b/src/duplicacy_entry.go index 6287fa7..bc6ab47 100644 --- a/src/duplicacy_entry.go +++ b/src/duplicacy_entry.go @@ -435,7 +435,7 @@ func (files FileInfoCompare) Less(i, j int) bool { // ListEntries returns a list of entries representing file and subdirectories under the directory 'path'. Entry paths // are normalized as relative to 'top'. 'patterns' are used to exclude or include certain files. -func ListEntries(top string, path string, fileList *[]*Entry, patterns []string, discardAttributes bool) (directoryList []*Entry, +func ListEntries(top string, path string, fileList *[]*Entry, patterns []string, nobackupFile string, discardAttributes bool) (directoryList []*Entry, skippedFiles []string, err error) { LOG_DEBUG("LIST_ENTRIES", "Listing %s", path) @@ -448,6 +448,15 @@ func ListEntries(top string, path string, fileList *[]*Entry, patterns []string, if err != nil { return directoryList, nil, err } + + // This binary search works because ioutil.ReadDir returns files sorted by Name() by default + if nobackupFile != "" { + ii := sort.Search(len(files), func(ii int) bool { return strings.Compare(files[ii].Name(), nobackupFile) >= 0}) + if ii < len(files) && files[ii].Name() == nobackupFile { + LOG_DEBUG("LIST_NOBACKUP", "%s is excluded due to nobackup file", path) + return directoryList, skippedFiles, nil + } + } normalizedPath := path if len(normalizedPath) > 0 && normalizedPath[len(normalizedPath)-1] != '/' { diff --git a/src/duplicacy_entry_test.go b/src/duplicacy_entry_test.go index a3d38fc..d809412 100644 --- a/src/duplicacy_entry_test.go +++ b/src/duplicacy_entry_test.go @@ -173,7 +173,7 @@ func TestEntryList(t *testing.T) { directory := directories[len(directories)-1] directories = directories[:len(directories)-1] entries = append(entries, directory) - subdirectories, _, err := ListEntries(testDir, directory.Path, &entries, nil, false) + subdirectories, _, err := ListEntries(testDir, directory.Path, &entries, nil, "", false) if err != nil { t.Errorf("ListEntries(%s, %s) returned an error: %s", testDir, directory.Path, err) } diff --git a/src/duplicacy_preference.go b/src/duplicacy_preference.go index f9eaeb1..ff4b271 100644 --- a/src/duplicacy_preference.go +++ b/src/duplicacy_preference.go @@ -22,6 +22,7 @@ type Preference struct { BackupProhibited bool `json:"no_backup"` RestoreProhibited bool `json:"no_restore"` DoNotSavePassword bool `json:"no_save_password"` + NobackupFile string `json:"nobackup_file"` Keys map[string]string `json:"keys"` } diff --git a/src/duplicacy_snapshot.go b/src/duplicacy_snapshot.go index 3a41197..3355dc4 100644 --- a/src/duplicacy_snapshot.go +++ b/src/duplicacy_snapshot.go @@ -57,7 +57,7 @@ func CreateEmptySnapshot(id string) (snapshto *Snapshot) { // CreateSnapshotFromDirectory creates a snapshot from the local directory 'top'. Only 'Files' // will be constructed, while 'ChunkHashes' and 'ChunkLengths' can only be populated after uploading. -func CreateSnapshotFromDirectory(id string, top string) (snapshot *Snapshot, skippedDirectories []string, +func CreateSnapshotFromDirectory(id string, top string, nobackupFile string) (snapshot *Snapshot, skippedDirectories []string, skippedFiles []string, err error) { snapshot = &Snapshot{ @@ -125,7 +125,7 @@ func CreateSnapshotFromDirectory(id string, top string) (snapshot *Snapshot, ski directory := directories[len(directories)-1] directories = directories[:len(directories)-1] snapshot.Files = append(snapshot.Files, directory) - subdirectories, skipped, err := ListEntries(top, directory.Path, &snapshot.Files, patterns, snapshot.discardAttributes) + subdirectories, skipped, err := ListEntries(top, directory.Path, &snapshot.Files, patterns, nobackupFile, snapshot.discardAttributes) if err != nil { LOG_WARN("LIST_FAILURE", "Failed to list subdirectory: %v", err) skippedDirectories = append(skippedDirectories, directory.Path) diff --git a/src/duplicacy_snapshotmanager.go b/src/duplicacy_snapshotmanager.go index 289237d..0f873be 100644 --- a/src/duplicacy_snapshotmanager.go +++ b/src/duplicacy_snapshotmanager.go @@ -1240,7 +1240,7 @@ func (manager *SnapshotManager) PrintFile(snapshotID string, revision int, path // Diff compares two snapshots, or two revision of a file if the file argument is given. func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []int, - filePath string, compareByHash bool) bool { + filePath string, compareByHash bool, nobackupFile string) bool { LOG_DEBUG("DIFF_PARAMETERS", "top: %s, id: %s, revision: %v, path: %s, compareByHash: %t", top, snapshotID, revisions, filePath, compareByHash) @@ -1253,7 +1253,7 @@ func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions [] if len(revisions) <= 1 { // Only scan the repository if filePath is not provided if len(filePath) == 0 { - rightSnapshot, _, _, err = CreateSnapshotFromDirectory(snapshotID, top) + rightSnapshot, _, _, err = CreateSnapshotFromDirectory(snapshotID, top, nobackupFile) if err != nil { LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err) return false