diff options
| -rw-r--r-- | drive/sync.go | 88 | ||||
| -rw-r--r-- | drive/sync_download.go | 75 | ||||
| -rw-r--r-- | gdrive.go | 30 | ||||
| -rw-r--r-- | handlers_drive.go | 25 | 
4 files changed, 209 insertions, 9 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()) +} @@ -394,15 +394,21 @@ func main() {                  cli.NewFlagGroup("global", globalFlags...),                  cli.NewFlagGroup("options",                      cli.BoolFlag{ -                        Name: "noProgress", -                        Patterns: []string{"--no-progress"}, -                        Description: "Hide progress", +                        Name: "keepRemote", +                        Patterns: []string{"--keep-remote"}, +                        Description: "Keep remote file when a conflict is encountered",                          OmitValue: true,                      },                      cli.BoolFlag{ -                        Name: "dryRun", -                        Patterns: []string{"--dry-run"}, -                        Description: "Show what would have been transferred", +                        Name: "keepLocal", +                        Patterns: []string{"--keep-local"}, +                        Description: "Keep local file when a conflict is encountered", +                        OmitValue: true, +                    }, +                    cli.BoolFlag{ +                        Name: "keepLargest", +                        Patterns: []string{"--keep-largest"}, +                        Description: "Keep largest file when a conflict is encountered",                          OmitValue: true,                      },                      cli.BoolFlag{ @@ -411,6 +417,18 @@ func main() {                          Description: "Delete extraneous local files",                          OmitValue: true,                      }, +                    cli.BoolFlag{ +                        Name: "dryRun", +                        Patterns: []string{"--dry-run"}, +                        Description: "Show what would have been transferred", +                        OmitValue: true, +                    }, +                    cli.BoolFlag{ +                        Name: "noProgress", +                        Patterns: []string{"--no-progress"}, +                        Description: "Hide progress", +                        OmitValue: true, +                    },                  ),              },          }, diff --git a/handlers_drive.go b/handlers_drive.go index a6f288a..aaf957c 100644 --- a/handlers_drive.go +++ b/handlers_drive.go @@ -68,6 +68,7 @@ func downloadSyncHandler(ctx cli.Context) {          RootId: args.String("id"),          DryRun: args.Bool("dryRun"),          DeleteExtraneous: args.Bool("deleteExtraneous"), +        Resolution: conflictResolution(args),          Comparer: NewCachedMd5Comparer(cachePath),      })      checkErr(err) @@ -324,3 +325,27 @@ func progressWriter(discard bool) io.Writer {      }      return os.Stderr  } + +func conflictResolution(args cli.Arguments) drive.ConflictResolution { +    keepLocal := args.Bool("keepLocal") +    keepRemote := args.Bool("keepRemote") +    keepLargest := args.Bool("keepLargest") + +    if (keepLocal && keepRemote) || (keepLocal && keepLargest) || (keepRemote && keepLargest) { +        ExitF("Only one conflict resolution flag can be given") +    } + +    if keepLocal { +        return drive.KeepLocal +    } + +    if keepRemote { +        return drive.KeepRemote +    } + +    if keepLargest { +        return drive.KeepLargest +    } + +    return drive.NoResolution +} | 
