%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/push.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"
"os/signal"
gopath "path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/odeke-em/drive/config"
"github.com/odeke-em/semalim"
)
var mkdirAllMu = sync.Mutex{}
// Pushes to remote if local path exists and in a gd context. If path is a
// directory, it recursively pushes to the remote if there are local changes.
// It doesn't check if there are local changes if isForce is set.
func (g *Commands) Push() error {
g.rem.encrypter = g.opts.Encrypter
g.rem.decrypter = g.opts.Decrypter
defer g.clearMountPoints()
var cl []*Change
g.log.Logln("Resolving...")
spin := g.playabler()
spin.play()
// To Ensure mount points are cleared in the event of external exceptions
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
go func() {
_ = <-c
spin.stop()
g.clearMountPoints()
os.Exit(1)
}()
clashes := []*Change{}
rootAbsPath := g.context.AbsPathOf("")
destAbsPath := g.context.AbsPathOf(g.opts.Destination)
remoteDestRelPath, err := filepath.Rel(rootAbsPath, destAbsPath)
if err != nil {
return err
}
for _, relToRootPath := range g.opts.Sources {
fsAbsPath := g.context.AbsPathOf(relToRootPath)
// Join this relative path to that of the remote relative path of the destination.
relToDestPath := remotePathJoin(remoteDestRelPath, relToRootPath)
ccl, cclashes, cErr := g.changeListResolve(relToDestPath, fsAbsPath, true)
clashes = append(clashes, cclashes...)
if cErr != nil && cErr != ErrClashesDetected {
spin.stop()
return cErr
}
if len(ccl) > 0 {
cl = append(cl, ccl...)
}
}
mount := g.opts.Mount
if mount != nil {
for _, mt := range mount.Points {
ccl, cclashes, cerr := g.changeListResolve(mt.Name, mt.MountPath, true)
if cerr == nil {
cl = append(cl, ccl...)
}
clashes = append(clashes, cclashes...)
}
}
if len(clashes) >= 1 {
if g.opts.FixClashes {
fn := g.opts.clashesHandler()
err := fn(g, clashes)
if err == nil {
g.log.Logln(MsgClashesFixedNowRetry)
}
return err
}
warnClashesPersist(g.log, clashes)
return ErrClashesDetected
}
spin.stop()
nonConflictsPtr, conflictsPtr := g.resolveConflicts(cl, true)
if conflictsPtr != nil {
warnConflictsPersist(g.log, *conflictsPtr)
return unresolvedConflictsErr(fmt.Errorf("conflicts have prevented a push operation"))
}
nonConflicts := *nonConflictsPtr
pushSize, modSize := reduceToSize(cl, SelectDest|SelectSrc)
// Compensate for deletions and modifications
pushSize -= modSize
// Warn about (near) quota exhaustion
quotaStatus, quotaErr := g.QuotaStatus(pushSize)
if quotaErr != nil {
return quotaErr
}
unSafe := false
switch quotaStatus {
case AlmostExceeded:
g.log.LogErrln("\033[92mAlmost exceeding your drive quota\033[00m")
case Exceeded:
g.log.LogErrln("\033[91mThis change will exceed your drive quota\033[00m")
unSafe = true
}
if unSafe {
unSafeQuotaMsg := fmt.Sprintf("projected size: (%d) %s\n", pushSize, prettyBytes(pushSize))
if !g.opts.canPrompt() {
return cannotPromptErr(fmt.Errorf("quota: noPrompt is set yet for quota %s", unSafeQuotaMsg))
}
g.log.LogErrf(" %s", unSafeQuotaMsg)
if status := promptForChanges(); !accepted(status) {
return status.Error()
}
}
clArg := changeListArg{
logy: g.log,
changes: nonConflicts,
noPrompt: !g.opts.canPrompt(),
noClobber: g.opts.NoClobber,
canPreview: g.opts.canPreview(),
}
status, opMap := printChangeList(&clArg)
if !accepted(status) {
return status.Error()
}
return g.playPushChanges(nonConflicts, opMap)
}
func (g *Commands) PushPiped() error {
g.rem.encrypter = g.opts.Encrypter
g.rem.decrypter = g.opts.Decrypter
// Cannot push asynchronously because the push order must be maintained
for _, relToRootPath := range g.opts.Sources {
rem, resErr := g.rem.FindByPath(relToRootPath)
if resErr != nil && resErr != ErrPathNotExists {
return resErr
}
if rem != nil && !g.opts.Force {
return overwriteAttemptedErr(fmt.Errorf("%s already exists remotely, use `%s` to override this behaviour.\n", relToRootPath, ForceKey))
}
if hasExportLinks(rem) {
return googleDocNonExportErr(fmt.Errorf("'%s' is a GoogleDoc/Sheet document cannot be pushed to raw.\n", relToRootPath))
}
base := filepath.Base(relToRootPath)
local := fauxLocalFile(base)
if rem == nil {
rem = local
}
parentPath := g.parentPather(relToRootPath)
parent, pErr := g.rem.FindByPath(parentPath)
if pErr != nil {
spin := g.playabler()
spin.play()
parent, pErr = g.remoteMkdirAll(parentPath)
spin.stop()
if pErr != nil || parent == nil {
g.log.LogErrf("%s: %v\n", relToRootPath, pErr)
return pErr
}
}
fauxSrc := DupFile(rem)
if fauxSrc != nil {
fauxSrc.ModTime = time.Now()
}
args := &upsertOpt{
uploadChunkSize: g.opts.UploadChunkSize,
parentId: parent.Id,
fsAbsPath: relToRootPath,
src: fauxSrc,
dest: rem,
mask: g.opts.TypeMask,
nonStatable: true,
ignoreChecksum: g.opts.IgnoreChecksum,
retryCount: g.opts.ExponentialBackoffRetryCount,
}
rem, _, rErr := g.rem.upsertByComparison(os.Stdin, args)
if rErr != nil {
g.log.LogErrf("%s: %v\n", relToRootPath, rErr)
return rErr
}
if rem == nil {
continue
}
index := rem.ToIndex()
wErr := g.context.SerializeIndex(index)
// TODO: Should indexing errors be reported?
if wErr != nil {
g.log.LogErrf("serializeIndex %s: %v\n", rem.Name, wErr)
}
}
return nil
}
func (g *Commands) deserializeIndex(identifier string) *config.Index {
index, err := g.context.DeserializeIndex(identifier)
if err != nil {
return nil
}
return index
}
func (g *Commands) playPushChanges(cl []*Change, opMap *map[Operation]sizeCounter) (err error) {
if opMap == nil {
result := opChangeCount(cl)
opMap = &result
}
totalSize := int64(0)
ops := *opMap
for op, counter := range ops {
totalSize += counter.sizeByOperation(op)
}
g.taskStart(totalSize)
defer close(g.rem.progressChan)
go func() {
for n := range g.rem.progressChan {
g.taskAdd(int64(n))
}
}()
type workPair struct {
fn func(*Change) error
arg *Change
}
n := maxProcs()
sort.Sort(ByPrecedence(cl))
jobsChan := make(chan semalim.Job)
go func() {
defer close(jobsChan)
throttle := time.Tick(time.Duration(1e9 / n))
for i, c := range cl {
if c == nil {
g.log.LogErrf("BUGON:: push: nil change found for change index %d\n", i)
continue
}
fn := remoteOpToChangerTranslator(g, c)
if fn == nil {
g.log.LogErrf("push: cannot find operator for %v", c.Op())
continue
}
cjs := changeJobSt{
change: c,
fn: fn,
verb: "Push",
throttle: throttle,
}
dofner := cjs.changeJober(g)
jobsChan <- jobSt{id: uint64(i), do: dofner}
}
}()
results := semalim.Run(jobsChan, uint64(n))
for result := range results {
res, resErr := result.Value(), result.Err()
if resErr != nil {
err = reComposeError(err, fmt.Sprintf("push: %s err: %v\n", res, resErr))
}
}
g.taskFinish()
return err
}
func (g *Commands) pathSplitter(absPath string) (dir, base string) {
p := strings.Split(absPath, "/")
pLen := len(p)
base = p[pLen-1]
p = append([]string{"/"}, p[:pLen-1]...)
dir = gopath.Join(p...)
return
}
func (g *Commands) parentPather(absPath string) string {
dir, _ := g.pathSplitter(absPath)
return dir
}
func (g *Commands) remoteMod(change *Change) (err error) {
if change.Dest == nil && change.Src == nil {
err = illogicalStateErr(fmt.Errorf("bug on: both dest and src cannot be nil"))
g.log.LogErrln(err)
return err
}
absPath := g.context.AbsPathOf(change.Path)
if change.Src != nil && change.Src.IsDir {
needsMkdirAll := change.Dest == nil || change.Src.Id == ""
if needsMkdirAll {
if destFile, _ := g.remoteMkdirAll(change.Path); destFile != nil {
change.Src.Id = destFile.Id
}
}
}
if change.Dest != nil && change.Src != nil && change.Src.Id == "" {
change.Src.Id = change.Dest.Id // TODO: bad hack
}
var parent *File
parentPath := g.parentPather(change.Path)
parent, err = g.remoteMkdirAll(parentPath)
if err != nil {
g.log.LogErrf("remoteMod/remoteMkdirAll: `%s` got %v\n", parentPath, err)
return err
}
if parent == nil {
err = errCannotMkdirAll(parentPath)
g.log.LogErrln(err)
return
}
args := &upsertOpt{
uploadChunkSize: g.opts.UploadChunkSize,
parentId: parent.Id,
fsAbsPath: absPath,
src: change.Src,
dest: change.Dest,
mask: g.opts.TypeMask,
ignoreChecksum: g.opts.IgnoreChecksum,
debug: g.opts.Verbose && g.opts.canPreview(),
retryCount: g.opts.ExponentialBackoffRetryCount,
}
coercedMimeKey, ok := g.coercedMimeKey()
if ok {
args.mimeKey = coercedMimeKey
} else if args.src != nil && !args.src.IsDir { // Infer it from the extension
args.mimeKey = filepath.Ext(args.src.Name)
}
rem, err := g.rem.UpsertByComparison(args)
if err != nil {
g.log.LogErrf("%s: %v\n", change.Path, err)
return
}
if rem == nil {
return
}
index := rem.ToIndex()
wErr := g.context.SerializeIndex(index)
// TODO: Should indexing errors be reported?
if wErr != nil {
g.log.LogErrf("serializeIndex %s: %v\n", rem.Name, wErr)
}
return
}
func (g *Commands) remoteAdd(change *Change) error {
return g.remoteMod(change)
}
func (g *Commands) remoteUntrash(change *Change) error {
target := change.Src
defer func() {
g.taskAdd(target.Size)
}()
if err := g.rem.Untrash(target.Id); err != nil {
return err
}
index := target.ToIndex()
wErr := g.context.SerializeIndex(index)
// TODO: Should indexing errors be reported?
if wErr != nil {
g.log.LogErrf("serializeIndex %s: %v\n", target.Name, wErr)
}
return nil
}
func remoteRemover(g *Commands, change *Change, fn func(string) error) error {
defer func() {
g.taskAdd(change.Dest.Size)
}()
if err := fn(change.Dest.Id); err != nil {
return err
}
if change.Dest.IsDir {
mkdirAllMu.Lock()
g.mkdirAllCache.Remove(change.Path)
mkdirAllMu.Unlock()
}
index := change.Dest.ToIndex()
err := g.context.RemoveIndex(index, g.context.AbsPathOf(""))
if err != nil {
if change.Src != nil {
g.log.LogErrf("%s \"%s\": remove indexfile %v\n", change.Path, change.Dest.Id, err)
}
}
return err
}
func (g *Commands) remoteTrash(change *Change) error {
return remoteRemover(g, change, g.rem.Trash)
}
func (g *Commands) remoteDelete(change *Change) error {
return remoteRemover(g, change, g.rem.Delete)
}
func (g *Commands) remoteMkdirAll(d string) (*File, error) {
mkdirAllMu.Lock()
cachedValue, ok := g.mkdirAllCache.Get(d)
if ok && cachedValue != nil {
castF, castOk := cachedValue.Value().(*File)
// g.log.Logln("CacheHit", d, castF, castOk)
if castOk && castF != nil {
mkdirAllMu.Unlock()
return castF, nil
}
}
// Try the lookup one last time in case a coroutine raced us to it.
retrFile, retryErr := g.rem.FindByPath(d)
if retryErr != nil && retryErr != ErrPathNotExists {
mkdirAllMu.Unlock()
return retrFile, retryErr
}
if retrFile != nil {
mkdirAllMu.Unlock()
return retrFile, nil
}
parDirPath, last := remotePathSplit(d)
parent, parentErr := g.rem.FindByPath(parDirPath)
if parentErr != nil && parentErr != ErrPathNotExists {
mkdirAllMu.Unlock()
return parent, parentErr
}
mkdirAllMu.Unlock()
if parent == nil {
parent, parentErr = g.remoteMkdirAll(parDirPath)
if parentErr != nil || parent == nil {
return parent, parentErr
}
}
mkdirAllMu.Lock()
defer mkdirAllMu.Unlock()
g.mkdirAllCache.Put(parDirPath, newExpirableCacheValue(parent))
remoteFile := &File{
IsDir: true,
Name: last,
ModTime: time.Now(),
}
args := upsertOpt{
uploadChunkSize: g.opts.UploadChunkSize,
parentId: parent.Id,
src: remoteFile,
debug: g.opts.Verbose && g.opts.canPreview(),
retryCount: g.opts.ExponentialBackoffRetryCount,
}
cur, curErr := g.rem.UpsertByComparison(&args)
if curErr != nil {
return cur, curErr
}
if cur == nil {
return cur, ErrPathNotExists
}
index := cur.ToIndex()
wErr := g.context.SerializeIndex(index)
// TODO: Should indexing errors be reported?
if wErr != nil {
g.log.LogErrf("serializeIndex %s: %v\n", cur.Name, wErr)
}
g.mkdirAllCache.Put(d, newExpirableCacheValue(cur))
return cur, nil
}
func namedPipe(mode os.FileMode) bool {
return (mode & os.ModeNamedPipe) != 0
}
func symlink(mode os.FileMode) bool {
return (mode & os.ModeSymlink) != 0
}