<commit_before>// Package onedrive provides an interface to the Microsoft OneDrive
// object storage system.
package onedrive

import (
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"path"
	"strings"
	"time"

	"github.com/ncw/rclone/lib/atexit"

	"github.com/ncw/rclone/backend/onedrive/api"
	"github.com/ncw/rclone/fs"
	"github.com/ncw/rclone/fs/config"
	"github.com/ncw/rclone/fs/config/configmap"
	"github.com/ncw/rclone/fs/config/configstruct"
	"github.com/ncw/rclone/fs/config/obscure"
	"github.com/ncw/rclone/fs/fserrors"
	"github.com/ncw/rclone/fs/hash"
	"github.com/ncw/rclone/lib/dircache"
	"github.com/ncw/rclone/lib/oauthutil"
	"github.com/ncw/rclone/lib/pacer"
	"github.com/ncw/rclone/lib/readers"
	"github.com/ncw/rclone/lib/rest"
	"github.com/pkg/errors"
	"golang.org/x/oauth2"
)

const (
	rcloneClientID              = "b15665d9-eda6-4092-8539-0eec376afd59"
	rcloneEncryptedClientSecret = <KEY>"
	minSleep                    = 10 * time.Millisecond
	maxSleep                    = 2 * time.Second
	decayConstant               = 2 // bigger for slower decay, exponential
	graphURL                    = "https://graph.microsoft.com/v1.0"
	configDriveID               = "drive_id"
	configDriveType             = "drive_type"
	driveTypePersonal           = "personal"
	driveTypeBusiness           = "business"
	driveTypeSharepoint         = "documentLibrary"
	defaultChunkSize            = 10 * fs.MebiByte
	chunkSizeMultiple           = 320 * fs.KibiByte
)

// Globals
var (
	// Description of how to auth for this app for a business account
	oauthConfig = &oauth2.Config{
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
			TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
		},
		Scopes:       []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"},
		ClientID:     rcloneClientID,
		ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
		RedirectURL:  oauthutil.RedirectLocalhostURL,
	}
)

// Register with Fs
func init() {
	fs.Register(&fs.RegInfo{
		Name:        "onedrive",
		Description: "Microsoft OneDrive",
		NewFs:       NewFs,
		Config: func(name string, m configmap.Mapper) {
			err := oauthutil.Config("onedrive", name, m, oauthConfig)
			if err != nil {
				log.Fatalf("Failed to configure token: %v", err)
				return
			}

			// Stop if we are running non-interactive config
			if fs.Config.AutoConfirm {
				return
			}

			type driveResource struct {
				DriveID   string `json:"id"`
				DriveName string `json:"name"`
				DriveType string `json:"driveType"`
			}
			type drivesResponse struct {
				Drives []driveResource `json:"value"`
			}

			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 {
				log.Fatalf("Failed to configure OneDrive: %v", err)
			}
			srv := rest.NewClient(oAuthClient)

			var opts rest.Opts
			var finalDriveID string
			var siteID string
			switch config.Choose("Your choice",
				[]string{"onedrive", "sharepoint", "driveid", "siteid", "search"},
				[]string{"OneDrive Personal or Business", "Root Sharepoint site", "Type in driveID", "Type in SiteID", "Search a Sharepoint site"},
				false) {

			case "onedrive":
				opts = rest.Opts{
					Method:  "GET",
					RootURL: graphURL,
					Path:    "/me/drives",
				}
			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,
				}

				sites := siteResponse{}
				_, err := srv.CallJSON(&opts, nil, &sites)
				if err != nil {
					log.Fatalf("Failed to query available sites: %v", err)
				}

				if len(sites.Sites) == 0 {
					log.Fatalf("Search for '%s' returned no results", searchTerm)
				} 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
				}
			}

			// if we have a siteID we need to ask for the drives
			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{}
				_, err := srv.CallJSON(&opts, nil, &drives)
				if err != nil {
					log.Fatalf("Failed to query available drives: %v", err)
				}

				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
			_, err = srv.CallJSON(&opts, nil, &rootItem)
			if err != nil {
				log.Fatalf("Failed to query root for drive %s: %v", finalDriveID, err)
			}

			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.ConfirmWithConfig(m, "config_drive_ok", true) {
				log.Fatalf("Cancelled by user")
			}

			m.Set(configDriveID, finalDriveID)
			m.Set(configDriveType, rootItem.ParentReference.DriveType)
			config.SaveConfig()
		},
		Options: []fs.Option{{
			Name: config.ConfigClientID,
			Help: "Microsoft App Client Id\nLeave blank normally.",
		}, {
			Name: config.ConfigClientSecret,
			Help: "Microsoft App Client Secret\nLeave blank normally.",
		}, {
			Name: "chunk_size",
			Help: `Chunk size to upload files with - must be multiple of 320k.

Above this size files will be chunked - must be multiple of 320k. Note
that the chunks will be buffered into memory.`,
			Default:  defaultChunkSize,
			Advanced: true,
		}, {
			Name:     "drive_id",
			Help:     "The ID of the drive to use",
			Default:  "",
			Advanced: true,
		}, {
			Name:     "drive_type",
			Help:     "The type of the drive ( " + driveTypePersonal + " | " + driveTypeBusiness + " | " + driveTypeSharepoint + " )",
			Default:  "",
			Advanced: true,
		}, {
			Name: "expose_onenote_files",
			Help: `Set to make OneNote files show up in directory listings.

By default rclone will hide OneNote files in directory listings because
operations like "Open" and "Update" won't work on them.  But this
behaviour may also prevent you from deleting them.  If you want to
delete OneNote files or otherwise want them to show up in directory
listing, set this option.`,
			Default:  false,
			Advanced: true,
		}},
	})
}

// Options defines the configuration for this backend
type Options struct {
	ChunkSize          fs.SizeSuffix `config:"chunk_size"`
	DriveID            string        `config:"drive_id"`
	DriveType          string        `config:"drive_type"`
	ExposeOneNoteFiles bool          `config:"expose_onenote_files"`
}

// Fs represents a remote one drive
type Fs struct {
	name         string             // name of this remote
	root         string             // the path we are working on
	opt          Options            // parsed options
	features     *fs.Features       // optional features
	srv          *rest.Client       // the connection to the one drive server
	dirCache     *dircache.DirCache // Map of directory path to directory id
	pacer        *fs.Pacer          // pacer for API calls
	tokenRenewer *oauthutil.Renew   // renew the token on expiry
	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
//
// 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
	isOneNoteFile bool      // Whether the object is a OneNote file
	size          int64     // size of the object
	modTime       time.Time // modification time of the object
	id            string    // ID of the object
	sha1          string    // SHA-1 of the object content
	quickxorhash  string    // QuickXorHash of the object content
	mimeType      string    // Content-Type of object from server (may not be as uploaded)
}

// ------------------------------------------------------------

// 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)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
	return f.features
}

// 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) {
	authRetry := false

	if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 {
		authRetry = true
		fs.Debugf(nil, "Should retry: %v", err)
	}
	return authRetry || fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
}

// readMetaDataForPathRelativeToID reads the metadata for a path relative to an item that is addressed by its normalized ID.
// if `relPath` == "", it reads the metadata for the item with that ID.
//
// We address items using the pattern `drives/driveID/items/itemID:/relativePath`
// instead of simply using `drives/driveID/root:/itemPath` because it works for
// "shared with me" folders in OneDrive Personal (See #2536, #2778)
// This path pattern comes from https://github.com/OneDrive/onedrive-api-docs/issues/908#issuecomment-417488480
func (f *Fs) readMetaDataForPathRelativeToID(normalizedID string, relPath string) (info *api.Item, resp *http.Response, err error) {
	opts := newOptsCall(normalizedID, "GET", ":/"+withTrailingColon(rest.URLPathEscape(replaceReservedChars(relPath))))
	err = f.pacer.Call(func() (bool, error) {
		resp, err = f.srv.CallJSON(&opts, nil, &info)
		return shouldRetry(resp, err)
	})

	return info, resp, err
}

// readMetaDataForPath reads the metadata from the path (relative to the absolute root)
func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
	firstSlashIndex := strings.IndexRune(path, '/')

	if f.driveType != driveTypePersonal || firstSlashIndex == -1 {
		var opts rest.Opts
		if len(path) == 0 {
			opts = rest.Opts{
				Method: "GET",
				Path:   "/root",
			}
		} else {
			opts = rest.Opts{
				Method: "GET",
				Path:   "/root:/" + rest.URLPathEscape(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
	}

	// The following branch handles the case when we're using OneDrive Personal and the path is in a folder.
	// For OneDrive Personal, we need to consider the "shared with me" folders.
	// An item in such a folder can only be addressed by its ID relative to the sharer's driveID or
	// by its path relative to the folder's ID relative to the sharer's driveID.
	// Note: A "shared with me" folder can only be placed in the sharee's absolute root.
	// So we read metadata relative to a suitable folder's normalized ID.
	var dirCacheFoundRoot bool
	var rootNormalizedID string
	if f.dirCache != nil {
		var ok bool
		if rootNormalizedID, ok = f.dirCache.Get(""); ok {
			dirCacheFoundRoot = true
		}
	}

	relPath, insideRoot := getRelativePathInsideBase(f.root, path)
	var firstDir, baseNormalizedID string
	if !insideRoot || !dirCacheFoundRoot {
		// We do not have the normalized ID in dirCache for our query to base on. Query it manually.
		firstDir, relPath = path[:firstSlashIndex], path[firstSlashIndex+1:]
		info, resp, err := f.readMetaDataForPath(firstDir)
		if err != nil {
			return info, resp, err
		}
		baseNormalizedID = info.GetID()
	} else {
		if f.root != "" {
			// Read metadata based on root
			baseNormalizedID = rootNormalizedID
		} else {
			// Read metadata based on firstDir
			firstDir, relPath = path[:firstSlashIndex], path[firstSlashIndex+1:]
			baseNormalizedID, err = f.dirCache.FindDir(firstDir, false)
			if err != nil {
				return nil, nil, err
			}
		}
	}<commit_msg>onedrive: More accurately check if root is found - fixes #3164<commit_after>// Package onedrive provides an interface to the Microsoft OneDrive
// object storage system.
package onedrive

import (
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"path"
	"strings"
	"time"

	"github.com/ncw/rclone/lib/atexit"

	"github.com/ncw/rclone/backend/onedrive/api"
	"github.com/ncw/rclone/fs"
	"github.com/ncw/rclone/fs/config"
	"github.com/ncw/rclone/fs/config/configmap"
	"github.com/ncw/rclone/fs/config/configstruct"
	"github.com/ncw/rclone/fs/config/obscure"
	"github.com/ncw/rclone/fs/fserrors"
	"github.com/ncw/rclone/fs/hash"
	"github.com/ncw/rclone/lib/dircache"
	"github.com/ncw/rclone/lib/oauthutil"
	"github.com/ncw/rclone/lib/pacer"
	"github.com/ncw/rclone/lib/readers"
	"github.com/ncw/rclone/lib/rest"
	"github.com/pkg/errors"
	"golang.org/x/oauth2"
)

const (
	rcloneClientID              = "b<PASSWORD>"
	rcloneEncryptedClientSecret = <KEY>"
	minSleep                    = 10 * time.Millisecond
	maxSleep                    = 2 * time.Second
	decayConstant               = 2 // bigger for slower decay, exponential
	graphURL                    = "https://graph.microsoft.com/v1.0"
	configDriveID               = "drive_id"
	configDriveType             = "drive_type"
	driveTypePersonal           = "personal"
	driveTypeBusiness           = "business"
	driveTypeSharepoint         = "documentLibrary"
	defaultChunkSize            = 10 * fs.MebiByte
	chunkSizeMultiple           = 320 * fs.KibiByte
)

// Globals
var (
	// Description of how to auth for this app for a business account
	oauthConfig = &oauth2.Config{
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
			TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
		},
		Scopes:       []string{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"},
		ClientID:     rcloneClientID,
		ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
		RedirectURL:  oauthutil.RedirectLocalhostURL,
	}
)

// Register with Fs
func init() {
	fs.Register(&fs.RegInfo{
		Name:        "onedrive",
		Description: "Microsoft OneDrive",
		NewFs:       NewFs,
		Config: func(name string, m configmap.Mapper) {
			err := oauthutil.Config("onedrive", name, m, oauthConfig)
			if err != nil {
				log.Fatalf("Failed to configure token: %v", err)
				return
			}

			// Stop if we are running non-interactive config
			if fs.Config.AutoConfirm {
				return
			}

			type driveResource struct {
				DriveID   string `json:"id"`
				DriveName string `json:"name"`
				DriveType string `json:"driveType"`
			}
			type drivesResponse struct {
				Drives []driveResource `json:"value"`
			}

			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 {
				log.Fatalf("Failed to configure OneDrive: %v", err)
			}
			srv := rest.NewClient(oAuthClient)

			var opts rest.Opts
			var finalDriveID string
			var siteID string
			switch config.Choose("Your choice",
				[]string{"onedrive", "sharepoint", "driveid", "siteid", "search"},
				[]string{"OneDrive Personal or Business", "Root Sharepoint site", "Type in driveID", "Type in SiteID", "Search a Sharepoint site"},
				false) {

			case "onedrive":
				opts = rest.Opts{
					Method:  "GET",
					RootURL: graphURL,
					Path:    "/me/drives",
				}
			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,
				}

				sites := siteResponse{}
				_, err := srv.CallJSON(&opts, nil, &sites)
				if err != nil {
					log.Fatalf("Failed to query available sites: %v", err)
				}

				if len(sites.Sites) == 0 {
					log.Fatalf("Search for '%s' returned no results", searchTerm)
				} 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
				}
			}

			// if we have a siteID we need to ask for the drives
			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{}
				_, err := srv.CallJSON(&opts, nil, &drives)
				if err != nil {
					log.Fatalf("Failed to query available drives: %v", err)
				}

				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
			_, err = srv.CallJSON(&opts, nil, &rootItem)
			if err != nil {
				log.Fatalf("Failed to query root for drive %s: %v", finalDriveID, err)
			}

			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.ConfirmWithConfig(m, "config_drive_ok", true) {
				log.Fatalf("Cancelled by user")
			}

			m.Set(configDriveID, finalDriveID)
			m.Set(configDriveType, rootItem.ParentReference.DriveType)
			config.SaveConfig()
		},
		Options: []fs.Option{{
			Name: config.ConfigClientID,
			Help: "Microsoft App Client Id\nLeave blank normally.",
		}, {
			Name: config.ConfigClientSecret,
			Help: "Microsoft App Client Secret\nLeave blank normally.",
		}, {
			Name: "chunk_size",
			Help: `Chunk size to upload files with - must be multiple of 320k.

Above this size files will be chunked - must be multiple of 320k. Note
that the chunks will be buffered into memory.`,
			Default:  defaultChunkSize,
			Advanced: true,
		}, {
			Name:     "drive_id",
			Help:     "The ID of the drive to use",
			Default:  "",
			Advanced: true,
		}, {
			Name:     "drive_type",
			Help:     "The type of the drive ( " + driveTypePersonal + " | " + driveTypeBusiness + " | " + driveTypeSharepoint + " )",
			Default:  "",
			Advanced: true,
		}, {
			Name: "expose_onenote_files",
			Help: `Set to make OneNote files show up in directory listings.

By default rclone will hide OneNote files in directory listings because
operations like "Open" and "Update" won't work on them.  But this
behaviour may also prevent you from deleting them.  If you want to
delete OneNote files or otherwise want them to show up in directory
listing, set this option.`,
			Default:  false,
			Advanced: true,
		}},
	})
}

// Options defines the configuration for this backend
type Options struct {
	ChunkSize          fs.SizeSuffix `config:"chunk_size"`
	DriveID            string        `config:"drive_id"`
	DriveType          string        `config:"drive_type"`
	ExposeOneNoteFiles bool          `config:"expose_onenote_files"`
}

// Fs represents a remote one drive
type Fs struct {
	name         string             // name of this remote
	root         string             // the path we are working on
	opt          Options            // parsed options
	features     *fs.Features       // optional features
	srv          *rest.Client       // the connection to the one drive server
	dirCache     *dircache.DirCache // Map of directory path to directory id
	pacer        *fs.Pacer          // pacer for API calls
	tokenRenewer *oauthutil.Renew   // renew the token on expiry
	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
//
// 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
	isOneNoteFile bool      // Whether the object is a OneNote file
	size          int64     // size of the object
	modTime       time.Time // modification time of the object
	id            string    // ID of the object
	sha1          string    // SHA-1 of the object content
	quickxorhash  string    // QuickXorHash of the object content
	mimeType      string    // Content-Type of object from server (may not be as uploaded)
}

// ------------------------------------------------------------

// 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)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
	return f.features
}

// 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) {
	authRetry := false

	if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 {
		authRetry = true
		fs.Debugf(nil, "Should retry: %v", err)
	}
	return authRetry || fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
}

// readMetaDataForPathRelativeToID reads the metadata for a path relative to an item that is addressed by its normalized ID.
// if `relPath` == "", it reads the metadata for the item with that ID.
//
// We address items using the pattern `drives/driveID/items/itemID:/relativePath`
// instead of simply using `drives/driveID/root:/itemPath` because it works for
// "shared with me" folders in OneDrive Personal (See #2536, #2778)
// This path pattern comes from https://github.com/OneDrive/onedrive-api-docs/issues/908#issuecomment-417488480
func (f *Fs) readMetaDataForPathRelativeToID(normalizedID string, relPath string) (info *api.Item, resp *http.Response, err error) {
	opts := newOptsCall(normalizedID, "GET", ":/"+withTrailingColon(rest.URLPathEscape(replaceReservedChars(relPath))))
	err = f.pacer.Call(func() (bool, error) {
		resp, err = f.srv.CallJSON(&opts, nil, &info)
		return shouldRetry(resp, err)
	})

	return info, resp, err
}

// readMetaDataForPath reads the metadata from the path (relative to the absolute root)
func (f *Fs) readMetaDataForPath(path string) (info *api.Item, resp *http.Response, err error) {
	firstSlashIndex := strings.IndexRune(path, '/')

	if f.driveType != driveTypePersonal || firstSlashIndex == -1 {
		var opts rest.Opts
		if len(path) == 0 {
			opts = rest.Opts{
				Method: "GET",
				Path:   "/root",
			}
		} else {
			opts = rest.Opts{
				Method: "GET",
				Path:   "/root:/" + rest.URLPathEscape(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
	}

	// The following branch handles the case when we're using OneDrive Personal and the path is in a folder.
	// For OneDrive Personal, we need to consider the "shared with me" folders.
	// An item in such a folder can only be addressed by its ID relative to the sharer's driveID or
	// by its path relative to the folder's ID relative to the sharer's driveID.
	// Note: A "shared with me" folder can only be placed in the sharee's absolute root.
	// So we read metadata relative to a suitable folder's normalized ID.
	var dirCacheFoundRoot bool
	var rootNormalizedID string
	if f.dirCache != nil {
		var dirCacheRootIDExists bool
		rootNormalizedID, dirCacheRootIDExists = f.dirCache.Get("")
		if f.root == "" {
			// if f.root == "", it means f.root is the absolute root of the drive
			// and its ID should have been found in NewFs
			dirCacheFoundRoot = dirCacheRootIDExists
		} else if _, err := f.dirCache.RootParentID(); err == nil {
			// if root is in a folder, it must have a parent folder, and
			// if dirCache has found root in NewFs, the parent folder's ID
			// should be present.
			// This RootParentID() check is a fix for #3164 which describes
			// a possible case where the root is not found.
			dirCacheFoundRoot = dirCacheRootIDExists
		}
	}

	relPath, insideRoot := getRelativePathInsideBase(f.root, path)
	var firstDir, baseNormalizedID string
	if !insideRoot || !dirCacheFoundRoot {
		// We do not have the normalized ID in dirCache for our query to base on. Query it manually.
		firstDir, relPath = path[:firstSlashIndex], path[firstSlashIndex+1:]
		info, resp, err := f.readMetaDataForPath(firstDir)
		if err != nil {
			return info, resp, err
		}
		baseNormalizedID = info.GetID()
	} else {
		if f.root != "" {
			// Read metadata based on root
			baseNormalizedID = rootNormalizedID
		} else {
			// Read metadata based on firstDir
			firstDir, relPath = path[:firstSlashIndex], path[firstSlashIndex+1:]
			baseNormalizedID, err = f.dirCache.FindDir(firstDir, false)
			if err != nil {
				return nil, nil, err
			}
		}
	}
{'fraction_non_alphanumeric': 0.12278613930696534, 'fraction_numerical': 0.007175358767938397, 'mean_word_length': 2.9272852233676976, 'pattern_counts': {'":': 12, '<': 6, '<?xml version=': 0, '>': 14, 'https://': 10, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5803515', 'n_tokens_mistral': 10431, 'n_tokens_neox': 9130, 'n_words': 2816}