%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/misc.go |
// Copyright 2015 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 (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
"google.golang.org/api/googleapi"
expirableCache "github.com/odeke-em/cache"
spinner "github.com/odeke-em/cli-spinner"
"github.com/odeke-em/drive/config"
)
var (
// ErrRejectedTerms is empty "" because messages might be too
// verbose to affirm a rejection that a user has already seen
ErrRejectedTerms = errors.New("")
)
const (
MimeTypeJoiner = "-"
RemoteDriveRootPath = "My Drive"
RemoteSeparator = "/"
FmtTimeString = "2006-01-02T15:04:05.000Z"
MsgClashesFixedNowRetry = "Clashes were fixed, please retry the operation"
MsgErrFileNotMutable = "File not mutable"
DriveIgnoreSuffix = ".driveignore"
DriveIgnoreNegativeLookAheadToken = "!"
)
const (
MaxFailedRetryCount = 20 // Arbitrary value
)
var DefaultMaxProcs = runtime.NumCPU()
var BytesPerKB = float64(1024)
type desktopEntry struct {
name string
url string
icon string
}
type playable struct {
play func()
pause func()
reset func()
stop func()
}
func noop() {
}
type tuple struct {
first interface{}
second interface{}
last interface{}
}
type jobSt struct {
id uint64
do func() (interface{}, error)
}
func (js jobSt) Id() interface{} {
return js.id
}
func (js jobSt) Do() (interface{}, error) {
return js.do()
}
type changeJobSt struct {
change *Change
verb string
throttle <-chan time.Time
fn func(*Change) error
}
func (cjs *changeJobSt) changeJober(g *Commands) func() (interface{}, error) {
return func() (interface{}, error) {
ch := cjs.change
verb := cjs.verb
canPrintSteps := g.opts.Verbose && g.opts.canPreview()
if canPrintSteps {
g.log.Logf("\033[01m%s::Started %s\033[00m\n", verb, ch.Path)
}
err := cjs.fn(ch)
if canPrintSteps {
g.log.Logf("\033[04m%s::Done %s\033[00m\n", verb, ch.Path)
}
<-cjs.throttle
return ch.Path, err
}
}
func retryableErrorCheck(v interface{}) (ok, retryable bool) {
pr, pOk := v.(*tuple)
if pr == nil || !pOk {
retryable = true
return
}
if pr.last == nil {
ok = true
return
}
err, assertOk := pr.last.(*googleapi.Error)
// In relation to https://github.com/google/google-api-go-client/issues/93
// where not every error is of googleapi.Error instance e.g io timeout errors
// etc, let's assume that non-nil errors are retryable
if !assertOk {
retryable = true
return
}
if err == nil {
ok = true
return
}
if strings.EqualFold(err.Message, MsgErrFileNotMutable) {
retryable = false
return
}
statusCode := err.Code
if statusCode >= 500 && statusCode <= 599 {
retryable = true
return
}
switch statusCode {
case 401, 403:
retryable = true
// TODO: Add other errors
}
return
}
func noopPlayable() *playable {
return &playable{
play: noop,
pause: noop,
reset: noop,
stop: noop,
}
}
func parseTime(ts string, round bool) (t time.Time) {
t, _ = time.Parse(FmtTimeString, ts)
if !round {
return
}
return t.Round(time.Second)
}
func parseTimeAndRound(ts string) (t time.Time) {
return parseTime(ts, true)
}
func internalIgnores() (ignores []string) {
if runtime.GOOS == OSLinuxKey {
ignores = append(ignores, "\\.\\s*desktop$")
}
return ignores
}
func newPlayable(freq int64) *playable {
spin := spinner.New(freq)
play := func() {
spin.Start()
}
return &playable{
play: play,
stop: spin.Stop,
reset: spin.Reset,
pause: spin.Stop,
}
}
func (g *Commands) playabler() *playable {
if !g.opts.canPrompt() {
return noopPlayable()
}
return newPlayable(10)
}
func rootLike(p string) bool {
return p == "/" || p == "" || p == "root"
}
func remoteRootLike(p string) bool {
return p == RemoteDriveRootPath
}
type byteDescription func(b int64) string
func memoizeBytes() byteDescription {
cache := map[int64]string{}
suffixes := []string{"B", "KB", "MB", "GB", "TB", "PB"}
maxLen := len(suffixes) - 1
var cacheMu sync.Mutex
return func(b int64) string {
cacheMu.Lock()
defer cacheMu.Unlock()
description, ok := cache[b]
if ok {
return description
}
bf := float64(b)
i := 0
description = ""
for {
if bf/BytesPerKB < 1 || i >= maxLen {
description = fmt.Sprintf("%.2f%s", bf, suffixes[i])
break
}
bf /= BytesPerKB
i += 1
}
cache[b] = description
return description
}
}
var prettyBytes = memoizeBytes()
func sepJoin(sep string, args ...string) string {
return strings.Join(args, sep)
}
func sepJoinNonEmpty(sep string, args ...string) string {
nonEmpties := NonEmptyStrings(args...)
return sepJoin(sep, nonEmpties...)
}
func _centricPathJoin(sep string, segments ...string) string {
// Always ensure that the first segment is the separator
segments = append([]string{sep}, segments...)
joins := sepJoinNonEmpty(sep, segments...)
return path.Clean(joins)
}
func localPathJoin(segments ...string) string {
return _centricPathJoin(UnescapedPathSep, segments...)
}
func remotePathJoin(segments ...string) string {
return _centricPathJoin(RemoteSeparator, segments...)
}
func isHidden(p string, ignore bool) bool {
if strings.HasPrefix(p, ".") {
return !ignore
}
return false
}
func prompt(r *os.File, w *os.File, promptText ...interface{}) (input string) {
fmt.Fprint(w, promptText...)
flushTTYin()
fmt.Fscanln(r, &input)
return
}
func nextPage() bool {
input := prompt(os.Stdin, os.Stdout, "---More---")
if len(input) >= 1 && strings.ToLower(input[:1]) == QuitShortKey {
return false
}
return true
}
func promptForChanges(args ...interface{}) Agreement {
argv := []interface{}{
"Proceed with the changes? [Y/n]: ",
}
if len(args) >= 1 {
argv = args
}
input := prompt(os.Stdin, os.Stdout, argv...)
if input == "" {
input = YesShortKey
}
if strings.ToUpper(input) == YesShortKey {
return Accepted
}
return Rejected
}
func (f *File) toDesktopEntry(urlMExt *urlMimeTypeExt) *desktopEntry {
name := f.Name
if urlMExt.ext != "" {
name = sepJoin("-", f.Name, urlMExt.ext)
}
return &desktopEntry{
name: name,
url: urlMExt.url,
icon: urlMExt.mimeType,
}
}
func (f *File) serializeAsDesktopEntry(destPath string, urlMExt *urlMimeTypeExt) (int, error) {
deskEnt := f.toDesktopEntry(urlMExt)
handle, err := os.Create(destPath)
if err != nil {
return 0, err
}
defer func() {
handle.Close()
chmodErr := os.Chmod(destPath, 0755)
if chmodErr != nil {
fmt.Fprintf(os.Stderr, "%s: [desktopEntry]::chmod %v\n", destPath, chmodErr)
}
chTimeErr := os.Chtimes(destPath, f.ModTime, f.ModTime)
if chTimeErr != nil {
fmt.Fprintf(os.Stderr, "%s: [desktopEntry]::chtime %v\n", destPath, chTimeErr)
}
}()
icon := strings.Replace(deskEnt.icon, UnescapedPathSep, MimeTypeJoiner, -1)
return fmt.Fprintf(handle, "[Desktop Entry]\nIcon=%s\nName=%s\nType=%s\nURL=%s\n",
icon, deskEnt.name, LinkKey, deskEnt.url)
}
func remotePathSplit(p string) (dir, base string) {
// Avoiding use of filepath.Split because of bug with trailing "/" not being stripped
sp := strings.Split(p, "/")
spl := len(sp)
dirL, baseL := sp[:spl-1], sp[spl-1:]
dir = strings.Join(dirL, "/")
base = strings.Join(baseL, "/")
return
}
func commonPrefix(values ...string) string {
vLen := len(values)
if vLen < 1 {
return ""
}
minIndex := 0
min := values[0]
minLen := len(min)
for i := 1; i < vLen; i += 1 {
st := values[i]
if st == "" {
return ""
}
lst := len(st)
if lst < minLen {
min = st
minLen = lst
minIndex = i + 0
}
}
prefix := make([]byte, minLen)
matchOn := true
for i := 0; i < minLen; i += 1 {
for j, other := range values {
if minIndex == j {
continue
}
if other[i] != min[i] {
matchOn = false
break
}
}
if !matchOn {
break
}
prefix[i] = min[i]
}
return string(prefix)
}
func ReadFullFile(p string) (clauses []string, err error) {
return readFile_(p, nil)
}
func fReadFile_(f io.Reader, ignorer func(string) bool) (clauses []string, err error) {
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
line = strings.Trim(line, " ")
line = strings.Trim(line, "\n")
if ignorer != nil && ignorer(line) {
continue
}
clauses = append(clauses, line)
}
return
}
func readFileFromStdin(ignorer func(string) bool) (clauses []string, err error) {
return fReadFile_(os.Stdin, ignorer)
}
func readFile_(p string, ignorer func(string) bool) (clauses []string, err error) {
f, fErr := os.Open(p)
if fErr != nil || f == nil {
err = fErr
return
}
defer f.Close()
clauses, err = fReadFile_(f, ignorer)
return
}
func readCommentedFile(p, comment string) (clauses []string, err error) {
ignorer := func(line string) bool {
return strings.HasPrefix(line, comment) || len(line) < 1
}
return readFile_(p, ignorer)
}
func chunkInt64(v int64) chan int {
var maxInt int
maxInt = 1<<31 - 1
maxIntCast := int64(maxInt)
chunks := make(chan int)
go func() {
q, r := v/maxIntCast, v%maxIntCast
for i := int64(0); i < q; i += 1 {
chunks <- maxInt
}
if r > 0 {
chunks <- int(r)
}
close(chunks)
}()
return chunks
}
func nonEmptyStrings(fn func(string) string, v ...string) (splits []string) {
for _, elem := range v {
if fn != nil {
elem = fn(elem)
}
if elem != "" {
splits = append(splits, elem)
}
}
return
}
func NonEmptyStrings(v ...string) (splits []string) {
return nonEmptyStrings(nil, v...)
}
func NonEmptyTrimmedStrings(v ...string) (splits []string) {
return nonEmptyStrings(strings.TrimSpace, v...)
}
var regExtStrMap = map[string]string{
"csv": "text/csv",
"html?": "text/html",
"te?xt": "text/plain",
"xml": "text/xml",
"gif": "image/gif",
"png": "image/png",
"svg": "image/svg+xml",
"jpe?g": "image/jpeg",
"odt": "application/vnd.oasis.opendocument.text",
"odm": "application/vnd.oasis.opendocument.text-master",
"ott": "application/vnd.oasis.opendocument.text-template",
"ods": "application/vnd.oasis.opendocument.spreadsheet",
"ots": "application/vnd.oasis.opendocument.spreadsheet-template",
"odg": "application/vnd.oasis.opendocument.graphics",
"otg": "application/vnd.oasis.opendocument.graphics-template",
"oth": "application/vnd.oasis.opendocument.text-web",
"odp": "application/vnd.oasis.opendocument.presentation",
"otp": "application/vnd.oasis.opendocument.presentation-template",
"odi": "application/vnd.oasis.opendocument.image",
"odb": "application/vnd.oasis.opendocument.database",
"oxt": "application/vnd.openofficeorg.extension",
"rtf": "application/rtf",
"pdf": "application/pdf",
"json": "application/json",
"js": "application/x-javascript",
"apk": "application/vnd.android.package-archive",
"bin": "application/octet-stream",
"tiff?": "image/tiff",
"tgz": "application/x-compressed",
"zip": "application/zip",
"mp3": "audio/mpeg",
// docs and docx should not collide if "docx?" is used so terminate with "$"
"docx?$": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"ppt$": "application/vnd.ms-powerpoint",
"pptx$": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"tsv": "text/tab-separated-values",
"xlsx?": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
func regMapper(srcMaps ...map[string]string) map[*regexp.Regexp]string {
regMap := make(map[*regexp.Regexp]string)
for _, srcMap := range srcMaps {
for regStr, resolve := range srcMap {
regExComp, err := regexp.Compile(regStr)
if err == nil {
regMap[regExComp] = resolve
}
}
}
return regMap
}
func cacher(regMap map[*regexp.Regexp]string) func(string) string {
var cache = make(map[string]string)
var cacheMu sync.Mutex
return func(ext string) string {
cacheMu.Lock()
defer cacheMu.Unlock()
memoized, ok := cache[ext]
if ok {
return memoized
}
bExt := []byte(ext)
for regEx, mimeType := range regMap {
if regEx != nil && regEx.Match(bExt) {
memoized = mimeType
break
}
}
cache[ext] = memoized
return memoized
}
}
func anyMatch(ignore func(string) bool, args ...string) bool {
if ignore == nil {
return false
}
for _, arg := range args {
if ignore(arg) {
return true
}
}
return false
}
func siftExcludes(clauses []string) (excludes, includes []string) {
alreadySeenExclude := map[string]bool{}
alreadySeenInclude := map[string]bool{}
for _, clause := range clauses {
memoizerPtr := &alreadySeenExclude
ptr := &excludes
// Because Go lacks the negative lookahead ?! capability
// it is necessary to avoid
if strings.HasPrefix(clause, DriveIgnoreNegativeLookAheadToken) {
ptr = &includes
rest := strings.Split(clause, DriveIgnoreNegativeLookAheadToken)
if len(rest) > 1 {
memoizerPtr = &alreadySeenInclude
clause = sepJoin("", rest[1:]...)
}
}
memoizer := *memoizerPtr
if _, alreadySeen := memoizer[clause]; alreadySeen {
continue
}
*ptr = append(*ptr, clause)
memoizer[clause] = true
}
return
}
func ignorerByClause(clauses ...string) (ignorer func(string) bool, err error) {
if len(clauses) < 1 {
return nil, nil
}
excludes, includes := siftExcludes(clauses)
var excludesRegComp, includesComp *regexp.Regexp
if len(excludes) >= 1 {
excRegComp, excRegErr := regexp.Compile(strings.Join(excludes, "|"))
if excRegErr != nil {
err = combineErrors(err, makeErrorWithStatus("excludeIgnoreRegErr", excRegErr, StatusIllogicalState))
return
}
excludesRegComp = excRegComp
}
if len(includes) >= 1 {
incRegComp, incRegErr := regexp.Compile(strings.Join(includes, "|"))
if incRegErr != nil {
err = combineErrors(err, makeErrorWithStatus("includeIgnoreRegErr", incRegErr, StatusIllogicalState))
return
}
includesComp = incRegComp
}
ignorer = func(s string) bool {
sb := []byte(s)
if excludesRegComp != nil && excludesRegComp.Match(sb) {
if includesComp != nil && includesComp.Match(sb) {
return false
}
return true
}
return false
}
return ignorer, nil
}
func combineIgnores(ignoresPath string) (ignorer func(string) bool, err error) {
clauses, err := readCommentedFile(ignoresPath, "#")
if err != nil && !os.IsNotExist(err) {
return nil, err
}
// TODO: Should internalIgnores only be added only
// after all the exclusion and exclusion steps.
clauses = append(clauses, internalIgnores()...)
return ignorerByClause(clauses...)
}
var mimeTypeFromQuery = cacher(regMapper(regExtStrMap, map[string]string{
"docs": "application/vnd.google-apps.document",
"folder": DriveFolderMimeType,
"form": "application/vnd.google-apps.form",
"mp4": "video/mp4",
"slides?|presentation": "application/vnd.google-apps.presentation",
"sheet": "application/vnd.google-apps.spreadsheet",
"script": "application/vnd.google-apps.script",
}))
var mimeTypeFromExt = cacher(regMapper(regExtStrMap))
func guessMimeType(p string) string {
resolvedMimeType := mimeTypeFromExt(p)
return resolvedMimeType
}
func CrudAtoi(ops ...string) CrudValue {
opValue := None
for _, op := range ops {
if len(op) < 1 {
continue
}
first := op[0]
if first == 'c' || first == 'C' {
opValue |= Create
} else if first == 'r' || first == 'R' {
opValue |= Read
} else if first == 'u' || first == 'U' {
opValue |= Update
} else if first == 'd' || first == 'D' {
opValue |= Delete
}
}
return opValue
}
func httpOk(statusCode int) bool {
return statusCode >= 200 && statusCode <= 299
}
func hasAnyPrefix(value string, prefixes ...string) bool {
return _hasAnyAtExtreme(value, strings.HasPrefix, prefixes)
}
func hasAnySuffix(value string, prefixes ...string) bool {
return _hasAnyAtExtreme(value, strings.HasSuffix, prefixes)
}
func _hasAnyAtExtreme(value string, fn func(string, string) bool, queries []string) bool {
for _, query := range queries {
if fn(value, query) {
return true
}
}
return false
}
func maxProcs() int {
maxProcs, err := strconv.ParseInt(os.Getenv(DriveGoMaxProcsKey), 10, 0)
if err != nil {
return DefaultMaxProcs
}
maxProcsInt := int(maxProcs)
if maxProcsInt < 1 {
return DefaultMaxProcs
}
return maxProcsInt
}
func customQuote(s string) string {
/*
See
+ https://github.com/golang/go/issues/11511
+ https://github.com/odeke-em/drive/issues/250
*/
return "\"" + strings.Replace(strings.Replace(s, "\\", "\\\\", -1), "\"", "\\\"", -1) + "\""
}
func newExpirableCacheValue(v interface{}) *expirableCache.ExpirableValue {
return expirableCache.NewExpirableValueWithOffset(v, uint64(time.Hour))
}
func combineErrors(prevErr, supplementaryErr error) error {
if prevErr == nil && supplementaryErr == nil {
return nil
}
if supplementaryErr == nil {
return prevErr
}
newErr := reComposeError(prevErr, supplementaryErr.Error())
if codedErr, ok := supplementaryErr.(*Error); ok {
return makeError(newErr, ErrorStatus(codedErr.Code()))
}
return newErr
}
// copyErrStatus copies the error status code from fromErr
// to toErr only if toErr and fromErr are both non-nil.
func copyErrStatusCode(toErr, fromErr error) error {
if toErr == nil || fromErr == nil {
return toErr
}
codedErr, hasCode := fromErr.(*Error)
if hasCode {
toErr = makeError(toErr, ErrorStatus(codedErr.Code()))
}
return toErr
}
func reComposeError(prevErr error, messages ...string) error {
if len(messages) < 1 {
return prevErr
}
joinedMessage := messages[0]
for i, n := 1, len(messages); i < n; i++ {
joinedMessage = fmt.Sprintf("%s\n%s", joinedMessage, messages[i])
}
if prevErr == nil {
if len(joinedMessage) < 1 {
return nil
}
} else {
joinedMessage = fmt.Sprintf("%v\n%s", prevErr, joinedMessage)
}
err := errors.New(joinedMessage)
codedErr, hasCode := prevErr.(*Error)
if !hasCode {
return err
}
return makeError(err, ErrorStatus(codedErr.Code()))
}
func CopyOptionsFromKeysIfNotSet(fromPtr, toPtr *Options, alreadySetKeys map[string]bool) {
from := *fromPtr
fromValue := reflect.ValueOf(from)
toValue := reflect.ValueOf(toPtr).Elem()
fromType := reflect.TypeOf(from)
for i, n := 0, fromType.NumField(); i < n; i++ {
fromFieldT := fromType.Field(i)
fromTag := fromFieldT.Tag.Get("cli")
_, alreadySet := alreadySetKeys[fromTag]
if alreadySet {
continue
}
fromFieldV := fromValue.Field(i)
toFieldV := toValue.Field(i)
if !toFieldV.CanSet() {
continue
}
toFieldV.Set(fromFieldV)
}
}
type CliSifter struct {
From interface{}
Defaults map[string]interface{}
AlreadyDefined map[string]bool
}
func SiftCliTags(cs *CliSifter) string {
from := cs.From
defaults := cs.Defaults
alreadyDefined := cs.AlreadyDefined
fromValue := reflect.ValueOf(from)
fromType := reflect.TypeOf(from)
mapping := map[string]string{}
for i, n := 0, fromType.NumField(); i < n; i++ {
fromFieldT := fromType.Field(i)
fromTag := fromFieldT.Tag.Get("json")
if fromTag == "" {
continue
}
fromFieldV := fromValue.Field(i)
elem := fromFieldV.Elem()
if _, defined := alreadyDefined[fromTag]; !defined {
if retr, defaultSet := defaults[fromTag]; defaultSet {
elem = reflect.ValueOf(retr)
}
}
stringified := ""
switch elem.Kind() {
case reflect.String:
stringified = fmt.Sprintf("%q", elem)
case reflect.Invalid:
continue
default:
stringified = fmt.Sprintf("%v", elem.Interface())
}
mapping[fromTag] = stringified
}
joined := []string{}
for k, v := range mapping {
joined = append(joined, fmt.Sprintf("%q:%v", k, v))
}
stringified := sepJoin(",", joined...)
return fmt.Sprintf("{%v}", stringified)
}
func decrementTraversalDepth(d int) int {
// Anything less than 0 is a request for infinite traversal
// 0 is the minimum positive traversal
if d <= 0 {
return d
}
return d - 1
}
type fsListingArg struct {
context *config.Context
parent string
hidden bool
ignore func(string) bool
depth int
}
func list(flArg *fsListingArg) (fileChan chan *File, err error) {
context := flArg.context
p := flArg.parent
hidden := flArg.hidden
ignore := flArg.ignore
depth := flArg.depth
absPath := context.AbsPathOf(p)
var f []os.FileInfo
f, err = ioutil.ReadDir(absPath)
fileChan = make(chan *File)
if err != nil {
close(fileChan)
return
}
go func() {
defer close(fileChan)
depth = decrementTraversalDepth(depth)
if depth == 0 {
return
}
for _, file := range f {
fileName := file.Name()
if fileName == config.GDDirSuffix {
continue
}
if isHidden(fileName, hidden) {
continue
}
resPath := path.Join(absPath, fileName)
if anyMatch(ignore, fileName, resPath) {
continue
}
// TODO: (@odeke-em) decide on how to deal with isFifo
if namedPipe(file.Mode()) {
fmt.Fprintf(os.Stderr, "%s (%s) is a named pipe, not reading from it\n", p, resPath)
continue
}
if !symlink(file.Mode()) {
fileChan <- NewLocalFile(resPath, file)
} else {
var symResolvPath string
symResolvPath, err = filepath.EvalSymlinks(resPath)
if err != nil {
continue
}
if anyMatch(ignore, symResolvPath) {
continue
}
var symInfo os.FileInfo
symInfo, err = os.Stat(symResolvPath)
if err != nil {
continue
}
lf := NewLocalFile(symResolvPath, symInfo)
// Retain the original name as appeared in
// the manifest instead of the resolved one
lf.Name = fileName
fileChan <- lf
}
}
}()
return
}
func resolver(g *Commands, byId bool, sources []string, fileOp func(*File) interface{}) (kvChan chan *keyValue) {
resolve := g.rem.FindByPath
if byId {
resolve = g.rem.FindById
}
kvChan = make(chan *keyValue)
go func() {
defer close(kvChan)
for _, source := range sources {
f, err := resolve(source)
kv := keyValue{key: source, value: err}
if err == nil {
kv.value = fileOp(f)
}
kvChan <- &kv
}
}()
return kvChan
}
func NotExist(err error) bool {
return os.IsNotExist(err) || err == ErrPathNotExists
}
func localOpToChangerTranslator(g *Commands, c *Change) func(*Change, []string) error {
var fn func(*Change, []string) error = nil
op := c.Op()
switch op {
case OpMod:
fn = g.localMod
case OpModConflict:
fn = g.localMod
case OpAdd:
fn = g.localAdd
case OpDelete:
fn = g.localDelete
case OpIndexAddition:
fn = g.localAddIndex
}
return fn
}
func remoteOpToChangerTranslator(g *Commands, c *Change) func(*Change) error {
var fn func(*Change) error = nil
op := c.Op()
switch op {
case OpMod:
fn = g.remoteMod
case OpModConflict:
fn = g.remoteMod
case OpAdd:
fn = g.remoteAdd
case OpDelete:
fn = g.remoteTrash
}
return fn
}
const (
// Since we'll be sharing `TypeMask` with other bit flags
// we'll need to avoid collisions with other args
InTrash int = 1 << (31 - 1 - iota)
Folder
Shared
Owners
Minimal
Starred
NonFolder
DiskUsageOnly
CurrentVersion
)
func folderExplicitly(mask int) bool { return (mask & Folder) == Folder }
func nonFolderExplicitly(mask int) bool { return (mask & NonFolder) == NonFolder }
type driveFileFilter func(*File) bool
func makeFileFilter(mask int) driveFileFilter {
return func(f *File) bool {
// TODO: Decide if nil files should be either a pass or fail?
if f == nil {
return true
}
truths := []bool{}
if nonFolderExplicitly(mask) {
truths = append(truths, !f.IsDir)
}
// Even though regular file & folder are mutually exclusive let's
// compare them separately in case we want this logical inconsistency
// instead of an if...else clause
if folderExplicitly(mask) {
truths = append(truths, f.IsDir)
}
return allTruthsHold(truths...)
}
}
func allTruthsHold(truths ...bool) bool {
for _, truth := range truths {
if !truth {
return false
}
}
return true
}
func parseDate(dateStr string, fmtSpecifiers ...string) (*time.Time, error) {
var err error
for _, fmtSpecifier := range fmtSpecifiers {
var t time.Time
t, err = time.Parse(fmtSpecifier, dateStr)
if err == nil {
return &t, err
}
}
return nil, err
}
func parseDurationOffsetFromNow(durationOffsetStr string) (*time.Time, error) {
d, err := time.ParseDuration(durationOffsetStr)
if err != nil {
return nil, err
}
offsetFromNow := time.Now().Add(d)
return &offsetFromNow, nil
}
// Debug returns true if DRIVE_DEBUG is set in the environment.
// Set it to anything non-empty, for example `DRIVE_DEBUG=true`.
func Debug() bool {
return os.Getenv("DRIVE_DEBUG") != ""
}
func DebugPrintf(fmt_ string, args ...interface{}) {
FDebugPrintf(os.Stdout, fmt_, args...)
}
// FDebugPrintf will only print output to the out writer if
// environment variable `DRIVE_DEBUG` is set. It prints out a header
// on a newline containing the introspection of the callsite,
// and then the formatted message you'd like,
// appending an obligatory newline at the end.
// The output will be of the form:
// [<FILE>:<FUNCTION>:<LINE_NUMBER>]
// <MSG>\n
func FDebugPrintf(f io.Writer, fmt_ string, args ...interface{}) {
if !Debug() {
return
}
if f == nil {
f = os.Stdout
}
programCounter, file, line, _ := runtime.Caller(2)
fn := runtime.FuncForPC(programCounter)
prefix := fmt.Sprintf("[\033[32m%s:%s:\033[33m%d\033[00m]\n%s\n", file, fn.Name(), line, fmt_)
fmt.Fprintf(f, prefix, args...)
}