onedrive: Support for OneDrive for Business added #254
- 2 test fail (MimeType and modification date when copying) - no headless setup - uses the credentials for the "rclonetest" app I have created
This commit is contained in:
parent
a91448c83a
commit
7ef18b6b35
2 changed files with 160 additions and 35 deletions
|
@ -61,6 +61,12 @@ client_id>
|
|||
Microsoft App Client Secret - leave blank normally.
|
||||
client_secret>
|
||||
Remote config
|
||||
Choose OneDrive account type?
|
||||
* Say b for a OneDrive business account
|
||||
* Say p for a personal OneDrive account
|
||||
b) Business
|
||||
p) Personal
|
||||
b/p> p
|
||||
Use auto config?
|
||||
* Say Y if not sure
|
||||
* Say N if you are working on a remote or headless machine
|
||||
|
@ -106,6 +112,23 @@ To copy a local directory to an OneDrive directory called backup
|
|||
|
||||
rclone copy /home/source remote:backup
|
||||
|
||||
### OneDrive for Business ###
|
||||
|
||||
There is experimental support for OneDrive for Business.
|
||||
Select "b" when ask
|
||||
```
|
||||
Choose OneDrive account type?
|
||||
* Say b for a OneDrive business account
|
||||
* Say p for a personal OneDrive account
|
||||
b) Business
|
||||
p) Personal
|
||||
b/p>
|
||||
```
|
||||
After that rclone requires two authentications. First to authenicate your account
|
||||
and second to get the final token to access your companies resources.
|
||||
|
||||
Headless authentication is not working at the moment.
|
||||
|
||||
### Modified time and hashes ###
|
||||
|
||||
OneDrive allows modification times to be set on objects accurate to 1
|
||||
|
@ -142,10 +165,6 @@ is 10MB.
|
|||
Note that OneDrive is case insensitive so you can't have a
|
||||
file called "Hello.doc" and one called "hello.doc".
|
||||
|
||||
Rclone only supports your default OneDrive, and doesn't work with One
|
||||
Drive for business. Both these issues may be fixed at some point
|
||||
depending on user demand!
|
||||
|
||||
There are quite a few characters that can't be in OneDrive file
|
||||
names. These can't occur on Windows platforms, but on non-Windows
|
||||
platforms they are common. Rclone will map these names to and from an
|
||||
|
|
|
@ -25,18 +25,22 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
rcloneClientID = "0000000044165769"
|
||||
rcloneEncryptedClientSecret = "ugVWLNhKkVT1-cbTRO-6z1MlzwdW6aMwpKgNaFG-qXjEn_WfDnG9TVyRA5yuoliU"
|
||||
minSleep = 10 * time.Millisecond
|
||||
maxSleep = 2 * time.Second
|
||||
decayConstant = 2 // bigger for slower decay, exponential
|
||||
rootURL = "https://api.onedrive.com/v1.0" // root URL for requests
|
||||
rclonePersonalClientID = "0000000044165769"
|
||||
rclonePersonalEncryptedClientSecret = "ugVWLNhKkVT1-cbTRO-6z1MlzwdW6aMwpKgNaFG-qXjEn_WfDnG9TVyRA5yuoliU"
|
||||
rcloneBusinessClientID = "52857fec-4bc2-483f-9f1b-5fe28e97532c"
|
||||
rcloneBusinessEncryptedClientSecret = "6t4pC8l6L66SFYVIi8PgECDyjXy_ABo1nsTaE-Lr9LpzC6yT4vNOwHsakwwdEui0O6B0kX8_xbBLj91J"
|
||||
minSleep = 10 * time.Millisecond
|
||||
maxSleep = 2 * time.Second
|
||||
decayConstant = 2 // bigger for slower decay, exponential
|
||||
rootURLPersonal = "https://api.onedrive.com/v1.0/drive" // root URL for requests
|
||||
discoveryServiceURL = "https://api.office.com/discovery/"
|
||||
configResourceURL = "resource_url"
|
||||
)
|
||||
|
||||
// Globals
|
||||
var (
|
||||
// Description of how to auth for this app
|
||||
oauthConfig = &oauth2.Config{
|
||||
// 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
|
||||
|
@ -46,10 +50,23 @@ var (
|
|||
AuthURL: "https://login.live.com/oauth20_authorize.srf",
|
||||
TokenURL: "https://login.live.com/oauth20_token.srf",
|
||||
},
|
||||
ClientID: rcloneClientID,
|
||||
ClientSecret: fs.MustReveal(rcloneEncryptedClientSecret),
|
||||
ClientID: rclonePersonalClientID,
|
||||
ClientSecret: fs.MustReveal(rclonePersonalEncryptedClientSecret),
|
||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
||||
}
|
||||
|
||||
// Description of how to auth for this app for a business account
|
||||
oauthBusinessConfig = &oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize",
|
||||
TokenURL: "https://login.microsoftonline.com/common/oauth2/token",
|
||||
},
|
||||
ClientID: rcloneBusinessClientID,
|
||||
ClientSecret: fs.MustReveal(rcloneBusinessEncryptedClientSecret),
|
||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
||||
}
|
||||
oauthBusinessResource = oauth2.SetAuthURLParam("resource", discoveryServiceURL)
|
||||
|
||||
chunkSize = fs.SizeSuffix(10 * 1024 * 1024)
|
||||
uploadCutoff = fs.SizeSuffix(10 * 1024 * 1024)
|
||||
)
|
||||
|
@ -61,9 +78,74 @@ func init() {
|
|||
Description: "Microsoft OneDrive",
|
||||
NewFs: NewFs,
|
||||
Config: func(name string) {
|
||||
err := oauthutil.Config("onedrive", name, oauthConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
// choose account type
|
||||
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 := fs.Command([]string{"bBusiness", "pPersonal"}) == 'p'
|
||||
|
||||
if isPersonal {
|
||||
// for personal accounts we don't safe a field about the account
|
||||
err := oauthutil.Config("onedrive", name, oauthPersonalConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
}
|
||||
} else {
|
||||
err := oauthutil.Config("onedrivebusiness", name, oauthBusinessConfig, oauthBusinessResource)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
}
|
||||
|
||||
type serviceResource struct {
|
||||
ServiceAPIVersion string `json:"serviceApiVersion"`
|
||||
ServiceEndpointURI string `json:"serviceEndpointUri"`
|
||||
ServiceResourceID string `json:"serviceResourceId"`
|
||||
}
|
||||
type serviceResponse struct {
|
||||
Services []serviceResource `json:"value"`
|
||||
}
|
||||
|
||||
oAuthClient, _, err := oauthutil.NewClient(name, oauthBusinessConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure OneDrive: %v", err)
|
||||
}
|
||||
srv := rest.NewClient(oAuthClient).SetRoot(discoveryServiceURL)
|
||||
|
||||
opts := rest.Opts{
|
||||
Method: "GET",
|
||||
Path: "/v2.0/me/services",
|
||||
}
|
||||
services := serviceResponse{}
|
||||
resp, err := srv.CallJSON(&opts, nil, &services)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query available services: %v", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
log.Fatalf("Failed to query available services: Got HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
foundService := false
|
||||
for _, service := range services.Services {
|
||||
if service.ServiceAPIVersion == "v2.0" {
|
||||
foundService = true
|
||||
fs.ConfigFileSet(name, configResourceURL, service.ServiceResourceID)
|
||||
oauthBusinessResource = oauth2.SetAuthURLParam("resource", service.ServiceResourceID)
|
||||
|
||||
fs.Logf(nil, "Found API %s endpoint %s", service.ServiceAPIVersion, service.ServiceEndpointURI)
|
||||
}
|
||||
// we only support 2.0 API
|
||||
fs.Logf(nil, "Skipping API %s endpoint %s", service.ServiceAPIVersion, service.ServiceEndpointURI)
|
||||
}
|
||||
|
||||
if !foundService {
|
||||
log.Fatalf("No Service found")
|
||||
}
|
||||
|
||||
fs.ConfigFileDeleteKey(name, fs.ConfigToken)
|
||||
err = oauthutil.Config("onedrivebusiness", name, oauthBusinessConfig, oauthBusinessResource)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to configure token: %v", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
Options: []fs.Option{{
|
||||
|
@ -74,6 +156,7 @@ func init() {
|
|||
Help: "Microsoft App Client Secret - leave blank normally.",
|
||||
}},
|
||||
})
|
||||
|
||||
fs.VarP(&chunkSize, "onedrive-chunk-size", "", "Above this size files will be chunked - must be multiple of 320k.")
|
||||
fs.VarP(&uploadCutoff, "onedrive-upload-cutoff", "", "Cutoff for switching to chunked upload - must be <= 100MB")
|
||||
}
|
||||
|
@ -87,6 +170,7 @@ type Fs struct {
|
|||
dirCache *dircache.DirCache // Map of directory path to directory id
|
||||
pacer *pacer.Pacer // pacer for API calls
|
||||
tokenRenewer *oauthutil.Renew // renew the token on expiry
|
||||
isBusiness bool // true if this is an OneDrive Business account
|
||||
}
|
||||
|
||||
// Object describes a one drive object
|
||||
|
@ -168,7 +252,7 @@ func shouldRetry(resp *http.Response, err error) (bool, error) {
|
|||
func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
|
||||
opts := rest.Opts{
|
||||
Method: "GET",
|
||||
Path: "/drive/root:/" + pathEscape(replaceReservedChars(path)),
|
||||
Path: "/root:/" + pathEscape(replaceReservedChars(path)),
|
||||
}
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(&opts, nil, &info)
|
||||
|
@ -193,6 +277,23 @@ func errorHandler(resp *http.Response) error {
|
|||
|
||||
// NewFs constructs an Fs from the path, container:path
|
||||
func NewFs(name, root string) (fs.Fs, error) {
|
||||
// get the resource URL from the config file0
|
||||
resourceURL := fs.ConfigFileGet(name, configResourceURL, "")
|
||||
// if we have a resource URL it's a business account otherwise a personal one
|
||||
var rootURL string
|
||||
var oauthConfig *oauth2.Config
|
||||
if resourceURL == "" {
|
||||
// personal account setup
|
||||
oauthConfig = oauthPersonalConfig
|
||||
rootURL = rootURLPersonal
|
||||
} else {
|
||||
// business account setup
|
||||
oauthConfig = oauthBusinessConfig
|
||||
rootURL = resourceURL + "_api/v2.0/drives/me"
|
||||
|
||||
// update the URL in the AuthOptions
|
||||
oauthBusinessResource = oauth2.SetAuthURLParam("resource", resourceURL)
|
||||
}
|
||||
root = parsePath(root)
|
||||
oAuthClient, ts, err := oauthutil.NewClient(name, oauthConfig)
|
||||
if err != nil {
|
||||
|
@ -200,14 +301,18 @@ func NewFs(name, root string) (fs.Fs, error) {
|
|||
}
|
||||
|
||||
f := &Fs{
|
||||
name: name,
|
||||
root: root,
|
||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
|
||||
name: name,
|
||||
root: root,
|
||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
|
||||
isBusiness: resourceURL != "",
|
||||
}
|
||||
f.features = (&fs.Features{
|
||||
CaseInsensitive: true,
|
||||
ReadMimeType: true,
|
||||
CaseInsensitive: true,
|
||||
// OneDrive for business doesn't support mime types properly
|
||||
// so we disable it until resolved
|
||||
// https://github.com/OneDrive/onedrive-api-docs/issues/643
|
||||
ReadMimeType: !f.isBusiness,
|
||||
CanHaveEmptyDirectories: true,
|
||||
}).Fill(f)
|
||||
f.srv.SetErrorHandler(errorHandler)
|
||||
|
@ -323,7 +428,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
|
|||
var info *api.Item
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/drive/items/" + pathID + "/children",
|
||||
Path: "/items/" + pathID + "/children",
|
||||
}
|
||||
mkdir := api.CreateItemRequest{
|
||||
Name: replaceReservedChars(leaf),
|
||||
|
@ -357,7 +462,7 @@ func (f *Fs) listAll(dirID string, directoriesOnly bool, filesOnly bool, fn list
|
|||
// https://dev.onedrive.com/odata/optional-query-parameters.htm
|
||||
opts := rest.Opts{
|
||||
Method: "GET",
|
||||
Path: "/drive/items/" + dirID + "/children?top=1000",
|
||||
Path: "/items/" + dirID + "/children?top=1000",
|
||||
}
|
||||
OUTER:
|
||||
for {
|
||||
|
@ -504,7 +609,7 @@ func (f *Fs) Mkdir(dir string) error {
|
|||
func (f *Fs) deleteObject(id string) error {
|
||||
opts := rest.Opts{
|
||||
Method: "DELETE",
|
||||
Path: "/drive/items/" + id,
|
||||
Path: "/items/" + id,
|
||||
NoResponse: true,
|
||||
}
|
||||
return f.pacer.Call(func() (bool, error) {
|
||||
|
@ -644,7 +749,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
|
|||
// Copy the object
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/drive/items/" + srcObj.id + "/action.copy",
|
||||
Path: "/items/" + srcObj.id + "/action.copy",
|
||||
ExtraHeaders: map[string]string{"Prefer": "respond-async"},
|
||||
NoResponse: true,
|
||||
}
|
||||
|
@ -712,7 +817,7 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
|
|||
// Move the object
|
||||
opts := rest.Opts{
|
||||
Method: "PATCH",
|
||||
Path: "/drive/items/" + srcObj.id,
|
||||
Path: "/items/" + srcObj.id,
|
||||
}
|
||||
move := api.MoveItemRequest{
|
||||
Name: replaceReservedChars(leaf),
|
||||
|
@ -863,7 +968,7 @@ func (o *Object) ModTime() time.Time {
|
|||
func (o *Object) setModTime(modTime time.Time) (*api.Item, error) {
|
||||
opts := rest.Opts{
|
||||
Method: "PATCH",
|
||||
Path: "/drive/root:/" + pathEscape(o.srvPath()),
|
||||
Path: "/root:/" + pathEscape(o.srvPath()),
|
||||
}
|
||||
update := api.SetFileSystemInfo{
|
||||
FileSystemInfo: api.FileSystemInfoFacet{
|
||||
|
@ -901,7 +1006,7 @@ func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
|||
var resp *http.Response
|
||||
opts := rest.Opts{
|
||||
Method: "GET",
|
||||
Path: "/drive/items/" + o.id + "/content",
|
||||
Path: "/items/" + o.id + "/content",
|
||||
Options: options,
|
||||
}
|
||||
err = o.fs.pacer.Call(func() (bool, error) {
|
||||
|
@ -918,7 +1023,7 @@ func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
|||
func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) {
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/drive/root:/" + pathEscape(o.srvPath()) + ":/upload.createSession",
|
||||
Path: "/root:/" + pathEscape(o.srvPath()) + ":/upload.createSession",
|
||||
}
|
||||
var resp *http.Response
|
||||
err = o.fs.pacer.Call(func() (bool, error) {
|
||||
|
@ -1023,9 +1128,10 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
|
|||
// This is for less than 100 MB of content
|
||||
var resp *http.Response
|
||||
opts := rest.Opts{
|
||||
Method: "PUT",
|
||||
Path: "/drive/root:/" + pathEscape(o.srvPath()) + ":/content",
|
||||
Body: in,
|
||||
Method: "PUT",
|
||||
Path: "/root:/" + pathEscape(o.srvPath()) + ":/content",
|
||||
ContentLength: &size,
|
||||
Body: in,
|
||||
}
|
||||
// for go1.8 (see release notes) we must nil the Body if we want a
|
||||
// "Content-Length: 0" header which onedrive requires for all files.
|
||||
|
|
Loading…
Reference in a new issue