Skip to content

Commit

Permalink
Add conflict handling and flags for downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
prasmussen committed Feb 13, 2016
1 parent ad4309f commit 5eae4f1
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 9 deletions.
88 changes: 88 additions & 0 deletions drive/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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})
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
75 changes: 72 additions & 3 deletions drive/sync_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"sort"
"time"
"bytes"
"path/filepath"
"google.golang.org/api/googleapi"
"google.golang.org/api/drive/v3"
Expand All @@ -18,6 +19,7 @@ type DownloadSyncArgs struct {
Path string
DryRun bool
DeleteExtraneous bool
Resolution ConflictResolution
Comparer FileComparer
}

Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -145,15 +158,19 @@ 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 {
fmt.Fprintf(args.Out, "\n%d remote files has changed\n", changedCount)
}

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)
Expand Down Expand Up @@ -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())
}
30 changes: 24 additions & 6 deletions gdrive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
},
),
},
},
Expand Down
25 changes: 25 additions & 0 deletions handlers_drive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

0 comments on commit 5eae4f1

Please sign in to comment.