Skip to content

Instantly share code, notes, and snippets.

@xiconet
Last active March 31, 2018 22:53
Show Gist options
  • Save xiconet/8f1c9c1f420e3b5a1dfb60ee612f4bd1 to your computer and use it in GitHub Desktop.
Save xiconet/8f1c9c1f420e3b5a1dfb60ee612f4bd1 to your computer and use it in GitHub Desktop.
Dropbox APIv2 client in golang
// 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)
}
@xiconet
Copy link
Author

xiconet commented Mar 31, 2018

No need for a complicated "SDK". Compile this single file to obtain a cli executable implementing most of the dropbox api endpoints

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment