Created
January 24, 2012 17:39
Revisions
-
There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,411 @@ package dropbox import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "time" ) const TimeFormat = time.RFC1123Z type OAuthToken struct { Key, Secret string } type token interface { key() string secret() string } func buildAuthString(consumerToken AppToken, tok token) string { var buf bytes.Buffer buf.WriteString(`OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT"`) fmt.Fprintf(&buf, `, oauth_consumer_key="%s"`, url.QueryEscape(consumerToken.Key)) fmt.Fprintf(&buf, `, oauth_timestamp="%v"`, time.Now().Unix()) sigend := "" if tok != nil { sigend = url.QueryEscape(tok.secret()) fmt.Fprintf(&buf, `, oauth_token="%s"`, url.QueryEscape(tok.key())) } fmt.Fprintf(&buf, `, oauth_signature="%s&%s"`, url.QueryEscape(consumerToken.Secret), sigend) return buf.String() } type Error struct { Code int Message string } func (e Error) Error() string { return fmt.Sprintf("%d: %s", e.Code, e.Message) } func doRequest(r *http.Request, consumerTok AppToken, accessTok token) (*FileReader, error) { r.Header.Set("Authorization", buildAuthString(consumerTok, accessTok)) resp, err := http.DefaultClient.Do(r) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { defer resp.Body.Close() var info struct { Error string `json:"error"` } json.NewDecoder(resp.Body).Decode(&info) return nil, Error{ Code: resp.StatusCode, Message: info.Error} } return newFileReader(resp), nil } func apiURL(path string) url.URL { return url.URL{ Scheme: "https", Host: apiHost, Path: "/" + apiVersion + path} } func GetAuthorizeURL(requestToken RequestToken, callback *url.URL) *url.URL { params := url.Values{"oauth_token": {requestToken.Key}} if callback != nil { params.Add("oauth_callback", callback.String()) } return &url.URL{ Scheme: "https", Host: webHost, Path: "/" + apiVersion + "/oauth/authorize", RawQuery: params.Encode()} } type AppToken OAuthToken type RequestToken OAuthToken type AccessToken OAuthToken func (at AccessToken) key() string { return at.Key } func (at AccessToken) secret() string { return at.Secret } func (rt RequestToken) key() string { return rt.Key } func (rt RequestToken) secret() string { return rt.Secret } func postForToken(u url.URL, appToken AppToken, accessToken token) (OAuthToken, error) { r, e := http.NewRequest("POST", u.String(), nil) if e != nil { return OAuthToken{}, e } rc, e := doRequest(r, appToken, accessToken) if e != nil { return OAuthToken{}, e } defer rc.Close() var buf bytes.Buffer buf.ReadFrom(rc) vals, e := url.ParseQuery(buf.String()) if e != nil { return OAuthToken{}, e } return OAuthToken{ Key: vals.Get("oauth_token"), Secret: vals.Get("oauth_token_secret")}, nil } func StartAuth(appToken AppToken) (RequestToken, error) { u := apiURL("/oauth/request_token") t, e := postForToken(u, appToken, nil) return RequestToken(t), e } func FinishAuth(appToken AppToken, requestToken RequestToken) (AccessToken, error) { u := apiURL("/oauth/access_token") t, e := postForToken(u, appToken, requestToken) return AccessToken(t), e } type accessType int const ( AppFolder accessType = iota Dropbox ) type Config struct { Access accessType Locale string } type Client struct { AppToken AppToken AccessToken AccessToken Config Config } type AccountInfo struct { ReferralLink string `json:"referral_link"` DisplayName string `json:"display_name"` Uid uint64 `json:"uid"` Country string `json:"country"` QuotaInfo struct { Shared uint64 `json:"shared"` Quota uint64 `json:"quota"` Normal uint64 `json:"normal"` } `json:"quota_info"` Email string `json:"email"` } type FileMetadata struct { Size string `json:"size"` Rev string `json:"rev"` ThumbExists bool `json:"thumb_exists"` Bytes int64 `json:"bytes"` Modified string `json:"modified"` Path string `json:"path"` IsDir bool `json:"is_dir"` Icon string `json:"icon"` Root string `json:"root"` MimeType string `json:"mime_type"` Revision int64 `json:"revision"` Hash *string `json:"hash"` Contents []FileMetadata `json:"contents"` } func (md *FileMetadata) ModTime() time.Time { t, _ := time.Parse(TimeFormat, md.Modified) return t } type Link struct { URL string `json:"url"` Expires string `json:"expires"` } func (s *Client) doGet(u url.URL) (*FileReader, error) { r, e := http.NewRequest("GET", u.String(), nil) if e != nil { return nil, e } return doRequest(r, s.AppToken, s.AccessToken) } func (s *Client) getForJson(u url.URL, jdata interface{}) error { buf, err := s.doGet(u) if err != nil { return err } defer buf.Close() return json.NewDecoder(buf).Decode(jdata) } func (s *Client) postForJson(u url.URL, jdata interface{}) error { r, e := http.NewRequest("POST", u.String(), nil) if e != nil { return e } rc, e := doRequest(r, s.AppToken, s.AccessToken) if e != nil { return e } defer rc.Close() return json.NewDecoder(rc).Decode(jdata) } const ( apiVersion = "1" apiHost = "api.dropbox.com" contentHost = "api-content.dropbox.com" webHost = "www.dropbox.com" ) func (s *Client) GetAccountInfo() (*AccountInfo, error) { u := apiURL("/account/info") u.RawQuery = s.Config.localeQuery() var info AccountInfo if e := s.getForJson(u, &info); e != nil { return nil, e } return &info, nil } func (s *Client) root() string { if s.Config.Access == Dropbox { return "dropbox" } return "sandbox" } func (s *Client) GetMetadata(path string, list bool) (*FileMetadata, error) { u := apiURL("/metadata/" + s.root() + path) v := url.Values{"list": {strconv.FormatBool(list)}} u.RawQuery = s.Config.setLocale(v).Encode() var md FileMetadata if e := s.getForJson(u, &md); e != nil { return nil, e } return &md, nil } func contentURL(path string) url.URL { return url.URL{ Scheme: "https", Host: contentHost, Path: "/" + apiVersion + path} } type ThumbSize string const ( ThumbSmall ThumbSize = "small" ThumbMedium ThumbSize = "medium" ThumbLarge ThumbSize = "large" ThumbL ThumbSize = "l" ThumbXL ThumbSize = "xl" ) func (s *Client) GetThumb(path string, size ThumbSize) (*FileReader, error) { u := contentURL("/thumbnails/" + s.root() + path) if size != "" { u.RawQuery = url.Values{"size": {string(size)}}.Encode() } rc, e := s.doGet(u) return rc, e } func (s *Client) AddFile(path string, contents io.Reader, size int64) (*FileMetadata, error) { return s.putFile(path, contents, size, url.Values{"overwrite": {"false"}}) } func (s *Client) UpdateFile(path string, contents io.Reader, size int64, parentRev string) (*FileMetadata, error) { return s.putFile(path, contents, size, url.Values{"parent_rev": {parentRev}}) } func (s *Client) ForceFile(path string, contents io.Reader, size int64) (*FileMetadata, error) { return s.putFile(path, contents, size, url.Values{"overwrite": {"true"}}) } func (s *Client) putFile(path string, contents io.Reader, size int64, vals url.Values) (*FileMetadata, error) { u := contentURL("/files_put/" + s.root() + path) if vals == nil { vals = make(url.Values) } u.RawQuery = s.Config.setLocale(vals).Encode() r, e := http.NewRequest("PUT", u.String(), contents) if e != nil { return nil, e } r.ContentLength = size buf, err := doRequest(r, s.AppToken, s.AccessToken) if err != nil { return nil, err } var md FileMetadata dec := json.NewDecoder(buf) if e := dec.Decode(&md); e != nil { return nil, e } return &md, nil } type FileReader struct { io.ReadCloser // -1 if unknown. Size int64 ContentType string } func newFileReader(r *http.Response) *FileReader { return &FileReader{ r.Body, r.ContentLength, r.Header.Get("Content-Type")} } func (s *Client) GetFile(path string) (*FileReader, error) { return s.doGet(contentURL("/files/" + s.root() + path)) } func (s *Client) GetLink(path string) (*Link, error) { u := apiURL("/shares/" + s.root() + path) u.RawQuery = s.Config.localeQuery() var link Link if e := s.postForJson(u, &link); e != nil { return nil, e } return &link, nil } func (s *Client) GetMedia(path string) (*Link, error) { u := apiURL("/media/" + s.root() + path) u.RawQuery = s.Config.localeQuery() var link Link if e := s.postForJson(u, &link); e != nil { return nil, e } return &link, nil } func (c *Config) localeQuery() string { return c.setLocale(url.Values{}).Encode() } func (c *Config) setLocale(v url.Values) url.Values { if c.Locale != "" { v.Set("locale", c.Locale) } return v } func (s *Client) fileOp(op string, vals url.Values) (*FileMetadata, error) { u := apiURL("/fileops/" + op) vals.Set("root", s.root()) u.RawQuery = s.Config.setLocale(vals).Encode() var md FileMetadata if e := s.postForJson(u, &md); e != nil { return nil, e } return &md, nil } func (s *Client) Move(from, to string) (*FileMetadata, error) { return s.fileOp("move", url.Values{"from_path": {from}, "to_path": {to}}) } func (s *Client) Copy(from, to string) (*FileMetadata, error) { return s.fileOp("copy", url.Values{"from_path": {from}, "to_path": {to}}) } func (s *Client) CreateDir(path string) (*FileMetadata, error) { return s.fileOp("create_folder", url.Values{"path": {path}}) } func (s *Client) Delete(path string) (*FileMetadata, error) { return s.fileOp("delete", url.Values{"path": {path}}) } func (c *Client) Search(path, query string, limit int) ([]FileMetadata, error) { u := apiURL("/search/" + c.root() + path) v := url.Values{"query": {query}} if limit > 0 { v.Set("limit", strconv.Itoa(limit)) } u.RawQuery = c.Config.setLocale(v).Encode() var md []FileMetadata if e := c.getForJson(u, &md); e != nil { return nil, e } return md, nil } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,292 @@ package main import ( "bufio" "dropbox" "encoding/json" "fmt" "io" "os" "os/exec" gpath "path" "strings" "text/tabwriter" "time" "unicode" ) func Ls(db *dropbox.Client, args []string) error { md, e := db.GetMetadata(Cwd, true) if e != nil { return e } w := tabwriter.NewWriter(os.Stdout, 0, 2, 1, ' ', 0) defer w.Flush() for _, f := range md.Contents { fmt.Fprintf(w, "%d\t%s\t%s\t\n", f.Bytes, f.ModTime().Format(time.Stamp), gpath.Base(f.Path)) } return nil } func Cd(db *dropbox.Client, args []string) error { dest := args[0] if dest == ".." { Cwd = gpath.Dir(Cwd) return nil } dest = mkabs(dest) md, e := db.GetMetadata(dest, false) if e != nil { return e } if md.IsDir { Cwd = dest return nil } return fmt.Errorf("No such dir: %s", dest) } func Cat(db *dropbox.Client, args []string) error { rc, e := db.GetFile(mkabs(args[0])) if e != nil { return e } defer rc.Close() if !strings.HasPrefix(rc.ContentType, "text/") { return fmt.Errorf("Not a content type you should cat: %s", rc.ContentType) } _, e = io.Copy(os.Stdout, rc) return e } func Put(db *dropbox.Client, args []string) error { srcfile := args[0] if !gpath.IsAbs(srcfile) { srcdir, e := os.Getwd() if e != nil { return e } srcfile = gpath.Join(srcdir, srcfile) } src, e := os.Open(srcfile) if e != nil { return e } defer src.Close() fi, e := src.Stat() if e != nil { return e } destpath := gpath.Join(Cwd, gpath.Base(srcfile)) fmt.Printf("Uploading to %s\n", destpath) _, e = db.AddFile(destpath, src, fi.Size()) return e } func Get(db *dropbox.Client, args []string) error { fname := mkabs(args[0]) destdir, e := os.Getwd() if e != nil { return e } destfile := gpath.Join(destdir, gpath.Base(fname)) r, e := db.GetFile(fname) if e != nil { return e } defer r.Close() fmt.Printf("Saving to %s\n", destfile) dest, e := os.Create(destfile) if e != nil { return e } defer dest.Close() _, e = io.Copy(dest, r) return e } func Share(db *dropbox.Client, args []string) error { link, e := db.GetLink(mkabs(args[0])) if e != nil { return e } fmt.Println(link.URL) return nil } func mkabs(path string) string { if !gpath.IsAbs(path) { return gpath.Join(Cwd, path) } return path } func Mv(db *dropbox.Client, args []string) error { from, to := mkabs(args[0]), mkabs(args[1]) _, e := db.Move(from, to) return e } func Cp(db *dropbox.Client, args []string) error { from, to := mkabs(args[0]), mkabs(args[1]) _, e := db.Copy(from, to) return e } func Rm(db *dropbox.Client, args []string) error { _, e := db.Delete(mkabs(args[0])) return e } func Mkdir(db *dropbox.Client, args []string) error { _, e := db.CreateDir(mkabs(args[0])) return e } func Whoami(db *dropbox.Client, args []string) error { ai, e := db.GetAccountInfo() if e != nil { return e } b, e := json.MarshalIndent(ai, "", " ") if e != nil { return e } fmt.Println(string(b)) return nil } func Find(db *dropbox.Client, args []string) error { r, e := db.Search(Cwd, args[0], 0) if e != nil { return e } for _, m := range r { fmt.Println(m.Path) } return nil } type Cmd struct { Fn func(*dropbox.Client, []string) error ArgCount int } func tryCmd(c *dropbox.Client, cname string, args []string) error { if len(args) == 0 { return fmt.Errorf("Last argument needs to be a dropbox path.") } fname := args[len(args)-1] args = args[:len(args)-1] f, e := c.GetFile(mkabs(fname)) if e != nil { return e } defer f.Close() cmd := exec.Command(cname, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = f return cmd.Run() } var cmds = map[string]Cmd{ "pwd": {func(*dropbox.Client, []string) error { fmt.Println(Cwd) return nil }, 0}, "mv": {Mv, 2}, "cp": {Cp, 2}, "cd": {Cd, 1}, "rm": {Rm, 1}, "mkdir": {Mkdir, 1}, "share": {Share, 1}, "cat": {Cat, 1}, "ls": {Ls, 0}, "find": {Find, 1}, "whoami": {Whoami, 0}, "help": {func(*dropbox.Client, []string) error { for k := range cmds { fmt.Println(k) } return nil }, 0}, "put": {Put, 1}, "get": {Get, 1}, "exit": {func(*dropbox.Client, []string) error { os.Exit(0) return nil }, 0}, } // Global mutable var. Oh noes. var Cwd = "/" func tokenize(s string) []string { curr := make([]rune, 0, 32) res := make([]string, 0, 10) var in_word, last_slash bool for _, r := range s { if last_slash || (!unicode.IsSpace(r) && (r != '\\')) { curr = append(curr, r) in_word = true } else { if in_word { res = append(res, string(curr)) curr = curr[0:0] in_word = false } } last_slash = r == '\\' } if len(curr) > 0 { res = append(res, string(curr)) } return res } var AppToken = dropbox.AppToken{ Key: "<redacted>", Secret: "<redacted>"} var AccessToken = dropbox.AccessToken{ Key: "<redacted>", Secret: "<redacted>"} func main() { db := &dropbox.Client{ AppToken: AppToken, AccessToken: AccessToken, Config: dropbox.Config{ Access: dropbox.Dropbox, Locale: "us", }} in := bufio.NewReader(os.Stdin) for { fmt.Printf("%s > ", gpath.Base(Cwd)) lineb, _, e := in.ReadLine() if e != nil { if e == io.EOF { break } panic(e) } tokens := tokenize(string(lineb)) if len(tokens) == 0 { continue } cmd, ok := cmds[tokens[0]] if !ok { if e := tryCmd(db, tokens[0], tokens[1:]); e != nil { fmt.Printf("ERROR: %v\n", e) } } else { args := tokens[1:] if len(args) != cmd.ArgCount && cmd.ArgCount != -1 { fmt.Printf("ERROR: %s expected %d args, got %d.\n", tokens[0], cmd.ArgCount, len(args)) } else if e := cmd.Fn(db, args); e != nil { fmt.Printf("ERROR: %v\n", e) } } } }