aboutsummaryrefslogtreecommitdiffstats
path: root/drive
diff options
context:
space:
mode:
authorPetter Rasmussen2016-02-12 23:19:03 +0100
committerPetter Rasmussen2016-02-13 01:10:17 +0100
commit5eae4f159d340d257e41e75604a0fc831cb76381 (patch)
tree9e2b0f0e2de1a9dad0124bcd06c74b57eea05c1a /drive
parentad4309f1028d165b8905a6a22be89191f787fc39 (diff)
downloadgdrive-5eae4f159d340d257e41e75604a0fc831cb76381.tar.bz2
Add conflict handling and flags for downloads
Diffstat (limited to 'drive')
-rw-r--r--drive/sync.go88
-rw-r--r--drive/sync_download.go75
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())
+}