aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetter Rasmussen2013-01-01 20:48:15 +0100
committerPetter Rasmussen2013-01-01 20:48:15 +0100
commit2f4fabf7bb74b42a7aa54c5b02538e4ca337361f (patch)
treedb087fb26d0e97ab494f8f8684dc0441630efb75
downloadgdrive-2f4fabf7bb74b42a7aa54c5b02538e4ca337361f.tar.bz2
version 1
-rw-r--r--.gitignore9
-rw-r--r--LICENSE22
-rw-r--r--auth/auth.go68
-rwxr-xr-xbuild-all.sh38
-rw-r--r--cli/cli.go307
-rw-r--r--config/config.go74
-rw-r--r--drive.go129
-rw-r--r--gdrive/gdrive.go53
-rw-r--r--util/drive.go15
-rw-r--r--util/generic.go196
10 files changed, 911 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..769fd25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Ignore bin folder and drive binary
+bin/
+drive
+
+# vim files
+.*.sw[a-z]
+*.un~
+Session.vim
+.netrwhist
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4f2fadb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2013 Petter Rasmussen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/auth/auth.go b/auth/auth.go
new file mode 100644
index 0000000..c4a67e3
--- /dev/null
+++ b/auth/auth.go
@@ -0,0 +1,68 @@
+package auth
+
+import (
+ "net/http"
+ "fmt"
+ "code.google.com/p/goauth2/oauth"
+ "../util"
+)
+
+// Get auth code from user
+func promptUserForAuthCode(config *oauth.Config) string {
+ authUrl := config.AuthCodeURL("state")
+ fmt.Println("Go to the following link in your browser:")
+ fmt.Printf("%v\n\n", authUrl)
+ return util.Prompt("Enter verification code: ")
+}
+
+// Returns true if we have a valid cached token
+func hasValidToken(cacheFile oauth.CacheFile, transport *oauth.Transport) bool {
+ // Check if we have a cached token
+ token, err := cacheFile.Token()
+ if err != nil {
+ return false
+ }
+
+ // Refresh token if its expired
+ if token.Expired() {
+ transport.Token = token
+ err = transport.Refresh()
+ if err != nil {
+ fmt.Println(err)
+ return false
+ }
+ }
+ return true
+}
+
+func GetOauth2Client(clientId, clientSecret, cachePath string) (*http.Client, error) {
+ cacheFile := oauth.CacheFile(cachePath)
+
+ config := &oauth.Config{
+ ClientId: clientId,
+ ClientSecret: clientSecret,
+ Scope: "https://www.googleapis.com/auth/drive",
+ RedirectURL: "urn:ietf:wg:oauth:2.0:oob",
+ AuthURL: "https://accounts.google.com/o/oauth2/auth",
+ TokenURL: "https://accounts.google.com/o/oauth2/token",
+ TokenCache: cacheFile,
+ }
+
+ transport := &oauth.Transport{
+ Config: config,
+ Transport: http.DefaultTransport,
+ }
+
+ // Return client if we have a valid token
+ if hasValidToken(cacheFile, transport) {
+ return transport.Client(), nil
+ }
+
+ // Get auth code from user and request a new token
+ code := promptUserForAuthCode(config)
+ _, err := transport.Exchange(code)
+ if err != nil {
+ return nil, err
+ }
+ return transport.Client(), nil
+}
diff --git a/build-all.sh b/build-all.sh
new file mode 100755
index 0000000..5170bdd
--- /dev/null
+++ b/build-all.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <app>"
+ exit 1
+fi
+
+PLATFORMS="darwin/386 darwin/amd64 freebsd/386 freebsd/amd64 linux/386 linux/amd64 linux/arm windows/386 windows/amd64"
+APP_NAME=$1
+
+# Remove old binaries
+rm bin/*
+
+# Load crosscompile environment
+source /Users/pii/scripts/golang-crosscompile/crosscompile.bash
+
+# Build binary for each platform in parallel
+for PLATFORM in $PLATFORMS; do
+ GOOS=${PLATFORM%/*}
+ GOARCH=${PLATFORM#*/}
+ BIN_NAME="${APP_NAME}-$GOOS-$GOARCH"
+
+ if [ $GOOS == "windows" ]; then
+ BIN_NAME="${BIN_NAME}.exe"
+ fi
+
+ BUILD_CMD="go-${GOOS}-${GOARCH} build -o bin/${BIN_NAME} $APP_NAME.go"
+
+ echo "Building $APP_NAME for ${GOOS}/${GOARCH}..."
+ $BUILD_CMD &
+done
+
+# Wait for builds to complete
+for job in $(jobs -p); do
+ wait $job
+done
+
+echo "All done"
diff --git a/cli/cli.go b/cli/cli.go
new file mode 100644
index 0000000..d78c838
--- /dev/null
+++ b/cli/cli.go
@@ -0,0 +1,307 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "io"
+ "path/filepath"
+ "strings"
+ "code.google.com/p/google-api-go-client/drive/v2"
+ "../util"
+ "../gdrive"
+)
+
+func List(d *gdrive.Drive, query, titleFilter string, maxResults int, sharedStatus bool) {
+ caller := d.Files.List()
+
+ if maxResults > 0 {
+ caller.MaxResults(int64(maxResults))
+ }
+
+ if titleFilter != "" {
+ q := fmt.Sprintf("title contains '%s'", titleFilter)
+ caller.Q(q)
+ }
+
+ if query != "" {
+ caller.Q(query)
+ }
+
+ list, err := caller.Do()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ items := make([]map[string]string, 0, 0)
+
+ for _, f := range list.Items {
+ // Skip files that dont have a download url (they are not stored on google drive)
+ if f.DownloadUrl == "" {
+ continue
+ }
+
+ items = append(items, map[string]string{
+ "Id": f.Id,
+ "Title": util.TruncateString(f.Title, 40),
+ "Size": util.FileSizeFormat(f.FileSize),
+ "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)
+}
+
+// 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) {
+ info, err := d.Files.Get(fileId).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+ printInfo(d, info)
+}
+
+func printInfo(d *gdrive.Drive, f *drive.File) {
+ fields := map[string]string{
+ "Id": f.Id,
+ "Title": f.Title,
+ "Description": f.Description,
+ "Size": util.FileSizeFormat(f.FileSize),
+ "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)),
+ }
+
+ order := []string{"Id", "Title", "Description", "Size", "Created", "Modified", "Owner", "Md5sum", "Shared"}
+ util.Print(fields, order)
+}
+
+// Upload file to drive
+func Upload(d *gdrive.Drive, input io.ReadCloser, title string, share bool) {
+ // Use filename or 'untitled' as title if no title is specified
+ if title == "" {
+ if f, ok := input.(*os.File); ok && input != os.Stdin {
+ title = filepath.Base(f.Name())
+ } else {
+ title = "untitled"
+ }
+ }
+
+ metadata := &drive.File{Title: title}
+ getRate := util.MeasureTransferRate()
+
+ info, err := d.Files.Insert(metadata).Media(input).Do()
+ if err != nil {
+ fmt.Printf("An error occurred uploading the document: %v\n", err)
+ return
+ }
+
+ // Total bytes transferred
+ bytes := info.FileSize
+
+ // Print information about uploaded file
+ printInfo(d, info)
+ fmt.Printf("Uploaded '%s' at %s, total %s\n", info.Title, getRate(bytes), util.FileSizeFormat(bytes))
+
+ // Share file if the share flag was provided
+ if share {
+ Share(d, info.Id)
+ }
+}
+
+func DownloadLatest(d *gdrive.Drive, stdout bool) {
+ list, err := d.Files.List().Do()
+
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ if len(list.Items) == 0 {
+ fmt.Println("No files found")
+ return
+ }
+
+ latestId := list.Items[0].Id
+ Download(d, latestId, stdout, true)
+}
+
+// Download file from drive
+func Download(d *gdrive.Drive, fileId string, stdout, deleteAfterDownload bool) {
+ // Get file info
+ info, err := d.Files.Get(fileId).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ if info.DownloadUrl == "" {
+ // If there is no DownloadUrl, there is no body
+ fmt.Println("An error occurred: File is not downloadable")
+ return
+ }
+
+ // Measure transfer rate
+ getRate := util.MeasureTransferRate()
+
+ // GET the download url
+ res, err := d.Client().Get(info.DownloadUrl)
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ // Close body on function exit
+ defer res.Body.Close()
+
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ // Write file content to stdout
+ if stdout {
+ io.Copy(os.Stdout, res.Body)
+ return
+ }
+
+ // Check if file exists
+ if util.FileExists(info.Title) {
+ fmt.Printf("An error occurred: '%s' already exists\n", info.Title)
+ return
+ }
+
+ // Create a new file
+ outFile, err := os.Create(info.Title)
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ // Close file on function exit
+ defer outFile.Close()
+
+ // Save file to disk
+ bytes, err := io.Copy(outFile, res.Body)
+ if err != nil {
+ fmt.Printf("An error occurred: %v")
+ return
+ }
+
+ fmt.Printf("Downloaded '%s' at %s, total %s\n", info.Title, getRate(bytes), util.FileSizeFormat(bytes))
+
+ if deleteAfterDownload {
+ Delete(d, fileId)
+ }
+}
+
+// Delete file with given file id
+func Delete(d *gdrive.Drive, fileId string) {
+ info, err := d.Files.Get(fileId).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ if err = d.Files.Delete(fileId).Do(); err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Removed file '%s'\n", info.Title)
+}
+
+// Make given file id readable by anyone -- auth not required to view/download file
+func Share(d *gdrive.Drive, fileId string) {
+ info, err := d.Files.Get(fileId).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ perm := &drive.Permission{
+ Value: "me",
+ Type: "anyone",
+ Role: "reader",
+ }
+
+ _, err = d.Permissions.Insert(fileId, perm).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ fmt.Printf("File '%s' is now readable by everyone @ %s\n", info.Title, util.PreviewUrl(fileId))
+}
+
+// Removes the 'anyone' permission -- auth will be required to view/download file
+func Unshare(d *gdrive.Drive, fileId string) {
+ info, err := d.Files.Get(fileId).Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ err = d.Permissions.Delete(fileId, "anyone").Do()
+ if err != nil {
+ fmt.Printf("An error occurred: %v\n", err)
+ return
+ }
+
+ fmt.Printf("File '%s' is now longer shared to 'anyone'\n", info.Title)
+}
+
+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)
+ return false
+ }
+
+ for _, perm := range r.Items {
+ if perm.Type == "anyone" {
+ return true
+ }
+ }
+ return false
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..33451e2
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,74 @@
+package config
+
+import (
+ "fmt"
+ "io/ioutil"
+ "encoding/json"
+ "../util"
+)
+
+// Client ID and secrect for installed applications
+const (
+ ClientId = "367116221053-7n0vf5akeru7on6o2fjinrecpdoe99eg.apps.googleusercontent.com"
+ ClientSecret = "1qsNodXNaWq1mQuBjUjmvhoO"
+)
+
+type Config struct {
+ ClientId string
+ ClientSecret string
+}
+
+func defaultConfig() *Config {
+ return &Config{
+ ClientId: ClientId,
+ ClientSecret: ClientSecret,
+ }
+}
+
+func promptUser() *Config {
+ return &Config{
+ ClientId: util.Prompt("Enter Client Id: "),
+ ClientSecret: util.Prompt("Enter Client Secret: "),
+ }
+}
+
+func load(fname string) (*Config, error) {
+ data, err := ioutil.ReadFile(fname)
+ if err != nil {
+ return nil, err
+ }
+ config := &Config{}
+ return config, json.Unmarshal(data, config)
+}
+
+func save(fname string, config *Config) error {
+ data, err := json.MarshalIndent(config, "", " ")
+ if err != nil {
+ return err
+ }
+
+ if err = util.Mkdir(fname); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(fname, data, 0600)
+}
+
+func Load(fname string, advancedUser bool) *Config {
+ config, err := load(fname)
+ if err != nil {
+ // Unable to read existing config, lets start from scracth
+ // Get config from user input for advanced users, or just use default settings
+ if advancedUser {
+ config = promptUser()
+ } else {
+ config = defaultConfig()
+ }
+
+ // Save config to file
+ err := save(fname, config)
+ if err != nil {
+ fmt.Printf("Failed to save config (%s)\n", err)
+ }
+ }
+ return config
+}
diff --git a/drive.go b/drive.go
new file mode 100644
index 0000000..33d64b3
--- /dev/null
+++ b/drive.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "github.com/voxelbrain/goptions"
+ "./gdrive"
+ "./util"
+ "./cli"
+)
+
+const (
+ VersionNumber = "1.0.0"
+)
+
+type Options struct {
+ Advanced bool `goptions:"-a, --advanced, description='Advanced Mode -- lets you specify your own oauth client id and secret on setup'"`
+ AppPath string `goptions:"-c, --config, description='Set application path where config and token is stored. Defaults to ~/.gdrive'"`
+ Version bool `goptions:"-v, --version, description='Print version'"`
+ goptions.Help `goptions:"-h, --help, description='Show this help'"`
+
+ goptions.Verbs
+
+ List struct {
+ MaxResults int `goptions:"-m, --max, description='Max results'"`
+ TitleFilter string `goptions:"-t, --title, mutexgroup='query', description='Title filter'"`
+ Query string `goptions:"-q, --query, mutexgroup='query', description='Query (see https://developers.google.com/drive/search-parameters)'"`
+ SharedStatus bool `goptions:"-s, --shared, description='Show shared status (Note: this will generate 1 http req per file)'"`
+ } `goptions:"list"`
+
+ Info struct {
+ FileId string `goptions:"-i, --id, obligatory, description='File Id'"`
+ } `goptions:"info"`
+
+ Upload struct {
+ File *os.File `goptions:"-f, --file, mutexgroup='input', obligatory, rdonly, description='File to upload'"`
+ Stdin bool `goptions:"-s, --stdin, mutexgroup='input', obligatory, description='Use stdin as file content'"`
+ Title string `goptions:"-t, --title, description='Title to give uploaded file. Defaults to filename'"`
+ Share bool `goptions:"--share, description='Share uploaded file'"`
+ } `goptions:"upload"`
+
+ Download struct {
+ FileId string `goptions:"-i, --id, mutexgroup='download', obligatory, description='File Id'"`
+ Stdout bool `goptions:"-s, --stdout, description='Write file content to stdout'"`
+ Pop bool `goptions:"--pop, mutexgroup='download', description='Download latest file, and remove it from google drive'"`
+ } `goptions:"download"`
+
+ Delete struct {
+ FileId string `goptions:"-i, --id, obligatory, description='File Id'"`
+ } `goptions:"delete"`
+
+ Share struct {
+ FileId string `goptions:"-i, --id, obligatory, description='File Id'"`
+ } `goptions:"share"`
+
+ Unshare struct {
+ FileId string `goptions:"-i, --id, obligatory, description='File Id'"`
+ } `goptions:"unshare"`
+
+ Url struct {
+ FileId string `goptions:"-i, --id, obligatory, description='File Id'"`
+ Preview bool `goptions:"-p, --preview, mutexgroup='urltype', description='Generate preview url (default)'"`
+ Download bool `goptions:"-d, --download, mutexgroup='urltype', description='Generate download url'"`
+ } `goptions:"url"`
+}
+
+func main() {
+ opts := &Options{}
+ goptions.ParseAndFail(opts)
+
+ // Print version number and exit if the version flag is set
+ if opts.Version {
+ fmt.Printf("gdrive v%s\n", VersionNumber)
+ return
+ }
+
+ // Get authorized drive client
+ drive, err := gdrive.New(opts.AppPath, opts.Advanced)
+ if err != nil {
+ fmt.Printf("An error occurred creating Drive client: %v\n", err)
+ return
+ }
+
+ switch opts.Verbs {
+ case "list":
+ args := opts.List
+ cli.List(drive, args.Query, args.TitleFilter, args.MaxResults, args.SharedStatus)
+
+ case "info":
+ cli.Info(drive, opts.Info.FileId)
+
+ case "upload":
+ args := opts.Upload
+ if args.Stdin {
+ cli.Upload(drive, os.Stdin, args.Title, args.Share)
+ } else {
+ cli.Upload(drive, args.File, args.Title, args.Share)
+ }
+
+ case "download":
+ args := opts.Download
+ if args.Pop {
+ cli.DownloadLatest(drive, args.Stdout)
+ } else {
+ cli.Download(drive, args.FileId, args.Stdout, false)
+ }
+
+ case "delete":
+ cli.Delete(drive, opts.Delete.FileId)
+
+ case "share":
+ cli.Share(drive, opts.Share.FileId)
+
+ case "unshare":
+ cli.Unshare(drive, opts.Unshare.FileId)
+
+ case "url":
+ if opts.Url.Download {
+ fmt.Println(util.DownloadUrl(opts.Url.FileId))
+ } else {
+ fmt.Println(util.PreviewUrl(opts.Url.FileId))
+ }
+
+ default:
+ goptions.PrintHelp()
+ }
+}
+
+
diff --git a/gdrive/gdrive.go b/gdrive/gdrive.go
new file mode 100644
index 0000000..547be0d
--- /dev/null
+++ b/gdrive/gdrive.go
@@ -0,0 +1,53 @@
+package gdrive
+
+import (
+ "path/filepath"
+ "net/http"
+ "code.google.com/p/google-api-go-client/drive/v2"
+ "../util"
+ "../config"
+ "../auth"
+)
+
+// File paths and names
+var (
+ AppPath = filepath.Join(util.Homedir(), ".gdrive")
+ ConfigFname = "config.json"
+ TokenFname = "token.json"
+ //ConfigPath = filepath.Join(ConfigDir, "config.json")
+ //TokenPath = filepath.Join(ConfigDir, "token.json")
+)
+
+type Drive struct {
+ *drive.Service
+ client *http.Client
+}
+
+// Returns the raw http client which has the oauth transport
+func (self *Drive) Client() *http.Client {
+ return self.client
+}
+
+func New(customAppPath string, advancedMode bool) (*Drive, error) {
+ if customAppPath != "" {
+ AppPath = customAppPath
+ }
+
+ // Build paths to config files
+ configPath := filepath.Join(AppPath, ConfigFname)
+ tokenPath := filepath.Join(AppPath, TokenFname)
+
+ config := config.Load(configPath, advancedMode)
+ client, err := auth.GetOauth2Client(config.ClientId, config.ClientSecret, tokenPath)
+ if err != nil {
+ return nil, err
+ }
+
+ drive, err := drive.New(client)
+ if err != nil {
+ return nil, err
+ }
+
+ // Return a new authorized Drive client.
+ return &Drive{drive, client}, nil
+}
diff --git a/util/drive.go b/util/drive.go
new file mode 100644
index 0000000..06140dd
--- /dev/null
+++ b/util/drive.go
@@ -0,0 +1,15 @@
+package util
+
+import (
+ "fmt"
+)
+
+func PreviewUrl(id string) string {
+ //return fmt.Sprintf("https://drive.google.com/uc?id=%s&export=preview", id)
+ return fmt.Sprintf("https://drive.google.com/uc?id=%s", id)
+}
+
+// Note to self: file.WebContentLink = https://docs.google.com/uc?id=<id>&export=download
+func DownloadUrl(id string) string {
+ return fmt.Sprintf("https://drive.google.com/uc?id=%s&export=download", id)
+}
diff --git a/util/generic.go b/util/generic.go
new file mode 100644
index 0000000..fec2c31
--- /dev/null
+++ b/util/generic.go
@@ -0,0 +1,196 @@
+package util
+
+import (
+ "fmt"
+ "os"
+ "time"
+ "strings"
+ "strconv"
+ "unicode/utf8"
+ "path/filepath"
+ "runtime"
+)
+
+// Prompt user to input data
+func Prompt(msg string) string {
+ fmt.Printf(msg)
+ var str string
+ fmt.Scanln(&str)
+ return str
+}
+
+// Returns true if file/directory exists
+func FileExists(path string) bool {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true
+ }
+ return false
+}
+
+func Mkdir(path string) error {
+ dir := filepath.Dir(path)
+ if FileExists(dir) {
+ return nil
+ }
+ return os.Mkdir(dir, 0700)
+}
+
+// Returns the users home dir
+func Homedir() string {
+ if runtime.GOOS == "windows" {
+ return os.Getenv("APPDATA")
+ }
+ return os.Getenv("HOME")
+}
+
+func FormatBool(b bool) string {
+ return strings.Title(strconv.FormatBool(b))
+}
+
+func FileSizeFormat(bytes int64) string {
+ units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
+
+ var i int
+ value := bytes
+
+ for value > 1000 {
+ value /= 1000
+ i++
+ }
+ return fmt.Sprintf("%d %s", value, units[i])
+}
+
+// Truncates string to given max length, and inserts ellipsis into
+// the middle of the string to signify that the string has been truncated
+func TruncateString(str string, maxRunes int) string {
+ indicator := "..."
+
+ // Number of runes in string
+ runeCount := utf8.RuneCountInString(str)
+
+ // Return input string if length of input string is less than max length
+ // Input string is also returned if max length is less than 9 which is the minmal supported length
+ if runeCount <= maxRunes || maxRunes < 9 {
+ return str
+ }
+
+ // Number of remaining runes to be removed
+ remaining := (runeCount - maxRunes) + utf8.RuneCountInString(indicator)
+
+ var truncated string
+ var skip bool
+
+ for leftOffset, char := range str {
+ rightOffset := runeCount - (leftOffset + remaining)
+
+ // Start skipping chars when the left and right offsets are equal
+ // Or in the case where we wont be able to do an even split: when the left offset is larger than the right offset
+ if leftOffset == rightOffset || (leftOffset > rightOffset && !skip) {
+ skip = true
+ truncated += indicator
+ }
+
+ if skip && remaining > 0 {
+ // Skip char and decrement the remaining skip counter
+ remaining--
+ continue
+ }
+
+ // Add char to result string
+ truncated += string(char)
+ }
+
+ // Return truncated string
+ return truncated
+}
+
+func ISODateToLocal(iso string) string {
+ t, err := time.Parse(time.RFC3339, iso)
+ if err != nil {
+ return iso
+ }
+ local := t.Local()
+ year, month, day := local.Date()
+ hour, min, sec := local.Clock()
+ return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec)
+}
+
+func MeasureTransferRate() func(int64)string {
+ start := time.Now()
+
+ return func(bytes int64) string {
+ seconds := int64(time.Now().Sub(start).Seconds())
+ if seconds < 1 {
+ return fmt.Sprintf("%s/s", FileSizeFormat(bytes))
+ }
+ bps := bytes / seconds
+ return fmt.Sprintf("%s/s", FileSizeFormat(bps))
+ }
+}
+
+// Prints a map in the provided order with one key-value-pair per line
+func Print(m map[string]string, keyOrder []string) {
+ for _, key := range keyOrder {
+ value, ok := m[key]
+ if ok && value != "" {
+ fmt.Printf("%s: %s\n", key, value)
+ }
+ }
+}
+
+// Prints items in columns with header and correct padding
+func PrintColumns(items []map[string]string, keyOrder []string, columnSpacing int) {
+ // Create header
+ header := make(map[string]string)
+ for _, key := range keyOrder {
+ header[key] = key
+ }
+
+ // Add header as the first element of items
+ items = append([]map[string]string{header}, items...)
+
+ // Get a padding function for each column
+ padFns := make(map[string]func(string)string)
+ for _, key := range keyOrder {
+ padFns[key] = columnPadder(items, key, columnSpacing)
+ }
+
+ // Loop, pad and print items
+ for _, item := range items {
+ var line string
+
+ // Add each column to line with correct padding
+ for _, key := range keyOrder {
+ value, _ := item[key]
+ line += padFns[key](value)
+ }
+
+ // Print line
+ fmt.Println(line)
+ }
+}
+
+// Returns a padding function, that pads input to the longest string in items
+func columnPadder(items []map[string]string, key string, spacing int) func(string)string {
+ // Holds length of longest string
+ var max int
+
+ // Find the longest string of type key in the array
+ for _, item := range items {
+ str := item[key]
+ length := utf8.RuneCountInString(str)
+ if length > max {
+ max = length
+ }
+ }
+
+ // Return padding function
+ return func(str string) string {
+ column := str
+ for utf8.RuneCountInString(column) < max + spacing {
+ column += " "
+ }
+ return column
+ }
+}