Last active
March 31, 2018 22:53
-
-
Save xiconet/8f1c9c1f420e3b5a1dfb60ee612f4bd1 to your computer and use it in GitHub Desktop.
Dropbox APIv2 client in golang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Command dbox is a client for the dropbox "rest" API | |
package main | |
import( | |
"os" | |
"os/exec" | |
"time" | |
"net/http" | |
"net/url" | |
"io" | |
"io/ioutil" | |
"fmt" | |
"log" | |
"sort" | |
"strings" | |
"bytes" | |
"strconv" | |
"menteslibres.net/gosexy/yaml" | |
"menteslibres.net/gosexy/to" | |
"encoding/json" | |
"github.com/codegangsta/cli" | |
"github.com/cheggaaa/pb" | |
"runtime" | |
pth "path" | |
ospath "path/filepath" | |
"sync" | |
"github.com/xiconet/utils" | |
fd "github.com/xiconet/godownload" | |
) | |
const( | |
api_url string = "https://api.dropbox.com/2" | |
content_url = "https://content.dropboxapi.com/2" | |
cfg_file = "path/to/config_file" // FIXME | |
) | |
var( | |
users = map[string]string{"user0": "0", "user1": "1", ..., "usern": "n"} // FIXME | |
audioTypes = []string{".mp3", ".flac", ".ape", ".wav", ".wv", ".mpc", ".ogg", ".m4a"} | |
unhandled = []string{".ape", ".wv", ".wav"} // should be handled by VLC | |
chunksize = int64(8*1024*1024) | |
) | |
func Userlist() (u []string) { | |
for user, _ := range users { | |
u = append(u, user) | |
} | |
return | |
} | |
func Uids() (u []string) { | |
for _, uid := range users { | |
u = append(u, uid) | |
} | |
return | |
} | |
func UidToUser(u string) (string, error) { | |
for user, uid := range users { | |
if u == uid { | |
return user, nil | |
} | |
} | |
return "", fmt.Errorf("usage error: unregistered user") | |
} | |
type Info_off struct { | |
Country string `json:"country"` | |
DisplayName string `json:"display_name"` | |
Email string `json:"email"` | |
EmailVerified bool `json:"email_verified"` | |
IsPaired bool `json:"is_paired"` | |
Locale string `json:"locale"` | |
NameDetails struct { | |
FamiliarName string `json:"familiar_name"` | |
GivenName string `json:"given_name"` | |
Surname string `json:"surname"` | |
} `json:"name_details"` | |
QuotaInfo struct { | |
Datastores int64 `json:"datastores"` | |
Normal int64 `json:"normal"` | |
Quota int64 `json:"quota"` | |
Shared int64 `json:"shared"` | |
} `json:"quota_info"` | |
ReferralLink string `json:"referral_link"` | |
Uid int `json:"uid"` | |
} | |
type Info struct { | |
AccountId string `json:"account_id"` | |
Name struct { | |
GivenName string `json:"given_name"` | |
Surname string `json:"surname"` | |
FamiliarName string `json:"familiar_name"` | |
DisplayName string `json:"display_name"` | |
AbbreviatedName string `json:"abbreviated_name"` | |
} `json:"name"` | |
Email string `json:"email"` | |
EmailVerified bool `json:"email_verified"` | |
Disabled bool `json:"disabled"` | |
Country string `json:"country"` | |
Locale string `json:"locale"` | |
ReferralLink string `json:"referral_link"` | |
IsPaired bool `json:"is_paired"` | |
AccountType struct { | |
Tag string `json:".tag"` | |
} `json:"account_type"` | |
RootInfo struct { | |
Tag string `json:".tag"` | |
RootNamespaceId string `json:"root_namespace_id"` | |
HomeNamespaceId string `json:"home_namespace_id"` | |
} `json:"root_info"` | |
} | |
type SpaceUsage struct { | |
Used int64 `json:"used"` | |
Allocation struct { | |
Tag string `json:".tag"` | |
Individual string `json:"individual"` | |
Allocated int64 `json:"allocated"` | |
} `json:"allocation"` | |
} | |
type Entry struct { | |
Tag string `json:".tag"` | |
Name string `json:"name"` | |
PathLower string `json:"path_lower"` | |
PathDisplay string `json:"path_display"` | |
Id string `json:"id"` | |
Size int64 `json:size"` | |
User string // to be set later on | |
} | |
type Entries []Entry | |
type ByName []Entry | |
func (a ByName) Len() int { return len(a) } | |
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } | |
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } | |
func (s Entries) SetUser(user string, i int) { | |
s[i].User = user | |
} | |
type DboxFolder struct { | |
Entries Entries `json:"entries"` | |
Cursor string `json:"cursor"` | |
HasMore bool `json:"has_more"` | |
} | |
type Meta struct { | |
ClientModified string `json:"client_modified"` | |
ContentHash string `json:"content_hash"` | |
Id string `json:"id"` | |
Name string `json:"name"` | |
PathLower string `json:"path_lower"` | |
PathDisplay string `json:"path_display"` | |
Rev string `json:"rev"` | |
ServerModified string `json:"server_modified"` | |
Size int64 `json:"size"` | |
Tag string `json:".tag"` | |
ErrorSummary string `json:"error_summary"` | |
Error DbxError `json:"error"` | |
User string // to be set later on | |
} | |
type DbxError struct { | |
Tag string `json:".tag"` | |
Path struct { | |
Tag string `json:".tag"` | |
Conflict struct { | |
Tag string `json:".tag"` | |
} `json:"conflict"` | |
} `json:"path"` | |
} | |
//create folder response | |
type FolderMeta struct { | |
Metadata struct { | |
Id string `json:"id"` | |
Name string `json:"name"` | |
PathDisplay string `json:"path_display"` | |
PathLower string `json:"path_lower"` | |
} `json:"metadata"` | |
ErrorSummary string `json:"error_summary"` | |
Error DbxError `json:"error"` | |
} | |
type Metaset []Meta | |
func (s Metaset) SetUser(user string, i int) { | |
s[i].User = user | |
} | |
type Item struct { | |
Bytes int64 `json:"bytes"` | |
Icon string `json:"icon"` | |
IsDeleted bool `json:"is_deleted"` | |
IsDir bool `json:"is_dir"` | |
Modified string `json:"modified"` | |
Path string `json:"path"` | |
ReadOnly bool `json:"read_only"` | |
Rev string `json:"rev"` | |
Revision int `json:"revision"` | |
Root string `json:"root"` | |
Size string `json:"size"` | |
ThumbExists bool `json:"thumb_exists"` | |
User string | |
} | |
type Dbox struct { | |
Bytes int64 `json:"bytes"` | |
Contents []Item `json:"contents"` | |
Hash string `json:"hash"` | |
Icon string `json:"icon"` | |
IsDir bool `json:"is_dir"` | |
Modified string `json:"modified"` | |
Path string `json:"path"` | |
ReadOnly bool `json:"read_only"` | |
Rev string `json:"rev"` | |
Revision int `json:"revision"` | |
Root string `json:"root"` | |
Size string `json:"size"` | |
ThumbExists bool `json:"thumb_exists"` | |
} | |
type ByPath []Item | |
func (a ByPath) Len() int { return len(a) } | |
func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } | |
func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } | |
type Itemset []Item | |
func (s Itemset) SetUser(user string, i int) { | |
s[i].User = user | |
} | |
type Cursor struct { | |
SessionId string `json:"session_id"` | |
Offset int64 `json:"offset"` | |
} | |
type Client struct { | |
BaseUrl string | |
CfgFile string | |
User string | |
Auth Auth | |
Endpoints map[string]string | |
} | |
type Auth struct { | |
TokenType string | |
Token string | |
} | |
func NewClient(baseUrl, cfgFile, user string, auth Auth, endpoints map[string]string) (c *Client){ | |
return &Client{baseUrl, cfgFile, user, Auth{}, map[string]string{}} | |
} | |
func (c *Client) setToken(user string) string { | |
config, err := yaml.Open(cfg_file) | |
if err != nil { | |
panic(err) | |
} | |
if user == "current_user" { | |
user = to.String(config.Get("users", "current_user")) | |
} | |
token := to.String(config.Get(user, "access_token")) | |
c.User = user | |
c.Auth.TokenType = "Bearer" | |
c.Auth.Token = token | |
return token | |
} | |
//get user account information | |
func (c *Client) Info(){ | |
status, body := c.apiRequest("POST", "/users/get_current_account", nil, nil, false) | |
if status != "200 OK" { | |
fmt.Println("error: bad server status:", status) | |
fmt.Println(string(body)) | |
os.Exit(1) | |
} | |
info := Info{} | |
err := json.Unmarshal([]byte(body), &info) | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
status, body = c.apiRequest("POST", "/users/get_space_usage", nil, nil, false) | |
if status != "200 OK" { | |
fmt.Println("error: bad server status:", status) | |
fmt.Println(string(body)) | |
os.Exit(1) | |
} | |
usage := SpaceUsage{} | |
err = json.Unmarshal([]byte(body), &usage) | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
left, _ := utils.NiceBytes(usage.Allocation.Allocated - usage.Used) | |
quota, _ := utils.NiceBytes(usage.Allocation.Allocated) | |
used, _ := utils.NiceBytes(usage.Used) | |
fmt.Printf("Email: %s\nDisplay name: %s\nAccount Id: %s\n", info.Email, info.Name.DisplayName, info.AccountId) | |
fmt.Printf("Quota: %d [%s]\n", usage.Allocation.Allocated, quota) | |
fmt.Printf("Used: %d [%s]\n", usage.Used, used) | |
fmt.Printf("Left space: %d byes [%s]\n", usage.Allocation.Allocated - usage.Used, left) | |
} | |
// generic api request | |
func (c *Client) apiRequest(method, endpoint string, params interface{}, data interface{}, isJson bool) (string, []byte) { | |
uri, err := url.Parse(c.BaseUrl) | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
uri.Path += endpoint | |
if params != nil { | |
p := params.(map[string]string) | |
q := uri.Query() | |
for k, v := range p { | |
q.Set(k, v) | |
} | |
uri.RawQuery = q.Encode() | |
} | |
var req *http.Request | |
if data != nil { | |
if isJson { | |
form := data.(map[string]string) | |
form_js, _ := json.Marshal(form) | |
req, err = http.NewRequest(method, uri.String(), strings.NewReader(string(form_js))) | |
} else { | |
form := data.(url.Values) | |
req, err = http.NewRequest(method, uri.String(), strings.NewReader(form.Encode())) | |
} | |
} else { | |
req, err = http.NewRequest(method, uri.String(), nil) | |
} | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
req.Header.Set("Authorization", "Bearer "+c.Auth.Token) | |
if method == "POST" || method == "PUT" { | |
if isJson { | |
req.Header.Add("Content-Type", "application/json") | |
} else if data != nil { | |
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | |
} | |
} | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println("error:", err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(1) | |
} | |
defer resp.Body.Close() | |
switch { | |
case method == "POST": | |
if resp.StatusCode != 200 { | |
if resp.StatusCode == 403 { | |
fmt.Println("server status for %q request: %s", method, resp.Status) | |
} else { | |
fmt.Printf("error: bad server status for %q request: %s\n", method, resp.Status) | |
} | |
} | |
default: | |
if resp.StatusCode/10 != 20 { | |
fmt.Printf("error: bad server status for %q request: %s\n", method, resp.Status) | |
fmt.Printf("request: %+v\n", req) | |
} | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
return resp.Status, body | |
} | |
func (c *Client) getMetadata(path string) (meta Meta, err error){ | |
ep := "/files/alpha/get_metadata" | |
params := map[string]string{"path":path} | |
isJson := true | |
status, body := c.apiRequest("POST", ep, nil, params, isJson) | |
if status != "200 OK" { | |
if strings.Contains(status, "path/not_file/"){ | |
meta.Tag = "folder" | |
} else { | |
err = fmt.Errorf("error: bad server status:", status+"\n"+string(body)) | |
} | |
return | |
} else { | |
err = json.Unmarshal([]byte(body), &meta) | |
return | |
} | |
} | |
func fromJson(body []byte) (data DboxFolder) { | |
err := json.Unmarshal(body, &data) | |
if err != nil { | |
fmt.Println("error while decoding response body:", err) | |
fmt.Println("body", string(body)) | |
} | |
return | |
} | |
func printRes(contents Entries) { | |
sort.Sort(ByName(contents)) | |
for _, v := range contents { | |
if v.Size == 0 { | |
fmt.Println(v.Name) | |
} else { | |
filesize, _ := utils.NiceBytes(v.Size) | |
fmt.Printf("%-68s %8s\n", v.Name, filesize) | |
} | |
} | |
} | |
func Legend(users map[string]string) string { | |
legend := make([]string, len(users)) | |
i := 0 | |
for k, v := range users { | |
legend[i] = fmt.Sprintf("[%s]=%s" , v, k) | |
i += 1 | |
} | |
return strings.Join(legend, ", ") | |
} | |
func printCompiled(contents []Entry) { | |
for _, v := range contents { | |
name := v.Name | |
userId := users[v.User] | |
if v.Size == 0 { | |
fmt.Printf("[%s] %s\n", userId, name) | |
} else { | |
size, _ := utils.NiceBytes(v.Size) | |
fmt.Printf("[%s] %-65s %s\n", userId, name, size) | |
} | |
} | |
fmt.Printf("\n[%s]\n", Legend(users)) | |
} | |
func (c *Client) getResource(path string) (data DboxFolder) { | |
ep := "/files/list_folder" | |
params := map[string]string{"path":path} | |
isJson := true | |
status, body := c.apiRequest("POST", ep, nil, params, isJson) | |
if status != "200 OK" { | |
fmt.Println("error: bad server status:", status+"\n"+string(body)) | |
} else { | |
//fmt.Println(string(body)) | |
data = fromJson(body) | |
} | |
return | |
} | |
func (c *Client) listFolder(path string){ | |
fmt.Printf("User: %s\n", c.User) | |
res := c.getResource(path) | |
printRes(res.Entries) | |
} | |
func (c *Client) getTree(path string, depth, d int) { | |
mx := 69 | |
i := strings.Repeat(" ", d) | |
entries := c.getResource(path).Entries | |
sort.Sort(ByName(entries)) | |
for _, e := range entries { | |
name := e.Name | |
if e.Size != 0 { | |
if len(name) + len(i) > mx { | |
name = utils.Shorten(name, mx - len(i)) | |
} | |
if len(name) + len(i) < mx { | |
name = utils.RightPad(name, " ", mx - (len(name) + len(i))) | |
} | |
fmt.Printf("%s%s %d\n", i, name, e.Size) | |
} else { | |
fmt.Printf("%s%s\n", i, name) | |
if depth == 0 || d+1 < depth { | |
c.getTree(e.PathLower, depth, d+1) | |
} | |
} | |
} | |
} | |
func (c *Client) listAll(path string) { | |
var compiled []Entry | |
for user, _ := range users { | |
c.setToken(user) | |
res := c.getResource(path) | |
var data Entries | |
data = res.Entries | |
for k, _ := range data { data.SetUser(user, k) } | |
compiled = append(data, compiled...) | |
} | |
sort.Sort(ByName(compiled)) | |
printCompiled(compiled) | |
} | |
//type Link struct { | |
// Expires string `json:"expires"` | |
// Url string `json:"url"` | |
//} | |
type Link struct { | |
Metadata Meta `json "metadata"` | |
Link string `json:"link"` | |
} | |
// get a streamable link to a file | |
func (c *Client) getLink(path string) (link Link, err error) { | |
ep := "/files/get_temporary_link" | |
data := map[string]string{"path":path} | |
status, body := c.apiRequest("POST", ep, nil, data, true) | |
if status != "200 OK" { | |
err = fmt.Errorf("error: bad server status: "+status+"\n"+string(body)) | |
return | |
} | |
err = json.Unmarshal(body, &link) | |
return | |
} | |
func (c *Client) getLinks(path string, stream bool) [][]string { | |
links := [][]string{} | |
meta, err := c.getMetadata(path) | |
if err != nil {fmt.Println(err); os.Exit(2)} | |
if meta.Tag == "folder" { | |
folderName := pth.Base(path) | |
res := c.getResource(path) | |
items := res.Entries | |
sort.Sort(ByName(items)) | |
for _, i := range items{ | |
if i.Tag != "folder" { | |
if stream && !isAudioExt(pth.Ext(i.Name)) { | |
continue | |
} | |
filePath := pth.Join(folderName, i.Name) | |
if link, err := c.getLink(i.PathDisplay); err == nil { | |
links = append(links, []string{filePath, link.Link}) | |
} | |
} | |
} | |
} else { | |
fileName := pth.Base(path) | |
if link, err := c.getLink(path); err == nil { | |
links = append(links, []string{fileName, link.Link}) | |
} | |
} | |
if !stream { | |
for _, k := range links { | |
fmt.Println(k) | |
} | |
} | |
return links | |
} | |
func isAudioExt(p string) bool { | |
return utils.StringInSlice(pth.Ext(p), audioTypes) | |
} | |
func (c *Client) streamLinks(path string) { | |
vlc := "C:/Program Files (x86)/VideoLAN/VLC/vlc.exe" | |
fb2k := "C:/Program Files (x86)/foobar2000/foobar2000.exe" | |
playerName := map[string]string{vlc: "VLC", fb2k: "foobar2000"} | |
links := c.getLinks(path, true) | |
if len(links) == 0 { | |
fmt.Printf("didn't find any registered audio type file in %s", path) | |
os.Exit(0) | |
} | |
player := fb2k | |
for _, k := range links { | |
if utils.StringInSlice(pth.Ext(k[1]), unhandled) { | |
player = vlc | |
break | |
} | |
} | |
fmt.Printf("got %d files\n", len(links)) | |
var args []string | |
if player == fb2k { args = append(args, "/add")} | |
for _, f := range links { | |
fmt.Println(f[0]) | |
args = append(args, f[1]) | |
} | |
if player == vlc { args = append(args, "--qt-start-minimized") } | |
fmt.Println("\nlaunching", playerName[player]) | |
cmd := exec.Command(player, args...) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
err := cmd.Start() | |
if err != nil {log.Fatal(err)} | |
} | |
func (c *Client) downloadFile(itemPath, filepath string, aria, fast bool, conns int) int64 { | |
uri := "https://content.dropboxapi.com/2/files/download" | |
auth := fmt.Sprintf("%s %s", c.Auth.TokenType, c.Auth.Token) | |
p := map[string]string{"path":itemPath} | |
params, _ := json.Marshal(p) | |
switch { | |
case fast: | |
uri += fmt.Sprintf("?access_token=%s", c.Auth.Token) | |
c.fastFileDownload(uri, conns, filepath) | |
return 0 | |
case aria : | |
cmd := exec.Command("aria2c" , "--file-allocation=falloc", "--max-connection-per-server=5" , "--min-split-size=1M", "--remote-time=true", "--header=Authorization:"+auth, "--header=Dropbox-API-Arg:"+string(params), uri, "-o "+filepath) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
err := cmd.Run() | |
if err != nil { | |
fmt.Println(err) | |
} | |
stat, err := os.Stat(filepath) | |
if err != nil {panic(err)} | |
return stat.Size() | |
default: | |
req, err := http.NewRequest("GET", uri, nil) | |
req.Header.Add("Authorization", auth) | |
req.Header.Add("Dropbox-API-Arg", string(params)) | |
client := &http.Client{} | |
resp, err := client.Do(req) | |
if err != nil { | |
panic(err) | |
} | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
return 0 | |
} | |
defer resp.Body.Close() | |
out, err := os.Create(filepath) | |
if err != nil {panic(err)} | |
defer out.Close() | |
i, _ := strconv.Atoi(resp.Header.Get("Content-Length")) | |
sourceSize := int64(i) | |
source := resp.Body | |
bar := pb.New(int(sourceSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10) | |
if sourceSize >= 1024 * 1024 { | |
bar.ShowSpeed = true | |
} | |
bar.Start() | |
writer := io.MultiWriter(out, bar) | |
n, err := io.Copy(writer, source) | |
if err != nil { | |
fmt.Println("Error while downloading", uri, "-", err) | |
return 0 | |
} | |
bar.Finish() | |
return n | |
} | |
} | |
func (c *Client) downsync(path, folderpath string, aria, fast bool, depth, r, conns int) { | |
err := os.MkdirAll(folderpath, 0777) | |
if err != nil { | |
fmt.Println("error: could not create new folder", folderpath, ":", err) | |
os.Exit(2) | |
} | |
res := c.getResource(path) | |
items := res.Entries | |
for _, e := range items { | |
if e.Tag != "folder" { | |
filepath := folderpath + "/" + e.Name | |
fmt.Println("downloading", filepath) | |
var dlfast bool | |
if fast && e.Size >= 1024*1024 { | |
dlfast = true | |
} | |
n := c.downloadFile(e.PathDisplay, filepath, aria, dlfast, conns) | |
if !fast && (n != e.Size) { | |
fmt.Printf("error: size mismatch, expected: %d bytes, actual: %d bytes\n", e.Size, n) | |
} | |
} | |
if (r < depth || depth == 0) && (e.Tag == "folder") { | |
c.downsync(e.PathDisplay, folderpath+"/"+e.Name , aria, fast, depth, r+1, conns) | |
} | |
} | |
} | |
func (c *Client) download(path, localPath string, aria, fast bool, depth, parallel, conns int) { | |
meta, err := c.getMetadata(path) | |
if err != nil {fmt.Println(err); os.Exit(2)} | |
if meta.Tag != "folder" { | |
c.downloadFile(path, localPath, aria, fast, conns) | |
} else { | |
data := c.getResource(path) | |
if parallel > 0 { | |
items := data.Entries | |
c.parallelDownload(items, localPath, parallel) | |
} else { | |
c.downsync(path, localPath, aria, fast, depth, 1, conns) | |
} | |
} | |
} | |
func (c *Client) fastFileDownload(url string, conns int, outfile string) { | |
d := fd.New() | |
size, filename, err := d.Init(url, conns, outfile) | |
var filesize string | |
if f, err := utils.NiceBytes(int64(size)); err == nil { | |
filesize = f | |
} | |
fmt.Printf("File size: %s; filename: %s\n", filesize, filename) | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(1) | |
} | |
d.StartDownload() | |
go d.Wait() | |
DisplayProgress(&d) | |
} | |
func DisplayProgress(dl *fd.Downloader) { | |
barWidth := float64(37) | |
for { | |
status, total, downloaded, elapsed := dl.GetProgress() | |
frac := float64(downloaded)/float64(total) | |
bps, _ := utils.NiceBytes(int64(float64(downloaded)/elapsed.Seconds())) | |
tot, _ := utils.NiceBytes(int64(total)) | |
if frac == 0 { continue } | |
fmt.Fprintf(os.Stdout, "[%-38s] %5.1f%% of %10s %10s/s %3.fs\r", | |
strings.Repeat("=", int(frac*barWidth))+">", frac*100, tot, bps, elapsed.Seconds()) | |
switch { | |
case status == fd.Completed: | |
fmt.Println("\nDownload successfully completed in", elapsed) | |
return | |
case status == fd.OnProgress: // needed? | |
case status == fd.NotStarted: // needed? | |
default: | |
fmt.Printf("\nDownload failed: %s\n", status) | |
os.Exit(1) | |
} | |
time.Sleep(time.Second) | |
} | |
} | |
func (c *Client) parallelDownload(items []Entry, localFolder string, p int){ | |
var d, k int | |
var dbytes int64 | |
err := os.Mkdir(localFolder, 0777) | |
if err != nil {panic(err)} | |
s := time.Now().Unix() | |
for k < len(items) { | |
var wg sync.WaitGroup | |
for d = 0; d < p; d += 1 { | |
if k+d < len(items) && items[k+d].Tag != "folder" { | |
wg.Add(1) | |
f := items[k+d] | |
// anonymous func can be replaced by a named one | |
// by passing a pointer to wg.SyncGroup | |
// see example in "parallel_downloads.go" | |
go func(f Entry) { | |
defer wg.Done() | |
uri := content_url + "/files/download" | |
req, _ := http.NewRequest("GET", uri, nil) | |
auth := fmt.Sprintf("%s %s", c.Auth.TokenType, c.Auth.Token) | |
req.Header.Set("Authorization", auth) | |
p := map[string]string{"path":f.PathDisplay} | |
params, _ := json.Marshal(p) | |
req.Header.Set("Dropbox-API-Arg", string(params)) | |
fmt.Println("downloading:", f.PathDisplay) | |
resp, _ := http.DefaultClient.Do(req) | |
defer resp.Body.Close() | |
fmt.Println(resp.Status) | |
fp := ospath.Join(localFolder, f.Name) | |
out, err := os.Create(fp) | |
if err != nil {panic(err)} | |
defer out.Close() | |
n, _ := io.Copy(out, resp.Body) | |
dbytes += n | |
}(f) | |
} | |
} | |
wg.Wait() | |
k += d | |
} | |
dtime := time.Now().Unix() - s | |
fmt.Printf("downloaded %d bytes in %d seconds\n", dbytes, dtime) | |
} | |
func (c *Client) getFile(f Entry, wg *sync.WaitGroup, localFolder string) { | |
p := map[string]string{"path": f.PathDisplay} | |
uri := content_url + "/files/download" | |
req, _ := http.NewRequest("GET", uri, nil) | |
auth := fmt.Sprintf("%s %s", c.Auth.TokenType, c.Auth.Token) | |
req.Header.Set("Authorization", auth) | |
params, _ := json.Marshal(p) | |
req.Header.Set("Dropbox-API-Arg", string(params)) | |
fmt.Println("downloading:", f.PathDisplay) | |
resp, _ := http.DefaultClient.Do(req) | |
defer resp.Body.Close() | |
fmt.Println(resp.Status) | |
filename := ospath.Base(f.PathDisplay) | |
out, err := os.Create(ospath.Join(localFolder, filename)) | |
if err != nil { | |
panic(err) | |
} | |
defer out.Close() | |
io.Copy(out, resp.Body) | |
wg.Done() | |
} | |
func (c *Client) pipedUpload(localPath, parent string) { | |
filename := ospath.Base(localPath) | |
remotePath := pth.Join(parent, filename) | |
if remotePath[:1] != "/" { | |
remotePath = "/" + remotePath | |
} | |
url := "https://content.dropboxapi.com/2/files/upload" | |
p := map[string]string{"path":remotePath} | |
params, _ := json.Marshal(p) | |
input, err := os.Open(localPath) | |
check(err) | |
defer input.Close() | |
stat, err := input.Stat() | |
check(err) | |
pipeOut, pipeIn := io.Pipe() | |
fsize := stat.Size() | |
bar := pb.New(int(fsize)).SetUnits(pb.U_BYTES) | |
if fsize >= 1024 { | |
bar.ShowSpeed = true | |
} | |
writer := io.Writer(pipeIn) | |
// do the request concurrently | |
var resp *http.Response | |
done := make(chan error) | |
go func() { | |
req, err := http.NewRequest("POST", url, pipeOut) | |
if err != nil { | |
done <- err | |
return | |
} | |
req.ContentLength = fsize | |
req.Header.Set("Authorization", "Bearer "+c.Auth.Token) | |
req.Header.Set("Dropbox-API-Arg", string(params)) | |
req.Header.Set("Content-Type", "application/octet-stream") | |
log.Println("Created Request") | |
bar.Start() | |
resp, err = http.DefaultClient.Do(req) | |
if err != nil { | |
done <- err | |
return | |
} | |
done <- nil | |
}() | |
out := io.MultiWriter(writer, bar) | |
_, err = io.Copy(out, input) | |
check(err) | |
check(pipeIn.Close()) | |
check(<-done) | |
bar.Finish() | |
body, _ := ioutil.ReadAll(resp.Body) | |
meta := Meta{} | |
err = json.Unmarshal(body, &meta) | |
if err != nil { | |
fmt.Println(err, string(body)) | |
} else { | |
fmt.Printf("%+v\n", meta) | |
} | |
} | |
func check(err error) { | |
_, file, line, _ := runtime.Caller(1) | |
if err != nil { | |
log.Fatalf("Fatal from <%s:%d>\nError:%s", file, line, err) | |
} | |
} | |
func (c *Client) upsync(localPath, parent string) { | |
fmt.Printf("creating folder %q in %q\n", ospath.Base(localPath), parent) | |
parentPath := c.mkfolder(ospath.Base(localPath), parent) | |
if parentPath == "409 Conflict" { | |
parentPath = pth.Join(parent, ospath.Base(localPath)) | |
fmt.Println("conflict: folder %q already exists\n", parentPath) | |
} | |
dirlist, err := ioutil.ReadDir(localPath) | |
if err != nil {panic(err)} | |
for _, f := range dirlist { | |
if !f.IsDir() && strings.ToLower(f.Name()) != "thumbs.db" { | |
filepath := ospath.Join(localPath, f.Name()) | |
fmt.Printf("uploading %q to %q\n", filepath, parentPath) | |
c.pipedUpload(filepath, parentPath) | |
} | |
if f.IsDir() { | |
folderPath := ospath.Join(localPath, f.Name()) | |
c.upsync(folderPath, parentPath) | |
} | |
} | |
} | |
func (c *Client) upload(localPath, parent string) { | |
stat, er := os.Stat(localPath) | |
if er != nil { | |
fmt.Println(er) | |
os.Exit(2) | |
} | |
if !stat.IsDir(){ | |
c.pipedUpload(localPath, parent) | |
} else { | |
c.upsync(localPath, parent) | |
} | |
} | |
func makeChunk(fh *os.File, offset int64) []byte { | |
p := make([]byte, chunksize) | |
//fmt.Println("offset:", offset) | |
n, _ := fh.ReadAt(p, offset) | |
return p[:n] | |
} | |
func (c *Client) startUploadSession(fh *os.File) (cursor Cursor){ | |
//Returns json {"session_id": <session_id>} | |
uri, _ := url.Parse(content_url + "/files/upload_session/start") | |
p := map[string]bool{"close": false} | |
params, _ := json.Marshal(p) | |
chunk := makeChunk(fh, 0) | |
data := bytes.NewReader(chunk) | |
req, err := http.NewRequest("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
req.Header.Add("Authorization", "Bearer "+c.Auth.Token) | |
req.Header.Add("Content-Type", "application/octet-stream") | |
req.Header.Add("Dropbox-API-Arg", string(params)) | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
defer resp.Body.Close() | |
body, _ := ioutil.ReadAll(resp.Body) | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
fmt.Println(string(body)) | |
} | |
err = json.Unmarshal(body, &cursor) | |
if err != nil { | |
fmt.Println(err) | |
} | |
return | |
} | |
func (c *Client) uploadSessionAppend(fh *os.File, cursor Cursor){ | |
//No return values. | |
uri, _ := url.Parse(content_url + "/files/upload_session/append_v2") | |
offset := cursor.Offset | |
chunk := makeChunk(fh, offset) | |
data := bytes.NewReader(chunk) | |
req, err := http.NewRequest("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
req.Header.Add("Authorization", "Bearer "+c.Auth.Token) | |
req.Header.Add("Content-Type", "application/octet-stream") | |
type Params struct { | |
Cursor Cursor `json:"cursor"` | |
Close bool `json:"close"` | |
} | |
p := Params{} | |
p.Cursor = cursor | |
p.Close = false | |
params, _ := json.Marshal(p) | |
//params map[string]Cursor{"cursor": cursor, "close": False} | |
req.Header.Add("Dropbox-API-Arg", string(params)) | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
defer resp.Body.Close() | |
body, _ := ioutil.ReadAll(resp.Body) | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
fmt.Println("response body:", string(body)) | |
} | |
} | |
func (c *Client) uploadSessionFinish(fh *os.File, cursor Cursor, remote_path string) (res Meta) { | |
//Returns file props. | |
uri, _ := url.Parse(content_url + "/files/upload_session/finish") | |
type Commit struct { | |
Path string `json:"path"` | |
Mode string `json:"mode"` | |
Autorename bool `json:"autorename"` | |
Mute bool `json:"mute"` | |
} | |
type Params struct { | |
Cursor Cursor `json:"cursor"` | |
Commit Commit `json:"commit"` | |
} | |
//params = map[string]{"cursor": cursor, "commit": {"path": remote_path, "mode": "add", "autorename": true, "mute": false} | |
p := Params{} | |
p.Cursor = cursor | |
commit := Commit{Path: remote_path, Mode: "add", Autorename: true, Mute: false} | |
p.Commit = commit | |
params, _ := json.Marshal(p) | |
offset := cursor.Offset | |
chunk := makeChunk(fh, offset) | |
data := bytes.NewReader(chunk) | |
req, err := http.NewRequest("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
req.Header.Add("Authorization", "Bearer "+c.Auth.Token) | |
req.Header.Add("Dropbox-API-Arg", string(params)) | |
req.Header.Add("Content-Type", "application/octet-stream") | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(2) | |
} | |
defer resp.Body.Close() | |
body, _ := ioutil.ReadAll(resp.Body) | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
fmt.Println("response body:", string(body)) | |
} | |
err = json.Unmarshal(body, &res) | |
if err != nil {fmt.Println(err)} | |
return | |
} | |
func (c *Client) chunkedUpload(localPath, parent string) { | |
stat, err := os.Stat(localPath) | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(2) | |
} | |
filesize := stat.Size() | |
fmt.Printf("filesize: %d bytes\n", filesize) | |
fh, er := os.Open(localPath) | |
if er != nil {fmt.Println(er); os.Exit(2)} | |
if filesize <= chunksize { | |
c.pipedUpload(localPath, parent) | |
} else { | |
remotePath := pth.Join(parent, ospath.Base(fh.Name())) | |
if remotePath[:1] != "/" { | |
remotePath = "/" + remotePath | |
} | |
fmt.Println("starting upload session") | |
fmt.Println("remote path:", remotePath) | |
cursor := c.startUploadSession(fh) | |
cursor.Offset += chunksize | |
position := chunksize | |
fmt.Printf("cursor: %+v\n", cursor) | |
for position < filesize { | |
if ((filesize - position) <= chunksize){ | |
fmt.Println("Last chunk, finishing upload session...") | |
finish := c.uploadSessionFinish(fh, cursor, remotePath) | |
fmt.Printf("%+v\n", finish) | |
position += chunksize | |
} else { | |
fmt.Println("appending data; offset:", cursor.Offset) | |
c.uploadSessionAppend(fh, cursor) | |
position += chunksize | |
cursor.Offset = position | |
} | |
} | |
} | |
} | |
type Search struct { | |
Matches []Match `json:"matches"` | |
More bool `json:"more"` | |
Start int `json:"start"` | |
} | |
type Match struct { | |
MatchType struct { | |
Tag string `json:".tag"` | |
} `json:"match_type"` | |
Metadata Meta `json:"metadata"` | |
} | |
type Matchset []Match | |
func(m Matchset) SetUser(user string, i int) { | |
m[i].Metadata.User = user | |
} | |
func (c *Client) doSearch(path, query string) Search { | |
var result Search | |
ep := "/files/search" | |
params := map[string]string{"path": path, "query":query} | |
status, body := c.apiRequest("POST", ep, nil, params, true) | |
if status == "200 OK" { | |
err := json.Unmarshal(body, &result) | |
if err != nil { fmt.Println(err);os.Exit(1) } | |
} | |
return result | |
} | |
func (c *Client) searchUser(path, query string) { | |
result := c.doSearch(path, query) | |
matches := result.Matches | |
count := len(matches) | |
if path == "" { | |
path = "/" | |
} else if path [:1] != "/" { path = "/"+path} | |
fmt.Printf("found %d item(s) in %s:\n\n", count, path) | |
for _,e := range matches { | |
if e.Metadata.Tag != "folder" { | |
size, _ := utils.NiceBytes(e.Metadata.Size) | |
fmt.Printf("%-70s %8s\n", e.Metadata.Name, size) | |
} else { | |
fmt.Println(e.Metadata.Name) | |
} | |
} | |
} | |
func (c *Client) searchAll(path, query string){ | |
var res []Match | |
for user, _ := range users { | |
var result Matchset | |
c.setToken(user) | |
resp := c.doSearch(path, query) | |
result = resp.Matches | |
for k, _ := range result { | |
result.SetUser(user, k) | |
} | |
res = append(result, res...) | |
} | |
//sort.Sort(ByName(res)) | |
if path == "" { | |
path = "/" | |
} else if path [:1] != "/" { path = "/"+path} | |
fmt.Printf("found %d items in %v:\n\n", len(res), path) | |
for _, e := range res { | |
if e.Metadata.Tag != "folder" { | |
size, _ := utils.NiceBytes(e.Metadata.Size) | |
fmt.Printf("[%s] %-65s %8s\n", users[e.Metadata.User], e.Metadata.PathDisplay, size) | |
} else { | |
fmt.Printf("[%s] %s\n", users[e.Metadata.User], e.Metadata.PathDisplay) | |
} | |
} | |
fmt.Printf("\n[%s]\n", Legend(users)) | |
} | |
func (c *Client) mkfolder(foldername, parent string) (p string) { | |
path := pth.Join(parent, foldername) | |
if path[:1] != "/" { | |
path = "/" + path | |
} | |
form := map[string]string{"path": path} | |
var res FolderMeta | |
status, body := c.apiRequest("POST", "/files/create_folder_v2", nil, form, true) | |
if status != "200 OK" { | |
fmt.Println("error: bad status:", status) | |
if status != "409 Conflict" { | |
return | |
} | |
} | |
err := json.Unmarshal(body, &res) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
fmt.Printf("%+v\n", res) | |
if status == "409 Conflict" { | |
return status | |
} | |
return res.Metadata.PathDisplay | |
} | |
func (c *Client) createFolder(foldername, parent string) { | |
c.mkfolder(foldername, parent) | |
} | |
func (c *Client) move(src, dest string) { | |
type Resp struct { | |
Metadata Meta `json:"metadata"` | |
} | |
if src[:1] != "/"{ | |
src = "/" + src | |
} | |
form := map[string]string{"from_path": src, "to_path": dest} | |
status, body := c.apiRequest("POST", "/files/move_v2", nil, form, true) | |
if status != "200 OK" { fmt.Println("bad status:", status); os.Exit(1)} | |
var res Resp | |
err := json.Unmarshal(body, &res) | |
if err != nil {fmt.Println(err); os.Exit(1)} | |
if res.Metadata.Tag != "folder" { | |
size, _ := utils.NiceBytes(res.Metadata.Size) | |
fmt.Printf("path:%s size:%s\n", res.Metadata.PathDisplay, size) | |
} else { | |
fmt.Println("path:", res.Metadata.PathDisplay) | |
} | |
} | |
func (c *Client) remove(path string) { | |
type Resp struct { | |
Metadata Meta `json:"metadata"` | |
} | |
if path[:1] != "/" { | |
path = "/" + path | |
} | |
params := map[string]string{"path":path} | |
status, body := c.apiRequest("POST", "/files/delete_v2", nil, params, true) | |
if status != "200 OK" { fmt.Println("bad status:", status); os.Exit(1)} | |
var res Resp | |
err := json.Unmarshal(body, &res) | |
if err != nil {panic(err)} | |
fmt.Printf("%+v\n", res.Metadata) | |
} | |
func main() { | |
userlist := Userlist() | |
uids := Uids() | |
app := cli.NewApp() | |
app.Name = "dropbox" | |
app.Version = "0.42" | |
app.Usage = "client for the dropbox 'rest' APIv2" | |
app.Flags = []cli.Flag { | |
cli.StringFlag{ | |
Name: "user, u", | |
Value: "current_user", | |
Usage: fmt.Sprintf("user name, one of %s", strings.Join(userlist, ", ")), | |
}, | |
cli.BoolFlag{ | |
Name: "info, i", | |
Usage: "get account info for the current/specified user", | |
}, | |
cli.BoolFlag{ | |
Name: "meta, M", | |
Usage: "get metadata for the specified path", | |
}, | |
cli.BoolFlag{ | |
Name: "tree, t", | |
Usage: "recursively list the specified path", | |
}, | |
cli.BoolFlag{ | |
Name: "all_users, a", | |
Usage: "list the specified path for all users", | |
}, | |
cli.BoolFlag{ | |
Name: "link, k", | |
Usage: "get streamable link(s) for item(s) under the specified path", | |
}, | |
cli.BoolFlag{ | |
Name: "play, S", | |
Usage: "stream link(s) for item(s) under the specified path in foobar2000 or VLC", | |
}, | |
cli.BoolFlag{ | |
Name: "download, d", | |
Usage: "download file(s) under the specified path", | |
}, | |
cli.IntFlag{ | |
Name: "depth, r", | |
Value: 1, | |
Usage: "recursion depth for folder downloading", | |
}, | |
cli.BoolFlag{ | |
Name: "aria, x", | |
Usage: "use external (and faster) aria2c downloader", | |
}, | |
cli.BoolFlag{ | |
Name: "fast, f", | |
Usage: "download using parallel connections (internal code)", | |
}, | |
cli.IntFlag{ | |
Name: "conns, c", | |
Value: 5, | |
Usage: "number of connections for 'fast' (parallel) downloading", | |
}, | |
cli.IntFlag{ | |
Name: "parallel, P", | |
Value: 0, | |
Usage: "use with --d to download a folder's files by batches of <n> parallel goroutines", | |
}, | |
cli.StringFlag{ | |
Name: "mkfolder, m", | |
Value: "", | |
Usage: "create a new folder in the specified parent folder path", | |
}, | |
cli.StringFlag{ | |
Name: "upload, p", | |
Value: "", | |
Usage: "upload file(s) to the specified parent folder path", | |
}, | |
cli.StringFlag{ | |
Name: "chunked_upload, cu", | |
Value: "", | |
Usage: "upload large file(s) by chunks to the specified parent folder path", | |
}, | |
cli.IntFlag{ | |
Name: "chunk_size, cs", | |
Value: 0, | |
Usage: "chunk size in MiB to the specified parent folder path", | |
}, | |
cli.StringFlag{ | |
Name: "move, mv", | |
Value: "", | |
Usage: "move and/or rename item(s) from <src> to <dest> full path", | |
}, | |
cli.StringFlag{ | |
Name: "search, s", | |
Value: "", | |
Usage: "search for the specified <query> string", | |
}, | |
cli.BoolFlag{ | |
Name: "remove, rm", | |
Usage: "remove item(s) at the specified path", | |
}, | |
} | |
app.Action = func(c *cli.Context) { | |
user := c.String("user") | |
path := "" | |
if len(c.Args()) > 0 { | |
path = c.Args()[0] | |
} | |
if user != "current_user" { | |
if _, ok := users[user]; !ok { | |
fmt.Printf("error: %q is not a registered user\n", user) | |
fmt.Println("use one of", strings.Join(userlist, ", ")) | |
os.Exit(2) | |
} | |
} | |
if utils.StringInSlice(strings.Split(path, "/")[0], userlist) { | |
user = strings.Split(path, "/")[0] | |
path = strings.Join(strings.Split(path, "/")[1:], "/") | |
} else if utils.StringInSlice(strings.Split(path, "/")[0], uids) { | |
var err error | |
user, err = UidToUser(strings.Split(path, "/")[0]) | |
if err != nil { | |
fmt.Println(err); os.Exit(1) | |
} | |
path = strings.Join(strings.Split(path, "/")[1:], "/") | |
} | |
if path != "" && path[:1] != "/" {path = "/"+path} | |
d := NewClient(api_url, cfg_file, "", Auth{}, map[string]string{}) | |
if !c.Bool("all") { d.setToken(user) } | |
switch { | |
case c.Bool("tree"): | |
depth := c.Int("depth") | |
if depth == 1 {depth = 0} | |
d.getTree(path, depth, 0) | |
case c.Bool("info"): | |
d.Info() | |
case c.Bool("download"): | |
localPath := pth.Base(path) | |
d.download(path, localPath, c.Bool("aria"), c.Bool("fast"), c.Int("depth"), c.Int("parallel"), c.Int("conns")) | |
case c.Bool("link"): | |
stream := false | |
d.getLinks(path, stream) | |
case c.Bool("play"): | |
d.streamLinks(path) | |
case c.String("search") != "" : | |
query := c.String("search") | |
if c.Bool("all_users") { | |
d.searchAll(path, query) | |
} else { | |
d.searchUser(path, query) | |
} | |
case c.String("move") != "" : | |
d.move(c.String("move"), path) | |
case c.String("upload") != "" : | |
d.upload(c.String("upload"), path) | |
case c.String("chunked_upload") != "" : | |
if c.Int("chunk_size") > 0 { | |
chunksize = int64(c.Int("chunk_size")*1024*1024) | |
} | |
d.chunkedUpload(c.String("chunked_upload"), path) | |
case c.String("mkfolder") != "" : | |
d.createFolder(c.String("mkfolder"), path) | |
case c.Bool("remove"): | |
if path == "/" { | |
fmt.Println("error: cannot remove root folder") | |
os.Exit(2) | |
} | |
d.remove(path) | |
case c.Bool("meta"): | |
meta, _ := d.getMetadata(path) | |
fmt.Printf("%+v\n", meta) | |
default: | |
if c.Bool("all_users") { | |
d.listAll(path) | |
} else { | |
d.listFolder(path) | |
} | |
} | |
} | |
app.Run(os.Args) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
No need for a complicated "SDK". Compile this single file to obtain a cli executable implementing most of the dropbox api endpoints