Factor REST library out of onedrive

This commit is contained in:
Nick Craig-Wood 2015-11-27 12:46:13 +00:00
parent 8057d668bb
commit 7a24532224
2 changed files with 57 additions and 37 deletions

View file

@ -18,6 +18,7 @@ import (
"github.com/ncw/rclone/oauthutil" "github.com/ncw/rclone/oauthutil"
"github.com/ncw/rclone/onedrive/api" "github.com/ncw/rclone/onedrive/api"
"github.com/ncw/rclone/pacer" "github.com/ncw/rclone/pacer"
"github.com/ncw/rclone/rest"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -27,7 +28,8 @@ const (
rcloneClientSecret = "0+be4+jYw+7018HY6P3t/Izo+pTc+Yvt8+fy8NHU094=" rcloneClientSecret = "0+be4+jYw+7018HY6P3t/Izo+pTc+Yvt8+fy8NHU094="
minSleep = 10 * time.Millisecond minSleep = 10 * time.Millisecond
maxSleep = 2 * time.Second maxSleep = 2 * time.Second
decayConstant = 2 // bigger for slower decay, exponential decayConstant = 2 // bigger for slower decay, exponential
rootURL = "https://api.onedrive.com/v1.0" // root URL for requests
) )
// Globals // Globals
@ -77,7 +79,7 @@ func init() {
// Fs represents a remote one drive // Fs represents a remote one drive
type Fs struct { type Fs struct {
name string // name of this remote name string // name of this remote
srv *api.Client // the connection to the one drive server srv *rest.Client // the connection to the one drive server
root string // the path we are working on root string // the path we are working on
dirCache *dircache.DirCache // Map of directory path to directory id dirCache *dircache.DirCache // Map of directory path to directory id
pacer *pacer.Pacer // pacer for API calls pacer *pacer.Pacer // pacer for API calls
@ -139,7 +141,7 @@ func shouldRetry(resp *http.Response, err error) (bool, error) {
// readMetaDataForPath reads the metadata from the path // readMetaDataForPath reads the metadata from the path
func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) { func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
opts := api.Opts{ opts := rest.Opts{
Method: "GET", Method: "GET",
Path: "/drive/root:/" + replaceReservedChars(path), Path: "/drive/root:/" + replaceReservedChars(path),
} }
@ -150,6 +152,20 @@ func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Respon
return info, resp, err return info, resp, err
} }
// errorHandler parses a non 2xx error response into an error
func errorHandler(resp *http.Response) error {
// Decode error response
errResponse := new(api.Error)
err := rest.DecodeJSON(resp, &errResponse)
if err != nil {
return err
}
if errResponse.ErrorInfo.Code == "" {
errResponse.ErrorInfo.Code = resp.Status
}
return errResponse
}
// NewFs constructs an Fs from the path, container:path // NewFs constructs an Fs from the path, container:path
func NewFs(name, root string) (fs.Fs, error) { func NewFs(name, root string) (fs.Fs, error) {
root = parsePath(root) root = parsePath(root)
@ -161,9 +177,10 @@ func NewFs(name, root string) (fs.Fs, error) {
f := &Fs{ f := &Fs{
name: name, name: name,
root: root, root: root,
srv: api.NewClient(oAuthClient), srv: rest.NewClient(oAuthClient, rootURL),
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
} }
f.srv.SetErrorHandler(errorHandler)
// Get rootID // Get rootID
rootInfo, _, err := f.readMetaDataForPath("") rootInfo, _, err := f.readMetaDataForPath("")
@ -266,7 +283,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
// fs.Debug(f, "CreateDir(%q, %q)\n", pathID, leaf) // fs.Debug(f, "CreateDir(%q, %q)\n", pathID, leaf)
var resp *http.Response var resp *http.Response
var info *api.Item var info *api.Item
opts := api.Opts{ opts := rest.Opts{
Method: "POST", Method: "POST",
Path: "/drive/items/" + pathID + "/children", Path: "/drive/items/" + pathID + "/children",
} }
@ -300,7 +317,7 @@ type listAllFn func(*api.Item) bool
func (f *Fs) listAll(dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) { func (f *Fs) listAll(dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
// Top parameter asks for bigger pages of data // Top parameter asks for bigger pages of data
// https://dev.onedrive.com/odata/optional-query-parameters.htm // https://dev.onedrive.com/odata/optional-query-parameters.htm
opts := api.Opts{ opts := rest.Opts{
Method: "GET", Method: "GET",
Path: "/drive/items/" + dirID + "/children?top=1000", Path: "/drive/items/" + dirID + "/children?top=1000",
} }
@ -484,7 +501,7 @@ func (f *Fs) Mkdir() error {
// deleteObject removes an object by ID // deleteObject removes an object by ID
func (f *Fs) deleteObject(id string) error { func (f *Fs) deleteObject(id string) error {
opts := api.Opts{ opts := rest.Opts{
Method: "DELETE", Method: "DELETE",
Path: "/drive/items/" + id, Path: "/drive/items/" + id,
NoResponse: true, NoResponse: true,
@ -544,7 +561,7 @@ func (f *Fs) Precision() time.Duration {
func (f *Fs) waitForJob(location string, o *Object) error { func (f *Fs) waitForJob(location string, o *Object) error {
deadline := time.Now().Add(fs.Config.Timeout) deadline := time.Now().Add(fs.Config.Timeout)
for time.Now().Before(deadline) { for time.Now().Before(deadline) {
opts := api.Opts{ opts := rest.Opts{
Method: "GET", Method: "GET",
Path: location, Path: location,
Absolute: true, Absolute: true,
@ -560,7 +577,7 @@ func (f *Fs) waitForJob(location string, o *Object) error {
} }
if resp.StatusCode == 202 { if resp.StatusCode == 202 {
var status api.AsyncOperationStatus var status api.AsyncOperationStatus
err = api.DecodeJSON(resp, &status) err = rest.DecodeJSON(resp, &status)
if err != nil { if err != nil {
return err return err
} }
@ -569,7 +586,7 @@ func (f *Fs) waitForJob(location string, o *Object) error {
} }
} else { } else {
var info api.Item var info api.Item
err = api.DecodeJSON(resp, &info) err = rest.DecodeJSON(resp, &info)
if err != nil { if err != nil {
return err return err
} }
@ -608,7 +625,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
} }
// Copy the object // Copy the object
opts := api.Opts{ opts := rest.Opts{
Method: "POST", Method: "POST",
Path: "/drive/items/" + srcObj.id + "/action.copy", Path: "/drive/items/" + srcObj.id + "/action.copy",
ExtraHeaders: map[string]string{"Prefer": "respond-async"}, ExtraHeaders: map[string]string{"Prefer": "respond-async"},
@ -741,7 +758,7 @@ func (o *Object) ModTime() time.Time {
// setModTime sets the modification time of the local fs object // setModTime sets the modification time of the local fs object
func (o *Object) setModTime(modTime time.Time) (*api.Item, error) { func (o *Object) setModTime(modTime time.Time) (*api.Item, error) {
opts := api.Opts{ opts := rest.Opts{
Method: "PATCH", Method: "PATCH",
Path: "/drive/root:/" + o.srvPath(), Path: "/drive/root:/" + o.srvPath(),
} }
@ -780,7 +797,7 @@ func (o *Object) Open() (in io.ReadCloser, err error) {
return nil, fmt.Errorf("Can't download no id") return nil, fmt.Errorf("Can't download no id")
} }
var resp *http.Response var resp *http.Response
opts := api.Opts{ opts := rest.Opts{
Method: "GET", Method: "GET",
Path: "/drive/items/" + o.id + "/content", Path: "/drive/items/" + o.id + "/content",
} }
@ -796,7 +813,7 @@ func (o *Object) Open() (in io.ReadCloser, err error) {
// createUploadSession creates an upload session for the object // createUploadSession creates an upload session for the object
func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) { func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) {
opts := api.Opts{ opts := rest.Opts{
Method: "POST", Method: "POST",
Path: "/drive/root:/" + o.srvPath() + ":/upload.createSession", Path: "/drive/root:/" + o.srvPath() + ":/upload.createSession",
} }
@ -811,7 +828,7 @@ func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err
// uploadFragment uploads a part // uploadFragment uploads a part
func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf []byte) (err error) { func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf []byte) (err error) {
bufSize := int64(len(buf)) bufSize := int64(len(buf))
opts := api.Opts{ opts := rest.Opts{
Method: "PUT", Method: "PUT",
Path: url, Path: url,
Absolute: true, Absolute: true,
@ -830,7 +847,7 @@ func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf []
// cancelUploadSession cancels an upload session // cancelUploadSession cancels an upload session
func (o *Object) cancelUploadSession(url string) (err error) { func (o *Object) cancelUploadSession(url string) (err error) {
opts := api.Opts{ opts := rest.Opts{
Method: "DELETE", Method: "DELETE",
Path: url, Path: url,
Absolute: true, Absolute: true,
@ -903,7 +920,7 @@ func (o *Object) Update(in io.Reader, modTime time.Time, size int64) (err error)
if size <= int64(uploadCutoff) { if size <= int64(uploadCutoff) {
// This is for less than 100 MB of content // This is for less than 100 MB of content
var resp *http.Response var resp *http.Response
opts := api.Opts{ opts := rest.Opts{
Method: "PUT", Method: "PUT",
Path: "/drive/root:/" + o.srvPath() + ":/content", Path: "/drive/root:/" + o.srvPath() + ":/content",
Body: in, Body: in,

View file

@ -1,5 +1,5 @@
// Package api implements the API for one drive // Package rest implements a simple REST wrapper
package api package rest
import ( import (
"bytes" "bytes"
@ -11,22 +11,34 @@ import (
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
) )
const (
rootURL = "https://api.onedrive.com/v1.0" // root URL for requests
)
// Client contains the info to sustain the API // Client contains the info to sustain the API
type Client struct { type Client struct {
c *http.Client c *http.Client
rootURL string
errorHandler func(resp *http.Response) error
} }
// NewClient takes an oauth http.Client and makes a new api instance // NewClient takes an oauth http.Client and makes a new api instance
func NewClient(c *http.Client) *Client { func NewClient(c *http.Client, rootURL string) *Client {
return &Client{ return &Client{
c: c, c: c,
rootURL: rootURL,
errorHandler: defaultErrorHandler,
} }
} }
// defaultErrorHandler doesn't attempt to parse the http body
func defaultErrorHandler(resp *http.Response) (err error) {
defer checkClose(resp.Body, &err)
return fmt.Errorf("HTTP error %v (%v) returned", resp.StatusCode, resp.Status)
}
// SetErrorHandler sets the handler to decode an error response when
// the HTTP status code is not 2xx. The handler should close resp.Body.
func (api *Client) SetErrorHandler(fn func(resp *http.Response) error) {
api.errorHandler = fn
}
// Opts contains parameters for Call, CallJSON etc // Opts contains parameters for Call, CallJSON etc
type Opts struct { type Opts struct {
Method string Method string
@ -69,7 +81,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
if opts.Absolute { if opts.Absolute {
url = opts.Path url = opts.Path
} else { } else {
url = rootURL + opts.Path url = api.rootURL + opts.Path
} }
req, err := http.NewRequest(opts.Method, url, opts.Body) req, err := http.NewRequest(opts.Method, url, opts.Body)
if err != nil { if err != nil {
@ -95,16 +107,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
return nil, err return nil, err
} }
if resp.StatusCode < 200 || resp.StatusCode > 299 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
// Decode error response return resp, api.errorHandler(resp)
errResponse := new(Error)
err = DecodeJSON(resp, &errResponse)
if err != nil {
return resp, err
}
if errResponse.ErrorInfo.Code == "" {
errResponse.ErrorInfo.Code = resp.Status
}
return resp, errResponse
} }
if opts.NoResponse { if opts.NoResponse {
return resp, resp.Body.Close() return resp, resp.Body.Close()