diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | cli/cli.go | 509 | ||||
| -rw-r--r-- | cli/context.go | 41 | ||||
| -rw-r--r-- | cli/flags.go | 132 | ||||
| -rw-r--r-- | cli/handler.go | 125 | ||||
| -rw-r--r-- | cli/parser.go | 307 | ||||
| -rw-r--r-- | client/auth.go | 32 | ||||
| -rw-r--r-- | client/client.go | 28 | ||||
| -rw-r--r-- | drive.go | 2 | ||||
| -rw-r--r-- | drive/files.go | 68 | ||||
| -rw-r--r-- | drive/types.go | 37 | ||||
| -rw-r--r-- | drive/util.go | 20 | ||||
| -rw-r--r-- | gdrive.go | 270 | ||||
| -rw-r--r-- | gdrive/handlers.go | 0 | ||||
| -rw-r--r-- | handlers.go | 100 | ||||
| -rw-r--r-- | util.go | 29 | 
16 files changed, 1191 insertions, 511 deletions
| @@ -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 +} @@ -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 +    } +} @@ -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) +} | 
