diff --git a/README.md b/README.md index 31325e70..3a79cbae 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ To pull specific files or directories, pass in one or more paths: $ drive pull photos/img001.png docs ``` +Pulling by id is also supported + +```shell +$ drive pull --id 0fM9rt0Yc9RTPaDdsNzg1dXVjM0E 0fM9rt0Yc9RTPaTVGc1pzODN1NjQ 0fM9rt0Yc9RTPV1NaNFp5WlV3dlU +``` + ## Note: Checksum verification: @@ -275,6 +281,12 @@ The `pub` command publishes a file or directory globally so that anyone can view $ drive pub photos ``` ++ Publishing by fileId is also supported + +```shell +$ drive pub --id 0fM9rt0Yc9RTPV1NaNFp5WlV3dlU 0fM9rt0Yc9RTPSTZEanBsamZjUXM +``` + ### Unpublishing The `unpub` command is the opposite of `pub`. It unpublishes a previously published file or directory. @@ -283,6 +295,12 @@ The `unpub` command is the opposite of `pub`. It unpublishes a previously publis $ drive unpub photos ``` ++ Publishing by fileId is also supported + +```shell +$ drive unpub --id 0fM9rt0Yc9RTPV1NaNFp5WlV3dlU 0fM9rt0Yc9RTPSTZEanBsamZjUXM +``` + ### Sharing and Emailing The `share` command enables you to share a set of files with specific users and assign them specific roles as well as specific generic access to the files. It also allows for email notifications on share. @@ -297,6 +315,12 @@ For example to share a file with users of a mailing list and a custom message $ drive share -emails drive-mailing-list@gmail.com -message "Here is the drive code" -role group mnt/drive ``` ++ Also supports sharing by fileId + +```shell +$ drive share --emails developers@developers.devs --message "Developers, developers developers" --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Unsharing The `unshare` command revokes access of a specific accountType to a set of files. @@ -305,6 +329,11 @@ The `unshare` command revokes access of a specific accountType to a set of files $ drive unshare -type group mnt/drive ``` ++ Also supports unsharing by fileId + +```shell +$ drive unshare --type group --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` ### Touching Files that exist remotely can be touched i.e their modification time updated to that on the remote server using the `touch` command: @@ -319,6 +348,12 @@ For example to touch all files that begin with digits 0 to 9: $ drive touch -matches $(seq 0 9) ``` ++ Also supports touching of files by fileId + +```shell +$ drive touch --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Trashing and Untrashing Files can be trashed using the `trash` command: @@ -347,6 +382,13 @@ To untrash files that match a certain prefix pattern $ drive untrash -matches pQueue photos Untitled ``` ++ Also supports trashing/untrashing by fileId + +```shell +$ drive trash --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +$ drive untrash --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Emptying the Trash @@ -368,6 +410,12 @@ $ drive delete flux.mp4 $ drive delete --matches onyx swp ``` ++ Also supports deletion by fileIds + +```shell +$ drive delete --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Listing Files @@ -403,6 +451,12 @@ To get detailed information about the listings e.g owner information and the ver $ drive list -owners -l -version ``` ++ Also supports listing by fileIds + +```shell +$ drive list -m 3 --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Stating Files The `stat` commands show detailed file information for example people with whom it is shared, their roles and accountTypes, and @@ -418,6 +472,12 @@ By default `stat` won't recursively stat a directory, to enable recursive statin $ drive stat -r mnt ``` ++ Also supports stat-ing by fileIds + +```shell +$ drive stat -r --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U +``` + ### Quota The `quota` command prints information about your drive, such as the account type, bytes used/free, and the total amount of storage available. @@ -479,6 +539,13 @@ $ drive copy -r blobStore.py mnt flagging $ drive copy blobStore.py blobStoreDuplicated.py ``` ++ Also supports copying by fileIds + +```shell +$ drive copy -r --id 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U ../content +``` + + ### Rename drive allows you to rename a file/folder remotely. To do so: @@ -488,6 +555,13 @@ $ drive rename url_test url_test_results $ drive rename openSrc/2015 2015-Contributions ``` ++ Also supports renaming by fileId + +```shell +$ drive rename 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 fluxing +``` + + ### Move drive allows you to move content remotely between folders. To do so: @@ -496,6 +570,12 @@ drive allows you to move content remotely between folders. To do so: $ drive move photos/2015 angles library archives/storage ``` ++ Also supports moving by fileId + +```shell +$ drive rename 0fM9rt0Yc9RTPeHRfRHRRU0dIY97 0fM9rt0Yc9kJRPSTFNk9kSTVvb0U ../../new_location +``` + ### DriveIgnore diff --git a/cmd/drive/main.go b/cmd/drive/main.go index 242c788b..992b87d0 100644 --- a/cmd/drive/main.go +++ b/cmd/drive/main.go @@ -144,6 +144,7 @@ func (cmd *quotaCmd) Run(args []string) { } type listCmd struct { + byId *bool hidden *bool pageCount *int recursive *bool @@ -176,23 +177,13 @@ func (cmd *listCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.recursive = fs.Bool("r", false, "recursively list subdirectories") cmd.matches = fs.Bool("matches", false, "list by prefix") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "list by id instead of path") return fs } func (cmd *listCmd) Run(args []string) { - var path string - var sources []string - var context *config.Context - - if !*cmd.matches { - sources, context, path = preprocessArgs(args) - } else { - cwd, err := os.Getwd() - exitWithError(err) - sources = args - _, context, path = preprocessArgs([]string{cwd}) - } + sources, context, path := preprocessArgsByToggle(args, (*cmd.byId || *cmd.matches)) typeMask := 0 if *cmd.directories { @@ -235,7 +226,7 @@ func (cmd *listCmd) Run(args []string) { } else if *cmd.matches { exitWithError(drive.New(context, &options).ListMatches()) } else { - exitWithError(drive.New(context, &options).List()) + exitWithError(drive.New(context, &options).List(*cmd.byId)) } } @@ -243,28 +234,37 @@ type statCmd struct { hidden *bool recursive *bool quiet *bool + byId *bool } func (cmd *statCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "discover hidden paths") cmd.recursive = fs.Bool("r", false, "recursively discover folders") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "stat by id instead of path") return fs } func (cmd *statCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) - exitWithError(drive.New(context, &drive.Options{ + opts := drive.Options{ Hidden: *cmd.hidden, Path: path, Recursive: *cmd.recursive, Sources: sources, Quiet: *cmd.quiet, - }).Stat()) + } + + if *cmd.byId { + exitWithError(drive.New(context, &opts).StatById()) + } else { + exitWithError(drive.New(context, &opts).Stat()) + } } type pullCmd struct { + byId *bool exportsDir *string export *string excludeOps *string @@ -297,23 +297,13 @@ func (cmd *pullCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.piped = fs.Bool("piped", false, "if true, read content from stdin") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") cmd.excludeOps = fs.String(drive.CLIOptionExcludeOperations, "", drive.DescExcludeOps) + cmd.byId = fs.Bool(drive.CLIOptionId, false, "pull by id instead of path") return fs } func (cmd *pullCmd) Run(args []string) { - var path string - var sources []string - var context *config.Context - - if !*cmd.matches { - sources, context, path = preprocessArgs(args) - } else { - cwd, err := os.Getwd() - exitWithError(err) - sources = args - _, context, path = preprocessArgs([]string{cwd}) - } + sources, context, path := preprocessArgsByToggle(args, (*cmd.byId || *cmd.matches)) excludes := drive.NonEmptyTrimmedStrings(strings.Split(*cmd.excludeOps, ",")...) excludeCrudMask := drive.CrudAtoi(excludes...) @@ -345,9 +335,9 @@ func (cmd *pullCmd) Run(args []string) { if *cmd.matches { exitWithError(drive.New(context, options).PullMatches()) } else if *cmd.piped { - exitWithError(drive.New(context, options).PullPiped()) + exitWithError(drive.New(context, options).PullPiped(*cmd.byId)) } else { - exitWithError(drive.New(context, options).Pull()) + exitWithError(drive.New(context, options).Pull(*cmd.byId)) } } @@ -411,6 +401,7 @@ func (cmd *pushCmd) Run(args []string) { } type touchCmd struct { + byId *bool hidden *bool recursive *bool matches *bool @@ -422,28 +413,25 @@ func (cmd *touchCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.recursive = fs.Bool("r", false, "toggles recursive touching") cmd.matches = fs.Bool("matches", false, "search by prefix and touch") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "share by id instead of path") return fs } func (cmd *touchCmd) Run(args []string) { + sources, context, path := preprocessArgsByToggle(args, *cmd.matches || *cmd.byId) + + opts := drive.Options{ + Hidden: *cmd.hidden, + Path: path, + Recursive: *cmd.recursive, + Sources: sources, + Quiet: *cmd.quiet, + } + if *cmd.matches { - cwd, err := os.Getwd() - exitWithError(err) - _, context, path := preprocessArgs([]string{cwd}) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: args, - Quiet: *cmd.quiet, - }).TouchByMatch()) + exitWithError(drive.New(context, &opts).TouchByMatch()) } else { - sources, context, path := preprocessArgs(args) - exitWithError(drive.New(context, &drive.Options{ - Hidden: *cmd.hidden, - Path: path, - Recursive: *cmd.recursive, - Sources: sources, - Quiet: *cmd.quiet, - }).Touch()) + exitWithError(drive.New(context, &opts).Touch(*cmd.byId)) } } @@ -588,26 +576,29 @@ func (cmd *diffCmd) Run(args []string) { type publishCmd struct { hidden *bool quiet *bool + byId *bool } type unpublishCmd struct { hidden *bool quiet *bool + byId *bool } func (cmd *unpublishCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "allows pulling of hidden paths") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "unpublish by id instead of path") return fs } func (cmd *unpublishCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) exitWithError(drive.New(context, &drive.Options{ Path: path, Sources: sources, Quiet: *cmd.quiet, - }).Unpublish()) + }).Unpublish(*cmd.byId)) } type emptyTrashCmd struct { @@ -633,32 +624,30 @@ type deleteCmd struct { hidden *bool matches *bool quiet *bool + byId *bool } func (cmd *deleteCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "allows trashing hidden paths") cmd.matches = fs.Bool("matches", false, "search by prefix and trash") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "delete by id instead of path") return fs } func (cmd *deleteCmd) Run(args []string) { + sources, context, path := preprocessArgsByToggle(args, *cmd.matches || *cmd.byId) + + opts := drive.Options{ + Path: path, + Sources: sources, + Quiet: *cmd.quiet, + } + if !*cmd.matches { - sources, context, path := preprocessArgs(args) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: sources, - Quiet: *cmd.quiet, - }).Delete()) + exitWithError(drive.New(context, &opts).Delete(*cmd.byId)) } else { - cwd, err := os.Getwd() - exitWithError(err) - _, context, path := preprocessArgs([]string{cwd}) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: args, - Quiet: *cmd.quiet, - }).DeleteByMatch()) + exitWithError(drive.New(context, &opts).DeleteByMatch()) } } @@ -666,43 +655,42 @@ type trashCmd struct { hidden *bool matches *bool quiet *bool + byId *bool } func (cmd *trashCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "allows trashing hidden paths") cmd.matches = fs.Bool("matches", false, "search by prefix and trash") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "trash by id instead of path") return fs } func (cmd *trashCmd) Run(args []string) { + sources, context, path := preprocessArgsByToggle(args, *cmd.matches || *cmd.byId) + opts := drive.Options{ + Path: path, + Sources: sources, + Quiet: *cmd.quiet, + } + if !*cmd.matches { - sources, context, path := preprocessArgs(args) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: sources, - Quiet: *cmd.quiet, - }).Trash()) + exitWithError(drive.New(context, &opts).Trash(*cmd.byId)) } else { - cwd, err := os.Getwd() - exitWithError(err) - _, context, path := preprocessArgs([]string{cwd}) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: args, - Quiet: *cmd.quiet, - }).TrashByMatch()) + exitWithError(drive.New(context, &opts).TrashByMatch()) } } type copyCmd struct { quiet *bool recursive *bool + byId *bool } func (cmd *copyCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.recursive = fs.Bool("r", false, "recursive copying") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "copy by id instead of path") return fs } @@ -710,78 +698,95 @@ func (cmd *copyCmd) Run(args []string) { if len(args) < 2 { args = append(args, ".") } - sources, context, path := preprocessArgs(args) + + end := len(args) - 1 + if end < 1 { + exitWithError(fmt.Errorf("copy: expected more than one path")) + } + + dest := args[end] + + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) + // Unshift by the end path + sources = sources[:len(sources)-1] + destRels, err := relativePaths(context.AbsPathOf(""), dest) + exitWithError(err) + + dest = destRels[0] + sources = append(sources, dest) + exitWithError(drive.New(context, &drive.Options{ Path: path, Sources: sources, Recursive: *cmd.recursive, Quiet: *cmd.quiet, - }).Copy()) + }).Copy(*cmd.byId)) } type untrashCmd struct { hidden *bool matches *bool quiet *bool + byId *bool } func (cmd *untrashCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "allows untrashing hidden paths") cmd.matches = fs.Bool("matches", false, "search by prefix and untrash") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "untrash by id instead of path") return fs } func (cmd *untrashCmd) Run(args []string) { + sources, context, path := preprocessArgsByToggle(args, *cmd.byId || *cmd.matches) + + opts := drive.Options{ + Path: path, + Sources: sources, + Quiet: *cmd.quiet, + } + if !*cmd.matches { - sources, context, path := preprocessArgs(args) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: sources, - Quiet: *cmd.quiet, - }).Untrash()) + exitWithError(drive.New(context, &opts).Untrash(*cmd.byId)) } else { - cwd, err := os.Getwd() - exitWithError(err) - _, context, path := preprocessArgs([]string{cwd}) - exitWithError(drive.New(context, &drive.Options{ - Path: path, - Sources: args, - Quiet: *cmd.quiet, - }).UntrashByMatch()) + exitWithError(drive.New(context, &opts).UntrashByMatch()) } } func (cmd *publishCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.hidden = fs.Bool("hidden", false, "allows publishing of hidden paths") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "publish by id instead of path") return fs } func (cmd *publishCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) exitWithError(drive.New(context, &drive.Options{ Path: path, Sources: sources, Quiet: *cmd.quiet, - }).Publish()) + }).Publish(*cmd.byId)) } type unshareCmd struct { noPrompt *bool accountType *string quiet *bool + byId *bool } func (cmd *unshareCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.accountType = fs.String("type", "", "scope of account to revoke access to") cmd.noPrompt = fs.Bool("no-prompt", false, "disables the prompt") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "unshare by id instead of path") return fs } func (cmd *unshareCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) meta := map[string][]string{ "accountType": uniqOrderedStr(drive.NonEmptyTrimmedStrings(strings.Split(*cmd.accountType, ",")...)), @@ -793,45 +798,62 @@ func (cmd *unshareCmd) Run(args []string) { Sources: sources, NoPrompt: *cmd.noPrompt, Quiet: *cmd.quiet, - }).Unshare()) + }).Unshare(*cmd.byId)) } type moveCmd struct { quiet *bool + byId *bool } func (cmd *moveCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "unshare by id instead of path") return fs } func (cmd *moveCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + argc := len(args) + if argc < 1 { + exitWithError(fmt.Errorf("move: expecting a path or more")) + } + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) + // Unshift by the end path + sources = sources[:len(sources)-1] + + dest := args[argc-1] + destRels, err := relativePaths(context.AbsPathOf(""), dest) + exitWithError(err) + + sources = append(sources, destRels[0]) + exitWithError(drive.New(context, &drive.Options{ Path: path, Sources: sources, Quiet: *cmd.quiet, - }).Move()) + }).Move(*cmd.byId)) } type renameCmd struct { force *bool quiet *bool + byId *bool } func (cmd *renameCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.force = fs.Bool("force", false, "coerce rename even if remote already exists") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "unshare by id instead of path") return fs } func (cmd *renameCmd) Run(args []string) { argc := len(args) if argc < 2 { - exitWithError(fmt.Errorf("move: expecting ")) + exitWithError(fmt.Errorf("rename: expecting ")) } rest, last := args[:argc-1], args[argc-1] - sources, context, path := preprocessArgs(rest) + sources, context, path := preprocessArgsByToggle(rest, *cmd.byId) sources = append(sources, last) exitWithError(drive.New(context, &drive.Options{ @@ -839,10 +861,11 @@ func (cmd *renameCmd) Run(args []string) { Sources: sources, Force: *cmd.force, Quiet: *cmd.quiet, - }).Rename()) + }).Rename(*cmd.byId)) } type shareCmd struct { + byId *bool emails *string message *string role *string @@ -860,11 +883,12 @@ func (cmd *shareCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.notify = fs.Bool("notify", true, "toggle whether to notify receipients about share") cmd.noPrompt = fs.Bool("no-prompt", false, "disables the prompt") cmd.quiet = fs.Bool(drive.QuietKey, false, "if set, do not log anything but errors") + cmd.byId = fs.Bool(drive.CLIOptionId, false, "share by id instead of path") return fs } func (cmd *shareCmd) Run(args []string) { - sources, context, path := preprocessArgs(args) + sources, context, path := preprocessArgsByToggle(args, *cmd.byId) meta := map[string][]string{ "emailMessage": []string{*cmd.message}, @@ -885,7 +909,7 @@ func (cmd *shareCmd) Run(args []string) { TypeMask: mask, NoPrompt: *cmd.noPrompt, Quiet: *cmd.quiet, - }).Share()) + }).Share(*cmd.byId)) } func initContext(args []string) *config.Context { @@ -961,7 +985,7 @@ func exitWithError(err error) { } } -func relativePaths(root string, args []string) ([]string, error) { +func relativePaths(root string, args ...string) ([]string, error) { return relativePathsOpt(root, args, false) } @@ -1008,8 +1032,20 @@ func preprocessArgs(args []string) ([]string, *config.Context, string) { args = []string{"."} } - relPaths, err := relativePaths(root, args) + relPaths, err := relativePaths(root, args...) exitWithError(err) return uniqOrderedStr(relPaths), context, path } + +func preprocessArgsByToggle(args []string, skipArgPreprocess bool) (sources []string, context *config.Context, path string) { + if !skipArgPreprocess { + return preprocessArgs(args) + } + + cwd, err := os.Getwd() + exitWithError(err) + _, context, path = preprocessArgs([]string{cwd}) + sources = uniqOrderedStr(args) + return sources, context, path +} diff --git a/src/copy.go b/src/copy.go index 1a4356ee..966e202b 100644 --- a/src/copy.go +++ b/src/copy.go @@ -27,7 +27,7 @@ type copyArgs struct { dest *File } -func (g *Commands) Copy() error { +func (g *Commands) Copy(byId bool) error { argc := len(g.opts.Sources) if argc < 2 { return fmt.Errorf("expecting src [src1....] dest got: %v", g.opts.Sources) @@ -52,8 +52,13 @@ func (g *Commands) Copy() error { } } + srcResolver := g.rem.FindByPath + if byId { + srcResolver = g.rem.FindById + } + for _, srcPath := range sources { - srcFile, srcErr := g.rem.FindByPath(srcPath) + srcFile, srcErr := srcResolver(srcPath) if srcErr != nil { g.log.LogErrf("%s: %v\n", srcPath, srcErr) continue diff --git a/src/help.go b/src/help.go index 70cfa2e2..4bee628d 100644 --- a/src/help.go +++ b/src/help.go @@ -95,6 +95,7 @@ const ( CLIOptionIgnoreConflict = "ignore-conflict" CLIOptionIgnoreNameClashes = "ignore-name-clashes" CLIOptionExcludeOperations = "exclude-ops" + CLIOptionId = "id" ) var skipChecksumNote = fmt.Sprintf( diff --git a/src/list.go b/src/list.go index 7a7b36ef..85dfe4aa 100644 --- a/src/list.go +++ b/src/list.go @@ -15,6 +15,7 @@ package drive import ( + "fmt" "strconv" "strings" @@ -85,19 +86,20 @@ func (g *Commands) ListMatches() error { return nil } -func (g *Commands) List() (err error) { +func (g *Commands) List(byId bool) error { + var kvList []*keyValue + resolver := g.rem.FindByPath - if g.opts.InTrash { + if byId { + resolver = g.rem.FindById + } else if g.opts.InTrash { resolver = g.rem.FindByPathTrashed } - var kvList []*keyValue - for _, relPath := range g.opts.Sources { r, rErr := resolver(relPath) - if rErr != nil { - g.log.LogErrf("%v: '%s'\n", rErr, relPath) - return + if rErr != nil && rErr != ErrPathNotExists { + return fmt.Errorf("%v: '%s'", rErr, relPath) } if r == nil { @@ -105,7 +107,12 @@ func (g *Commands) List() (err error) { continue } - parentPath := g.parentPather(relPath) + parentPath := "" + if !byId { + parentPath = g.parentPather(relPath) + } else { + parentPath = r.Id + } if remoteRootLike(parentPath) { parentPath = "" @@ -140,7 +147,8 @@ func (g *Commands) List() (err error) { } } spin.stop() - return + + return nil } func (g *Commands) ListShared() (err error) { diff --git a/src/move.go b/src/move.go index 2a10156c..7279c877 100644 --- a/src/move.go +++ b/src/move.go @@ -19,7 +19,13 @@ import ( "path/filepath" ) -func (g *Commands) Move() (err error) { +type moveOpt struct { + src string + dest string + byId bool +} + +func (g *Commands) Move(byId bool) (err error) { argc := len(g.opts.Sources) if argc < 2 { return fmt.Errorf("move: expected [src...] , instead got: %v", g.opts.Sources) @@ -35,47 +41,60 @@ func (g *Commands) Move() (err error) { return fmt.Errorf("%s cannot be nested into %s", src, dest) } - err = g.move(src, dest) + opt := moveOpt{ + src: src, + dest: dest, + byId: byId, + } + + err = g.move(&opt) if err != nil { // TODO: Actually throw the error? Impact on UX if thrown? - fmt.Printf("%s: %v\n", src, err) + fmt.Printf("move: %s: %v\n", src, err) } } return nil } -func (g *Commands) move(src, dest string) (err error) { +func (g *Commands) move(opt *moveOpt) (err error) { var newParent, remSrc *File - if remSrc, err = g.rem.FindByPath(src); err != nil { - return fmt.Errorf("src('%s') %v", src, err) + srcResolver := g.rem.FindByPath + if opt.byId { + srcResolver = g.rem.FindById + } + + if remSrc, err = srcResolver(opt.src); err != nil { + return fmt.Errorf("src('%s') %v", opt.src, err) } if remSrc == nil { - return fmt.Errorf("src: '%s' could not be found", src) + return fmt.Errorf("src: '%s' could not be found", opt.src) } - if newParent, err = g.rem.FindByPath(dest); err != nil { - return fmt.Errorf("dest: '%s' %v", dest, err) + if newParent, err = g.rem.FindByPath(opt.dest); err != nil { + return fmt.Errorf("dest: '%s' %v", opt.dest, err) } if newParent == nil || !newParent.IsDir { - return fmt.Errorf("dest: '%s' must be an existant folder", dest) + return fmt.Errorf("dest: '%s' must be an existant folder", opt.dest) } - parentPath := g.parentPather(src) - oldParent, parErr := g.rem.FindByPath(parentPath) - if parErr != nil && parErr != ErrPathNotExists { - return parErr - } + if !opt.byId { + parentPath := g.parentPather(opt.src) + oldParent, parErr := g.rem.FindByPath(parentPath) + if parErr != nil && parErr != ErrPathNotExists { + return parErr + } - // TODO: If oldParent is not found, retry since it may have been moved temporarily at least - if oldParent != nil && oldParent.Id == newParent.Id { - return nil + // TODO: If oldParent is not found, retry since it may have been moved temporarily at least + if oldParent != nil && oldParent.Id == newParent.Id { + return nil + } } - newFullPath := filepath.Join(dest, remSrc.Name) + newFullPath := filepath.Join(opt.dest, remSrc.Name) // Check for a duplicate var dupCheck *File @@ -95,14 +114,17 @@ func (g *Commands) move(src, dest string) (err error) { // Avoid self-nesting if remSrc.Id == newParent.Id { - return fmt.Errorf("move: cannot move '%s' to itself", src) + return fmt.Errorf("move: cannot move '%s' to itself", opt.src) } if err = g.rem.insertParent(remSrc.Id, newParent.Id); err != nil { return err } - return g.removeParent(remSrc.Id, src) + if opt.byId { // TODO: Also take out this current parent + return nil + } + return g.removeParent(remSrc.Id, opt.src) } func (g *Commands) removeParent(fileId, relToRootPath string) error { @@ -117,13 +139,17 @@ func (g *Commands) removeParent(fileId, relToRootPath string) error { return g.rem.removeParent(fileId, parent.Id) } -func (g *Commands) Rename() error { +func (g *Commands) Rename(byId bool) error { if len(g.opts.Sources) < 2 { return fmt.Errorf("rename: expecting ") } src := g.opts.Sources[0] - remSrc, err := g.rem.FindByPath(src) + resolver := g.rem.FindByPath + if byId { + resolver = g.rem.FindById + } + remSrc, err := resolver(src) if err != nil { return fmt.Errorf("%s: %v", src, err) } @@ -131,7 +157,12 @@ func (g *Commands) Rename() error { return fmt.Errorf("%s does not exist", src) } - parentPath := g.parentPather(src) + var parentPath string + if !byId { + parentPath = g.parentPather(src) + } else { + parentPath = g.opts.Path + } newName := g.opts.Sources[1] urlBoundName := urlToPath(newName, true) diff --git a/src/publish.go b/src/publish.go index 7afea85c..3f9f28cb 100644 --- a/src/publish.go +++ b/src/publish.go @@ -14,21 +14,34 @@ package drive -func (c *Commands) Publish() (err error) { +import ( + "fmt" +) + +func (c *Commands) Publish(byId bool) (err error) { for _, relToRoot := range c.opts.Sources { - if pubErr := c.pub(relToRoot); pubErr != nil { + if pubErr := c.pub(relToRoot, byId); pubErr != nil { c.log.LogErrf("\033[91mPub\033[00m %s: %v\n", relToRoot, pubErr) } } return } -func (c *Commands) pub(relToRoot string) (err error) { - var file *File - file, err = c.rem.FindByPath(relToRoot) +func (c *Commands) remFileResolve(relToRoot string, byId bool) (*File, error) { + resolver := c.rem.FindByPath + if byId { + resolver = c.rem.FindById + } + + return resolver(relToRoot) +} + +func (c *Commands) pub(relToRoot string, byId bool) (err error) { + file, err := c.remFileResolve(relToRoot, byId) if err != nil { return err } + var link string link, err = c.rem.Publish(file.Id) if err != nil { @@ -37,23 +50,29 @@ func (c *Commands) pub(relToRoot string) (err error) { if hasExportLinks(file) { link = file.AlternateLink } - c.log.Logf("%s Published on %s\n", relToRoot, link) + + if byId { + relToRoot = fmt.Sprintf("%s aka %s", relToRoot, file.Name) + } + + c.log.Logf("%s published on %s\n", relToRoot, link) return } -func (c *Commands) Unpublish() error { +func (c *Commands) Unpublish(byId bool) error { for _, relToRoot := range c.opts.Sources { - if unpubErr := c.unpub(relToRoot); unpubErr != nil { + if unpubErr := c.unpub(relToRoot, byId); unpubErr != nil { c.log.LogErrf("\033[91mUnpub\033[00m %s: %v\n", relToRoot, unpubErr) } } return nil } -func (c *Commands) unpub(relToRoot string) error { - file, err := c.rem.FindByPath(relToRoot) +func (c *Commands) unpub(relToRoot string, byId bool) error { + file, err := c.remFileResolve(relToRoot, byId) if err != nil { return err } + return c.rem.Unpublish(file.Id) } diff --git a/src/pull.go b/src/pull.go index 7eb5db60..ecdfbf21 100644 --- a/src/pull.go +++ b/src/pull.go @@ -47,7 +47,7 @@ type downloadArg struct { // Pull from remote if remote path exists and in a god context. If path is a // directory, it recursively pulls from the remote if there are remote changes. // It doesn't check if there are remote changes if isForce is set. -func (g *Commands) Pull() (err error) { +func (g *Commands) Pull(byId bool) (err error) { var cl []*Change g.log.Logln("Resolving...") @@ -55,17 +55,12 @@ func (g *Commands) Pull() (err error) { spin := g.playabler() spin.play() - for _, relToRootPath := range g.opts.Sources { - fsPath := g.context.AbsPathOf(relToRootPath) - ccl, cErr := g.changeListResolve(relToRootPath, fsPath, false) - if cErr != nil { - return cErr - } - if len(ccl) > 0 { - cl = append(cl, ccl...) - } + resolver := g.pullByPath + if byId { + resolver = g.pullById } + cl, err = resolver() spin.stop() nonConflictsPtr, conflictsPtr := g.resolveConflicts(cl, false) @@ -132,10 +127,14 @@ func (g *Commands) PullMatches() (err error) { return g.playPullChanges(nonConflicts, g.opts.Exports, opMap) } -func (g *Commands) PullPiped() (err error) { - // Cannot pull asynchronously because the pull order must be maintained +func (g *Commands) PullPiped(byId bool) (err error) { + resolver := g.rem.FindByPath + if byId { + resolver = g.rem.FindById + } + for _, relToRootPath := range g.opts.Sources { - rem, err := g.rem.FindByPath(relToRootPath) + rem, err := resolver(relToRootPath) if err != nil { return fmt.Errorf("%s: %v", relToRootPath, err) } @@ -143,20 +142,72 @@ func (g *Commands) PullPiped() (err error) { continue } - if hasExportLinks(rem) { - g.log.LogErrf("'%s' is a GoogleDoc/Sheet document cannot be pulled from raw, only exported.\n", relToRootPath) - continue + err = g.pullAndDownload(relToRootPath, os.Stdout, rem, true) + if err != nil { + return err } - blobHandle, dlErr := g.rem.Download(rem.Id, "") - if dlErr != nil { - return dlErr + } + return nil +} + +func (g *Commands) pullById() (cl []*Change, err error) { + for _, srcId := range g.opts.Sources { + rem, remErr := g.rem.FindById(srcId) + if remErr != nil { + return cl, fmt.Errorf("pullById: %s: %v", srcId, remErr) } - if blobHandle == nil { + + if rem == nil { + g.log.LogErrf("%s does not exist\n", srcId) continue } - _, err = io.Copy(os.Stdout, blobHandle) - blobHandle.Close() + + relToRootPath := filepath.Join(g.opts.Path, rem.Name) + curAbsPath := g.context.AbsPathOf(relToRootPath) + local, resErr := g.resolveToLocalFile(rem.Name, curAbsPath) + if resErr != nil { + return cl, resErr + } + + ccl, clErr := g.doChangeListRecv(relToRootPath, curAbsPath, local, rem, false) + if clErr != nil { + return cl, clErr + } + cl = append(cl, ccl...) + } + + return cl, nil +} + +func (g *Commands) pullByPath() (cl []*Change, err error) { + for _, relToRootPath := range g.opts.Sources { + fsPath := g.context.AbsPathOf(relToRootPath) + ccl, cErr := g.changeListResolve(relToRootPath, fsPath, false) + if cErr != nil { + return cl, cErr + } + if len(ccl) > 0 { + cl = append(cl, ccl...) + } } + + return cl, nil +} + +func (g *Commands) pullAndDownload(relToRootPath string, fh io.Writer, rem *File, piped bool) (err error) { + if hasExportLinks(rem) { + return fmt.Errorf("'%s' is a GoogleDoc/Sheet document cannot be pulled from raw, only exported.\n", relToRootPath) + } + blobHandle, dlErr := g.rem.Download(rem.Id, "") + if dlErr != nil { + return dlErr + } + if blobHandle == nil { + return nil + } + + _, err = io.Copy(fh, blobHandle) + blobHandle.Close() return } diff --git a/src/share.go b/src/share.go index b8f9b91e..bf83e8e3 100644 --- a/src/share.go +++ b/src/share.go @@ -127,14 +127,19 @@ func stringToAccountType() func(string) AccountType { var reverseRoleResolve = stringToRole() var reverseAccountTypeResolve = stringToAccountType() -func (g *Commands) resolveRemotePaths(relToRootPaths []string) (files []*File) { +func (g *Commands) resolveRemotePaths(relToRootPaths []string, byId bool) (files []*File) { var wg sync.WaitGroup + resolver := g.rem.FindByPath + if byId { + resolver = g.rem.FindById + } + wg.Add(len(relToRootPaths)) for _, relToRoot := range relToRootPaths { go func(p string, wgg *sync.WaitGroup) { defer wgg.Done() - file, err := g.rem.FindByPath(p) + file, err := resolver(p) if err != nil || file == nil { return } @@ -162,12 +167,12 @@ func emailsToIds(g *Commands, emails []string) map[string]string { return emailToIds } -func (c *Commands) Unshare() (err error) { - return c.share(true) +func (c *Commands) Unshare(byId bool) (err error) { + return c.share(true, byId) } -func (c *Commands) Share() (err error) { - return c.share(false) +func (c *Commands) Share(byId bool) (err error) { + return c.share(false, byId) } func showPromptShareChanges(logy *log.Logger, change *shareChange) bool { @@ -238,8 +243,8 @@ func (c *Commands) playShareChanges(change *shareChange) error { return nil } -func (c *Commands) share(revoke bool) (err error) { - files := c.resolveRemotePaths(c.opts.Sources) +func (c *Commands) share(revoke, byId bool) (err error) { + files := c.resolveRemotePaths(c.opts.Sources, byId) var role Role var accountType AccountType diff --git a/src/stat.go b/src/stat.go index d827b1f9..aab3131a 100644 --- a/src/stat.go +++ b/src/stat.go @@ -28,6 +28,22 @@ type keyValue struct { value interface{} } +func (g *Commands) StatById() error { + for _, srcId := range g.opts.Sources { + f, err := g.rem.FindById(srcId) + if err != nil { + g.log.LogErrf("statById: %s err: %v\n", srcId, err) + continue + } + + fch := g.stat(srcId, f) + for _ = range fch { + } + } + + return nil +} + func (g *Commands) Stat() error { channelMap := make(map[int]chan *keyValue) var wg sync.WaitGroup @@ -99,7 +115,9 @@ func prettyFileStat(logf log.Loggerf, relToRootPath string, file *File) { } logf("\n\033[92m%s\033[00m\n", relToRootPath) + kvList := []*keyValue{ + &keyValue{"Filename", file.Name}, &keyValue{"FileId", file.Id}, &keyValue{"Bytes", fmt.Sprintf("%v", file.Size)}, &keyValue{"Size", prettyBytes(file.Size)}, @@ -107,12 +125,28 @@ func prettyFileStat(logf log.Loggerf, relToRootPath string, file *File) { &keyValue{"MimeType", file.MimeType}, &keyValue{"Etag", file.Etag}, &keyValue{"ModTime", fmt.Sprintf("%v", file.ModTime)}, + &keyValue{"Owners", sepJoin(" & ", file.OwnerNames...)}, + &keyValue{"LastModifyingUsername", file.LastModifyingUsername}, + } + + if file.Name != file.OriginalFilename { + kvList = append(kvList, &keyValue{"OriginalFilename", file.OriginalFilename}) } + + if file.Labels != nil { + kvList = append(kvList, + &keyValue{"Starred", fmt.Sprintf("%v", file.Labels.Starred)}, + &keyValue{"Viewed", fmt.Sprintf("%v", file.Labels.Viewed)}, + &keyValue{"Trashed", fmt.Sprintf("%v", file.Labels.Trashed)}, + &keyValue{"ViewersCanDownload", fmt.Sprintf("%v", file.Labels.Restricted)}, + ) + } + if !file.IsDir { kvList = append(kvList, &keyValue{"Md5Checksum", file.Md5Checksum}) } for _, kv := range kvList { - logf("%-20s %-30v\n", kv.key, kv.value.(string)) + logf("%-25s %-30v\n", kv.key, kv.value.(string)) } } diff --git a/src/touch.go b/src/touch.go index 84ba7258..61fb2083 100644 --- a/src/touch.go +++ b/src/touch.go @@ -18,14 +18,18 @@ import ( "time" ) -func (g *Commands) Touch() (err error) { +func (g *Commands) Touch(byId bool) (err error) { // Arbitrary value for rate limiter throttle := time.Tick(1e9 / 10) chanMap := map[int]chan *keyValue{} for i, relToRootPath := range g.opts.Sources { - chanMap[i] = g.touch(relToRootPath, "") + fileId := "" + if byId { + fileId = relToRootPath + } + chanMap[i] = g.touch(relToRootPath, fileId) <-throttle } diff --git a/src/trash.go b/src/trash.go index 1e32092a..0d063dd4 100644 --- a/src/trash.go +++ b/src/trash.go @@ -16,18 +16,40 @@ package drive import ( "fmt" + // "path/filepath" ) -func (g *Commands) Trash() (err error) { - return g.reduceForTrash(g.opts.Sources, true, false) +type trashOpt struct { + permanent bool + toTrash bool + byId bool } -func (g *Commands) Delete() (err error) { - return g.reduceForTrash(g.opts.Sources, true, true) +func (g *Commands) Trash(byId bool) (err error) { + opt := trashOpt{ + toTrash: true, + permanent: false, + byId: byId, + } + return g.reduceForTrash(g.opts.Sources, &opt) +} + +func (g *Commands) Delete(byId bool) (err error) { + opt := trashOpt{ + toTrash: true, + permanent: true, + byId: byId, + } + return g.reduceForTrash(g.opts.Sources, &opt) } -func (g *Commands) Untrash() (err error) { - return g.reduceForTrash(g.opts.Sources, false, false) +func (g *Commands) Untrash(byId bool) (err error) { + opt := trashOpt{ + toTrash: false, + permanent: false, + byId: byId, + } + return g.reduceForTrash(g.opts.Sources, &opt) } func (g *Commands) EmptyTrash() error { @@ -68,28 +90,39 @@ func (g *Commands) EmptyTrash() error { return err } -func (g *Commands) trasher(relToRoot string, toTrash bool) (change *Change, err error) { +func (g *Commands) trasher(relToRoot string, opt *trashOpt) (*Change, error) { var file *File - if relToRoot == "/" && toTrash { + if relToRoot == "/" && opt.toTrash { return nil, fmt.Errorf("Will not try to trash root.") } - if toTrash { - file, err = g.rem.FindByPath(relToRoot) - } else { - file, err = g.rem.FindByPathTrashed(relToRoot) + resolver := g.rem.FindByPathTrashed + if opt.byId { + resolver = g.rem.FindById + } else if opt.toTrash { + resolver = g.rem.FindByPath } + file, err := resolver(relToRoot) if err != nil { - return + return nil, err } - change = &Change{Path: relToRoot} - if toTrash { + if opt.byId { + if file.Labels != nil { + if file.Labels.Trashed == opt.toTrash { + return nil, fmt.Errorf("toTrash=%v set yet already file.Trash=%v", opt.toTrash, file.Labels.Trashed) + } + } + relToRoot = fmt.Sprintf("%s (%s)", relToRoot, file.Name) + } + + change := &Change{Path: relToRoot} + if opt.toTrash { change.Dest = file } else { change.Src = file } - return + return change, nil } func (g *Commands) trashByMatch(inTrash, permanent bool) error { @@ -125,7 +158,12 @@ func (g *Commands) trashByMatch(inTrash, permanent bool) error { return nil } - return g.playTrashChangeList(cl, toTrash, permanent) + opt := trashOpt{ + toTrash: toTrash, + permanent: permanent, + } + + return g.playTrashChangeList(cl, &opt) } func (g *Commands) TrashByMatch() error { @@ -140,10 +178,10 @@ func (g *Commands) DeleteByMatch() error { return g.trashByMatch(false, true) } -func (g *Commands) reduceForTrash(args []string, toTrash, permanent bool) error { +func (g *Commands) reduceForTrash(args []string, opt *trashOpt) error { var cl []*Change for _, relToRoot := range args { - c, cErr := g.trasher(relToRoot, toTrash) + c, cErr := g.trasher(relToRoot, opt) if cErr != nil { g.log.LogErrf("\033[91m'%s': %v\033[00m\n", relToRoot, cErr) } else if c != nil { @@ -155,24 +193,24 @@ func (g *Commands) reduceForTrash(args []string, toTrash, permanent bool) error if !ok { return nil } - if permanent && g.opts.canPrompt() { + if opt.permanent && g.opts.canPrompt() { if !promptForChanges("This operation is irreversible. Continue [Y/N] ") { return nil } } - return g.playTrashChangeList(cl, toTrash, permanent) + return g.playTrashChangeList(cl, opt) } -func (g *Commands) playTrashChangeList(cl []*Change, toTrash, permanent bool) (err error) { +func (g *Commands) playTrashChangeList(cl []*Change, opt *trashOpt) (err error) { trashSize, unTrashSize := reduceToSize(cl, SelectDest|SelectSrc) g.taskStart(trashSize + unTrashSize) var fn func(*Change) error - if permanent { + if opt.permanent { fn = g.remoteDelete } else { fn = g.remoteUntrash - if toTrash { + if opt.toTrash { fn = g.remoteTrash } } diff --git a/src/types.go b/src/types.go index d555e557..09e549ad 100644 --- a/src/types.go +++ b/src/types.go @@ -97,7 +97,10 @@ type File struct { // The onwers of this file. OwnerNames []string // Permissions contains the overall permissions for this file - Permissions []*drive.Permission + Permissions []*drive.Permission + LastModifyingUsername string + OriginalFilename string + Labels *drive.FileLabels } func NewRemoteFile(f *drive.File) *File { @@ -115,13 +118,16 @@ func NewRemoteFile(f *drive.File) *File { MimeType: f.MimeType, ModTime: mtime, // We must convert each title to match that on the FS. - Name: urlToPath(f.Title, true), - Size: f.FileSize, - Shared: f.Shared, - UserPermission: f.UserPermission, - Version: f.Version, - OwnerNames: f.OwnerNames, - Permissions: f.Permissions, + Name: urlToPath(f.Title, true), + Size: f.FileSize, + Shared: f.Shared, + UserPermission: f.UserPermission, + Version: f.Version, + OwnerNames: f.OwnerNames, + Permissions: f.Permissions, + LastModifyingUsername: f.LastModifyingUserName, + OriginalFilename: f.OriginalFilename, + Labels: f.Labels, } }