diff --git a/onedrive/onedrive.go b/onedrive/onedrive.go index 73a82a6af..4e96a49c1 100644 --- a/onedrive/onedrive.go +++ b/onedrive/onedrive.go @@ -18,6 +18,7 @@ import ( "github.com/ncw/rclone/oauthutil" "github.com/ncw/rclone/onedrive/api" "github.com/ncw/rclone/pacer" + "github.com/ncw/rclone/rest" "github.com/spf13/pflag" "golang.org/x/oauth2" ) @@ -27,7 +28,8 @@ const ( rcloneClientSecret = "0+be4+jYw+7018HY6P3t/Izo+pTc+Yvt8+fy8NHU094=" minSleep = 10 * time.Millisecond 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 @@ -77,7 +79,7 @@ func init() { // Fs represents a remote one drive type Fs struct { 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 dirCache *dircache.DirCache // Map of directory path to directory id 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 func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) { - opts := api.Opts{ + opts := rest.Opts{ Method: "GET", 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 } +// 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 func NewFs(name, root string) (fs.Fs, error) { root = parsePath(root) @@ -161,9 +177,10 @@ func NewFs(name, root string) (fs.Fs, error) { f := &Fs{ name: name, root: root, - srv: api.NewClient(oAuthClient), + srv: rest.NewClient(oAuthClient, rootURL), pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), } + f.srv.SetErrorHandler(errorHandler) // Get rootID 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) var resp *http.Response var info *api.Item - opts := api.Opts{ + opts := rest.Opts{ Method: "POST", 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) { // Top parameter asks for bigger pages of data // https://dev.onedrive.com/odata/optional-query-parameters.htm - opts := api.Opts{ + opts := rest.Opts{ Method: "GET", Path: "/drive/items/" + dirID + "/children?top=1000", } @@ -484,7 +501,7 @@ func (f *Fs) Mkdir() error { // deleteObject removes an object by ID func (f *Fs) deleteObject(id string) error { - opts := api.Opts{ + opts := rest.Opts{ Method: "DELETE", Path: "/drive/items/" + id, NoResponse: true, @@ -544,7 +561,7 @@ func (f *Fs) Precision() time.Duration { func (f *Fs) waitForJob(location string, o *Object) error { deadline := time.Now().Add(fs.Config.Timeout) for time.Now().Before(deadline) { - opts := api.Opts{ + opts := rest.Opts{ Method: "GET", Path: location, Absolute: true, @@ -560,7 +577,7 @@ func (f *Fs) waitForJob(location string, o *Object) error { } if resp.StatusCode == 202 { var status api.AsyncOperationStatus - err = api.DecodeJSON(resp, &status) + err = rest.DecodeJSON(resp, &status) if err != nil { return err } @@ -569,7 +586,7 @@ func (f *Fs) waitForJob(location string, o *Object) error { } } else { var info api.Item - err = api.DecodeJSON(resp, &info) + err = rest.DecodeJSON(resp, &info) if err != nil { return err } @@ -608,7 +625,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) { } // Copy the object - opts := api.Opts{ + opts := rest.Opts{ Method: "POST", Path: "/drive/items/" + srcObj.id + "/action.copy", 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 func (o *Object) setModTime(modTime time.Time) (*api.Item, error) { - opts := api.Opts{ + opts := rest.Opts{ Method: "PATCH", 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") } var resp *http.Response - opts := api.Opts{ + opts := rest.Opts{ Method: "GET", 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 func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) { - opts := api.Opts{ + opts := rest.Opts{ Method: "POST", Path: "/drive/root:/" + o.srvPath() + ":/upload.createSession", } @@ -811,7 +828,7 @@ func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err // uploadFragment uploads a part func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf []byte) (err error) { bufSize := int64(len(buf)) - opts := api.Opts{ + opts := rest.Opts{ Method: "PUT", Path: url, Absolute: true, @@ -830,7 +847,7 @@ func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf [] // cancelUploadSession cancels an upload session func (o *Object) cancelUploadSession(url string) (err error) { - opts := api.Opts{ + opts := rest.Opts{ Method: "DELETE", Path: url, Absolute: true, @@ -903,7 +920,7 @@ func (o *Object) Update(in io.Reader, modTime time.Time, size int64) (err error) if size <= int64(uploadCutoff) { // This is for less than 100 MB of content var resp *http.Response - opts := api.Opts{ + opts := rest.Opts{ Method: "PUT", Path: "/drive/root:/" + o.srvPath() + ":/content", Body: in, diff --git a/onedrive/api/api.go b/rest/rest.go similarity index 77% rename from onedrive/api/api.go rename to rest/rest.go index 5f88f41a8..cf6e8ec0f 100644 --- a/onedrive/api/api.go +++ b/rest/rest.go @@ -1,5 +1,5 @@ -// Package api implements the API for one drive -package api +// Package rest implements a simple REST wrapper +package rest import ( "bytes" @@ -11,22 +11,34 @@ import ( "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 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 -func NewClient(c *http.Client) *Client { +func NewClient(c *http.Client, rootURL string) *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 type Opts struct { Method string @@ -69,7 +81,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { if opts.Absolute { url = opts.Path } else { - url = rootURL + opts.Path + url = api.rootURL + opts.Path } req, err := http.NewRequest(opts.Method, url, opts.Body) if err != nil { @@ -95,16 +107,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { return nil, err } if resp.StatusCode < 200 || resp.StatusCode > 299 { - // Decode error response - 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 + return resp, api.errorHandler(resp) } if opts.NoResponse { return resp, resp.Body.Close()