aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--cli/cli.go509
-rw-r--r--cli/context.go41
-rw-r--r--cli/flags.go132
-rw-r--r--cli/handler.go125
-rw-r--r--cli/parser.go307
-rw-r--r--client/auth.go32
-rw-r--r--client/client.go28
-rw-r--r--drive.go2
-rw-r--r--drive/files.go68
-rw-r--r--drive/types.go37
-rw-r--r--drive/util.go20
-rw-r--r--gdrive.go270
-rw-r--r--gdrive/handlers.go0
-rw-r--r--handlers.go100
-rw-r--r--util.go29
16 files changed, 1191 insertions, 511 deletions
diff --git a/.gitignore b/.gitignore
index 6b18ed8..36d220a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,9 @@
# Ignore bin folder and drive binary
_release/bin
-drive
# vim files
.*.sw[a-z]
*.un~
Session.vim
.netrwhist
+drive_old
diff --git a/cli/cli.go b/cli/cli.go
deleted file mode 100644
index b662841..0000000
--- a/cli/cli.go
+++ /dev/null
@@ -1,509 +0,0 @@
-package cli
-
-import (
- "fmt"
- "github.com/prasmussen/gdrive/gdrive"
- "github.com/prasmussen/gdrive/util"
- "github.com/prasmussen/google-api-go-client/drive/v2"
- "golang.org/x/net/context"
- "io"
- "mime"
- "os"
- "path/filepath"
- "strings"
-)
-
-// List of google docs mime types excluding vnd.google-apps.folder
-var googleMimeTypes = []string{
- "application/vnd.google-apps.audio",
- "application/vnd.google-apps.document",
- "application/vnd.google-apps.drawing",
- "application/vnd.google-apps.file",
- "application/vnd.google-apps.form",
- "application/vnd.google-apps.fusiontable",
- "application/vnd.google-apps.photo",
- "application/vnd.google-apps.presentation",
- "application/vnd.google-apps.script",
- "application/vnd.google-apps.sites",
- "application/vnd.google-apps.spreadsheet",
- "application/vnd.google-apps.unknown",
- "application/vnd.google-apps.video",
- "application/vnd.google-apps.map",
-}
-
-func List(d *gdrive.Drive, query, titleFilter string, maxResults int, sharedStatus, noHeader, includeDocs, sizeInBytes bool) error {
- caller := d.Files.List()
- queryList := []string{}
-
- if maxResults > 0 {
- caller.MaxResults(int64(maxResults))
- }
-
- if titleFilter != "" {
- q := fmt.Sprintf("title contains '%s'", titleFilter)
- queryList = append(queryList, q)
- }
-
- if query != "" {
- queryList = append(queryList, query)
- } else {
- // Skip trashed files
- queryList = append(queryList, "trashed = false")
-
- // Skip google docs
- if !includeDocs {
- for _, mime := range googleMimeTypes {
- q := fmt.Sprintf("mimeType != '%s'", mime)
- queryList = append(queryList, q)
- }
- }
- }
-
- if len(queryList) > 0 {
- q := strings.Join(queryList, " and ")
- caller.Q(q)
- }
-
- list, err := caller.Do()
- if err != nil {
- return err
- }
-
- files := list.Items
-
- for list.NextPageToken != "" {
- if maxResults > 0 && len(files) > maxResults {
- break
- }
-
- caller.PageToken(list.NextPageToken)
- list, err = caller.Do()
- if err != nil {
- return err
- }
- files = append(files, list.Items...)
- }
-
- items := make([]map[string]string, 0, 0)
-
- for _, f := range files {
- if maxResults > 0 && len(items) >= maxResults {
- break
- }
-
- items = append(items, map[string]string{
- "Id": f.Id,
- "Title": util.TruncateString(f.Title, 40),
- "Size": util.FileSizeFormat(f.FileSize, sizeInBytes),
- "Created": util.ISODateToLocal(f.CreatedDate),
- })
- }
-
- columnOrder := []string{"Id", "Title", "Size", "Created"}
-
- if sharedStatus {
- addSharedStatus(d, items)
- columnOrder = append(columnOrder, "Shared")
- }
-
- util.PrintColumns(items, columnOrder, 3, noHeader)
- return nil
-}
-
-// Adds the key-value-pair 'Shared: True/False' to the map
-func addSharedStatus(d *gdrive.Drive, items []map[string]string) {
- // Limit to 10 simultaneous requests
- active := make(chan bool, 10)
- done := make(chan bool)
-
- // Closure that performs the check
- checkStatus := func(item map[string]string) {
- // Wait for an empty spot in the active queue
- active <- true
-
- // Perform request
- shared := isShared(d, item["Id"])
- item["Shared"] = util.FormatBool(shared)
-
- // Decrement the active queue and notify that we are done
- <-active
- done <- true
- }
-
- // Go, go, go!
- for _, item := range items {
- go checkStatus(item)
- }
-
- // Wait for all goroutines to finish
- for i := 0; i < len(items); i++ {
- <-done
- }
-}
-
-func Info(d *gdrive.Drive, fileId string, sizeInBytes bool) error {
- info, err := d.Files.Get(fileId).Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
- printInfo(d, info, sizeInBytes)
- return nil
-}
-
-func printInfo(d *gdrive.Drive, f *drive.File, sizeInBytes bool) {
- fields := map[string]string{
- "Id": f.Id,
- "Title": f.Title,
- "Description": f.Description,
- "Size": util.FileSizeFormat(f.FileSize, sizeInBytes),
- "Created": util.ISODateToLocal(f.CreatedDate),
- "Modified": util.ISODateToLocal(f.ModifiedDate),
- "Owner": strings.Join(f.OwnerNames, ", "),
- "Md5sum": f.Md5Checksum,
- "Shared": util.FormatBool(isShared(d, f.Id)),
- "Parents": util.ParentList(f.Parents),
- }
-
- order := []string{
- "Id",
- "Title",
- "Description",
- "Size",
- "Created",
- "Modified",
- "Owner",
- "Md5sum",
- "Shared",
- "Parents",
- }
- util.Print(fields, order)
-}
-
-// Create folder in drive
-func Folder(d *gdrive.Drive, title string, parentId string, share bool) error {
- info, err := makeFolder(d, title, parentId, share)
- if err != nil {
- return err
- }
- printInfo(d, info, false)
- fmt.Printf("Folder '%s' created\n", info.Title)
- return nil
-}
-
-func makeFolder(d *gdrive.Drive, title string, parentId string, share bool) (*drive.File, error) {
- // File instance
- f := &drive.File{Title: title, MimeType: "application/vnd.google-apps.folder"}
- // Set parent (if provided)
- if parentId != "" {
- p := &drive.ParentReference{Id: parentId}
- f.Parents = []*drive.ParentReference{p}
- }
- // Create folder
- info, err := d.Files.Insert(f).Do()
- if err != nil {
- return nil, fmt.Errorf("An error occurred creating the folder: %v\n", err)
- }
- // Share folder if the share flag was provided
- if share {
- Share(d, info.Id)
- }
- return info, err
-}
-
-// Upload file to drive
-func UploadStdin(d *gdrive.Drive, input io.ReadCloser, title string, parentId string, share bool, mimeType string, convert bool) error {
- // File instance
- f := &drive.File{Title: title}
- // Set parent (if provided)
- if parentId != "" {
- p := &drive.ParentReference{Id: parentId}
- f.Parents = []*drive.ParentReference{p}
- }
- getRate := util.MeasureTransferRate()
-
- if convert {
- fmt.Printf("Converting to Google Docs format enabled\n")
- }
-
- info, err := d.Files.Insert(f).Convert(convert).Media(input).Do()
- if err != nil {
- return fmt.Errorf("An error occurred uploading the document: %v\n", err)
- }
-
- // Total bytes transferred
- bytes := info.FileSize
-
- // Print information about uploaded file
- printInfo(d, info, false)
- fmt.Printf("MIME Type: %s\n", mimeType)
- fmt.Printf("Uploaded '%s' at %s, total %s\n", info.Title, getRate(bytes), util.FileSizeFormat(bytes, false))
-
- // Share file if the share flag was provided
- if share {
- err = Share(d, info.Id)
- }
- return err
-}
-
-func Upload(d *gdrive.Drive, input *os.File, title string, parentId string, share bool, mimeType string, convert bool) error {
- // Grab file info
- inputInfo, err := input.Stat()
- if err != nil {
- return err
- }
-
- if inputInfo.IsDir() {
- return uploadDirectory(d, input, inputInfo, title, parentId, share, mimeType, convert)
- } else {
- return uploadFile(d, input, inputInfo, title, parentId, share, mimeType, convert)
- }
-
- return nil
-}
-
-func uploadDirectory(d *gdrive.Drive, input *os.File, inputInfo os.FileInfo, title string, parentId string, share bool, mimeType string, convert bool) error {
- // Create folder
- folder, err := makeFolder(d, filepath.Base(inputInfo.Name()), parentId, share)
- if err != nil {
- return err
- }
-
- // Read all files in directory
- files, err := input.Readdir(0)
- if err != nil {
- return err
- }
-
- // Get current dir
- currentDir, err := os.Getwd()
- if err != nil {
- return err
- }
-
- // Go into directory
- dstDir := filepath.Join(currentDir, inputInfo.Name())
- err = os.Chdir(dstDir)
- if err != nil {
- return err
- }
-
- // Change back to original directory when done
- defer func() {
- os.Chdir(currentDir)
- }()
-
- for _, fi := range files {
- f, err := os.Open(fi.Name())
- if err != nil {
- return err
- }
-
- if fi.IsDir() {
- err = uploadDirectory(d, f, fi, "", folder.Id, share, mimeType, convert)
- } else {
- err = uploadFile(d, f, fi, "", folder.Id, share, mimeType, convert)
- }
-
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func uploadFile(d *gdrive.Drive, input *os.File, inputInfo os.FileInfo, title string, parentId string, share bool, mimeType string, convert bool) error {
- if title == "" {
- title = filepath.Base(inputInfo.Name())
- }
-
- if mimeType == "" {
- mimeType = mime.TypeByExtension(filepath.Ext(title))
- }
-
- // File instance
- f := &drive.File{Title: title, MimeType: mimeType}
- // Set parent (if provided)
- if parentId != "" {
- p := &drive.ParentReference{Id: parentId}
- f.Parents = []*drive.ParentReference{p}
- }
- getRate := util.MeasureTransferRate()
-
- if convert {
- fmt.Printf("Converting to Google Docs format enabled\n")
- }
-
- info, err := d.Files.Insert(f).Convert(convert).ResumableMedia(context.Background(), input, inputInfo.Size(), mimeType).Do()
- if err != nil {
- return fmt.Errorf("An error occurred uploading the document: %v\n", err)
- }
-
- // Total bytes transferred
- bytes := info.FileSize
-
- // Print information about uploaded file
- printInfo(d, info, false)
- fmt.Printf("MIME Type: %s\n", mimeType)
- fmt.Printf("Uploaded '%s' at %s, total %s\n", info.Title, getRate(bytes), util.FileSizeFormat(bytes, false))
-
- // Share file if the share flag was provided
- if share {
- err = Share(d, info.Id)
- }
- return err
-}
-
-func DownloadLatest(d *gdrive.Drive, stdout bool, format string, force bool) error {
- list, err := d.Files.List().Do()
- if err != nil {
- return err
- }
-
- if len(list.Items) == 0 {
- return fmt.Errorf("No files found")
- }
-
- latestId := list.Items[0].Id
- return Download(d, latestId, stdout, true, format, force)
-}
-
-// Download file from drive
-func Download(d *gdrive.Drive, fileId string, stdout, deleteAfterDownload bool, format string, force bool) error {
- // Get file info
- info, err := d.Files.Get(fileId).Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- downloadUrl, extension, err := util.InternalDownloadUrlAndExtension(info, format)
- if err != nil {
- return err
- }
-
- // Measure transfer rate
- getRate := util.MeasureTransferRate()
-
- // GET the download url
- res, err := d.Client().Get(downloadUrl)
- if err != nil {
- return fmt.Errorf("An error occurred: %v", err)
- }
-
- // Close body on function exit
- defer res.Body.Close()
-
- // Write file content to stdout
- if stdout {
- io.Copy(os.Stdout, res.Body)
- return nil
- }
-
- fileName := fmt.Sprintf("%s%s", info.Title, extension)
-
- // Check if file exists
- if !force && util.FileExists(fileName) {
- return fmt.Errorf("An error occurred: '%s' already exists", fileName)
- }
-
- // Create a new file
- outFile, err := os.Create(fileName)
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- // Close file on function exit
- defer outFile.Close()
-
- // Save file to disk
- bytes, err := io.Copy(outFile, res.Body)
- if err != nil {
- return fmt.Errorf("An error occurred: %s", err)
- }
-
- fmt.Printf("Downloaded '%s' at %s, total %s\n", fileName, getRate(bytes), util.FileSizeFormat(bytes, false))
-
- if deleteAfterDownload {
- err = Delete(d, fileId)
- }
- return err
-}
-
-// Delete file with given file id
-func Delete(d *gdrive.Drive, fileId string) error {
- info, err := d.Files.Get(fileId).Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- if err := d.Files.Delete(fileId).Do(); err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
-
- }
-
- fmt.Printf("Removed file '%s'\n", info.Title)
- return nil
-}
-
-// Make given file id readable by anyone -- auth not required to view/download file
-func Share(d *gdrive.Drive, fileId string) error {
- info, err := d.Files.Get(fileId).Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- perm := &drive.Permission{
- Value: "me",
- Type: "anyone",
- Role: "reader",
- }
-
- if _, err := d.Permissions.Insert(fileId, perm).Do(); err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- fmt.Printf("File '%s' is now readable by everyone @ %s\n", info.Title, util.PreviewUrl(fileId))
- return nil
-}
-
-// Removes the 'anyone' permission -- auth will be required to view/download file
-func Unshare(d *gdrive.Drive, fileId string) error {
- info, err := d.Files.Get(fileId).Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- if err := d.Permissions.Delete(fileId, "anyone").Do(); err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- fmt.Printf("File '%s' is no longer shared to 'anyone'\n", info.Title)
- return nil
-}
-
-func Quota(d *gdrive.Drive, sizeInBytes bool) error {
- info, err := d.About.Get().Do()
- if err != nil {
- return fmt.Errorf("An error occurred: %v\n", err)
- }
-
- fmt.Printf("Used: %s\n", util.FileSizeFormat(info.QuotaBytesUsed, sizeInBytes))
- fmt.Printf("Free: %s\n", util.FileSizeFormat(info.QuotaBytesTotal-info.QuotaBytesUsed, sizeInBytes))
- fmt.Printf("Total: %s\n", util.FileSizeFormat(info.QuotaBytesTotal, sizeInBytes))
- return nil
-}
-
-func isShared(d *gdrive.Drive, fileId string) bool {
- r, err := d.Permissions.List(fileId).Do()
- if err != nil {
- fmt.Printf("An error occurred: %v\n", err)
- os.Exit(1)
- }
-
- for _, perm := range r.Items {
- if perm.Type == "anyone" {
- return true
- }
- }
- return false
-}
diff --git a/cli/context.go b/cli/context.go
new file mode 100644
index 0000000..b1037b0
--- /dev/null
+++ b/cli/context.go
@@ -0,0 +1,41 @@
+package cli
+
+import (
+ "strconv"
+)
+
+type Context struct {
+ args Arguments
+ handlers []*Handler
+}
+
+func (self Context) Args() Arguments {
+ return self.args
+}
+
+func (self Context) Handlers() []*Handler {
+ return self.handlers
+}
+
+func (self Context) FilterHandlers(prefix string) []*Handler {
+ return filterHandlers(self.handlers, prefix)
+}
+
+type Arguments map[string]string
+
+func (self Arguments) String(key string) string {
+ value, _ := self[key]
+ return value
+}
+
+func (self Arguments) Int64(key string) int64 {
+ value, _ := self[key]
+ n, _ := strconv.ParseInt(value, 10, 64)
+ return n
+}
+
+func (self Arguments) Bool(key string) bool {
+ value, _ := self[key]
+ b, _ := strconv.ParseBool(value)
+ return b
+}
diff --git a/cli/flags.go b/cli/flags.go
new file mode 100644
index 0000000..a5aa276
--- /dev/null
+++ b/cli/flags.go
@@ -0,0 +1,132 @@
+package cli
+
+// TODO
+// Default values? Default string values? Parser must always return a value
+// Support invalid flag combinations?
+
+
+type Flag interface {
+ GetPatterns() []string
+ GetName() string
+ GetDescription() string
+ GetParser() Parser
+}
+
+func getFlagParser(flags []Flag) Parser {
+ var parsers []Parser
+
+ for _, flag := range flags {
+ parsers = append(parsers, flag.GetParser())
+ }
+
+ return FlagParser{parsers}
+}
+
+
+type BoolFlag struct {
+ Patterns []string
+ Name string
+ Description string
+ DefaultValue bool
+ OmitValue bool
+}
+
+func (self BoolFlag) GetName() string {
+ return self.Name
+}
+
+func (self BoolFlag) GetPatterns() []string {
+ return self.Patterns
+}
+
+func (self BoolFlag) GetDescription() string {
+ return self.Description
+}
+
+func (self BoolFlag) GetParser() Parser {
+ var parsers []Parser
+ for _, p := range self.Patterns {
+ parsers = append(parsers, BoolFlagParser{
+ pattern: p,
+ key: self.Name,
+ omitValue: self.OmitValue,
+ defaultValue: self.DefaultValue,
+ })
+ }
+
+ if len(parsers) == 1 {
+ return parsers[0]
+ }
+ return ShortCircuitParser{parsers}
+}
+
+
+type StringFlag struct {
+ Patterns []string
+ Name string
+ Description string
+ DefaultValue string
+}
+
+func (self StringFlag) GetName() string {
+ return self.Name
+}
+
+func (self StringFlag) GetPatterns() []string {
+ return self.Patterns
+}
+
+func (self StringFlag) GetDescription() string {
+ return self.Description
+}
+
+func (self StringFlag) GetParser() Parser {
+ var parsers []Parser
+ for _, p := range self.Patterns {
+ parsers = append(parsers, StringFlagParser{
+ pattern: p,
+ key: self.Name,
+ defaultValue: self.DefaultValue,
+ })
+ }
+
+ if len(parsers) == 1 {
+ return parsers[0]
+ }
+ return ShortCircuitParser{parsers}
+}
+
+type IntFlag struct {
+ Patterns []string
+ Name string
+ Description string
+ DefaultValue int64
+}
+
+func (self IntFlag) GetName() string {
+ return self.Name
+}
+
+func (self IntFlag) GetPatterns() []string {
+ return self.Patterns
+}
+
+func (self IntFlag) GetDescription() string {
+ return self.Description
+}
+
+func (self IntFlag) GetParser() Parser {
+ var parsers []Parser
+ for _, p := range self.Patterns {
+ parsers = append(parsers, IntFlagParser{
+ pattern: p,
+ key: self.Name,
+ defaultValue: self.DefaultValue,
+ })
+ }
+
+ if len(parsers) == 1 {
+ return parsers[0]
+ }
+ return ShortCircuitParser{parsers}
+}
diff --git a/cli/handler.go b/cli/handler.go
new file mode 100644
index 0000000..5cd13f8
--- /dev/null
+++ b/cli/handler.go
@@ -0,0 +1,125 @@
+package cli
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+type Flags map[string][]Flag
+
+var handlers []*Handler
+
+type Handler struct {
+ Pattern string
+ Flags Flags
+ Callback func(Context)
+ Description string
+}
+
+func (self *Handler) getParser() Parser {
+ var parsers []Parser
+
+ for _, pattern := range splitPattern(self.Pattern) {
+ if isOptional(pattern) {
+ name := optionalName(pattern)
+ parser := getFlagParser(self.Flags[name])
+ parsers = append(parsers, parser)
+ } else if isCaptureGroup(pattern) {
+ parsers = append(parsers, CaptureGroupParser{pattern})
+ } else {
+ parsers = append(parsers, EqualParser{pattern})
+ }
+ }
+
+ return CompleteParser{parsers}
+}
+
+func SetHandlers(h []*Handler) {
+ handlers = h
+}
+
+func AddHandler(pattern string, flags Flags, callback func(Context), desc string) {
+ handlers = append(handlers, &Handler{
+ Pattern: pattern,
+ Flags: flags,
+ Callback: callback,
+ Description: desc,
+ })
+}
+
+func findHandler(args []string) *Handler {
+ for _, h := range handlers {
+ if _, ok := h.getParser().Match(args); ok {
+ return h
+ }
+ }
+ return nil
+}
+
+
+func Handle(args []string) bool {
+ h := findHandler(args)
+ if h == nil {
+ return false
+ }
+
+ _, data := h.getParser().Capture(args)
+ fmt.Println(data)
+ ctx := Context{
+ args: data,
+ handlers: handlers,
+ }
+ h.Callback(ctx)
+ return true
+}
+
+func filterHandlers(handlers []*Handler, prefix string) []*Handler {
+ matches := []*Handler{}
+
+ for _, h := range handlers {
+ pattern := strings.Join(stripOptionals(splitPattern(h.Pattern)), " ")
+ if strings.HasPrefix(pattern, prefix) {
+ matches = append(matches, h)
+ }
+ }
+
+ return matches
+}
+
+
+// Split on spaces but ignore spaces inside <...> and [...]
+func splitPattern(pattern string) []string {
+ re := regexp.MustCompile(`(<[^>]+>|\[[^\]]+]|\S+)`)
+ matches := []string{}
+
+ for _, value := range re.FindAllStringSubmatch(pattern, -1) {
+ matches = append(matches, value[1])
+ }
+
+ return matches
+}
+
+func isCaptureGroup(arg string) bool {
+ return strings.HasPrefix(arg, "<") && strings.HasSuffix(arg, ">")
+}
+
+func isOptional(arg string) bool {
+ return strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]")
+}
+
+func optionalName(s string) string {
+ return s[1:len(s) - 1]
+}
+
+// Strip optional groups from pattern
+func stripOptionals(pattern []string) []string {
+ newArgs := []string{}
+
+ for _, arg := range pattern {
+ if !isOptional(arg) {
+ newArgs = append(newArgs, arg)
+ }
+ }
+ return newArgs
+}
diff --git a/cli/parser.go b/cli/parser.go
new file mode 100644
index 0000000..433a4b1
--- /dev/null
+++ b/cli/parser.go
@@ -0,0 +1,307 @@
+package cli
+
+import (
+ "fmt"
+ "strconv"
+)
+
+type Parser interface {
+ Match([]string) ([]string, bool)
+ Capture([]string) ([]string, map[string]string)
+}
+
+type CompleteParser struct {
+ parsers []Parser
+}
+
+func (self CompleteParser) Match(values []string) ([]string, bool) {
+ remainingValues := values
+
+ for _, parser := range self.parsers {
+ var ok bool
+ remainingValues, ok = parser.Match(remainingValues)
+ if !ok {
+ return remainingValues, false
+ }
+ }
+
+ return remainingValues, len(remainingValues) == 0
+}
+
+func (self CompleteParser) Capture(values []string) ([]string, map[string]string) {
+ remainingValues := values
+ data := map[string]string{}
+
+ for _, parser := range self.parsers {
+ var captured map[string]string
+ remainingValues, captured = parser.Capture(remainingValues)
+ for key, value := range captured {
+ data[key] = value
+ }
+ }
+
+ return remainingValues, data
+}
+
+func (self CompleteParser) String() string {
+ return fmt.Sprintf("CompleteParser %v", self.parsers)
+}
+
+
+type EqualParser struct {
+ value string
+}
+
+func (self EqualParser) Match(values []string) ([]string, bool) {
+ if len(values) == 0 {
+ return values, false
+ }
+
+ if self.value == values[0] {
+ return values[1:], true
+ }
+
+ return values, false
+}
+
+func (self EqualParser) Capture(values []string) ([]string, map[string]string) {
+ remainingValues, _ := self.Match(values)
+ return remainingValues, nil
+}
+
+func (self EqualParser) String() string {
+ return fmt.Sprintf("EqualParser '%s'", self.value)
+}
+
+
+type CaptureGroupParser struct {
+ value string
+}
+
+func (self CaptureGroupParser) Match(values []string) ([]string, bool) {
+ if len(values) == 0 {
+ return values, false
+ }
+
+ return values[1:], true
+}
+
+func (self CaptureGroupParser) key() string {
+ return self.value[1:len(self.value) - 1]
+}
+
+func (self CaptureGroupParser) Capture(values []string) ([]string, map[string]string) {
+ if remainingValues, ok := self.Match(values); ok {
+ return remainingValues, map[string]string{self.key(): values[0]}
+ }
+
+ return values, nil
+}
+
+func (self CaptureGroupParser) String() string {
+ return fmt.Sprintf("CaptureGroupParser '%s'", self.value)
+}
+
+
+
+type BoolFlagParser struct {
+ pattern string
+ key string
+ omitValue bool
+ defaultValue bool
+}
+
+func (self BoolFlagParser) Match(values []string) ([]string, bool) {
+ if self.omitValue {
+ if len(values) == 0 {
+ return values, false
+ }
+
+ if self.pattern == values[0] {
+ return values[1:], true
+ }
+
+ return values, false
+ } else {
+ if len(values) < 2 {
+ return values, false
+ }
+
+ if self.pattern != values[0] {
+ return values, false
+ }
+
+ // Check that value is a valid boolean
+ if _, err := strconv.ParseBool(values[1]); err != nil {
+ return values, false
+ }
+
+ return values[2:], true
+ }
+}
+
+func (self BoolFlagParser) Capture(values []string) ([]string, map[string]string) {
+ remainingValues, ok := self.Match(values)
+ if !ok && !self.omitValue {
+ return remainingValues, map[string]string{self.key: fmt.Sprintf("%t", self.defaultValue)}
+ }
+ return remainingValues, map[string]string{self.key: fmt.Sprintf("%t", ok)}
+}
+
+func (self BoolFlagParser) String() string {
+ return fmt.Sprintf("BoolFlagParser '%s'", self.pattern)
+}
+
+type StringFlagParser struct {
+ pattern string
+ key string
+ defaultValue string
+}
+
+func (self StringFlagParser) Match(values []string) ([]string, bool) {
+ if len(values) < 2 {
+ return values, false
+ }
+
+ if self.pattern != values[0] {
+ return values, false
+ }
+
+ return values[2:], true
+}
+
+func (self StringFlagParser) Capture(values []string) ([]string, map[string]string) {
+ remainingValues, ok := self.Match(values)
+ if ok {
+ return remainingValues, map[string]string{self.key: values[1]}
+ }
+
+ return values, map[string]string{self.key: self.defaultValue}
+}
+
+func (self StringFlagParser) String() string {
+ return fmt.Sprintf("StringFlagParser '%s'", self.pattern)
+}
+
+type IntFlagParser struct {
+ pattern string
+ key string
+ defaultValue int64
+}
+
+func (self IntFlagParser) Match(values []string) ([]string, bool) {
+ if len(values) < 2 {
+ return values, false
+ }
+
+ if self.pattern != values[0] {
+ return values, false
+ }
+
+ // Check that value is a valid integer
+ if _, err := strconv.ParseInt(values[1], 10, 64); err != nil {
+ return values, false
+ }
+
+ return values[2:], true
+}
+
+func (self IntFlagParser) Capture(values []string) ([]string, map[string]string) {
+ remainingValues, ok := self.Match(values)
+ if ok {
+ return remainingValues, map[string]string{self.key: values[1]}
+ }
+
+ return values, map[string]string{self.key: fmt.Sprintf("%d", self.defaultValue)}
+}
+
+func (self IntFlagParser) String() string {
+ return fmt.Sprintf("IntFlagParser '%s'", self.pattern)
+}
+
+
+type FlagParser struct {
+ parsers []Parser
+}
+
+func (self FlagParser) Match(values []string) ([]string, bool) {
+ remainingValues := values
+ var oneOrMoreMatches bool
+
+ for _, parser := range self.parsers {
+ var ok bool
+ remainingValues, ok = parser.Match(remainingValues)
+ if ok {
+ oneOrMoreMatches = true
+ }
+ }
+
+ // Recurse while we have one or more matches
+ if oneOrMoreMatches {
+ return self.Match(remainingValues)
+ }
+
+ return remainingValues, true
+}
+
+func (self FlagParser) Capture(values []string) ([]string, map[string]string) {
+ data := map[string]string{}
+ remainingValues := values
+
+ for _, parser := range self.parsers {
+ var captured map[string]string
+ remainingValues, captured = parser.Capture(remainingValues)
+ for key, value := range captured {
+ // Skip value if it already exists and new value is an empty string
+ if _, exists := data[key]; exists && value == "" {
+ continue
+ }
+
+ data[key] = value
+ }
+ }
+ return remainingValues, data
+}
+
+func (self FlagParser) String() string {
+ return fmt.Sprintf("FlagParser %v", self.parsers)
+}
+
+
+type ShortCircuitParser struct {
+ parsers []Parser
+}
+
+func (self ShortCircuitParser) Match(values []string) ([]string, bool) {
+ remainingValues := values
+
+ for _, parser := range self.parsers {
+ var ok bool
+ remainingValues, ok = parser.Match(remainingValues)
+ if ok {
+ return remainingValues, true
+ }
+ }
+
+ return remainingValues, false
+}
+
+func (self ShortCircuitParser) Capture(values []string) ([]string, map[string]string) {
+ if len(self.parsers) == 0 {
+ return values, nil
+ }
+
+ for _, parser := range self.parsers {
+ if _, ok := parser.Match(values); ok {
+ return parser.Capture(values)
+ }
+ }
+
+ // No parsers matched at this point,
+ // just return the capture value of the first one
+ return self.parsers[0].Capture(values)
+}
+
+func (self ShortCircuitParser) String() string {
+ return fmt.Sprintf("ShortCircuitParser %v", self.parsers)
+}
diff --git a/client/auth.go b/client/auth.go
new file mode 100644
index 0000000..fb6852d
--- /dev/null
+++ b/client/auth.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+ "net/http"
+ "golang.org/x/oauth2"
+ "go4.org/oauthutil"
+)
+
+type authCodeFn func(string) func() string
+
+func NewOauthClient(clientId, clientSecret, cacheFile string, authFn authCodeFn) *http.Client {
+ conf := &oauth2.Config{
+ ClientID: clientId,
+ ClientSecret: clientSecret,
+ Scopes: []string{"https://www.googleapis.com/auth/drive"},
+ RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
+ Endpoint: oauth2.Endpoint{
+ AuthURL: "https://accounts.google.com/o/oauth2/auth",
+ TokenURL: "https://accounts.google.com/o/oauth2/token",
+ },
+ }
+
+ authUrl := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
+
+ tokenSource := oauthutil.TokenSource{
+ Config: conf,
+ CacheFile: cacheFile,
+ AuthCode: authFn(authUrl),
+ }
+
+ return oauth2.NewClient(oauth2.NoContext, tokenSource)
+}
diff --git a/client/client.go b/client/client.go
new file mode 100644
index 0000000..1b48bbb
--- /dev/null
+++ b/client/client.go
@@ -0,0 +1,28 @@
+package client
+
+import (
+ "net/http"
+ "google.golang.org/api/drive/v3"
+)
+
+type Client struct {
+ service *drive.Service
+ http *http.Client
+}
+
+func (self *Client) Service() *drive.Service {
+ return self.service
+}
+
+func (self *Client) Http() *http.Client {
+ return self.http
+}
+
+func NewClient(client *http.Client) (*Client, error) {
+ service, err := drive.New(client)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{service, client}, nil
+}
diff --git a/drive.go b/drive.go
index 67dab79..2466087 100644
--- a/drive.go
+++ b/drive.go
@@ -1,4 +1,4 @@
-package main
+package foo
import (
"fmt"
diff --git a/drive/files.go b/drive/files.go
new file mode 100644
index 0000000..ed3c349
--- /dev/null
+++ b/drive/files.go
@@ -0,0 +1,68 @@
+package drive
+
+import (
+ "fmt"
+ "io"
+ "os"
+)
+
+func (self *Drive) List(args ListFilesArgs) {
+ fileList, err := self.service.Files.List().PageSize(args.MaxFiles).Q(args.Query).Fields("nextPageToken", "files(id,name,size,createdTime)").Do()
+ if err != nil {
+ exitF("Failed listing files: %s\n", err.Error())
+ }
+
+ for _, f := range fileList.Files {
+ fmt.Printf("%s %s %d %s\n", f.Id, f.Name, f.Size, f.CreatedTime)
+ }
+}
+
+
+func (self *Drive) Download(args DownloadFileArgs) {
+ getFile := self.service.Files.Get(args.Id)
+
+ f, err := getFile.Do()
+ if err != nil {
+ exitF("Failed to get file: %s", err.Error())
+ }
+
+ res, err := getFile.Download()
+ if err != nil {
+ exitF("Failed to download file: %s", err.Error())
+ }
+
+ // Close body on function exit
+ defer res.Body.Close()
+
+ if args.Stdout {
+ // Write file content to stdout
+ io.Copy(os.Stdout, res.Body)
+ return
+ }
+
+ // Check if file exists
+ if !args.Force && fileExists(f.Name) {
+ exitF("File '%s' already exists, use --force to overwrite", f.Name)
+ }
+
+ // Create new file
+ outFile, err := os.Create(f.Name)
+ if err != nil {
+ exitF("Unable to create new file: %s", err.Error())
+ }
+
+ // Close file on function exit
+ defer outFile.Close()
+
+ // Save file to disk
+ bytes, err := io.Copy(outFile, res.Body)
+ if err != nil {
+ exitF("Failed saving file: %s", err.Error())
+ }
+
+ fmt.Printf("Downloaded '%s' at %s, total %d\n", f.Name, "x/s", bytes)
+
+ //if deleteSourceFile {
+ // self.Delete(args.Id)
+ //}
+}
diff --git a/drive/types.go b/drive/types.go
new file mode 100644
index 0000000..d70dfb5
--- /dev/null
+++ b/drive/types.go
@@ -0,0 +1,37 @@
+package drive
+
+import (
+ "net/http"
+ "google.golang.org/api/drive/v3"
+)
+
+type Client interface {
+ Service() *drive.Service
+ Http() *http.Client
+}
+
+type Drive struct {
+ service *drive.Service
+ http *http.Client
+}
+
+func NewDrive(client Client) *Drive {
+ return &Drive{
+ service: client.Service(),
+ http: client.Http(),
+ }
+}
+
+type ListFilesArgs struct {
+ MaxFiles int64
+ Query string
+ SkipHeader bool
+ SizeInBytes bool
+}
+
+type DownloadFileArgs struct {
+ Id string
+ Force bool
+ NoProgress bool
+ Stdout bool
+}
diff --git a/drive/util.go b/drive/util.go
new file mode 100644
index 0000000..db48d28
--- /dev/null
+++ b/drive/util.go
@@ -0,0 +1,20 @@
+package drive
+
+import (
+ "fmt"
+ "os"
+)
+
+func exitF(format string, a ...interface{}) {
+ fmt.Fprintf(os.Stderr, format, a...)
+ fmt.Println("")
+ os.Exit(1)
+}
+
+func fileExists(path string) bool {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true
+ }
+ return false
+}
diff --git a/gdrive.go b/gdrive.go
new file mode 100644
index 0000000..13c8a81
--- /dev/null
+++ b/gdrive.go
@@ -0,0 +1,270 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "./cli"
+)
+
+const Name = "gdrive"
+const Version = "2.0.0"
+
+const ClientId = "367116221053-7n0vf5akeru7on6o2fjinrecpdoe99eg.apps.googleusercontent.com"
+const ClientSecret = "1qsNodXNaWq1mQuBjUjmvhoO"
+
+const DefaultMaxFiles = 100
+const DefaultChunkSize = 4194304
+
+var DefaultConfigDir = GetDefaultConfigDir()
+var DefaultTokenFilePath = GetDefaultTokenFilePath()
+
+
+func main() {
+ globalFlags := []cli.Flag{
+ cli.StringFlag{
+ Name: "configDir",
+ Patterns: []string{"-c", "--config"},
+ Description: fmt.Sprintf("Application path, default: %s", DefaultConfigDir),
+ DefaultValue: DefaultConfigDir,
+ },
+ }
+
+ handlers := []*cli.Handler{
+ &cli.Handler{
+ Pattern: "[global options] list [options]",
+ Description: "List files",
+ Callback: listHandler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.IntFlag{
+ Name: "maxFiles",
+ Patterns: []string{"-m", "--max"},
+ Description: fmt.Sprintf("Max files to list, default: %d", DefaultMaxFiles),
+ DefaultValue: DefaultMaxFiles,
+ },
+ cli.StringFlag{
+ Name: "query",
+ Patterns: []string{"-q", "--query"},
+ Description: "Query, see https://developers.google.com/drive/search-parameters",
+ },
+ cli.BoolFlag{
+ Name: "skipHeader",
+ Patterns: []string{"--noheader"},
+ Description: "Dont print the header",
+ OmitValue: true,
+ },
+ cli.BoolFlag{
+ Name: "sizeInBytes",
+ Patterns: []string{"--bytes"},
+ Description: "Size in bytes",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] download [options] <id>",
+ Description: "Download file or directory",
+ Callback: downloadHandler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "force",
+ Patterns: []string{"-f", "--force"},
+ Description: "Overwrite existing file",
+ OmitValue: true,
+ },
+ cli.BoolFlag{
+ Name: "noProgress",
+ Patterns: []string{"--noprogress"},
+ Description: "Hide progress",
+ OmitValue: true,
+ },
+ cli.BoolFlag{
+ Name: "stdout",
+ Patterns: []string{"--stdout"},
+ Description: "Write file content to stdout",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] upload [options] <path>",
+ Description: "Upload file or directory",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "recursive",
+ Patterns: []string{"-r", "--recursive"},
+ Description: "Upload directory recursively",
+ OmitValue: true,
+ },
+ cli.StringFlag{
+ Name: "parent",
+ Patterns: []string{"-p", "--parent"},
+ Description: "Parent id, used to upload file to a specific directory",
+ },
+ cli.StringFlag{
+ Name: "name",
+ Patterns: []string{"--name"},
+ Description: "Filename",
+ },
+ cli.BoolFlag{
+ Name: "progress",
+ Patterns: []string{"--progress"},
+ Description: "Show progress",
+ OmitValue: true,
+ },
+ cli.BoolFlag{
+ Name: "stdin",
+ Patterns: []string{"--stdin"},
+ Description: "Use stdin as file content",
+ OmitValue: true,
+ },
+ cli.StringFlag{
+ Name: "mime",
+ Patterns: []string{"--mime"},
+ Description: "Force mime type",
+ },
+ cli.BoolFlag{
+ Name: "share",
+ Patterns: []string{"--share"},
+ Description: "Share file",
+ OmitValue: true,
+ },
+ cli.BoolFlag{
+ Name: "share",
+ Patterns: []string{"--convert"},
+ Description: "Convert file to google docs format",
+ OmitValue: true,
+ },
+ cli.IntFlag{
+ Name: "chunksize",
+ Patterns: []string{"--chunksize"},
+ Description: fmt.Sprintf("Set chunk size in bytes. Minimum is 262144, default is %d", DefaultChunkSize),
+ DefaultValue: DefaultChunkSize,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] info [options] <id>",
+ Description: "Show file info",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "sizeInBytes",
+ Patterns: []string{"--bytes"},
+ Description: "Show size in bytes",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] mkdir [options] <name>",
+ Description: "Create directory",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.StringFlag{
+ Name: "parent",
+ Patterns: []string{"-p", "--parent"},
+ Description: "Parent id of created directory",
+ },
+ cli.BoolFlag{
+ Name: "share",
+ Patterns: []string{"--share"},
+ Description: "Share created directory",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] share <id>",
+ Description: "Share file or directory",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "revoke",
+ Patterns: []string{"--revoke"},
+ Description: "Unshare file or directory",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] url [options] <id>",
+ Description: "Get url to file or directory",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "download",
+ Patterns: []string{"--download"},
+ Description: "Download url",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] delete <id>",
+ Description: "Delete file or directory",
+ Callback: deleteHandler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ },
+ },
+ &cli.Handler{
+ Pattern: "[global options] quota [options]",
+ Description: "Show free space",
+ Callback: handler,
+ Flags: cli.Flags{
+ "global options": globalFlags,
+ "options": []cli.Flag{
+ cli.BoolFlag{
+ Name: "sizeInBytes",
+ Patterns: []string{"--bytes"},
+ Description: "Show size in bytes",
+ OmitValue: true,
+ },
+ },
+ },
+ },
+ &cli.Handler{
+ Pattern: "version",
+ Description: "Print application version",
+ Callback: printVersion,
+ },
+ &cli.Handler{
+ Pattern: "help",
+ Description: "Print help",
+ Callback: printHelp,
+ },
+ &cli.Handler{
+ Pattern: "help <subcommand>",
+ Description: "Print subcommand help",
+ Callback: printCommandHelp,
+ },
+ }
+
+ cli.SetHandlers(handlers)
+
+ if ok := cli.Handle(os.Args[1:]); !ok {
+ ExitF("No valid arguments given, use '%s help' to see available commands", Name)
+ }
+}
diff --git a/gdrive/handlers.go b/gdrive/handlers.go
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gdrive/handlers.go
diff --git a/handlers.go b/handlers.go
new file mode 100644
index 0000000..69aad94
--- /dev/null
+++ b/handlers.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+ "./cli"
+ "./client"
+ "./drive"
+)
+
+func listHandler(ctx cli.Context) {
+ args := ctx.Args()
+ gdrive := newDrive()
+
+ gdrive.List(drive.ListFilesArgs{
+ MaxFiles: args.Int64("maxFiles"),
+ Query: args.String("query"),
+ SkipHeader: args.Bool("skipHeader"),
+ SizeInBytes: args.Bool("sizeInBytes"),
+ })
+}
+
+func downloadHandler(ctx cli.Context) {
+ args := ctx.Args()
+ gdrive := newDrive()
+
+ gdrive.Download(drive.DownloadFileArgs{
+ Id: args.String("id"),
+ Force: args.Bool("force"),
+ Stdout: args.Bool("stdout"),
+ NoProgress: args.Bool("noprogress"),
+ })
+}
+
+func deleteHandler(ctx cli.Context) {
+ fmt.Println("Deleting...")
+}
+
+func handler(ctx cli.Context) {
+ fmt.Println("handler...")
+}
+
+func printVersion(ctx cli.Context) {
+ fmt.Printf("%s v%s\n", Name, Version)
+}
+
+func printHelp(ctx cli.Context) {
+ fmt.Printf("%s usage:\n\n", Name)
+
+ for _, h := range ctx.Handlers() {
+ fmt.Printf("%s %s (%s)\n", Name, h.Pattern, h.Description)
+ }
+}
+
+func printCommandHelp(ctx cli.Context) {
+ handlers := ctx.FilterHandlers(ctx.Args().String("subcommand"))
+
+ if len(handlers) == 0 {
+ ExitF("Subcommand not found")
+ }
+
+ if len(handlers) > 1 {
+ ExitF("More than one matching subcommand, be more specific")
+ }
+
+ handler := handlers[0]
+
+ fmt.Printf("%s %s (%s)\n", Name, handler.Pattern, handler.Description)
+ for name, flags := range handler.Flags {
+ fmt.Printf("\n%s:\n", name)
+ for _, flag := range flags {
+ fmt.Printf(" %s (%s)\n", strings.Join(flag.GetPatterns(), ", "), flag.GetDescription())
+ }
+ }
+}
+
+func newDrive() *drive.Drive {
+ oauth := client.NewOauthClient(ClientId, ClientSecret, DefaultTokenFilePath, authCodePrompt)
+ client, err := client.NewClient(oauth)
+ if err != nil {
+ ExitF("Failed getting drive: %s", err.Error())
+ }
+
+ return drive.NewDrive(client)
+}
+
+func authCodePrompt(url string) func() string {
+ return func() string {
+ fmt.Println("Authentication needed")
+ fmt.Println("Go to the following url in your browser:")
+ fmt.Printf("%s\n\n", url)
+ fmt.Print("Enter verification code: ")
+
+ var code string
+ if _, err := fmt.Scan(&code); err != nil {
+ fmt.Printf("Failed reading code: %s", err.Error())
+ }
+ return code
+ }
+}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..064ce3f
--- /dev/null
+++ b/util.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "runtime"
+ "path/filepath"
+ "fmt"
+ "os"
+)
+
+func GetDefaultConfigDir() string {
+ return filepath.Join(Homedir(), ".gdrive")
+}
+
+func GetDefaultTokenFilePath() string {
+ return filepath.Join(GetDefaultConfigDir(), "token.json")
+}
+
+func Homedir() string {
+ if runtime.GOOS == "windows" {
+ return os.Getenv("APPDATA")
+ }
+ return os.Getenv("HOME")
+}
+
+func ExitF(format string, a ...interface{}) {
+ fmt.Fprintf(os.Stderr, format, a...)
+ fmt.Println("")
+ os.Exit(1)
+}