diff --git a/amazonclouddrive/amazonclouddrive.go b/amazonclouddrive/amazonclouddrive.go
index a4b665336..85edcacd0 100644
--- a/amazonclouddrive/amazonclouddrive.go
+++ b/amazonclouddrive/amazonclouddrive.go
@@ -479,7 +479,7 @@ func (f *FsAcd) purgeCheck(check bool) error {
if check {
// check directory is empty
empty := true
- _, err := f.listAll(rootID, "", false, false, func(node *acd.Node) bool {
+ _, err = f.listAll(rootID, "", false, false, func(node *acd.Node) bool {
switch *node.Kind {
case folderKind:
empty = false
diff --git a/docs/content/about.md b/docs/content/about.md
index b0c8b35ad..b45a2c4d9 100644
--- a/docs/content/about.md
+++ b/docs/content/about.md
@@ -19,6 +19,7 @@ Rclone is a command line program to sync files and directories to and from
* Dropbox
* Google Cloud Storage
* Amazon Cloud Drive
+ * Microsoft One Drive
* The local filesystem
Features
diff --git a/docs/content/onedrive.md b/docs/content/onedrive.md
new file mode 100644
index 000000000..0c6ca7a1a
--- /dev/null
+++ b/docs/content/onedrive.md
@@ -0,0 +1,111 @@
+---
+title: "Microsoft One Drive"
+description: "Rclone docs for Microsoft One Drive"
+date: "2015-10-14"
+---
+
+ Microsoft One Drive
+-----------------------------------------
+
+Paths are specified as `remote:path`
+
+Paths may be as deep as required, eg `remote:directory/subdirectory`.
+
+The initial setup for One Drive involves getting a token from
+Microsoft which you need to do in your browser. `rclone config` walks
+you through it.
+
+Here is an example of how to make a remote called `remote`. First run:
+
+ rclone config
+
+This will guide you through an interactive setup process:
+
+```
+n) New remote
+d) Delete remote
+q) Quit config
+e/n/d/q> n
+name> remote
+What type of source is it?
+Choose a number from below
+ 1) amazon cloud drive
+ 2) drive
+ 3) dropbox
+ 4) google cloud storage
+ 5) local
+ 6) onedrive
+ 7) s3
+ 8) swift
+type> 6
+Microsoft App Client Id - leave blank normally.
+client_id>
+Microsoft App Client Secret - leave blank normally.
+client_secret>
+Remote config
+If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth
+Log in and authorize rclone for access
+Waiting for code...
+Got code
+--------------------
+[remote]
+client_id =
+client_secret =
+token = {"access_token":"XXXXXX"}
+--------------------
+y) Yes this is OK
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+```
+
+Note that rclone runs a webserver on your local machine to collect the
+token as returned from Microsoft. This only runs from the moment it
+opens your browser to the moment you get back the verification
+code. This is on `http://127.0.0.1:53682/` and this it may require
+you to unblock it temporarily if you are running a host firewall.
+
+Once configured you can then use `rclone` like this,
+
+List directories in top level of your One Drive
+
+ rclone lsd remote:
+
+List all the files in your One Drive
+
+ rclone ls remote:
+
+To copy a local directory to an One Drive directory called backup
+
+ rclone copy /home/source remote:backup
+
+### Modified time and MD5SUMs ###
+
+One Drive allows modification times to be set on objects accurate to 1
+second. These will be used to detect whether objects need syncing or
+not.
+
+One drive does not support MD5SUMs. This means the `--checksum` flag
+will be equivalent to the `--size-only` flag.
+
+### Deleting files ###
+
+Any files you delete with rclone will end up in the trash. Microsoft
+doesn't provide an API to permanently delete files, nor to empty the
+trash, so you will have to do that with one of Microsoft's apps or via
+the One Drive website.
+
+### Limitations ###
+
+Note that One Drive is case sensitive so you can't have a
+file called "Hello.doc" and one called "hello.doc".
+
+Rclone only supports your default One Drive, 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 One Drive 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
+identical looking unicode equivalent. For example if a file has a `?`
+in it will be mapped to `?` instead.
diff --git a/docs/content/overview.md b/docs/content/overview.md
index 4b0761e5d..56db86982 100644
--- a/docs/content/overview.md
+++ b/docs/content/overview.md
@@ -23,6 +23,7 @@ Here is an overview of the major features of each cloud storage system.
| Dropbox | No | No | Yes | No |
| Google Cloud Storage | Yes | Yes | No | No |
| Amazon Cloud Drive | Yes | No | Yes | No |
+| Microsoft One Drive | No | Yes | Yes | No |
| The local filesystem | Yes | Yes | Depends | No |
### MD5SUM ###
diff --git a/docs/layouts/chrome/navbar.html b/docs/layouts/chrome/navbar.html
index d134c04c0..d0ae7f562 100644
--- a/docs/layouts/chrome/navbar.html
+++ b/docs/layouts/chrome/navbar.html
@@ -37,6 +37,7 @@
Dropbox
Google Cloud Storage
Amazon Cloud Drive
+ Microsoft One Drive
Local
diff --git a/fs/operations_test.go b/fs/operations_test.go
index 0f32c31cb..c9d2692df 100644
--- a/fs/operations_test.go
+++ b/fs/operations_test.go
@@ -25,6 +25,7 @@ import (
_ "github.com/ncw/rclone/dropbox"
_ "github.com/ncw/rclone/googlecloudstorage"
_ "github.com/ncw/rclone/local"
+ _ "github.com/ncw/rclone/onedrive"
_ "github.com/ncw/rclone/s3"
_ "github.com/ncw/rclone/swift"
)
diff --git a/fstest/fstests/gen_tests.go b/fstest/fstests/gen_tests.go
index ff5a7a24f..de137a4fb 100644
--- a/fstest/fstests/gen_tests.go
+++ b/fstest/fstests/gen_tests.go
@@ -65,7 +65,7 @@ import (
)
func init() {
- fstests.NilObject = fs.Object((*{{ .FsName }}.FsObject{{ .ObjectName }})(nil))
+ fstests.NilObject = fs.Object((*{{ .FsName }}.{{ .ObjectName }})(nil))
fstests.RemoteName = "{{ .TestName }}"
}
@@ -126,12 +126,13 @@ func generateTestProgram(t *template.Template, fns []string, Fsname, ObjectName
func main() {
fns := findTestFunctions()
t := template.Must(template.New("main").Parse(testProgram))
- generateTestProgram(t, fns, "Local", "Local")
- generateTestProgram(t, fns, "Swift", "Swift")
- generateTestProgram(t, fns, "S3", "S3")
- generateTestProgram(t, fns, "Drive", "Drive")
- generateTestProgram(t, fns, "GoogleCloudStorage", "Storage")
- generateTestProgram(t, fns, "Dropbox", "Dropbox")
- generateTestProgram(t, fns, "AmazonCloudDrive", "Acd")
+ generateTestProgram(t, fns, "Local", "FsObjectLocal")
+ generateTestProgram(t, fns, "Swift", "FsObjectSwift")
+ generateTestProgram(t, fns, "S3", "FsObjectS3")
+ generateTestProgram(t, fns, "Drive", "FsObjectDrive")
+ generateTestProgram(t, fns, "GoogleCloudStorage", "FsObjectStorage")
+ generateTestProgram(t, fns, "Dropbox", "FsObjectDropbox")
+ generateTestProgram(t, fns, "AmazonCloudDrive", "FsObjectAcd")
+ generateTestProgram(t, fns, "OneDrive", "Object")
log.Printf("Done")
}
diff --git a/graphics/rclone-50x50.png b/graphics/rclone-50x50.png
new file mode 100644
index 000000000..24cc315d4
Binary files /dev/null and b/graphics/rclone-50x50.png differ
diff --git a/onedrive/api/api.go b/onedrive/api/api.go
new file mode 100644
index 000000000..57b74b82e
--- /dev/null
+++ b/onedrive/api/api.go
@@ -0,0 +1,129 @@
+// Package api implements the API for one drive
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/ncw/rclone/fs"
+)
+
+const (
+ rootURL = "https://api.onedrive.com/v1.0" // root URL for requests
+)
+
+// Client contains the info to sustain the API
+type Client struct {
+ c *http.Client
+}
+
+// NewClient takes an oauth http.Client and makes a new api instance
+func NewClient(c *http.Client) *Client {
+ return &Client{
+ c: c,
+ }
+}
+
+// Opts contains parameters for Call, CallJSON etc
+type Opts struct {
+ Method string
+ Path string
+ Absolute bool // Path is absolute
+ Body io.Reader
+ NoResponse bool // set to close Body
+ ContentType string
+ ContentLength *int64
+ ContentRange string
+}
+
+// checkClose is a utility function used to check the return from
+// Close in a defer statement.
+func checkClose(c io.Closer, err *error) {
+ cerr := c.Close()
+ if *err == nil {
+ *err = cerr
+ }
+}
+
+// decodeJSON decodes resp.Body into json
+func (api *Client) decodeJSON(resp *http.Response, result interface{}) (err error) {
+ defer checkClose(resp.Body, &err)
+ decoder := json.NewDecoder(resp.Body)
+ return decoder.Decode(result)
+}
+
+// Call makes the call and returns the http.Response
+//
+// if err != nil then resp.Body will need to be closed
+//
+// it will return resp if at all possible, even if err is set
+func (api *Client) Call(opts *Opts) (resp *http.Response, err error) {
+ if opts == nil {
+ return nil, fmt.Errorf("call() called with nil opts")
+ }
+ var url string
+ if opts.Absolute {
+ url = opts.Path
+ } else {
+ url = rootURL + opts.Path
+ }
+ req, err := http.NewRequest(opts.Method, url, opts.Body)
+ if err != nil {
+ return
+ }
+ if opts.ContentType != "" {
+ req.Header.Add("Content-Type", opts.ContentType)
+ }
+ if opts.ContentLength != nil {
+ req.ContentLength = *opts.ContentLength
+ }
+ if opts.ContentRange != "" {
+ req.Header.Add("Content-Range", opts.ContentRange)
+ }
+ req.Header.Add("User-Agent", fs.UserAgent)
+ resp, err = api.c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
+ // Decode error response
+ errResponse := new(Error)
+ err = api.decodeJSON(resp, &errResponse)
+ if err != nil {
+ return resp, err
+ }
+ return resp, errResponse
+ }
+ if opts.NoResponse {
+ return resp, resp.Body.Close()
+ }
+ return resp, nil
+}
+
+// CallJSON runs Call and decodes the body as a JSON object into result
+//
+// If request is not nil then it will be JSON encoded as the body of the request
+//
+// It will return resp if at all possible, even if err is set
+func (api *Client) CallJSON(opts *Opts, request interface{}, response interface{}) (resp *http.Response, err error) {
+ // Set the body up as a JSON object if required
+ if opts.Body == nil && request != nil {
+ body, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ var newOpts = *opts
+ newOpts.Body = bytes.NewBuffer(body)
+ newOpts.ContentType = "application/json"
+ opts = &newOpts
+ }
+ resp, err = api.Call(opts)
+ if err != nil {
+ return resp, err
+ }
+ err = api.decodeJSON(resp, response)
+ return resp, err
+}
diff --git a/onedrive/api/types.go b/onedrive/api/types.go
new file mode 100644
index 000000000..99cc95d89
--- /dev/null
+++ b/onedrive/api/types.go
@@ -0,0 +1,191 @@
+// Types passed and returned to and from the API
+
+package api
+
+import "time"
+
+const (
+ timeFormat = `"` + time.RFC3339 + `"`
+)
+
+// Error is returned from one drive when things go wrong
+type Error struct {
+ ErrorInfo struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ InnerError struct {
+ Code string `json:"code"`
+ } `json:"innererror"`
+ } `json:"error"`
+}
+
+// Error returns a string for the error and statistifes the error interface
+func (e *Error) Error() string {
+ out := e.ErrorInfo.Code
+ if e.ErrorInfo.InnerError.Code != "" {
+ out += ": " + e.ErrorInfo.InnerError.Code
+ }
+ out += ": " + e.ErrorInfo.Message
+ return out
+}
+
+// Check Error statisfies the error interface
+var _ error = (*Error)(nil)
+
+// Identity represents an identity of an actor. For example, and actor
+// can be a user, device, or application.
+type Identity struct {
+ DisplayName string `json:"displayName"`
+ ID string `json:"id"`
+}
+
+// IdentitySet is a keyed collection of Identity objects. It is used
+// to represent a set of identities associated with various events for
+// an item, such as created by or last modified by.
+type IdentitySet struct {
+ User Identity `json:"user"`
+ Application Identity `json:"application"`
+ Device Identity `json:"device"`
+}
+
+// Quota groups storage space quota-related information on OneDrive into a single structure.
+type Quota struct {
+ Total int `json:"total"`
+ Used int `json:"used"`
+ Remaining int `json:"remaining"`
+ Deleted int `json:"deleted"`
+ State string `json:"state"` // normal | nearing | critical | exceeded
+}
+
+// Drive is a representation of a drive resource
+type Drive struct {
+ ID string `json:"id"`
+ DriveType string `json:"driveType"`
+ Owner IdentitySet `json:"owner"`
+ Quota Quota `json:"quota"`
+}
+
+// Timestamp represents represents date and time information for the
+// OneDrive API, by using ISO 8601 and is always in UTC time.
+type Timestamp time.Time
+
+// MarshalJSON turns a Timestamp into JSON (in UTC)
+func (t *Timestamp) MarshalJSON() (out []byte, err error) {
+ out = (*time.Time)(t).UTC().AppendFormat(out, timeFormat)
+ return out, nil
+}
+
+// UnmarshalJSON turns JSON into a Timestamp
+func (t *Timestamp) UnmarshalJSON(data []byte) error {
+ newT, err := time.Parse(timeFormat, string(data))
+ if err != nil {
+ return err
+ }
+ *t = Timestamp(newT)
+ return nil
+}
+
+// ItemReference groups data needed to reference a OneDrive item
+// across the service into a single structure.
+type ItemReference struct {
+ 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.
+ Path string `json:"path"` // Path that used to navigate to the item. Read/Write.
+}
+
+// FolderFacet groups folder-related data on OneDrive into a single structure
+type FolderFacet struct {
+ ChildCount int64 `json:"childCount"` // Number of children contained immediately within this container.
+}
+
+// HashesType groups different types of hashes into a single structure, for an item on OneDrive.
+type HashesType struct {
+ Sha1Hash string `json:"sha1Hash"` // base64 encoded SHA1 hash for the contents of the file (if available)
+ Crc32Hash string `json:"crc32Hash"` // base64 encoded CRC32 value of the file (if available)
+}
+
+// FileFacet groups file-related data on OneDrive into a single structure.
+type FileFacet struct {
+ MimeType string `json:"mimeType"` // The MIME type for the file. This is determined by logic on the server and might not be the value provided when the file was uploaded.
+ Hashes HashesType `json:"hashes"` // Hashes of the file's binary content, if available.
+}
+
+// FileSystemInfoFacet contains properties that are reported by the
+// device's local file system for the local version of an item. This
+// facet can be used to specify the last modified date or created date
+// of the item as it was on the local device.
+type FileSystemInfoFacet struct {
+ CreatedDateTime Timestamp `json:"createdDateTime"` // The UTC date and time the file was created on a client.
+ LastModifiedDateTime Timestamp `json:"lastModifiedDateTime"` // The UTC date and time the file was last modified on a client.
+}
+
+// DeletedFacet indicates that the item on OneDrive has been
+// deleted. In this version of the API, the presence (non-null) of the
+// facet value indicates that the file was deleted. A null (or
+// missing) value indicates that the file is not deleted.
+type DeletedFacet struct {
+}
+
+// Item represents metadata for an item in OneDrive
+type Item struct {
+ ID string `json:"id"` // The unique identifier of the item within the Drive. Read-only.
+ Name string `json:"name"` // The name of the item (filename and extension). Read-write.
+ ETag string `json:"eTag"` // eTag for the entire item (metadata + content). Read-only.
+ CTag string `json:"cTag"` // An eTag for the content of the item. This eTag is not changed if only the metadata is changed. Read-only.
+ CreatedBy IdentitySet `json:"createdBy"` // Identity of the user, device, and application which created the item. Read-only.
+ LastModifiedBy IdentitySet `json:"lastModifiedBy"` // Identity of the user, device, and application which last modified the item. Read-only.
+ CreatedDateTime Timestamp `json:"createdDateTime"` // Date and time of item creation. Read-only.
+ LastModifiedDateTime Timestamp `json:"lastModifiedDateTime"` // Date and time the item was last modified. Read-only.
+ Size int64 `json:"size"` // Size of the item in bytes. Read-only.
+ ParentReference *ItemReference `json:"parentReference"` // Parent information, if the item has a parent. Read-write.
+ WebURL string `json:"webUrl"` // URL that displays the resource in the browser. Read-only.
+ Description string `json:"description"` // Provide a user-visible description of the item. Read-write.
+ Folder *FolderFacet `json:"folder"` // Folder metadata, if the item is a folder. Read-only.
+ File *FileFacet `json:"file"` // File metadata, if the item is a file. Read-only.
+ FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"` // File system information on client. Read-write.
+ // Image *ImageFacet `json:"image"` // Image metadata, if the item is an image. Read-only.
+ // Photo *PhotoFacet `json:"photo"` // Photo metadata, if the item is a photo. Read-only.
+ // Audio *AudioFacet `json:"audio"` // Audio metadata, if the item is an audio file. Read-only.
+ // Video *VideoFacet `json:"video"` // Video metadata, if the item is a video. Read-only.
+ // Location *LocationFacet `json:"location"` // Location metadata, if the item has location data. Read-only.
+ Deleted *DeletedFacet `json:"deleted"` // Information about the deleted state of the item. Read-only.
+}
+
+// ViewDeltaResponse is the response to the view delta method
+type ViewDeltaResponse struct {
+ Value []Item `json:"value"` // An array of Item objects which have been created, modified, or deleted.
+ NextLink string `json:"@odata.nextLink"` // A URL to retrieve the next available page of changes.
+ DeltaLink string `json:"@odata.deltaLink"` // A URL returned instead of @odata.nextLink after all current changes have been returned. Used to read the next set of changes in the future.
+ DeltaToken string `json:"@delta.token"` // A token value that can be used in the query string on manually-crafted calls to view.delta. Not needed if you're using nextLink and deltaLink.
+}
+
+// ListChildrenResponse is the response to the list children method
+type ListChildrenResponse struct {
+ Value []Item `json:"value"` // An array of Item objects
+ NextLink string `json:"@odata.nextLink"` // A URL to retrieve the next available page of items.
+}
+
+// CreateItemRequest is the request to create an item object
+type CreateItemRequest struct {
+ Name string `json:"name"` // Name of the folder to be created.
+ Folder FolderFacet `json:"folder"` // Empty Folder facet to indicate that folder is the type of resource to be created.
+ ConflictBehavior string `json:"@name.conflictBehavior"` // Determines what to do if an item with a matching name already exists in this folder. Accepted values are: rename, replace, and fail (the default).
+}
+
+// SetFileSystemInfo is used to Update an object's FileSystemInfo.
+type SetFileSystemInfo struct {
+ FileSystemInfo FileSystemInfoFacet `json:"fileSystemInfo"` // File system information on client. Read-write.
+}
+
+// CreateUploadResponse is the response from creating an upload session
+type CreateUploadResponse struct {
+ UploadURL string `json:"uploadUrl"` // "https://sn3302.up.1drv.com/up/fe6987415ace7X4e1eF866337",
+ ExpirationDateTime Timestamp `json:"expirationDateTime"` // "2015-01-29T09:21:55.523Z",
+ NextExpectedRanges []string `json:"nextExpectedRanges"` // ["0-"]
+}
+
+// UploadFragmentResponse is the response from uploading a fragment
+type UploadFragmentResponse struct {
+ ExpirationDateTime Timestamp `json:"expirationDateTime"` // "2015-01-29T09:21:55.523Z",
+ NextExpectedRanges []string `json:"nextExpectedRanges"` // ["0-"]
+}
diff --git a/onedrive/onedrive.go b/onedrive/onedrive.go
new file mode 100644
index 000000000..e9de87b78
--- /dev/null
+++ b/onedrive/onedrive.go
@@ -0,0 +1,852 @@
+// Package onedrive provides an interface to the Microsoft One Drive
+// object storage system.
+package onedrive
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ncw/rclone/dircache"
+ "github.com/ncw/rclone/fs"
+ "github.com/ncw/rclone/oauthutil"
+ "github.com/ncw/rclone/onedrive/api"
+ "github.com/ncw/rclone/pacer"
+ "github.com/spf13/pflag"
+ "golang.org/x/oauth2"
+)
+
+const (
+ rcloneClientID = "0000000044165769"
+ rcloneClientSecret = "0+be4+jYw+7018HY6P3t/Izo+pTc+Yvt8+fy8NHU094="
+ minSleep = 10 * time.Millisecond
+ maxSleep = 2 * time.Second
+ decayConstant = 2 // bigger for slower decay, exponential
+)
+
+// Globals
+var (
+ // Description of how to auth for this app
+ oauthConfig = &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: rcloneClientID,
+ ClientSecret: fs.Reveal(rcloneClientSecret),
+ RedirectURL: oauthutil.RedirectPublicURL,
+ }
+ chunkSize = fs.SizeSuffix(10 * 1024 * 1024)
+ uploadCutoff = fs.SizeSuffix(10 * 1024 * 1024)
+)
+
+// Register with Fs
+func init() {
+ fs.Register(&fs.Info{
+ Name: "onedrive",
+ NewFs: NewFs,
+ Config: func(name string) {
+ err := oauthutil.Config(name, oauthConfig)
+ if err != nil {
+ log.Fatalf("Failed to configure token: %v", err)
+ }
+ },
+ Options: []fs.Option{{
+ Name: oauthutil.ConfigClientID,
+ Help: "Microsoft App Client Id - leave blank normally.",
+ }, {
+ Name: oauthutil.ConfigClientSecret,
+ Help: "Microsoft App Client Secret - leave blank normally.",
+ }},
+ })
+ pflag.VarP(&chunkSize, "onedrive-chunk-size", "", "Above this size files will be chunked - must be multiple of 320k.")
+ pflag.VarP(&uploadCutoff, "onedrive-upload-cutoff", "", "Cutoff for switching to chunked upload - must be <= 100MB")
+}
+
+// Fs represents a remote one drive
+type Fs struct {
+ name string // name of this remote
+ srv *api.Client // the connection to the one drive server
+ root string // the path we are working on
+ dirCache *dircache.DirCache // Map of directory path to directory id
+ pacer *pacer.Pacer // pacer for API calls
+}
+
+// Object describes a one drive object
+//
+// Will definitely have info but maybe not meta
+type Object struct {
+ fs *Fs // what this object is part of
+ remote string // The remote path
+ hasMetaData bool // whether info below has been set
+ size int64 // size of the object
+ modTime time.Time // modification time of the object
+ id string // ID of the object
+}
+
+// ------------------------------------------------------------
+
+// Name of the remote (as passed into NewFs)
+func (f *Fs) Name() string {
+ return f.name
+}
+
+// Root of the remote (as passed into NewFs)
+func (f *Fs) Root() string {
+ return f.root
+}
+
+// String converts this Fs to a string
+func (f *Fs) String() string {
+ return fmt.Sprintf("One drive root '%s'", f.root)
+}
+
+// Pattern to match a one drive path
+var matcher = regexp.MustCompile(`^([^/]*)(.*)$`)
+
+// parsePath parses an one drive 'url'
+func parsePath(path string) (root string) {
+ root = strings.Trim(path, "/")
+ return
+}
+
+// retryErrorCodes is a slice of error codes that we will retry
+var retryErrorCodes = []int{
+ 429, // Too Many Requests.
+ 500, // Internal Server Error
+ 502, // Bad Gateway
+ 503, // Service Unavailable
+ 504, // Gateway Timeout
+ 509, // Bandwidth Limit Exceeded
+}
+
+// shouldRetry returns a boolean as to whether this resp and err
+// deserve to be retried. It returns the err as a convenience
+func shouldRetry(resp *http.Response, err error) (bool, error) {
+ return fs.ShouldRetry(err) || fs.ShouldRetryHTTP(resp, retryErrorCodes), err
+}
+
+// readMetaDataForPath reads the metadata from the path
+func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
+ opts := api.Opts{
+ Method: "GET",
+ Path: "/drive/root:/" + replaceReservedChars(path),
+ }
+ err = f.pacer.Call(func() (bool, error) {
+ resp, err = f.srv.CallJSON(&opts, nil, &info)
+ return shouldRetry(resp, err)
+ })
+ return info, resp, err
+}
+
+// NewFs constructs an Fs from the path, container:path
+func NewFs(name, root string) (fs.Fs, error) {
+ root = parsePath(root)
+ oAuthClient, err := oauthutil.NewClient(name, oauthConfig)
+ if err != nil {
+ log.Fatalf("Failed to configure One Drive: %v", err)
+ }
+
+ f := &Fs{
+ name: name,
+ root: root,
+ srv: api.NewClient(oAuthClient),
+ pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant),
+ }
+
+ // Get rootID
+ rootInfo, _, err := f.readMetaDataForPath("")
+ if err != nil || rootInfo.ID == "" {
+ return nil, fmt.Errorf("Failed to get root: %v", err)
+ }
+
+ f.dirCache = dircache.New(root, rootInfo.ID, f)
+
+ // Find the current root
+ err = f.dirCache.FindRoot(false)
+ if err != nil {
+ // Assume it is a file
+ newRoot, remote := dircache.SplitPath(root)
+ newF := *f
+ newF.dirCache = dircache.New(newRoot, rootInfo.ID, &newF)
+ newF.root = newRoot
+ // Make new Fs which is the parent
+ err = newF.dirCache.FindRoot(false)
+ if err != nil {
+ // No root so return old f
+ return f, nil
+ }
+ obj := newF.newObjectWithInfo(remote, nil)
+ if obj == nil {
+ // File doesn't exist so return old f
+ return f, nil
+ }
+ // return a Fs Limited to this object
+ return fs.NewLimited(&newF, obj), nil
+ }
+ return f, nil
+}
+
+// rootSlash returns root with a slash on if it is empty, otherwise empty string
+func (f *Fs) rootSlash() string {
+ if f.root == "" {
+ return f.root
+ }
+ return f.root + "/"
+}
+
+// Return an Object from a path
+//
+// May return nil if an error occurred
+func (f *Fs) newObjectWithInfo(remote string, info *api.Item) fs.Object {
+ o := &Object{
+ fs: f,
+ remote: remote,
+ }
+ if info != nil {
+ // Set info
+ o.setMetaData(info)
+ } else {
+ err := o.readMetaData() // reads info and meta, returning an error
+ if err != nil {
+ // logged already FsDebug("Failed to read info: %s", err)
+ return nil
+ }
+ }
+ return o
+}
+
+// NewFsObject returns an Object from a path
+//
+// May return nil if an error occurred
+func (f *Fs) NewFsObject(remote string) fs.Object {
+ return f.newObjectWithInfo(remote, nil)
+}
+
+// FindLeaf finds a directory of name leaf in the folder with ID pathID
+func (f *Fs) FindLeaf(pathID, leaf string) (pathIDOut string, found bool, err error) {
+ // fs.Debug(f, "FindLeaf(%q, %q)", pathID, leaf)
+ parent, ok := f.dirCache.GetInv(pathID)
+ if !ok {
+ return "", false, fmt.Errorf("Couldn't find parent ID")
+ }
+ path := leaf
+ if parent != "" {
+ path = parent + "/" + path
+ }
+ if f.dirCache.FoundRoot() {
+ path = f.rootSlash() + path
+ }
+ info, resp, err := f.readMetaDataForPath(path)
+ if err != nil {
+ if resp != nil && resp.StatusCode == http.StatusNotFound {
+ return "", false, nil
+ }
+ return "", false, err
+ }
+ if info.Folder == nil {
+ return "", false, fmt.Errorf("Found file when looking for folder")
+ }
+ return info.ID, true, nil
+}
+
+// CreateDir makes a directory with pathID as parent and name leaf
+func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
+ // fs.Debug(f, "CreateDir(%q, %q)\n", pathID, leaf)
+ var resp *http.Response
+ var info *api.Item
+ opts := api.Opts{
+ Method: "POST",
+ Path: "/drive/items/" + pathID + "/children",
+ }
+ mkdir := api.CreateItemRequest{
+ Name: leaf,
+ ConflictBehavior: "fail",
+ }
+ err = f.pacer.Call(func() (bool, error) {
+ resp, err = f.srv.CallJSON(&opts, &mkdir, &info)
+ return shouldRetry(resp, err)
+ })
+ if err != nil {
+ //fmt.Printf("...Error %v\n", err)
+ return "", err
+ }
+ //fmt.Printf("...Id %q\n", *info.Id)
+ return info.ID, nil
+}
+
+// list the objects into the function supplied
+//
+// If directories is set it only sends directories
+// User function to process a File item from listAll
+//
+// Should return true to finish processing
+type listAllFn func(*api.Item) bool
+
+// Lists the directory required calling the user function on each item found
+//
+// If the user fn ever returns true then it early exits with found = true
+func (f *Fs) listAll(dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
+ // Top parameter asks for bigger pages of data
+ // https://dev.onedrive.com/odata/optional-query-parameters.htm
+ opts := api.Opts{
+ Method: "GET",
+ Path: "/drive/items/" + dirID + "/children?top=1000",
+ }
+OUTER:
+ for {
+ var result api.ListChildrenResponse
+ var resp *http.Response
+ err = f.pacer.Call(func() (bool, error) {
+ resp, err = f.srv.CallJSON(&opts, nil, &result)
+ return shouldRetry(resp, err)
+ })
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(f, "Couldn't list files: %v", err)
+ break
+ }
+ if len(result.Value) == 0 {
+ break
+ }
+ for i := range result.Value {
+ item := &result.Value[i]
+ isFolder := item.Folder != nil
+ if isFolder {
+ if filesOnly {
+ continue
+ }
+ } else {
+ if directoriesOnly {
+ continue
+ }
+ }
+ if item.Deleted != nil {
+ continue
+ }
+ item.Name = restoreReservedChars(item.Name)
+ if fn(item) {
+ found = true
+ break OUTER
+ }
+ }
+ if result.NextLink == "" {
+ break
+ }
+ opts.Path = result.NextLink
+ opts.Absolute = true
+ }
+ return
+}
+
+// Path should be directory path either "" or "path/"
+//
+// List the directory using a recursive list from the root
+//
+// This fetches the minimum amount of stuff but does more API calls
+// which makes it slow
+func (f *Fs) listDirRecursive(dirID string, path string, out fs.ObjectsChan) error {
+ var subError error
+ // Make the API request
+ var wg sync.WaitGroup
+ _, err := f.listAll(dirID, false, false, func(info *api.Item) bool {
+ // Recurse on directories
+ if info.Folder != nil {
+ wg.Add(1)
+ folder := path + info.Name + "/"
+ fs.Debug(f, "Reading %s", folder)
+ go func() {
+ defer wg.Done()
+ err := f.listDirRecursive(info.ID, folder, out)
+ if err != nil {
+ subError = err
+ fs.ErrorLog(f, "Error reading %s:%s", folder, err)
+ }
+ }()
+ } else {
+ if fs := f.newObjectWithInfo(path+info.Name, info); fs != nil {
+ out <- fs
+ }
+ }
+ return false
+ })
+ wg.Wait()
+ fs.Debug(f, "Finished reading %s", path)
+ if err != nil {
+ return err
+ }
+ if subError != nil {
+ return subError
+ }
+ return nil
+}
+
+// List walks the path returning a channel of Objects
+func (f *Fs) List() fs.ObjectsChan {
+ out := make(fs.ObjectsChan, fs.Config.Checkers)
+ go func() {
+ defer close(out)
+ err := f.dirCache.FindRoot(false)
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(f, "Couldn't find root: %s", err)
+ } else {
+ err = f.listDirRecursive(f.dirCache.RootID(), "", out)
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(f, "List failed: %s", err)
+ }
+ }
+ }()
+ return out
+}
+
+// ListDir lists the directories
+func (f *Fs) ListDir() fs.DirChan {
+ out := make(fs.DirChan, fs.Config.Checkers)
+ go func() {
+ defer close(out)
+ err := f.dirCache.FindRoot(false)
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(f, "Couldn't find root: %s", err)
+ } else {
+ _, err := f.listAll(f.dirCache.RootID(), true, false, func(item *api.Item) bool {
+ dir := &fs.Dir{
+ Name: item.Name,
+ Bytes: -1,
+ Count: -1,
+ When: time.Time(item.LastModifiedDateTime),
+ }
+ if item.Folder != nil {
+ dir.Count = item.Folder.ChildCount
+ }
+ out <- dir
+ return false
+ })
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(f, "ListDir failed: %s", err)
+ }
+ }
+ }()
+ return out
+}
+
+// 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)
+ if err != nil {
+ return nil, err
+ }
+ // Temporary Object under construction
+ o := &Object{
+ fs: f,
+ remote: remote,
+ }
+ return o, o.Update(in, modTime, size)
+}
+
+// Mkdir creates the container if it doesn't exist
+func (f *Fs) Mkdir() error {
+ return f.dirCache.FindRoot(true)
+}
+
+// deleteObject removes an object by ID
+func (f *Fs) deleteObject(id string) error {
+ opts := api.Opts{
+ Method: "DELETE",
+ Path: "/drive/items/" + id,
+ NoResponse: true,
+ }
+ return f.pacer.Call(func() (bool, error) {
+ resp, err := f.srv.Call(&opts)
+ return shouldRetry(resp, err)
+ })
+}
+
+// purgeCheck removes the root directory, if check is set then it
+// refuses to do so if it has anything in
+func (f *Fs) purgeCheck(check bool) error {
+ if f.root == "" {
+ return fmt.Errorf("Can't purge root directory")
+ }
+ dc := f.dirCache
+ err := dc.FindRoot(false)
+ if err != nil {
+ return err
+ }
+ rootID := dc.RootID()
+ item, _, err := f.readMetaDataForPath(f.root)
+ if err != nil {
+ return err
+ }
+ if item.Folder == nil {
+ return fmt.Errorf("Not a folder")
+ }
+ if check && item.Folder.ChildCount != 0 {
+ return fmt.Errorf("Folder not empty")
+ }
+ err = f.deleteObject(rootID)
+ if err != nil {
+ return err
+ }
+ f.dirCache.ResetRoot()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Rmdir deletes the root folder
+//
+// Returns an error if it isn't empty
+func (f *Fs) Rmdir() error {
+ return f.purgeCheck(true)
+}
+
+// Precision return the precision of this Fs
+func (f *Fs) Precision() time.Duration {
+ return time.Second
+}
+
+// Copy src to this remote using server side copy operations.
+//
+// This is stored with the remote path given
+//
+// It returns the destination Object and a possible error
+//
+// 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
+//}
+
+// Purge deletes all the files and the container
+//
+// Optional interface: Only implement this if you have a way of
+// deleting all the files quicker than just running Remove() on the
+// result of List()
+func (f *Fs) Purge() error {
+ return f.purgeCheck(false)
+}
+
+// ------------------------------------------------------------
+
+// Fs returns the parent Fs
+func (o *Object) Fs() fs.Fs {
+ return o.fs
+}
+
+// Return a string version
+func (o *Object) String() string {
+ if o == nil {
+ return ""
+ }
+ return o.remote
+}
+
+// Remote returns the remote path
+func (o *Object) Remote() string {
+ return o.remote
+}
+
+// srvPath returns a path for use in server
+func (o *Object) srvPath() string {
+ return replaceReservedChars(o.fs.rootSlash() + o.remote)
+}
+
+// Md5sum returns the Md5sum of an object returning a lowercase hex string
+func (o *Object) Md5sum() (string, error) {
+ return "", nil // not supported by one drive
+}
+
+// Size returns the size of an object in bytes
+func (o *Object) Size() int64 {
+ err := o.readMetaData()
+ if err != nil {
+ fs.Log(o, "Failed to read metadata: %s", err)
+ return 0
+ }
+ return o.size
+}
+
+// setMetaData sets the metadata from info
+func (o *Object) setMetaData(info *api.Item) {
+ o.hasMetaData = true
+ o.size = info.Size
+ if info.FileSystemInfo != nil {
+ o.modTime = time.Time(info.FileSystemInfo.LastModifiedDateTime)
+ } else {
+ o.modTime = time.Time(info.LastModifiedDateTime)
+ }
+ o.id = info.ID
+}
+
+// readMetaData gets the metadata if it hasn't already been fetched
+//
+// it also sets the info
+func (o *Object) readMetaData() (err error) {
+ if o.hasMetaData {
+ return nil
+ }
+ // leaf, directoryID, err := o.fs.dirCache.FindPath(o.remote, false)
+ // if err != nil {
+ // return err
+ // }
+ info, _, err := o.fs.readMetaDataForPath(o.srvPath())
+ if err != nil {
+ fs.Debug(o, "Failed to read info: %s", err)
+ return err
+ }
+ o.setMetaData(info)
+ return nil
+}
+
+// ModTime returns the modification time of the object
+//
+//
+// It attempts to read the objects mtime and if that isn't present the
+// LastModified returned in the http headers
+func (o *Object) ModTime() time.Time {
+ err := o.readMetaData()
+ if err != nil {
+ fs.Log(o, "Failed to read metadata: %s", err)
+ return time.Now()
+ }
+ return o.modTime
+}
+
+// setModTime sets the modification time of the local fs object
+func (o *Object) setModTime(modTime time.Time) (*api.Item, error) {
+ opts := api.Opts{
+ Method: "PATCH",
+ Path: "/drive/root:/" + o.srvPath(),
+ }
+ update := api.SetFileSystemInfo{
+ FileSystemInfo: api.FileSystemInfoFacet{
+ CreatedDateTime: api.Timestamp(modTime),
+ LastModifiedDateTime: api.Timestamp(modTime),
+ },
+ }
+ var info *api.Item
+ err := o.fs.pacer.Call(func() (bool, error) {
+ resp, err := o.fs.srv.CallJSON(&opts, &update, &info)
+ return shouldRetry(resp, err)
+ })
+ return info, err
+}
+
+// SetModTime sets the modification time of the local fs object
+func (o *Object) SetModTime(modTime time.Time) {
+ info, err := o.setModTime(modTime)
+ if err != nil {
+ fs.Stats.Error()
+ fs.ErrorLog(o, "Failed to update remote mtime: %v", err)
+ }
+ o.setMetaData(info)
+}
+
+// Storable returns a boolean showing whether this object storable
+func (o *Object) Storable() bool {
+ return true
+}
+
+// Open an object for read
+func (o *Object) Open() (in io.ReadCloser, err error) {
+ if o.id == "" {
+ return nil, fmt.Errorf("Can't download no id")
+ }
+ var resp *http.Response
+ opts := api.Opts{
+ Method: "GET",
+ Path: "/drive/items/" + o.id + "/content",
+ }
+ err = o.fs.pacer.Call(func() (bool, error) {
+ resp, err = o.fs.srv.Call(&opts)
+ return shouldRetry(resp, err)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return resp.Body, err
+}
+
+// createUploadSession creates an upload session for the object
+func (o *Object) createUploadSession() (response *api.CreateUploadResponse, err error) {
+ opts := api.Opts{
+ Method: "POST",
+ Path: "/drive/root:/" + o.srvPath() + ":/upload.createSession",
+ }
+ var resp *http.Response
+ err = o.fs.pacer.Call(func() (bool, error) {
+ resp, err = o.fs.srv.CallJSON(&opts, nil, &response)
+ return shouldRetry(resp, err)
+ })
+ return
+}
+
+// uploadFragment uploads a part
+func (o *Object) uploadFragment(url string, start int64, totalSize int64, buf []byte) (err error) {
+ bufSize := int64(len(buf))
+ opts := api.Opts{
+ Method: "PUT",
+ Path: url,
+ Absolute: true,
+ ContentLength: &bufSize,
+ ContentRange: fmt.Sprintf("bytes %d-%d/%d", start, start+bufSize-1, totalSize),
+ Body: bytes.NewReader(buf),
+ }
+ var response api.UploadFragmentResponse
+ var resp *http.Response
+ err = o.fs.pacer.Call(func() (bool, error) {
+ resp, err = o.fs.srv.CallJSON(&opts, nil, &response)
+ return shouldRetry(resp, err)
+ })
+ return err
+}
+
+// cancelUploadSession cancels an upload session
+func (o *Object) cancelUploadSession(url string) (err error) {
+ opts := api.Opts{
+ Method: "DELETE",
+ Path: url,
+ Absolute: true,
+ NoResponse: true,
+ }
+ var resp *http.Response
+ err = o.fs.pacer.Call(func() (bool, error) {
+ resp, err = o.fs.srv.Call(&opts)
+ return shouldRetry(resp, err)
+ })
+ return
+}
+
+// uploadMultipart uploads a file using multipart upload
+func (o *Object) uploadMultipart(in io.Reader, size int64) (err error) {
+ if chunkSize%(320*1024) != 0 {
+ return fmt.Errorf("Chunk size %d is not a multiple of 320k", chunkSize)
+ }
+
+ // Create upload session
+ fs.Debug(o, "Starting multipart upload")
+ session, err := o.createUploadSession()
+ if err != nil {
+ return err
+ }
+ uploadURL := session.UploadURL
+
+ // Cancel the session if something went wrong
+ defer func() {
+ if err != nil {
+ fs.Debug(o, "Cancelling multipart upload")
+ cancelErr := o.cancelUploadSession(uploadURL)
+ if cancelErr != nil {
+ fs.Log(o, "Failed to cancel multipart upload: %v", err)
+ }
+ }
+ }()
+
+ // Upload the chunks
+ remaining := size
+ position := int64(0)
+ buf := make([]byte, int64(chunkSize))
+ for remaining > 0 {
+ n := int64(chunkSize)
+ if remaining < n {
+ n = remaining
+ buf = buf[:n]
+ }
+ _, err = io.ReadFull(in, buf)
+ if err != nil {
+ return err
+ }
+ fs.Debug(o, "Uploading segment %d/%d size %d", position, size, n)
+ err = o.uploadFragment(uploadURL, position, size, buf)
+ if err != nil {
+ return err
+ }
+ remaining -= n
+ position += n
+ }
+
+ return nil
+}
+
+// Update the object with the contents of the io.Reader, modTime and size
+//
+// The new object may have been created if an error is returned
+func (o *Object) Update(in io.Reader, modTime time.Time, size int64) (err error) {
+ var info *api.Item
+ if size <= int64(uploadCutoff) {
+ // This is for less than 100 MB of content
+ var resp *http.Response
+ opts := api.Opts{
+ Method: "PUT",
+ Path: "/drive/root:/" + o.srvPath() + ":/content",
+ Body: in,
+ }
+ err = o.fs.pacer.CallNoRetry(func() (bool, error) {
+ resp, err = o.fs.srv.CallJSON(&opts, nil, &info)
+ return shouldRetry(resp, err)
+ })
+ if err != nil {
+ return err
+ }
+ o.setMetaData(info)
+ } else {
+ err = o.uploadMultipart(in, size)
+ if err != nil {
+ return err
+ }
+ }
+ // Set the mod time now and read metadata
+ info, err = o.setModTime(modTime)
+ if err != nil {
+ return err
+ }
+ o.setMetaData(info)
+ return nil
+}
+
+// Remove an object
+func (o *Object) Remove() error {
+ return o.fs.deleteObject(o.id)
+}
+
+// Check the interfaces are satisfied
+var (
+ _ fs.Fs = (*Fs)(nil)
+ _ fs.Purger = (*Fs)(nil)
+ // _ fs.Copier = (*Fs)(nil)
+ // _ fs.Mover = (*Fs)(nil)
+ // _ fs.DirMover = (*Fs)(nil)
+ _ fs.Object = (*Object)(nil)
+)
diff --git a/onedrive/onedrive_test.go b/onedrive/onedrive_test.go
new file mode 100644
index 000000000..392997367
--- /dev/null
+++ b/onedrive/onedrive_test.go
@@ -0,0 +1,56 @@
+// Test OneDrive filesystem interface
+//
+// Automatically generated - DO NOT EDIT
+// Regenerate with: make gen_tests
+package onedrive_test
+
+import (
+ "testing"
+
+ "github.com/ncw/rclone/fs"
+ "github.com/ncw/rclone/fstest/fstests"
+ "github.com/ncw/rclone/onedrive"
+)
+
+func init() {
+ fstests.NilObject = fs.Object((*onedrive.Object)(nil))
+ fstests.RemoteName = "TestOneDrive:"
+}
+
+// Generic tests for the Fs
+func TestInit(t *testing.T) { fstests.TestInit(t) }
+func TestFsString(t *testing.T) { fstests.TestFsString(t) }
+func TestFsRmdirEmpty(t *testing.T) { fstests.TestFsRmdirEmpty(t) }
+func TestFsRmdirNotFound(t *testing.T) { fstests.TestFsRmdirNotFound(t) }
+func TestFsMkdir(t *testing.T) { fstests.TestFsMkdir(t) }
+func TestFsListEmpty(t *testing.T) { fstests.TestFsListEmpty(t) }
+func TestFsListDirEmpty(t *testing.T) { fstests.TestFsListDirEmpty(t) }
+func TestFsNewFsObjectNotFound(t *testing.T) { fstests.TestFsNewFsObjectNotFound(t) }
+func TestFsPutFile1(t *testing.T) { fstests.TestFsPutFile1(t) }
+func TestFsPutFile2(t *testing.T) { fstests.TestFsPutFile2(t) }
+func TestFsListDirFile2(t *testing.T) { fstests.TestFsListDirFile2(t) }
+func TestFsListDirRoot(t *testing.T) { fstests.TestFsListDirRoot(t) }
+func TestFsListRoot(t *testing.T) { fstests.TestFsListRoot(t) }
+func TestFsListFile1(t *testing.T) { fstests.TestFsListFile1(t) }
+func TestFsNewFsObject(t *testing.T) { fstests.TestFsNewFsObject(t) }
+func TestFsListFile1and2(t *testing.T) { fstests.TestFsListFile1and2(t) }
+func TestFsCopy(t *testing.T) { fstests.TestFsCopy(t) }
+func TestFsMove(t *testing.T) { fstests.TestFsMove(t) }
+func TestFsDirMove(t *testing.T) { fstests.TestFsDirMove(t) }
+func TestFsRmdirFull(t *testing.T) { fstests.TestFsRmdirFull(t) }
+func TestFsPrecision(t *testing.T) { fstests.TestFsPrecision(t) }
+func TestObjectString(t *testing.T) { fstests.TestObjectString(t) }
+func TestObjectFs(t *testing.T) { fstests.TestObjectFs(t) }
+func TestObjectRemote(t *testing.T) { fstests.TestObjectRemote(t) }
+func TestObjectMd5sum(t *testing.T) { fstests.TestObjectMd5sum(t) }
+func TestObjectModTime(t *testing.T) { fstests.TestObjectModTime(t) }
+func TestObjectSetModTime(t *testing.T) { fstests.TestObjectSetModTime(t) }
+func TestObjectSize(t *testing.T) { fstests.TestObjectSize(t) }
+func TestObjectOpen(t *testing.T) { fstests.TestObjectOpen(t) }
+func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) }
+func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) }
+func TestLimitedFs(t *testing.T) { fstests.TestLimitedFs(t) }
+func TestLimitedFsNotFound(t *testing.T) { fstests.TestLimitedFsNotFound(t) }
+func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) }
+func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) }
+func TestFinalise(t *testing.T) { fstests.TestFinalise(t) }
diff --git a/onedrive/replace.go b/onedrive/replace.go
new file mode 100644
index 000000000..1d38d56df
--- /dev/null
+++ b/onedrive/replace.go
@@ -0,0 +1,91 @@
+/*
+Translate file names for one drive
+
+OneDrive reserved characters
+
+The following characters are OneDrive reserved characters, and can't
+be used in OneDrive folder and file names.
+
+ onedrive-reserved = "/" / "\" / "*" / "<" / ">" / "?" / ":" / "|"
+ onedrive-business-reserved
+ = "/" / "\" / "*" / "<" / ">" / "?" / ":" / "|" / "#" / "%"
+
+Note: Folder names can't end with a period (.).
+
+Note: OneDrive for Business file or folder names cannot begin with a
+tilde ('~').
+
+*/
+
+package onedrive
+
+import (
+ "regexp"
+ "strings"
+)
+
+// charMap holds replacements for characters
+//
+// Onedrive has a restricted set of characters compared to other cloud
+// storage systems, so we to map these to the FULLWIDTH unicode
+// equivalents
+//
+// http://unicode-search.net/unicode-namesearch.pl?term=SOLIDUS
+var (
+ charMap = map[rune]rune{
+ '\\': '\', // FULLWIDTH REVERSE SOLIDUS
+ '*': '*', // FULLWIDTH ASTERISK
+ '<': '<', // FULLWIDTH LESS-THAN SIGN
+ '>': '>', // FULLWIDTH GREATER-THAN SIGN
+ '?': '?', // FULLWIDTH QUESTION MARK
+ ':': ':', // FULLWIDTH COLON
+ '|': '|', // FULLWIDTH VERTICAL LINE
+ '#': '#', // FULLWIDTH NUMBER SIGN
+ '%': '%', // FULLWIDTH PERCENT SIGN
+ '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
+ '.': '.', // FULLWIDTH FULL STOP
+ '~': '~', // FULLWIDTH TILDE
+ ' ': '␠', // SYMBOL FOR SPACE
+ }
+ invCharMap map[rune]rune
+ fixEndingInPeriod = regexp.MustCompile(`\.(/|$)`)
+ fixStartingWithTilde = regexp.MustCompile(`(/|^)~`)
+ fixStartingWithSpace = regexp.MustCompile(`(/|^) `)
+)
+
+func init() {
+ // Create inverse charMap
+ invCharMap = make(map[rune]rune, len(charMap))
+ for k, v := range charMap {
+ invCharMap[v] = k
+ }
+}
+
+// replaceReservedChars takes a path and substitutes any reserved
+// characters in it
+func replaceReservedChars(in string) string {
+ // Folder names can't end with a period '.'
+ in = fixEndingInPeriod.ReplaceAllString(in, string(charMap['.'])+"$1")
+ // OneDrive for Business file or folder names cannot begin with a tilde '~'
+ in = fixStartingWithTilde.ReplaceAllString(in, "$1"+string(charMap['~']))
+ // Apparently file names can't start with space either
+ in = fixStartingWithSpace.ReplaceAllString(in, "$1"+string(charMap[' ']))
+ // Replace reserved characters
+ return strings.Map(func(c rune) rune {
+ if replacement, ok := charMap[c]; ok && c != '.' && c != '~' && c != ' ' {
+ return replacement
+ }
+ return c
+ }, in)
+}
+
+// restoreReservedChars takes a path and undoes any substitutions
+// made by replaceReservedChars
+func restoreReservedChars(in string) string {
+ return strings.Map(func(c rune) rune {
+ if replacement, ok := invCharMap[c]; ok {
+ return replacement
+ }
+ return c
+ }, in)
+}
diff --git a/onedrive/replace_test.go b/onedrive/replace_test.go
new file mode 100644
index 000000000..bac8a590e
--- /dev/null
+++ b/onedrive/replace_test.go
@@ -0,0 +1,30 @@
+package onedrive
+
+import "testing"
+
+func TestReplace(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ out string
+ }{
+ {"", ""},
+ {"abc 123", "abc 123"},
+ {`\*<>?:|#%".~`, `\*<>?:|#%".~`},
+ {`\*<>?:|#%".~/\*<>?:|#%".~`, `\*<>?:|#%".~/\*<>?:|#%".~`},
+ {" leading space", "␠leading space"},
+ {"~leading tilde", "~leading tilde"},
+ {"trailing dot.", "trailing dot."},
+ {" leading space/ leading space/ leading space", "␠leading space/␠leading space/␠leading space"},
+ {"~leading tilde/~leading tilde/~leading tilde", "~leading tilde/~leading tilde/~leading tilde"},
+ {"trailing dot./trailing dot./trailing dot.", "trailing dot./trailing dot./trailing dot."},
+ } {
+ got := replaceReservedChars(test.in)
+ if got != test.out {
+ t.Errorf("replaceReservedChars(%q) want %q got %q", test.in, test.out, got)
+ }
+ got2 := restoreReservedChars(got)
+ if got2 != test.in {
+ t.Errorf("restoreReservedChars(%q) want %q got %q", got, test.in, got2)
+ }
+ }
+}
diff --git a/rclone.go b/rclone.go
index c241227f0..ae6784020 100644
--- a/rclone.go
+++ b/rclone.go
@@ -21,6 +21,7 @@ import (
_ "github.com/ncw/rclone/dropbox"
_ "github.com/ncw/rclone/googlecloudstorage"
_ "github.com/ncw/rclone/local"
+ _ "github.com/ncw/rclone/onedrive"
_ "github.com/ncw/rclone/s3"
_ "github.com/ncw/rclone/swift"
)