onedrive: rework to support Microsoft Graph

The initial work on this was done by Oliver Heyme with updates from
Cnly.

Oliver Heyme:

* Changed to Microsoft graph
* Enable writing
* Added more options for adding a OneDrive Remote
* Better error handling
* Send modDate at create upload session and fix list children

Cnly:

* Simple upload API only supports max 4MB files
* Fix supported hash types for different drive types
* Fix unchecked err

Co-authored-by: Oliver Heyme <olihey@googlemail.com>
Co-authored-by: Cnly <minecnly@gmail.com>
This commit is contained in:
Cnly 2018-08-18 18:06:22 +08:00 committed by Nick Craig-Wood
parent 05a615ef22
commit b709f73aab
2 changed files with 217 additions and 237 deletions

View File

@ -94,6 +94,7 @@ type ItemReference struct {
DriveID string `json:"driveId"` // Unique identifier for the Drive that contains the item. Read-only. DriveID string `json:"driveId"` // Unique identifier for the Drive that contains the item. Read-only.
ID string `json:"id"` // Unique identifier for the item. Read/Write. ID string `json:"id"` // Unique identifier for the item. Read/Write.
Path string `json:"path"` // Path that used to navigate to the item. Read/Write. Path string `json:"path"` // Path that used to navigate to the item. Read/Write.
DriveType string `json:"driveType"` // Type of the drive, Read-Only
} }
// RemoteItemFacet groups data needed to reference a OneDrive remote item // RemoteItemFacet groups data needed to reference a OneDrive remote item
@ -244,7 +245,6 @@ type MoveItemRequest struct {
// Copy Item // Copy Item
// Upload From URL // Upload From URL
type AsyncOperationStatus struct { 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. 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" 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

@ -10,7 +10,6 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"net/url"
"path" "path"
"strings" "strings"
"time" "time"
@ -33,48 +32,33 @@ import (
) )
const ( const (
rclonePersonalClientID = "0000000044165769" rcloneClientID = "b15665d9-eda6-4092-8539-0eec376afd59"
rclonePersonalEncryptedClientSecret = "ugVWLNhKkVT1-cbTRO-6z1MlzwdW6aMwpKgNaFG-qXjEn_WfDnG9TVyRA5yuoliU" rcloneEncryptedClientSecret = "_JUdzh3LnKNqSPcf4Wu5fgMFIQOI8glZu_akYgR8yf6egowNBg-R"
rcloneBusinessClientID = "52857fec-4bc2-483f-9f1b-5fe28e97532c"
rcloneBusinessEncryptedClientSecret = "6t4pC8l6L66SFYVIi8PgECDyjXy_ABo1nsTaE-Lr9LpzC6yT4vNOwHsakwwdEui0O6B0kX8_xbBLj91J"
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
rootURLPersonal = "https://api.onedrive.com/v1.0/drive" // root URL for requests graphURL = "https://graph.microsoft.com/v1.0"
discoveryServiceURL = "https://api.office.com/discovery/" configGraphID = "graphID"
configResourceURL = "resource_url" configDriveType = "driveType"
driveTypePersonal = "personal"
driveTypeBusiness = "business"
driveTypeSharepoint = "documentLibrary"
singlePartUploadSizeLimit = fs.SizeSuffix(4 * 1024 * 1024)
) )
// Globals // Globals
var ( var (
// Description of how to auth for this app for a personal account
oauthPersonalConfig = &oauth2.Config{
Scopes: []string{
"wl.signin", // Allow single sign-on capabilities
"wl.offline_access", // Allow receiving a refresh token
"onedrive.readwrite", // r/w perms to all of a user's OneDrive files
},
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.live.com/oauth20_authorize.srf",
TokenURL: "https://login.live.com/oauth20_token.srf",
},
ClientID: rclonePersonalClientID,
ClientSecret: obscure.MustReveal(rclonePersonalEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL,
}
// Description of how to auth for this app for a business account // Description of how to auth for this app for a business account
oauthBusinessConfig = &oauth2.Config{ oauthConfig = &oauth2.Config{
Endpoint: oauth2.Endpoint{ Endpoint: oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize", AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenURL: "https://login.microsoftonline.com/common/oauth2/token", TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
}, },
ClientID: rcloneBusinessClientID, Scopes: []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"},
ClientSecret: obscure.MustReveal(rcloneBusinessEncryptedClientSecret), ClientID: rcloneClientID,
ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
RedirectURL: oauthutil.RedirectLocalhostURL, RedirectURL: oauthutil.RedirectLocalhostURL,
} }
oauthBusinessResource = oauth2.SetAuthURLParam("resource", discoveryServiceURL)
sharedURL = "https://api.onedrive.com/v1.0/drives" // root URL for remote shared resources
) )
// Register with Fs // Register with Fs
@ -84,28 +68,7 @@ func init() {
Description: "Microsoft OneDrive", Description: "Microsoft OneDrive",
NewFs: NewFs, NewFs: NewFs,
Config: func(name string, m configmap.Mapper) { Config: func(name string, m configmap.Mapper) {
// choose account type err := oauthutil.Config("onedrive", name, m, oauthConfig)
fmt.Printf("Choose OneDrive account type?\n")
fmt.Printf(" * Say b for a OneDrive business account\n")
fmt.Printf(" * Say p for a personal OneDrive account\n")
isPersonal := config.Command([]string{"bBusiness", "pPersonal"}) == 'p'
if isPersonal {
// for personal accounts we don't safe a field about the account
err := oauthutil.Config("onedrive", name, m, oauthPersonalConfig)
if err != nil {
log.Fatalf("Failed to configure token: %v", err)
}
} else {
err := oauthutil.ConfigErrorCheck("onedrive", name, m, func(req *http.Request) oauthutil.AuthError {
var resp oauthutil.AuthError
resp.Name = req.URL.Query().Get("error")
resp.Code = strings.Split(req.URL.Query().Get("error_description"), ":")[0] // error_description begins with XXXXXXXXXXXX:
resp.Description = strings.Join(strings.Split(req.URL.Query().Get("error_description"), ":")[1:], ":")
resp.HelpURL = "https://rclone.org/onedrive/#troubleshooting"
return resp
}, oauthBusinessConfig, oauthBusinessResource)
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
return return
@ -117,114 +80,139 @@ func init() {
return return
} }
type serviceResource struct { type driveResource struct {
ServiceAPIVersion string `json:"serviceApiVersion"` DriveID string `json:"id"`
ServiceEndpointURI string `json:"serviceEndpointUri"` DriveName string `json:"name"`
ServiceResourceID string `json:"serviceResourceId"` DriveType string `json:"driveType"`
} }
type serviceResponse struct { type drivesResponse struct {
Services []serviceResource `json:"value"` Drives []driveResource `json:"value"`
} }
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthBusinessConfig) type siteResource struct {
SiteID string `json:"id"`
SiteName string `json:"displayName"`
SiteURL string `json:"webUrl"`
}
type siteResponse struct {
Sites []siteResource `json:"value"`
}
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
if err != nil { if err != nil {
log.Fatalf("Failed to configure OneDrive: %v", err) log.Fatalf("Failed to configure OneDrive: %v", err)
return
} }
srv := rest.NewClient(oAuthClient) srv := rest.NewClient(oAuthClient)
opts := rest.Opts{ var opts rest.Opts
Method: "GET", var finalDriveID string
RootURL: discoveryServiceURL, var siteID string
Path: "/v2.0/me/services", switch config.Choose("Your choice",
} []string{"onedrive", "sharepoint", "driveid", "siteid", "search"},
services := serviceResponse{} []string{"OneDrive Personal or Business", "Sharepoint site", "Type in driveID", "Type in SiteID", "Search a Sharepoint site"},
resp, err := srv.CallJSON(&opts, nil, &services) false) {
if err != nil {
fs.Errorf(nil, "Failed to query available services: %v", err)
return
}
if resp.StatusCode != 200 {
fs.Errorf(nil, "Failed to query available services: Got HTTP error code %d", resp.StatusCode)
return
}
var resourcesURL []string case "onedrive":
var resourcesID []string
for _, service := range services.Services {
if service.ServiceAPIVersion == "v2.0" {
resourcesID = append(resourcesID, service.ServiceResourceID)
resourcesURL = append(resourcesURL, service.ServiceEndpointURI)
}
// we only support 2.0 API
fs.Infof(nil, "Skipping API %s endpoint %s", service.ServiceAPIVersion, service.ServiceEndpointURI)
}
var foundService string
if len(resourcesID) == 0 {
fs.Errorf(nil, "No Service found")
return
} else if len(resourcesID) == 1 {
foundService = resourcesID[0]
} else {
foundService = config.Choose("Choose resource URL", resourcesID, resourcesURL, false)
}
m.Set(configResourceURL, foundService)
oauthBusinessResource = oauth2.SetAuthURLParam("resource", foundService)
// get the token from the inital config
// we need to update the token with a resource
// specific token we will query now
token, err := oauthutil.GetToken(name, m)
if err != nil {
fs.Errorf(nil, "Error while getting token: %s", err)
return
}
// values for the token query
values := url.Values{}
values.Set("refresh_token", token.RefreshToken)
values.Set("grant_type", "refresh_token")
values.Set("resource", foundService)
values.Set("client_id", oauthBusinessConfig.ClientID)
values.Set("client_secret", oauthBusinessConfig.ClientSecret)
opts = rest.Opts{ opts = rest.Opts{
Method: "POST", Method: "GET",
RootURL: oauthBusinessConfig.Endpoint.TokenURL, RootURL: graphURL,
ContentType: "application/x-www-form-urlencoded", Path: "/me/drives",
Body: strings.NewReader(values.Encode()), }
case "sharepoint":
opts = rest.Opts{
Method: "GET",
RootURL: graphURL,
Path: "/sites/root/drives",
}
case "driveid":
fmt.Printf("Paste your Drive ID here> ")
finalDriveID = config.ReadLine()
case "siteid":
fmt.Printf("Paste your Site ID here> ")
siteID = config.ReadLine()
case "search":
fmt.Printf("What to search for> ")
searchTerm := config.ReadLine()
opts = rest.Opts{
Method: "GET",
RootURL: graphURL,
Path: "/sites?search=" + searchTerm,
} }
// tokenJSON is the struct representing the HTTP response from OAuth2 sites := siteResponse{}
// providers returning a token in JSON form. resp, err := srv.CallJSON(&opts, nil, &sites)
// we are only interested in the new tokens, all other fields we don't care
type tokenJSON struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
jsonToken := tokenJSON{}
resp, err = srv.CallJSON(&opts, nil, &jsonToken)
if err != nil { if err != nil {
fs.Errorf(nil, "Failed to get resource token: %v", err) log.Fatalf("Failed to query available sites: %v", err)
return
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
fs.Errorf(nil, "Failed to get resource token: Got HTTP error code %d", resp.StatusCode) log.Fatalf("Failed to query available sites: Got HTTP error code %d", resp.StatusCode)
return
} }
// update the tokens if len(sites.Sites) == 0 {
token.AccessToken = jsonToken.AccessToken log.Fatalf("Search for '%s' returned no results", searchTerm)
token.RefreshToken = jsonToken.RefreshToken } else {
fmt.Printf("Found %d sites, please select the one you want to use:\n", len(sites.Sites))
for index, site := range sites.Sites {
fmt.Printf("%d: %s (%s) id=%s\n", index, site.SiteName, site.SiteURL, site.SiteID)
}
siteID = sites.Sites[config.ChooseNumber("Chose drive to use:", 0, len(sites.Sites)-1)].SiteID
}
}
// finally save them in the config // if we have a siteID we need to ask for the drives
err = oauthutil.PutToken(name, m, token, true) if siteID != "" {
opts = rest.Opts{
Method: "GET",
RootURL: graphURL,
Path: "/sites/" + siteID + "/drives",
}
}
// We don't have the final ID yet?
// query Microsoft Graph
if finalDriveID == "" {
drives := drivesResponse{}
resp, err := srv.CallJSON(&opts, nil, &drives)
if err != nil { if err != nil {
fs.Errorf(nil, "Error while setting token: %s", err) log.Fatalf("Failed to query available drives: %v", err)
}
if resp.StatusCode != 200 {
log.Fatalf("Failed to query available drives: Got HTTP error code %d", resp.StatusCode)
}
if len(drives.Drives) == 0 {
log.Fatalf("No drives found")
} else {
fmt.Printf("Found %d drives, please select the one you want to use:\n", len(drives.Drives))
for index, drive := range drives.Drives {
fmt.Printf("%d: %s (%s) id=%s\n", index, drive.DriveName, drive.DriveType, drive.DriveID)
}
finalDriveID = drives.Drives[config.ChooseNumber("Chose drive to use:", 0, len(drives.Drives)-1)].DriveID
} }
} }
// Test the driveID and get drive type
opts = rest.Opts{
Method: "GET",
RootURL: graphURL,
Path: "/drives/" + finalDriveID + "/root"}
var rootItem api.Item
resp, err := srv.CallJSON(&opts, nil, &rootItem)
if err != nil {
log.Fatalf("Failed to query root for drive %s: %v", finalDriveID, err)
}
if resp.StatusCode != 200 {
log.Fatalf("Failed to query root for drive %s: Got HTTP error code %d", finalDriveID, resp.StatusCode)
}
fmt.Printf("Found drive '%s' of type '%s', URL: %s\nIs that okay?\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)
// This does not work, YET :)
if !config.Confirm() {
log.Fatalf("Cancelled by user")
}
config.FileSet(name, configGraphID, finalDriveID)
config.FileSet(name, configDriveType, rootItem.ParentReference.DriveType)
}, },
Options: []fs.Option{{ Options: []fs.Option{{
Name: config.ConfigClientID, Name: config.ConfigClientID,
@ -244,7 +232,6 @@ func init() {
// Options defines the configuration for this backend // Options defines the configuration for this backend
type Options struct { type Options struct {
ChunkSize fs.SizeSuffix `config:"chunk_size"` ChunkSize fs.SizeSuffix `config:"chunk_size"`
ResourceURL string `config:"resource_url"`
} }
// Fs represents a remote one drive // Fs represents a remote one drive
@ -257,7 +244,8 @@ type Fs struct {
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
tokenRenewer *oauthutil.Renew // renew the token on expiry tokenRenewer *oauthutil.Renew // renew the token on expiry
isBusiness bool // true if this is an OneDrive Business account driveID string // ID to use for querying Microsoft Graph
driveType string // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/drive
} }
// Object describes a one drive object // Object describes a one drive object
@ -327,10 +315,18 @@ 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 := rest.Opts{ var opts rest.Opts
if len(path) == 0 {
opts = rest.Opts{
Method: "GET",
Path: "/root",
}
} else {
opts = rest.Opts{
Method: "GET", Method: "GET",
Path: "/root:/" + rest.URLPathEscape(replaceReservedChars(path)), Path: "/root:/" + rest.URLPathEscape(replaceReservedChars(path)),
} }
}
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(&opts, nil, &info) resp, err = f.srv.CallJSON(&opts, nil, &info)
return shouldRetry(resp, err) return shouldRetry(resp, err)
@ -364,23 +360,11 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
if opt.ChunkSize%(320*1024) != 0 { if opt.ChunkSize%(320*1024) != 0 {
return nil, errors.Errorf("chunk size %d is not a multiple of 320k", opt.ChunkSize) return nil, errors.Errorf("chunk size %d is not a multiple of 320k", opt.ChunkSize)
} }
// if we have a resource URL it's a business account otherwise a personal one
isBusiness := opt.ResourceURL != ""
var rootURL string
var oauthConfig *oauth2.Config
if !isBusiness {
// personal account setup
oauthConfig = oauthPersonalConfig
rootURL = rootURLPersonal
} else {
// business account setup
oauthConfig = oauthBusinessConfig
rootURL = opt.ResourceURL + "_api/v2.0/drives/me"
sharedURL = opt.ResourceURL + "_api/v2.0/drives"
// update the URL in the AuthOptions // get the graphID part from the config file
oauthBusinessResource = oauth2.SetAuthURLParam("resource", opt.ResourceURL) driveID := config.FileGet(name, configGraphID, "")
} driveType := config.FileGet(name, configDriveType)
root = parsePath(root) root = parsePath(root)
oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig) oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig)
if err != nil { if err != nil {
@ -391,16 +375,14 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
name: name, name: name,
root: root, root: root,
opt: *opt, opt: *opt,
srv: rest.NewClient(oAuthClient).SetRoot(rootURL), driveID: driveID,
driveType: driveType,
srv: rest.NewClient(oAuthClient).SetRoot(graphURL + "/drives/" + driveID),
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
isBusiness: isBusiness,
} }
f.features = (&fs.Features{ f.features = (&fs.Features{
CaseInsensitive: true, CaseInsensitive: true,
// OneDrive for business doesn't support mime types properly ReadMimeType: true,
// so we disable it until resolved
// https://github.com/OneDrive/onedrive-api-docs/issues/643
ReadMimeType: !f.isBusiness,
CanHaveEmptyDirectories: true, CanHaveEmptyDirectories: true,
}).Fill(f) }).Fill(f)
f.srv.SetErrorHandler(errorHandler) f.srv.SetErrorHandler(errorHandler)
@ -546,8 +528,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 := newOptsCall(dirID, "GET", "/children?top=1000") opts := newOptsCall(dirID, "GET", "/children?$top=1000")
OUTER: OUTER:
for { for {
var result api.ListChildrenResponse var result api.ListChildrenResponse
@ -757,16 +738,11 @@ 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 := rest.Opts{
Method: "GET",
RootURL: location,
IgnoreStatus: true, // Ignore the http status response since it seems to return valid info on 500 errors
}
var resp *http.Response var resp *http.Response
var err error var err error
var body []byte var body []byte
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(&opts) resp, err = http.Get(location)
if err != nil { if err != nil {
return fserrors.ShouldRetry(err), err return fserrors.ShouldRetry(err), err
} }
@ -782,19 +758,18 @@ func (f *Fs) waitForJob(location string, o *Object) error {
if err != nil { if err != nil {
return errors.Wrapf(err, "async status result not JSON: %q", body) return errors.Wrapf(err, "async status result not JSON: %q", body)
} }
// See if we decoded anything...
if !(status.Operation == "" && status.PercentageComplete == 0 && status.Status == "") { switch status.Status {
if status.Status == "failed" || status.Status == "deleteFailed" { case "failed":
return errors.Errorf("%s: async operation %q returned %q", o.remote, status.Operation, status.Status) case "deleteFailed":
{
return errors.Errorf("%s: async operation returned %q", o.remote, status.Status)
} }
} else if resp.StatusCode == 200 { case "completed":
var info api.Item err = o.readMetaData()
err = json.Unmarshal(body, &info) return errors.Wrapf(err, "async operation completed but readMetaData failed")
if err != nil {
return errors.Wrapf(err, "async item result not JSON: %q", body)
}
return o.setMetaData(&info)
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
return errors.Errorf("async operation didn't complete after %v", fs.Config.Timeout) return errors.Errorf("async operation didn't complete after %v", fs.Config.Timeout)
@ -833,7 +808,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
} }
// Copy the object // Copy the object
opts := newOptsCall(srcObj.id, "POST", "/action.copy") opts := newOptsCall(srcObj.id, "POST", "/copy")
opts.ExtraHeaders = map[string]string{"Prefer": "respond-async"} opts.ExtraHeaders = map[string]string{"Prefer": "respond-async"}
opts.NoResponse = true opts.NoResponse = true
@ -843,6 +818,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
copyReq := api.CopyItemRequest{ copyReq := api.CopyItemRequest{
Name: &replacedLeaf, Name: &replacedLeaf,
ParentReference: api.ItemReference{ ParentReference: api.ItemReference{
DriveID: f.driveID,
ID: id, ID: id,
}, },
} }
@ -867,9 +843,8 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
return nil, err return nil, err
} }
// Copy does NOT copy the modTime from the source and there seems to // we need to copy the modification date from the source
// be no way to set date before // since onedrive sets the current date for the newly created file
// This will create TWO versions on OneDrive
err = dstObj.SetModTime(srcObj.ModTime()) err = dstObj.SetModTime(srcObj.ModTime())
if err != nil { if err != nil {
return nil, err return nil, err
@ -1079,10 +1054,10 @@ func (f *Fs) About() (usage *fs.Usage, err error) {
// Hashes returns the supported hash sets. // Hashes returns the supported hash sets.
func (f *Fs) Hashes() hash.Set { func (f *Fs) Hashes() hash.Set {
if f.isBusiness { if f.driveType == driveTypePersonal {
return hash.Set(hash.QuickXorHash)
}
return hash.Set(hash.SHA1) return hash.Set(hash.SHA1)
}
return hash.Set(hash.QuickXorHash)
} }
// ------------------------------------------------------------ // ------------------------------------------------------------
@ -1112,16 +1087,16 @@ func (o *Object) srvPath() string {
// Hash returns the SHA-1 of an object returning a lowercase hex string // Hash returns the SHA-1 of an object returning a lowercase hex string
func (o *Object) Hash(t hash.Type) (string, error) { func (o *Object) Hash(t hash.Type) (string, error) {
if o.fs.isBusiness { if o.fs.driveType == driveTypePersonal {
if t != hash.QuickXorHash { if t == hash.SHA1 {
return "", hash.ErrUnsupported return o.sha1, nil
} }
} else {
if t == hash.QuickXorHash {
return o.quickxorhash, nil return o.quickxorhash, nil
} }
if t != hash.SHA1 {
return "", hash.ErrUnsupported
} }
return o.sha1, nil return "", hash.ErrUnsupported
} }
// Size returns the size of an object in bytes // Size returns the size of an object in bytes
@ -1282,12 +1257,12 @@ func (o *Object) createUploadSession(modTime time.Time) (response *api.CreateUpl
opts = rest.Opts{ opts = rest.Opts{
Method: "POST", Method: "POST",
RootURL: rootURL, RootURL: rootURL,
Path: "/" + drive + "/items/" + id + ":/" + rest.URLPathEscape(leaf) + ":/upload.createSession", Path: "/" + drive + "/items/" + id + ":/" + rest.URLPathEscape(leaf) + ":/createUploadSession",
} }
} else { } else {
opts = rest.Opts{ opts = rest.Opts{
Method: "POST", Method: "POST",
Path: "/root:/" + rest.URLPathEscape(o.srvPath()) + ":/upload.createSession", Path: "/root:/" + rest.URLPathEscape(o.srvPath()) + ":/createUploadSession",
} }
} }
createRequest := api.CreateUploadRequest{} createRequest := api.CreateUploadRequest{}
@ -1443,8 +1418,7 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
modTime := src.ModTime() modTime := src.ModTime()
var info *api.Item var info *api.Item
if size <= 0 { if size <= int64(singlePartUploadSizeLimit) {
// This is for 0 length files, or files with an unknown size
info, err = o.uploadSinglepart(in, size, modTime) info, err = o.uploadSinglepart(in, size, modTime)
} else { } else {
info, err = o.uploadMultipart(in, size, modTime) info, err = o.uploadMultipart(in, size, modTime)
@ -1452,7 +1426,13 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
if err != nil { if err != nil {
return err return err
} }
// is the info set by either the multipart upload OR the single PUT for smaller files?
if nil != info {
return o.setMetaData(info) return o.setMetaData(info)
}
return nil
} }
// Remove an object // Remove an object
@ -1489,7 +1469,7 @@ func newOptsCall(id string, method string, route string) (opts rest.Opts) {
func parseDirID(ID string) (string, string, string) { func parseDirID(ID string) (string, string, string) {
if strings.Index(ID, "#") >= 0 { if strings.Index(ID, "#") >= 0 {
s := strings.Split(ID, "#") s := strings.Split(ID, "#")
return s[1], s[0], sharedURL return s[1], s[0], graphURL + "/drives"
} }
return ID, "", "" return ID, "", ""
} }