// 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": } 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 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 to full path", }, cli.StringFlag{ Name: "search, s", Value: "", Usage: "search for the specified 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) }