onedrive: implement Copy

This commit is contained in:
Nick Craig-Wood 2015-10-30 08:40:14 +00:00
parent be6115fbfa
commit 8f2999b6af
3 changed files with 155 additions and 26 deletions

View File

@ -37,6 +37,7 @@ type Opts struct {
ContentType string ContentType string
ContentLength *int64 ContentLength *int64
ContentRange string ContentRange string
ExtraHeaders map[string]string
} }
// checkClose is a utility function used to check the return from // 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 // DecodeJSON decodes resp.Body into result
func (api *Client) decodeJSON(resp *http.Response, result interface{}) (err error) { func DecodeJSON(resp *http.Response, result interface{}) (err error) {
defer checkClose(resp.Body, &err) defer checkClose(resp.Body, &err)
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
return decoder.Decode(result) return decoder.Decode(result)
@ -83,6 +84,11 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
if opts.ContentRange != "" { if opts.ContentRange != "" {
req.Header.Add("Content-Range", 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) req.Header.Add("User-Agent", fs.UserAgent)
resp, err = api.c.Do(req) resp, err = api.c.Do(req)
if err != nil { 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 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
// Decode error response // Decode error response
errResponse := new(Error) errResponse := new(Error)
err = api.decodeJSON(resp, &errResponse) err = DecodeJSON(resp, &errResponse)
if err != nil { if err != nil {
return resp, err return resp, err
} }
if errResponse.ErrorInfo.Code == "" {
errResponse.ErrorInfo.Code = resp.Status
}
return resp, errResponse return resp, errResponse
} }
if opts.NoResponse { if opts.NoResponse {
@ -103,7 +112,7 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
return resp, nil 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 // 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 { if err != nil {
return resp, err return resp, err
} }
err = api.decodeJSON(resp, response) if opts.NoResponse {
return resp, nil
}
err = DecodeJSON(resp, response)
return resp, err return resp, err
} }

View File

@ -189,3 +189,25 @@ type UploadFragmentResponse struct {
ExpirationDateTime Timestamp `json:"expirationDateTime"` // "2015-01-29T09:21:55.523Z", ExpirationDateTime Timestamp `json:"expirationDateTime"` // "2015-01-29T09:21:55.523Z",
NextExpectedRanges []string `json:"nextExpectedRanges"` // ["0-"] 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"
}

View File

@ -271,7 +271,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
Path: "/drive/items/" + pathID + "/children", Path: "/drive/items/" + pathID + "/children",
} }
mkdir := api.CreateItemRequest{ mkdir := api.CreateItemRequest{
Name: leaf, Name: replaceReservedChars(leaf),
ConflictBehavior: "fail", ConflictBehavior: "fail",
} }
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
@ -444,22 +444,36 @@ func (f *Fs) ListDir() fs.DirChan {
return out 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 // Put the object into the container
// //
// Copy the reader in to the new object which is returned // Copy the reader in to the new object which is returned
// //
// The new object may have been created if an error 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) { 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 o, _, _, err := f.createObject(remote, modTime, size)
_, _, err := f.dirCache.FindPath(remote, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Temporary Object under construction
o := &Object{
fs: f,
remote: remote,
}
return o, o.Update(in, modTime, size) return o, o.Update(in, modTime, size)
} }
@ -526,6 +540,47 @@ func (f *Fs) Precision() time.Duration {
return time.Second 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. // Copy src to this remote using server side copy operations.
// //
// This is stored with the remote path given // 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() // Will only be called if src.Fs().Name() == f.Name()
// //
// If it isn't possible then return fs.ErrorCantCopy // If it isn't possible then return fs.ErrorCantCopy
//func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) { func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
// srcObj, ok := src.(*Object) srcObj, ok := src.(*Object)
// if !ok { if !ok {
// fs.Debug(src, "Can't copy - not same remote type") fs.Debug(src, "Can't copy - not same remote type")
// return nil, fs.ErrorCantCopy return nil, fs.ErrorCantCopy
// } }
// srcFs := srcObj.acd err := srcObj.readMetaData()
// _, err := f.c.ObjectCopy(srcFs.container, srcFs.root+srcObj.remote, f.container, f.root+remote, nil) if err != nil {
// if err != nil { return nil, err
// return nil, err }
// }
// return f.NewFsObject(remote), nil // 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, &copy, 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 // Purge deletes all the files and the container
// //