forked from TrueCloudLab/rclone
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.
|
Microsoft App Client Secret - leave blank normally.
|
||||||
client_secret>
|
client_secret>
|
||||||
Remote config
|
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?
|
Use auto config?
|
||||||
* Say Y if not sure
|
* Say Y if not sure
|
||||||
* Say N if you are working on a remote or headless machine
|
* 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
|
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 ###
|
### Modified time and hashes ###
|
||||||
|
|
||||||
OneDrive allows modification times to be set on objects accurate to 1
|
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
|
Note that OneDrive is case insensitive so you can't have a
|
||||||
file called "Hello.doc" and one called "hello.doc".
|
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
|
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
|
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
|
platforms they are common. Rclone will map these names to and from an
|
||||||
|
|
|
@ -25,18 +25,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rcloneClientID = "0000000044165769"
|
rclonePersonalClientID = "0000000044165769"
|
||||||
rcloneEncryptedClientSecret = "ugVWLNhKkVT1-cbTRO-6z1MlzwdW6aMwpKgNaFG-qXjEn_WfDnG9TVyRA5yuoliU"
|
rclonePersonalEncryptedClientSecret = "ugVWLNhKkVT1-cbTRO-6z1MlzwdW6aMwpKgNaFG-qXjEn_WfDnG9TVyRA5yuoliU"
|
||||||
|
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
|
||||||
rootURL = "https://api.onedrive.com/v1.0" // root URL for requests
|
rootURLPersonal = "https://api.onedrive.com/v1.0/drive" // root URL for requests
|
||||||
|
discoveryServiceURL = "https://api.office.com/discovery/"
|
||||||
|
configResourceURL = "resource_url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
var (
|
var (
|
||||||
// Description of how to auth for this app
|
// Description of how to auth for this app for a personal account
|
||||||
oauthConfig = &oauth2.Config{
|
oauthPersonalConfig = &oauth2.Config{
|
||||||
Scopes: []string{
|
Scopes: []string{
|
||||||
"wl.signin", // Allow single sign-on capabilities
|
"wl.signin", // Allow single sign-on capabilities
|
||||||
"wl.offline_access", // Allow receiving a refresh token
|
"wl.offline_access", // Allow receiving a refresh token
|
||||||
|
@ -46,10 +50,23 @@ var (
|
||||||
AuthURL: "https://login.live.com/oauth20_authorize.srf",
|
AuthURL: "https://login.live.com/oauth20_authorize.srf",
|
||||||
TokenURL: "https://login.live.com/oauth20_token.srf",
|
TokenURL: "https://login.live.com/oauth20_token.srf",
|
||||||
},
|
},
|
||||||
ClientID: rcloneClientID,
|
ClientID: rclonePersonalClientID,
|
||||||
ClientSecret: fs.MustReveal(rcloneEncryptedClientSecret),
|
ClientSecret: fs.MustReveal(rclonePersonalEncryptedClientSecret),
|
||||||
RedirectURL: oauthutil.RedirectLocalhostURL,
|
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)
|
chunkSize = fs.SizeSuffix(10 * 1024 * 1024)
|
||||||
uploadCutoff = fs.SizeSuffix(10 * 1024 * 1024)
|
uploadCutoff = fs.SizeSuffix(10 * 1024 * 1024)
|
||||||
)
|
)
|
||||||
|
@ -61,10 +78,75 @@ func init() {
|
||||||
Description: "Microsoft OneDrive",
|
Description: "Microsoft OneDrive",
|
||||||
NewFs: NewFs,
|
NewFs: NewFs,
|
||||||
Config: func(name string) {
|
Config: func(name string) {
|
||||||
err := oauthutil.Config("onedrive", name, oauthConfig)
|
// 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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to configure token: %v", err)
|
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{{
|
Options: []fs.Option{{
|
||||||
Name: fs.ConfigClientID,
|
Name: fs.ConfigClientID,
|
||||||
|
@ -74,6 +156,7 @@ func init() {
|
||||||
Help: "Microsoft App Client Secret - leave blank normally.",
|
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(&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")
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes a one drive object
|
// 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) {
|
func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/drive/root:/" + pathEscape(replaceReservedChars(path)),
|
Path: "/root:/" + pathEscape(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)
|
||||||
|
@ -193,6 +277,23 @@ func errorHandler(resp *http.Response) error {
|
||||||
|
|
||||||
// NewFs constructs an Fs from the path, container:path
|
// NewFs constructs an Fs from the path, container:path
|
||||||
func NewFs(name, root string) (fs.Fs, error) {
|
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)
|
root = parsePath(root)
|
||||||
oAuthClient, ts, err := oauthutil.NewClient(name, oauthConfig)
|
oAuthClient, ts, err := oauthutil.NewClient(name, oauthConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -204,10 +305,14 @@ func NewFs(name, root string) (fs.Fs, error) {
|
||||||
root: root,
|
root: root,
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||||
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
|
pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
|
||||||
|
isBusiness: resourceURL != "",
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: true,
|
CaseInsensitive: true,
|
||||||
ReadMimeType: 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,
|
CanHaveEmptyDirectories: true,
|
||||||
}).Fill(f)
|
}).Fill(f)
|
||||||
f.srv.SetErrorHandler(errorHandler)
|
f.srv.SetErrorHandler(errorHandler)
|
||||||
|
@ -323,7 +428,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
|
||||||
var info *api.Item
|
var info *api.Item
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/drive/items/" + pathID + "/children",
|
Path: "/items/" + pathID + "/children",
|
||||||
}
|
}
|
||||||
mkdir := api.CreateItemRequest{
|
mkdir := api.CreateItemRequest{
|
||||||
Name: replaceReservedChars(leaf),
|
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
|
// https://dev.onedrive.com/odata/optional-query-parameters.htm
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/drive/items/" + dirID + "/children?top=1000",
|
Path: "/items/" + dirID + "/children?top=1000",
|
||||||
}
|
}
|
||||||
OUTER:
|
OUTER:
|
||||||
for {
|
for {
|
||||||
|
@ -504,7 +609,7 @@ func (f *Fs) Mkdir(dir string) error {
|
||||||
func (f *Fs) deleteObject(id string) error {
|
func (f *Fs) deleteObject(id string) error {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "DELETE",
|
Method: "DELETE",
|
||||||
Path: "/drive/items/" + id,
|
Path: "/items/" + id,
|
||||||
NoResponse: true,
|
NoResponse: true,
|
||||||
}
|
}
|
||||||
return f.pacer.Call(func() (bool, error) {
|
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
|
// Copy the object
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/drive/items/" + srcObj.id + "/action.copy",
|
Path: "/items/" + srcObj.id + "/action.copy",
|
||||||
ExtraHeaders: map[string]string{"Prefer": "respond-async"},
|
ExtraHeaders: map[string]string{"Prefer": "respond-async"},
|
||||||
NoResponse: true,
|
NoResponse: true,
|
||||||
}
|
}
|
||||||
|
@ -712,7 +817,7 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
|
||||||
// Move the object
|
// Move the object
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "PATCH",
|
Method: "PATCH",
|
||||||
Path: "/drive/items/" + srcObj.id,
|
Path: "/items/" + srcObj.id,
|
||||||
}
|
}
|
||||||
move := api.MoveItemRequest{
|
move := api.MoveItemRequest{
|
||||||
Name: replaceReservedChars(leaf),
|
Name: replaceReservedChars(leaf),
|
||||||
|
@ -863,7 +968,7 @@ func (o *Object) ModTime() time.Time {
|
||||||
func (o *Object) setModTime(modTime time.Time) (*api.Item, error) {
|
func (o *Object) setModTime(modTime time.Time) (*api.Item, error) {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "PATCH",
|
Method: "PATCH",
|
||||||
Path: "/drive/root:/" + pathEscape(o.srvPath()),
|
Path: "/root:/" + pathEscape(o.srvPath()),
|
||||||
}
|
}
|
||||||
update := api.SetFileSystemInfo{
|
update := api.SetFileSystemInfo{
|
||||||
FileSystemInfo: api.FileSystemInfoFacet{
|
FileSystemInfo: api.FileSystemInfoFacet{
|
||||||
|
@ -901,7 +1006,7 @@ func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/drive/items/" + o.id + "/content",
|
Path: "/items/" + o.id + "/content",
|
||||||
Options: options,
|
Options: options,
|
||||||
}
|
}
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
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) {
|
func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) {
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Path: "/drive/root:/" + pathEscape(o.srvPath()) + ":/upload.createSession",
|
Path: "/root:/" + pathEscape(o.srvPath()) + ":/upload.createSession",
|
||||||
}
|
}
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
@ -1024,7 +1129,8 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "PUT",
|
Method: "PUT",
|
||||||
Path: "/drive/root:/" + pathEscape(o.srvPath()) + ":/content",
|
Path: "/root:/" + pathEscape(o.srvPath()) + ":/content",
|
||||||
|
ContentLength: &size,
|
||||||
Body: in,
|
Body: in,
|
||||||
}
|
}
|
||||||
// for go1.8 (see release notes) we must nil the Body if we want a
|
// for go1.8 (see release notes) we must nil the Body if we want a
|
||||||
|
|
Loading…
Add table
Reference in a new issue