%PDF- %PDF-
Direktori : /home/waritko/go/src/github.com/odeke-em/drive/src/ |
Current File : //home/waritko/go/src/github.com/odeke-em/drive/src/changes.go |
// Copyright 2013 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package drive import ( "fmt" "os" "path" "path/filepath" "strings" "sync" "time" "github.com/odeke-em/drive/config" "github.com/odeke-em/drive/src/dcrypto" "github.com/odeke-em/log" ) type destination int const ( SelectSrc destination = 1 << iota SelectDest ) type Agreement int const ( NotApplicable Agreement = 1 << iota Rejected Accepted AcceptedImplicitly ) type dirList struct { remote *File local *File } func (d *dirList) Name() string { if d.remote != nil { return d.remote.Name } return d.local.Name } type sizeCounter struct { count int64 src int64 dest int64 } func (t *sizeCounter) String() string { str := fmt.Sprintf("count %v", t.count) if t.src > 0 { str = fmt.Sprintf("%s src: %v", str, prettyBytes(t.src)) } if t.dest > 0 { str = fmt.Sprintf("%s dest: %v", str, prettyBytes(t.dest)) } return str } // There are operations for which the size of the target // is reported in the progress channel but absent for the // size counter for example OpDelete. // See Issue https://github.com/odeke-em/drive/issues/177. func (sc *sizeCounter) sizeByOperation(op Operation) int64 { var size int64 = sc.src switch op { case OpDelete: if sc.src == 0 && sc.dest > 0 { size = sc.dest } } return size } // Resolves the local path relative to the root directory // Returns the path relative to the remote, the abspath on disk and an error if any func (g *Commands) pathResolve() (relPath, absPath string, err error) { root := g.context.AbsPathOf("") absPath = g.context.AbsPathOf(g.opts.Path) relPath = "" if absPath != root { relPath, err = filepath.Rel(root, absPath) if err != nil { return } } else { var cwd string if cwd, err = os.Getwd(); err != nil { return } if cwd == root { relPath = "" } else if relPath, err = filepath.Rel(root, cwd); err != nil { return } } relPath = strings.Join([]string{"", relPath}, "/") return } func (g *Commands) resolveToLocalFile(relToRoot string, fsPaths ...string) (local *File, err error) { checks := append([]string{relToRoot}, fsPaths...) if anyMatch(g.opts.Ignorer, checks...) { err = illogicalStateErr(fmt.Errorf("\n'%s' is set to be ignored yet is being processed. Use `%s` to override this\n", relToRoot, ForceKey)) return } for _, fsPath := range fsPaths { localInfo, statErr := os.Stat(fsPath) if statErr != nil && !os.IsNotExist(statErr) { err = statErr return } else if localInfo != nil { if namedPipe(localInfo.Mode()) { err = namedPipeReadAttemptErr(fmt.Errorf("%s (%s) is a named pipe, yet not reading from it", relToRoot, fsPath)) return } local = NewLocalFile(fsPath, localInfo) return } } return } func (g *Commands) byRemoteResolve(relToRoot, fsPath string, r *File, push bool) (cl, clashes []*Change, err error) { var l *File l, err = g.resolveToLocalFile(relToRoot, r.localAliases(fsPath)...) if err != nil { return cl, clashes, err } return g.doChangeListRecv(relToRoot, fsPath, l, r, push) } func (g *Commands) changeListResolve(relToRoot, fsPath string, push bool) (cl, clashes []*Change, err error) { pagePair := g.rem.FindByPathM(relToRoot) iterCount := uint64(0) noClashThreshold := uint64(1) errsChan := pagePair.errsChan remotesChan := pagePair.filesChan working := true for working { select { case rErr := <-errsChan: if rErr != nil { return nil, nil, rErr } case rem, stillHasContent := <-remotesChan: if !stillHasContent { working = false break } if rem != nil { if anyMatch(g.opts.Ignorer, rem.Name) { return } iterCount++ } g.DebugPrintf("[changeListResolve] relToRoot: %s remoteFile: %#v isPush: %v\n", relToRoot, rem, push) ccl, cclashes, cErr := g.byRemoteResolve(relToRoot, fsPath, rem, push) cl = append(cl, ccl...) clashes = append(clashes, cclashes...) if cErr != nil { err = combineErrors(err, cErr) } } } if iterCount > noClashThreshold && len(clashes) < 1 { clashes = append(clashes, cl...) // err = reComposeError(err, ErrClashesDetected.Error()) } return } func directionalComplement(local, remote *File, push bool) *File { first, other := remote, local if push { first, other = local, remote } // If the first == nil, then fall back to the other if first != nil { return first } return other } func (g *Commands) doChangeListRecv(relToRoot, fsPath string, l, r *File, push bool) (cl, clashes []*Change, err error) { if l == nil && r == nil { err = illogicalStateErr(fmt.Errorf("'%s' aka '%s' doesn't exist locally nor remotely", relToRoot, fsPath)) return } dirname := path.Dir(relToRoot) remoteBase := relToRoot localBase := relToRoot if relBase, err := filepath.Rel(g.context.AbsPathOf(""), fsPath); err == nil { localBase = relBase } // Issue #618. Ensure that any base is always direction-centric separator prefixed localBase = localPathJoin(localBase) remoteBase = remotePathJoin(remoteBase) clr := &changeListResolve{ dir: dirname, localBase: localBase, remoteBase: remoteBase, local: l, push: push, remote: r, depth: g.opts.Depth, filter: makeFileFilter(g.opts.TypeMask), } return g.resolveChangeListRecv(clr) } func (g *Commands) clearMountPoints() { if g.opts.Mount == nil { return } mount := g.opts.Mount for _, point := range mount.Points { point.Unmount() } if mount.CreatedMountDir != "" { if rmErr := os.RemoveAll(mount.CreatedMountDir); rmErr != nil { g.log.LogErrf("clearMountPoints removing %s: %v\n", mount.CreatedMountDir, rmErr) } } if mount.ShortestMountRoot != "" { if rmErr := os.RemoveAll(mount.ShortestMountRoot); rmErr != nil { g.log.LogErrf("clearMountPoints: shortestMountRoot: %v\n", mount.ShortestMountRoot, rmErr) } } } func (g *Commands) differ(a, b *File) bool { return fileDifferences(a, b, g.opts.IgnoreChecksum) == DifferNone } func (g *Commands) coercedMimeKey() (coerced string, ok bool) { if g.opts.Meta == nil { return } var values []string dict := *g.opts.Meta values, ok = dict[CoercedMimeKeyKey] if len(values) >= 1 { coerced = values[0] } else { ok = false } return } type changeListResolve struct { dir string filter driveFileFilter push bool depth int local *File remote *File localBase string remoteBase string } type changeSliceArg struct { id int wg *sync.WaitGroup depth int push bool filter driveFileFilter mu *sync.Mutex dirList []*dirList localParent string remoteParent string clashesMap map[int][]*Change changeListPtr *[]*Change } func (g *Commands) resolveChangeListRecv(clr *changeListResolve) (cl, clashes []*Change, err error) { l := clr.local r := clr.remote dir := clr.dir var change *Change cl = make([]*Change, 0) clashes = make([]*Change, 0) matchChecks := []string{clr.localBase} if l != nil { matchChecks = append(matchChecks, l.Name) } if r != nil { matchChecks = append(matchChecks, r.Name) } if anyMatch(g.opts.Ignorer, matchChecks...) { return } explicitlyRequested := g.opts.ExplicitlyExport && hasExportLinks(r) && len(g.opts.Exports) >= 1 if clr.push { // Handle the case of doc files for which we don't have a direct download // url but have exportable links. These files should not be clobbered on push if hasExportLinks(r) { return cl, clashes, nil } change = &Change{Path: clr.remoteBase, Src: l, Dest: r, Parent: dir, g: g} } else { exportable := !g.opts.Force && hasExportLinks(r) if exportable && !explicitlyRequested { // The case when we have files that don't provide the download urls // but exportable links, we just need to check that mod times are the same. mask := fileDifferences(r, l, g.opts.IgnoreChecksum) if !dirTypeDiffers(mask) && !modTimeDiffers(mask) { return cl, clashes, nil } } change = &Change{Path: clr.remoteBase, Src: r, Dest: l, Parent: dir, g: g} } change.NoClobber = g.opts.NoClobber change.IgnoreChecksum = g.opts.IgnoreChecksum if explicitlyRequested { change.Force = true } else { change.Force = g.opts.Force } forbiddenOp := (g.opts.ExcludeCrudMask & change.crudValue()) != 0 if forbiddenOp { return cl, clashes, nil } if change.Op() != OpNone { subject := directionalComplement(l, r, clr.push) if clr.filter == nil || clr.filter(subject) { cl = append(cl, change) } } if !g.opts.Recursive { return cl, clashes, nil } // Let's handle the case where remote and local don't have // the same dirType ie one is a file, the other is a directory. // Note: This case currently is handled only when that // specific object is directly addressed ie push <this> or pull <this>. if l != nil && r != nil && !l.sameDirType(r) { relPath := sepJoin("/", clr.remoteBase) err := illogicalStateErr(fmt.Errorf("%s: local is a %v while remote is a %v", relPath, l.dirTypeNomenclature(), r.dirTypeNomenclature())) return cl, clashes, err } if !clr.push && r != nil && !r.IsDir { return cl, clashes, nil } if clr.push && l != nil && !l.IsDir { return cl, clashes, nil } originalDepth := clr.depth remoteTraversalDepth := decrementTraversalDepth(originalDepth) if remoteTraversalDepth == 0 { return cl, clashes, nil } // look-up for children var localChildren chan *File if l == nil || !l.IsDir { localChildren = make(chan *File) close(localChildren) } else { fslArg := fsListingArg{ parent: clr.localBase, context: g.context, hidden: g.opts.Hidden, depth: originalDepth, // local listing needs to start from original depth ignore: g.opts.Ignorer, } var lErr error localChildren, lErr = list(&fslArg) if lErr != nil && !os.IsNotExist(lErr) { err = lErr return } } var pagePair *paginationPair if r != nil { pagePair = g.rem.FindByParentId(r.Id, g.opts.Hidden) } else { // TODO: Figure out if the condition // file == nil && err == nil // is an inconsistent state. errsChan := make(chan error) go close(errsChan) filesChan := make(chan *File) go close(filesChan) pagePair = &paginationPair{errsChan: errsChan, filesChan: filesChan} } dirlist, clashingFiles, err := merge(pagePair, localChildren, g.opts.IgnoreNameClashes) if err != nil { return nil, nil, err } if !g.opts.IgnoreNameClashes && len(clashingFiles) >= 1 { remoteBase := clr.remoteBase if rootLike(remoteBase) { remoteBase = "" } for _, dup := range clashingFiles { clashes = append(clashes, &Change{Path: sepJoin("/", remoteBase, dup.Name), Src: dup, g: g}) } // Ensure all clashes are retrieved and listed // TODO: Stop as soon a single clash is detected? if false { err = ErrClashesDetected return cl, clashes, err } } // Arbitrary value. TODO: Calibrate or calculate this value chunkSize := 100 srcLen := len(dirlist) chunkCount, remainder := srcLen/chunkSize, srcLen%chunkSize i := 0 if remainder != 0 { chunkCount += 1 } var wg sync.WaitGroup wg.Add(chunkCount) mu := &sync.Mutex{} clashesMap := make(map[int][]*Change) for j := 0; j < chunkCount; j += 1 { end := i + chunkSize if end >= srcLen { end = srcLen } cslArgs := changeSliceArg{ id: j, wg: &wg, push: clr.push, dirList: dirlist[i:end], remoteParent: clr.remoteBase, localParent: clr.localBase, depth: remoteTraversalDepth, changeListPtr: &cl, clashesMap: clashesMap, mu: mu, filter: clr.filter, } go g.changeSlice(&cslArgs) i += chunkSize } wg.Wait() for _, cclashes := range clashesMap { clashes = append(clashes, cclashes...) } if len(clashes) >= 1 { err = ErrClashesDetected } return cl, clashes, err } func (g *Commands) changeSlice(cslArg *changeSliceArg) { cl := cslArg.changeListPtr id := cslArg.id wg := cslArg.wg push := cslArg.push dlist := cslArg.dirList clashesMap := cslArg.clashesMap defer wg.Done() for _, l := range dlist { // Avoiding path.Join which normalizes '/+' to '/' localBase := remotePathJoin(cslArg.localParent, l.Name()) remoteBase := remotePathJoin(cslArg.remoteParent, l.Name()) nonDirRemote := l.remote != nil && !l.remote.IsDir if nonDirRemote && g.opts.CryptoEnabled() { l.remote.Size -= int64(dcrypto.Overhead) } clr := &changeListResolve{ push: push, dir: cslArg.localParent, localBase: localBase, remoteBase: remoteBase, remote: l.remote, local: l.local, depth: cslArg.depth, filter: cslArg.filter, } childChanges, childClashes, cErr := g.resolveChangeListRecv(clr) if cErr == nil { cslArg.mu.Lock() *cl = append(*cl, childChanges...) cslArg.mu.Unlock() continue } if cErr == ErrClashesDetected { clashesMap[id] = childClashes continue } else if cErr != ErrPathNotExists { g.log.LogErrf("%s: %v\n", localBase, cErr) break } } } func merge(remotePagePair *paginationPair, locals chan *File, ignoreClashes bool) (merged []*dirList, clashes []*File, err error) { localsMap := map[string]*File{} remotesMap := map[string]*File{} uniqClashes := map[string]bool{} registerClash := func(v *File) { key := v.Id if key == "" { key = v.Name } _, ok := uniqClashes[key] if !ok { uniqClashes[key] = true clashes = append(clashes, v) } } // TODO: Add support for FileSystems that allow same names but different files. for l := range locals { localsMap[l.Name] = l } working := true for working { select { case err := <-remotePagePair.errsChan: // It is imperative that as soon as we catch an error // from the remote, let's immediately return // otherwise we'll be reporting false results. // See Issues: // https://github.com/odeke-em/drive/issues/480 // https://github.com/odeke-em/drive/issues/668 // https://github.com/odeke-em/drive/issues/728 // https://github.com/odeke-em/drive/issues/738 // and a multitude of other issues that were caused by error responses // from remote falsely being translated as "the file doesn't exist" if err != nil { return merged, clashes, err } case r, stillHasContent := <-remotePagePair.filesChan: if !stillHasContent { working = false break } list := &dirList{remote: r} if !ignoreClashes { prev, present := remotesMap[r.Name] if present { registerClash(r) registerClash(prev) continue } remotesMap[r.Name] = r } l, ok := localsMap[r.Name] // look for local if ok && l != nil && l.IsDir == r.IsDir { list.local = l delete(localsMap, r.Name) } merged = append(merged, list) } } // if anything left in locals, add to the dir listing for _, l := range localsMap { merged = append(merged, &dirList{local: l}) } return } func reduceToSize(changes []*Change, destMask destination) (srcSize, destSize int64) { fromSrc := (destMask & SelectSrc) != 0 fromDest := (destMask & SelectDest) != 0 for _, c := range changes { if fromSrc && c.Src != nil { srcSize += c.Src.Size } if fromDest && c.Dest != nil { destSize += c.Dest.Size } } return } func conflict(src, dest *File, index *config.Index, push bool) bool { // Never been indexed means no local record. if index == nil { return false } // Check if this was only a one sided edit for a push if push && dest != nil && dest.ModTime.Unix() == index.ModTime { return false } rounded := src.ModTime.UTC().Round(time.Second) if rounded.Unix() != index.ModTime && src.Md5Checksum != index.Md5Checksum { return true } return false } func resolveConflicts(conflicts []*Change, push bool, indexFiler func(string) *config.Index) (resolved, unresolved []*Change) { for _, ch := range conflicts { l, r := ch.Dest, ch.Src if push { l, r = ch.Src, ch.Dest } fileId := "" if l != nil { fileId = l.Id } if fileId == "" && r != nil { fileId = r.Id } if !conflict(l, r, indexFiler(fileId), push) { // Time to disregard this conflict if any if ch.Op() == OpModConflict { ch.IgnoreConflict = true } resolved = append(resolved, ch) } else { unresolved = append(unresolved, ch) } } return } func (g *Commands) resolveConflicts(cl []*Change, push bool) (*[]*Change, *[]*Change) { if g.opts.IgnoreConflict { return &cl, nil } nonConflicts, conflicts := sift(cl) resolved, unresolved := resolveConflicts(conflicts, push, g.deserializeIndex) if conflictsPersist(unresolved) { return &resolved, &unresolved } for _, ch := range unresolved { resolved = append(resolved, ch) } for _, ch := range resolved { nonConflicts = append(nonConflicts, ch) } return &nonConflicts, nil } func sift(changes []*Change) (nonConflicts, conflicts []*Change) { // Firstly detect the conflicting changes and if present return false for _, c := range changes { if c.Op() == OpModConflict { conflicts = append(conflicts, c) } else { nonConflicts = append(nonConflicts, c) } } return } func conflictsPersist(conflicts []*Change) bool { return len(conflicts) >= 1 } func warnConflictsPersist(logy *log.Logger, conflicts []*Change) { _warnChangeStopper(logy, conflicts, "\033[31mX\033[00m", "These %d file(s) would be overwritten. Use -%s to override this behaviour\n", len(conflicts), CLIOptionIgnoreConflict) } func warnClashesPersist(logy *log.Logger, conflicts []*Change) { _warnChangeStopper(logy, conflicts, "\033[31mX\033[00m", "These paths clash\n") } func _warnChangeStopper(logy *log.Logger, items []*Change, perItemPrefix, fmt_ string, args ...interface{}) { logy.LogErrf(fmt_, args...) for _, item := range items { if item != nil { fileId := "" if item.Src != nil && item.Src.Id != "" { fileId = item.Src.Id } else if item.Dest != nil && item.Dest.Id != "" { fileId = item.Dest.Id } logy.LogErrln(perItemPrefix, item.Path, fileId) } } } func opChangeCount(changes []*Change) map[Operation]sizeCounter { opMap := map[Operation]sizeCounter{} for _, c := range changes { op := c.Op() if op == OpNone { continue } counter := opMap[op] counter.count += 1 if c.Src != nil && !c.Src.IsDir { counter.src += c.Src.Size } if c.Dest != nil && !c.Dest.IsDir { counter.dest += c.Dest.Size } opMap[op] = counter } return opMap } type changeListArg struct { logy *log.Logger changes []*Change noPrompt bool noClobber bool canPreview bool } func previewChanges(clArgs *changeListArg, reduce bool, opMap map[Operation]sizeCounter) { logy := clArgs.logy cl := clArgs.changes for _, c := range cl { op := c.Op() if op != OpNone { logy.Logln(c.Symbol(), c.Path) } } if reduce { for op, counter := range opMap { if counter.count < 1 { continue } _, name := op.description() logy.Logf("%s %s\n", name, counter.String()) } } } func rejected(status Agreement) bool { return (status & Rejected) != 0 } func accepted(status Agreement) bool { return (status&Accepted) != 0 || (status&AcceptedImplicitly) != 0 } func notApplicable(status Agreement) bool { return (status & NotApplicable) != 0 } func (ag *Agreement) Error() error { switch *ag { case Rejected: return ErrRejectedTerms } return nil } func printChangeList(clArg *changeListArg) (Agreement, *map[Operation]sizeCounter) { if len(clArg.changes) == 0 { clArg.logy.Logln("Everything is up-to-date.") return NotApplicable, nil } if !clArg.canPreview { return AcceptedImplicitly, nil } opMap := opChangeCount(clArg.changes) previewChanges(clArg, true, opMap) status := Rejected if clArg.noPrompt { status = AcceptedImplicitly } if !accepted(status) { status = promptForChanges() } return status, &opMap }