diff options
| author | Petter Rasmussen | 2016-02-12 23:19:03 +0100 |
|---|---|---|
| committer | Petter Rasmussen | 2016-02-13 01:10:17 +0100 |
| commit | 5eae4f159d340d257e41e75604a0fc831cb76381 (patch) | |
| tree | 9e2b0f0e2de1a9dad0124bcd06c74b57eea05c1a /drive | |
| parent | ad4309f1028d165b8905a6a22be89191f787fc39 (diff) | |
| download | gdrive-5eae4f159d340d257e41e75604a0fc831cb76381.tar.bz2 | |
Add conflict handling and flags for downloads
Diffstat (limited to 'drive')
| -rw-r--r-- | drive/sync.go | 88 | ||||
| -rw-r--r-- | drive/sync_download.go | 75 |
2 files changed, 160 insertions, 3 deletions
diff --git a/drive/sync.go b/drive/sync.go index 86ddb3b..f39c1c6 100644 --- a/drive/sync.go +++ b/drive/sync.go @@ -4,8 +4,10 @@ import ( "time" "fmt" "os" + "io" "strings" "path/filepath" + "text/tabwriter" "github.com/soniakeys/graph" "github.com/sabhiram/go-git-ignore" "google.golang.org/api/drive/v3" @@ -14,6 +16,31 @@ import ( const DefaultIgnoreFile = ".gdriveignore" +type ModTime int + +const ( + LocalLastModified ModTime = iota + RemoteLastModified + EqualModifiedTime +) + +type LargestSize int + +const ( + LocalLargestSize LargestSize = iota + RemoteLargestSize + EqualSize +) + +type ConflictResolution int + +const ( + NoResolution ConflictResolution = iota + KeepLocal + KeepRemote + KeepLargest +) + func (self *Drive) prepareSyncFiles(localPath string, root *drive.File, cmp FileComparer) (*syncFiles, error) { localCh := make(chan struct{files []*LocalFile; err error}) remoteCh := make(chan struct{files []*RemoteFile; err error}) @@ -281,6 +308,36 @@ func (self RemoteFile) Modified() time.Time { return t } +func (self *changedFile) compareModTime() ModTime { + localTime := self.local.Modified() + remoteTime := self.remote.Modified() + + if localTime.After(remoteTime) { + return LocalLastModified + } + + if remoteTime.After(localTime) { + return RemoteLastModified + } + + return EqualModifiedTime +} + +func (self *changedFile) compareSize() LargestSize { + localSize := self.local.Size() + remoteSize := self.remote.Size() + + if localSize > remoteSize { + return LocalLargestSize + } + + if remoteSize > localSize { + return RemoteLargestSize + } + + return EqualSize +} + func (self *syncFiles) filterMissingRemoteDirs() []*LocalFile { var files []*LocalFile @@ -441,6 +498,18 @@ func (self *syncFiles) findLocalByPath(relPath string) (*LocalFile, bool) { return nil, false } +func findLocalConflicts(files []*changedFile) []*changedFile { + var conflicts []*changedFile + + for _, cf := range files { + if cf.compareModTime() == LocalLastModified { + conflicts = append(conflicts, cf) + } + } + + return conflicts +} + type byLocalPathLength []*LocalFile func (self byLocalPathLength) Len() int { @@ -501,3 +570,22 @@ func prepareIgnorer(path string) (ignoreFunc, error) { return ignorer.MatchesPath, nil } + +func formatConflicts(conflicts []*changedFile, out io.Writer) { + w := new(tabwriter.Writer) + w.Init(out, 0, 0, 3, ' ', 0) + + fmt.Fprintln(w, "Path\tSize Local\tSize Remote\tModified Local\tModified Remote") + + for _, cf := range conflicts { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + truncateString(cf.local.relPath, 60), + formatSize(cf.local.Size(), false), + formatSize(cf.remote.Size(), false), + cf.local.Modified().Local().Format("Jan _2 2006 15:04:05.000"), + cf.remote.Modified().Local().Format("Jan _2 2006 15:04:05.000"), + ) + } + + w.Flush() +} diff --git a/drive/sync_download.go b/drive/sync_download.go index f947142..9dcd719 100644 --- a/drive/sync_download.go +++ b/drive/sync_download.go @@ -6,6 +6,7 @@ import ( "os" "sort" "time" + "bytes" "path/filepath" "google.golang.org/api/googleapi" "google.golang.org/api/drive/v3" @@ -18,6 +19,7 @@ type DownloadSyncArgs struct { Path string DryRun bool DeleteExtraneous bool + Resolution ConflictResolution Comparer FileComparer } @@ -37,8 +39,19 @@ func (self *Drive) DownloadSync(args DownloadSyncArgs) error { return err } + // Find changed files + changedFiles := files.filterChangedRemoteFiles() + fmt.Fprintf(args.Out, "Found %d local files and %d remote files\n", len(files.local), len(files.remote)) + // Ensure that that we don't overwrite any local changes + if args.Resolution == NoResolution { + err = ensureNoLocalModifications(changedFiles) + if err != nil { + return fmt.Errorf("Conflict detected!\nThe following files have changed and the local file are newer than it's remote counterpart:\n\n%s\nNo conflict resolution was given, aborting...", err) + } + } + // Create missing directories err = self.createMissingLocalDirs(files, args) if err != nil { @@ -52,7 +65,7 @@ func (self *Drive) DownloadSync(args DownloadSyncArgs) error { } // Download files that has changed - err = self.downloadChangedFiles(files, args) + err = self.downloadChangedFiles(changedFiles, args) if err != nil { return err } @@ -145,8 +158,7 @@ func (self *Drive) downloadMissingFiles(files *syncFiles, args DownloadSyncArgs) return nil } -func (self *Drive) downloadChangedFiles(files *syncFiles, args DownloadSyncArgs) error { - changedFiles := files.filterChangedRemoteFiles() +func (self *Drive) downloadChangedFiles(changedFiles []*changedFile, args DownloadSyncArgs) error { changedCount := len(changedFiles) if changedCount > 0 { @@ -154,6 +166,11 @@ func (self *Drive) downloadChangedFiles(files *syncFiles, args DownloadSyncArgs) } for i, cf := range changedFiles { + if skip, reason := checkLocalConflict(cf, args.Resolution); skip { + fmt.Fprintf(args.Out, "[%04d/%04d] Skipping %s (%s)\n", i + 1, changedCount, cf.remote.relPath, reason) + continue + } + absPath, err := filepath.Abs(filepath.Join(args.Path, cf.remote.relPath)) if err != nil { return fmt.Errorf("Failed to determine local absolute path: %s", err) @@ -246,3 +263,55 @@ func (self *Drive) deleteExtraneousLocalFiles(files *syncFiles, args DownloadSyn return nil } + +func checkLocalConflict(cf *changedFile, resolution ConflictResolution) (bool, string) { + // No conflict unless local file was last modified + if cf.compareModTime() != LocalLastModified { + return false, "" + } + + // Don't skip if want to keep the remote file + if resolution == KeepRemote { + return false, "" + } + + // Skip if we want to keep the local file + if resolution == KeepLocal { + return true, "conflicting file, keeping local file" + } + + if resolution == KeepLargest { + largest := cf.compareSize() + + // Skip if the local file is largest + if largest == LocalLargestSize { + return true, "conflicting file, local file is largest, keeping local" + } + + // Don't skip if the remote file is largest + if largest == RemoteLargestSize { + return false, "" + } + + // Keep local if both files have the same size + if largest == EqualSize { + return true, "conflicting file, file sizes are equal, keeping local" + } + } + + // The conditionals above should cover all cases, + // unless the programmer did something wrong, + // in which case we default to being non-destructive and skip the file + return true, "conflicting file, unhandled case" +} + +func ensureNoLocalModifications(files []*changedFile) error { + conflicts := findLocalConflicts(files) + if len(conflicts) == 0 { + return nil + } + + buffer := bytes.NewBufferString("") + formatConflicts(conflicts, buffer) + return fmt.Errorf(buffer.String()) +} |
