-
-
Save carlosmcevilly/5894278 to your computer and use it in GitHub Desktop.
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
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 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
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) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment