From 8f2999b6af1ad85cf62c3f725a5b262814cd90f1 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Fri, 30 Oct 2015 08:40:14 +0000 Subject: [PATCH] onedrive: implement Copy --- onedrive/api/api.go | 22 +++++-- onedrive/api/types.go | 22 +++++++ onedrive/onedrive.go | 137 +++++++++++++++++++++++++++++++++++------- 3 files changed, 155 insertions(+), 26 deletions(-) diff --git a/onedrive/api/api.go b/onedrive/api/api.go index 57b74b82e..5f88f41a8 100644 --- a/onedrive/api/api.go +++ b/onedrive/api/api.go @@ -37,6 +37,7 @@ type Opts struct { ContentType string ContentLength *int64 ContentRange string + ExtraHeaders map[string]string } // checkClose is a utility function used to check the return from @@ -48,8 +49,8 @@ func checkClose(c io.Closer, err *error) { } } -// decodeJSON decodes resp.Body into json -func (api *Client) decodeJSON(resp *http.Response, result interface{}) (err error) { +// DecodeJSON decodes resp.Body into result +func DecodeJSON(resp *http.Response, result interface{}) (err error) { defer checkClose(resp.Body, &err) decoder := json.NewDecoder(resp.Body) return decoder.Decode(result) @@ -83,6 +84,11 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { if opts.ContentRange != "" { req.Header.Add("Content-Range", opts.ContentRange) } + if opts.ExtraHeaders != nil { + for k, v := range opts.ExtraHeaders { + req.Header.Add(k, v) + } + } req.Header.Add("User-Agent", fs.UserAgent) resp, err = api.c.Do(req) if err != nil { @@ -91,10 +97,13 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { if resp.StatusCode < 200 || resp.StatusCode > 299 { // Decode error response errResponse := new(Error) - err = api.decodeJSON(resp, &errResponse) + 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 { @@ -103,7 +112,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { return resp, nil } -// CallJSON runs Call and decodes the body as a JSON object into result +// CallJSON runs Call and decodes the body as a JSON object into response (if not nil) // // If request is not nil then it will be JSON encoded as the body of the request // @@ -124,6 +133,9 @@ func (api *Client) CallJSON(opts *Opts, request interface{}, response interface{ if err != nil { return resp, err } - err = api.decodeJSON(resp, response) + if opts.NoResponse { + return resp, nil + } + err = DecodeJSON(resp, response) return resp, err } diff --git a/onedrive/api/types.go b/onedrive/api/types.go index 99cc95d89..b81cd0ea2 100644 --- a/onedrive/api/types.go +++ b/onedrive/api/types.go @@ -189,3 +189,25 @@ type UploadFragmentResponse struct { ExpirationDateTime Timestamp `json:"expirationDateTime"` // "2015-01-29T09:21:55.523Z", NextExpectedRanges []string `json:"nextExpectedRanges"` // ["0-"] } + +// CopyItemRequest is the request to copy an item object +// +// Note: The parentReference should include either an id or path but +// not both. If both are included, they need to reference the same +// item or an error will occur. +type CopyItemRequest struct { + ParentReference ItemReference `json:"parentReference"` // Reference to the parent item the copy will be created in. + Name *string `json:"name"` // Optional The new name for the copy. If this isn't provided, the same name will be used as the original. +} + +// AsyncOperationStatus provides information on the status of a asynchronous job progress. +// +// The following API calls return AsyncOperationStatus resources: +// +// Copy Item +// Upload From URL +type AsyncOperationStatus struct { + Operation string `json:"operation"` // The type of job being run. + PercentageComplete float64 `json:"percentageComplete"` // An float value between 0 and 100 that indicates the percentage complete. + Status string `json:"status"` // A string value that maps to an enumeration of possible values about the status of the job. "notStarted | inProgress | completed | updating | failed | deletePending | deleteFailed | waiting" +} diff --git a/onedrive/onedrive.go b/onedrive/onedrive.go index e9de87b78..5c1e9f269 100644 --- a/onedrive/onedrive.go +++ b/onedrive/onedrive.go @@ -271,7 +271,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) { Path: "/drive/items/" + pathID + "/children", } mkdir := api.CreateItemRequest{ - Name: leaf, + Name: replaceReservedChars(leaf), ConflictBehavior: "fail", } err = f.pacer.Call(func() (bool, error) { @@ -444,22 +444,36 @@ func (f *Fs) ListDir() fs.DirChan { return out } +// Creates from the parameters passed in a half finished Object which +// must have setMetaData called on it +// +// Returns the object, leaf, directoryID and error +// +// Used to create new objects +func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) { + // Create the directory for the object if it doesn't exist + leaf, directoryID, err = f.dirCache.FindPath(remote, true) + if err != nil { + return nil, leaf, directoryID, err + } + // Temporary Object under construction + o = &Object{ + fs: f, + remote: remote, + } + return o, leaf, directoryID, nil +} + // Put the object into the container // // Copy the reader in to the new object which is returned // // The new object may have been created if an error is returned func (f *Fs) Put(in io.Reader, remote string, modTime time.Time, size int64) (fs.Object, error) { - // Create the directory for the object if it doesn't exist - _, _, err := f.dirCache.FindPath(remote, true) + o, _, _, err := f.createObject(remote, modTime, size) if err != nil { return nil, err } - // Temporary Object under construction - o := &Object{ - fs: f, - remote: remote, - } return o, o.Update(in, modTime, size) } @@ -526,6 +540,47 @@ func (f *Fs) Precision() time.Duration { return time.Second } +// waitForJob waits for the job with status in url to complete +func (f *Fs) waitForJob(location string, o *Object) error { + deadline := time.Now().Add(fs.Config.Timeout) + for time.Now().Before(deadline) { + opts := api.Opts{ + Method: "GET", + Path: location, + Absolute: true, + } + var resp *http.Response + var err error + err = f.pacer.Call(func() (bool, error) { + resp, err = f.srv.Call(&opts) + return shouldRetry(resp, err) + }) + if err != nil { + return err + } + if resp.StatusCode == 202 { + var status api.AsyncOperationStatus + err = api.DecodeJSON(resp, &status) + if err != nil { + return err + } + if status.Status == "failed" || status.Status == "deleteFailed" { + return fmt.Errorf("Async operation %q returned %q", status.Operation, status.Status) + } + } else { + var info api.Item + err = api.DecodeJSON(resp, &info) + if err != nil { + return err + } + o.setMetaData(&info) + return nil + } + time.Sleep(1 * time.Second) + } + return fmt.Errorf("Async operation didn't complete after %v", fs.Config.Timeout) +} + // Copy src to this remote using server side copy operations. // // This is stored with the remote path given @@ -535,19 +590,59 @@ func (f *Fs) Precision() time.Duration { // Will only be called if src.Fs().Name() == f.Name() // // If it isn't possible then return fs.ErrorCantCopy -//func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) { -// srcObj, ok := src.(*Object) -// if !ok { -// fs.Debug(src, "Can't copy - not same remote type") -// return nil, fs.ErrorCantCopy -// } -// srcFs := srcObj.acd -// _, err := f.c.ObjectCopy(srcFs.container, srcFs.root+srcObj.remote, f.container, f.root+remote, nil) -// if err != nil { -// return nil, err -// } -// return f.NewFsObject(remote), nil -//} +func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) { + srcObj, ok := src.(*Object) + if !ok { + fs.Debug(src, "Can't copy - not same remote type") + return nil, fs.ErrorCantCopy + } + err := srcObj.readMetaData() + if err != nil { + return nil, err + } + + // Create temporary object + dstObj, leaf, directoryID, err := f.createObject(remote, srcObj.modTime, srcObj.size) + if err != nil { + return nil, err + } + + // Copy the object + opts := api.Opts{ + Method: "POST", + Path: "/drive/items/" + srcObj.id + "/action.copy", + ExtraHeaders: map[string]string{"Prefer": "respond-async"}, + NoResponse: true, + } + replacedLeaf := replaceReservedChars(leaf) + copy := api.CopyItemRequest{ + Name: &replacedLeaf, + ParentReference: api.ItemReference{ + ID: directoryID, + }, + } + var resp *http.Response + err = f.pacer.Call(func() (bool, error) { + resp, err = f.srv.CallJSON(&opts, ©, nil) + return shouldRetry(resp, err) + }) + if err != nil { + return nil, err + } + + // read location header + location := resp.Header.Get("Location") + if location == "" { + return nil, fmt.Errorf("Didn't receive location header in copy response") + } + + // Wait for job to finish + err = f.waitForJob(location, dstObj) + if err != nil { + return nil, err + } + return dstObj, nil +} // Purge deletes all the files and the container //