rclone/backend/iclouddrive/iclouddrive.go

1175 lines
30 KiB
Go
Raw Normal View History

2024-09-28 08:54:05 +02:00
//go:build !plan9 && !solaris
// Package iclouddrive implements the iCloud Drive backend
package iclouddrive
import (
"bytes"
"context"
"path"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/backend/iclouddrive/api"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/pacer"
)
/*
- dirCache operates on relative path to root
- path sanitization
- rule of thumb: sanitize before use, but store things as-is
- the paths cached in dirCache are after sanitizing
- the remote/dir passed in aren't, and are stored as-is
*/
const (
configAppleID = "apple_id"
configPassword = "password"
configClientID = "client_id"
configCookies = "cookies"
configTrustToken = "trust_token"
minSleep = 10 * time.Millisecond
maxSleep = 2 * time.Second
decayConstant = 2
)
// Register with Fs
func init() {
fs.Register(&fs.RegInfo{
Name: "iclouddrive",
Description: "iCloud Drive",
Config: Config,
NewFs: NewFs,
Options: []fs.Option{{
Name: configAppleID,
Help: "Apple ID.",
Required: true,
Sensitive: true,
}, {
Name: configPassword,
Help: "Password.",
Required: true,
IsPassword: true,
Sensitive: true,
}, {
Name: configTrustToken,
Help: "Trust token (internal use)",
IsPassword: false,
Required: false,
Sensitive: true,
Hide: fs.OptionHideBoth,
}, {
Name: configCookies,
Help: "cookies (internal use only)",
Required: false,
Advanced: false,
Sensitive: true,
Hide: fs.OptionHideBoth,
}, {
Name: configClientID,
Help: "Client id",
Required: false,
Advanced: true,
Default: "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d",
}, {
Name: config.ConfigEncoding,
Help: config.ConfigEncodingHelp,
Advanced: true,
Default: (encoder.Display |
//encoder.EncodeDot |
encoder.EncodeBackSlash |
encoder.EncodeInvalidUtf8),
}},
})
}
// Options defines the configuration for this backend
type Options struct {
AppleID string `config:"apple_id"`
Password string `config:"password"`
Photos bool `config:"photos"`
TrustToken string `config:"trust_token"`
Cookies string `config:"cookies"`
ClientID string `config:"client_id"`
Enc encoder.MultiEncoder `config:"encoding"`
}
// Fs represents a remote icloud drive
type Fs struct {
name string // name of this remote
root string // the path we are working on.
rootID string
opt Options // parsed config options
features *fs.Features // optional features
dirCache *dircache.DirCache // Map of directory path to directory id
icloud *api.Client
service *api.DriveService
pacer *fs.Pacer // pacer for API calls
}
// Object describes an icloud drive object
type Object struct {
fs *Fs // what this object is part of
remote string // The remote path (relative to the fs.root)
size int64 // size of the object (on server, after encryption)
modTime time.Time // modification time of the object
createdTime time.Time // creation time of the object
driveID string // item ID of the object
docID string // document ID of the object
itemID string // item ID of the object
etag string
downloadURL string
}
// Config configures the iCloud remote.
func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
var err error
appleid, _ := m.Get(configAppleID)
if appleid == "" {
return nil, errors.New("a apple ID is required")
}
password, _ := m.Get(configPassword)
if password != "" {
password, err = obscure.Reveal(password)
if err != nil {
return nil, err
}
}
trustToken, _ := m.Get(configTrustToken)
cookieRaw, _ := m.Get(configCookies)
clientID, _ := m.Get(configClientID)
cookies := ReadCookies(cookieRaw)
switch config.State {
case "":
icloud, err := api.New(appleid, password, trustToken, clientID, cookies, nil)
if err != nil {
return nil, err
}
if err := icloud.Authenticate(ctx); err != nil {
return nil, err
}
m.Set(configCookies, icloud.Session.GetCookieString())
if icloud.Session.Requires2FA() {
return fs.ConfigInput("2fa_do", "config_2fa", "Two-factor authentication: please enter your 2FA code")
}
return nil, nil
case "2fa_do":
code := config.Result
if code == "" {
return fs.ConfigError("authenticate", "2FA codes can't be blank")
}
icloud, err := api.New(appleid, password, trustToken, clientID, cookies, nil)
if err != nil {
return nil, err
}
if err := icloud.SignIn(ctx); err != nil {
return nil, err
}
if err := icloud.Session.Validate2FACode(ctx, code); err != nil {
return nil, err
}
m.Set(configTrustToken, icloud.Session.TrustToken)
m.Set(configCookies, icloud.Session.GetCookieString())
return nil, nil
case "2fa_error":
if config.Result == "true" {
return fs.ConfigGoto("2fa")
}
return nil, errors.New("2fa authentication failed")
}
return nil, fmt.Errorf("unknown state %q", config.State)
}
// find item by path. Will not return any children for the item
func (f *Fs) findItem(ctx context.Context, dir string) (item *api.DriveItem, found bool, err error) {
var resp *http.Response
if err = f.pacer.Call(func() (bool, error) {
item, resp, err = f.service.GetItemByPath(ctx, path.Join(f.root, dir))
return shouldRetry(ctx, resp, err)
}); err != nil {
if item == nil && resp.StatusCode == 404 {
return nil, false, nil
}
return nil, false, err
}
return item, true, nil
}
func (f *Fs) findLeafItem(ctx context.Context, pathID string, leaf string) (item *api.DriveItem, found bool, err error) {
items, err := f.listAll(ctx, pathID)
if err != nil {
return nil, false, err
}
for _, item := range items {
if strings.EqualFold(item.FullName(), leaf) {
return item, true, nil
}
}
return nil, false, nil
}
// FindLeaf finds a directory of name leaf in the folder with ID pathID
func (f *Fs) FindLeaf(ctx context.Context, pathID string, leaf string) (pathIDOut string, found bool, err error) {
item, found, err := f.findLeafItem(ctx, pathID, leaf)
if err != nil {
return "", found, err
}
if !found {
return "", false, err
}
if !item.IsFolder() {
return "", false, fs.ErrorIsFile
}
return f.IDJoin(item.Drivewsid, item.Etag), true, nil
}
// Features implements fs.Fs.
func (f *Fs) Features() *fs.Features {
return f.features
}
// Hashes are not exposed anywhere
func (f *Fs) Hashes() hash.Set {
return hash.Set(hash.None)
}
func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
root := path.Join(f.root, dir)
if root == "" {
return errors.New("can't purge root directory")
}
directoryID, etag, err := f.FindDir(ctx, dir, false)
if err != nil {
return err
}
if check {
item, found, err := f.findItem(ctx, dir)
if err != nil {
return err
}
if found && item.DirectChildrenCount > 0 {
return fs.ErrorDirectoryNotEmpty
}
}
var _ *api.DriveItem
var resp *http.Response
if err = f.pacer.Call(func() (bool, error) {
_, resp, err = f.service.MoveItemToTrashByID(ctx, directoryID, etag, true)
return retryResultUnknown(ctx, resp, err)
}); err != nil {
return err
}
// flush everything from the left of the dir
f.dirCache.FlushDir(dir)
return nil
}
// Purge all files in the directory specified
//
// Implement this if you have a way of deleting all the files
// quicker than just running Remove() on the result of List()
//
// Return an error if it doesn't exist
func (f *Fs) Purge(ctx context.Context, dir string) error {
if dir == "" {
return fs.ErrorCantPurge
}
return f.purgeCheck(ctx, dir, false)
}
func (f *Fs) listAll(ctx context.Context, dirID string) (items []*api.DriveItem, err error) {
var item *api.DriveItem
var resp *http.Response
if err = f.pacer.Call(func() (bool, error) {
id, _ := f.parseNormalizedID(dirID)
item, resp, err = f.service.GetItemByDriveID(ctx, id, true)
return shouldRetry(ctx, resp, err)
}); err != nil {
return nil, err
}
items = item.Items
for i, item := range items {
item.Name = f.opt.Enc.ToStandardName(item.Name)
item.Extension = f.opt.Enc.ToStandardName(item.Extension)
items[i] = item
}
return items, nil
}
// List implements fs.Fs.
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
dirRemoteID, err := f.dirCache.FindDir(ctx, dir, false)
if err != nil {
return nil, err
}
entries = make(fs.DirEntries, 0)
items, err := f.listAll(ctx, dirRemoteID)
if err != nil {
return nil, err
}
for _, item := range items {
id := item.Drivewsid
name := item.FullName()
remote := path.Join(dir, name)
if item.IsFolder() {
jid := f.putFolderCache(id, item.Etag, remote)
d := fs.NewDir(remote, item.DateModified).SetID(jid).SetSize(item.AssetQuota)
entries = append(entries, d)
} else {
o, err := f.NewObjectFromDriveItem(ctx, remote, item)
if err != nil {
return nil, err
}
entries = append(entries, o)
}
}
return entries, nil
}
// Mkdir implements fs.Fs.
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
_, _, err := f.FindDir(ctx, dir, true)
return err
}
// Name implements fs.Fs.
func (f *Fs) Name() string {
return f.name
}
// Precision implements fs.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
//
//nolint:all
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
// ICloud cooy endpoint is broken. Once they fixed it this can be re-enabled.
return nil, fs.ErrorCantCopy
// note: so many calls its only just faster then a reupload for big files.
srcObj, ok := src.(*Object)
if !ok {
fs.Debugf(src, "Can't copy - not same remote type")
return nil, fs.ErrorCantCopy
}
file, pathID, _, err := f.FindPath(ctx, remote, true)
if err != nil {
return nil, err
}
var resp *http.Response
var info *api.DriveItemRaw
// make a copy
if err = f.pacer.Call(func() (bool, error) {
info, resp, err = f.service.CopyDocByItemID(ctx, srcObj.itemID)
return retryResultUnknown(ctx, resp, err)
}); err != nil {
return nil, err
}
// renaming in CopyDocByID endpoint does not work :/ so do it the hard way
// get new document
var doc *api.Document
if err = f.pacer.Call(func() (bool, error) {
doc, resp, err = f.service.GetDocByItemID(ctx, info.ItemID)
return shouldRetry(ctx, resp, err)
}); err != nil {
return nil, err
}
// get parentdrive id
var dirDoc *api.Document
if err = f.pacer.Call(func() (bool, error) {
dirDoc, resp, err = f.service.GetDocByItemID(ctx, pathID)
return shouldRetry(ctx, resp, err)
}); err != nil {
return nil, err
}
// build request
// cant use normal rename as file needs to be "activated" first
r := api.NewUpdateFileInfo()
r.DocumentID = doc.DocumentID
r.Path.Path = file
r.Path.StartingDocumentID = dirDoc.DocumentID
r.Data.Signature = doc.Data.Signature
r.Data.ReferenceSignature = doc.Data.ReferenceSignature
r.Data.WrappingKey = doc.Data.WrappingKey
r.Data.Size = doc.Data.Size
r.Mtime = srcObj.modTime.UnixMilli()
r.Btime = srcObj.modTime.UnixMilli()
var item *api.DriveItem
if err = f.pacer.Call(func() (bool, error) {
item, resp, err = f.service.UpdateFile(ctx, &r)
return retryResultUnknown(ctx, resp, err)
}); err != nil {
return nil, err
}
o, err := f.NewObjectFromDriveItem(ctx, remote, item)
if err != nil {
return nil, err
}
obj := o.(*Object)
// cheat unit tests
obj.modTime = srcObj.modTime
obj.createdTime = srcObj.createdTime
return obj, nil
}
// Put in to the remote path with the modTime given of the given size
//
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
// But for unknown-sized objects (indicated by src.Size() == -1), Put should either
// return an error or upload it properly (rather than e.g. calling panic).
//
// May create the object even if it returns an error - if so
// will return the object and the error, otherwise will return
// nil and the error
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
size := src.Size()
if size < 0 {
return nil, errors.New("file size unknown")
}
existingObj, err := f.NewObject(ctx, src.Remote())
switch err {
case nil:
// object is found
return existingObj, existingObj.Update(ctx, in, src, options...)
case fs.ErrorObjectNotFound:
// object not found, so we need to create it
remote := src.Remote()
size := src.Size()
modTime := src.ModTime(ctx)
obj, err := f.createObject(ctx, remote, modTime, size)
if err != nil {
return nil, err
}
return obj, obj.Update(ctx, in, src, options...)
default:
// real error caught
return nil, err
}
}
// DirCacheFlush resets the directory cache - used in testing as an
// optional interface
func (f *Fs) DirCacheFlush() {
f.dirCache.ResetRoot()
}
// parseNormalizedID parses a normalized ID (may be in the form `driveID#itemID` or just `itemID`)
// and returns itemID, driveID, rootURL.
// Such a normalized ID can come from (*Item).GetID()
//
// Parameters:
// - rid: the normalized ID to be parsed
//
// Returns:
// - id: the itemID extracted from the normalized ID
// - etag: the driveID extracted from the normalized ID, or an empty string if not present
func (f *Fs) parseNormalizedID(rid string) (id string, etag string) {
split := strings.Split(rid, "#")
if len(split) == 1 {
return split[0], ""
}
return split[0], split[1]
}
// FindPath finds the leaf and directoryID from a normalized path
func (f *Fs) FindPath(ctx context.Context, remote string, create bool) (leaf, directoryID, etag string, err error) {
leaf, jDirectoryID, err := f.dirCache.FindPath(ctx, remote, create)
if err != nil {
return "", "", "", err
}
directoryID, etag = f.parseNormalizedID(jDirectoryID)
return leaf, directoryID, etag, nil
}
// FindDir finds the directory passed in returning the directory ID
// starting from pathID
func (f *Fs) FindDir(ctx context.Context, path string, create bool) (pathID string, etag string, err error) {
jDirectoryID, err := f.dirCache.FindDir(ctx, path, create)
if err != nil {
return "", "", err
}
directoryID, etag := f.parseNormalizedID(jDirectoryID)
return directoryID, etag, nil
}
// IDJoin joins the given ID and ETag into a single string with a "#" delimiter.
func (f *Fs) IDJoin(id string, etag string) string {
if strings.Contains(id, "#") {
// already contains an etag, replace
id, _ = f.parseNormalizedID(id)
}
return strings.Join([]string{id, etag}, "#")
}
func (f *Fs) putFolderCache(id, etag, remote string) string {
jid := f.IDJoin(id, etag)
f.dirCache.Put(remote, f.IDJoin(id, etag))
return jid
}
// Rmdir implements fs.Fs.
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
return f.purgeCheck(ctx, dir, true)
}
// Root implements fs.Fs.
func (f *Fs) Root() string {
return f.opt.Enc.ToStandardPath(f.root)
}
// String implements fs.Fs.
func (f *Fs) String() string {
return f.root
}
// CreateDir makes a directory with pathID as parent and name leaf
//
// This should be implemented by the backend and will be called by the
// dircache package when appropriate.
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (string, error) {
var item *api.DriveItem
var err error
var found bool
var resp *http.Response
if err = f.pacer.Call(func() (bool, error) {
id, _ := f.parseNormalizedID(pathID)
item, resp, err = f.service.CreateNewFolderByDriveID(ctx, id, f.opt.Enc.FromStandardName(leaf))
// check if it went oke
if requestError, ok := err.(*api.RequestError); ok {
if requestError.Status == "unknown" {
fs.Debugf(requestError, " checking if dir is created with separate call.")
time.Sleep(1 * time.Second) // sleep to give icloud time to clear up its mind
item, found, err = f.findLeafItem(ctx, pathID, leaf)
if err != nil {
return false, err
}
if !found {
// lets assume it failed and retry
return true, err
}
// success, clear err
err = nil
}
}
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return "", err
}
return f.IDJoin(item.Drivewsid, item.Etag), err
}
// DirMove moves src, srcRemote to this remote at dstRemote
// using server-side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
srcFs, ok := src.(*Fs)
if !ok {
fs.Debugf(srcFs, "Can't move directory - not same remote type")
return fs.ErrorCantDirMove
}
srcID, jsrcDirectoryID, srcLeaf, jdstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
if err != nil {
return err
}
srcDirectoryID, srcEtag := f.parseNormalizedID(jsrcDirectoryID)
dstDirectoryID, _ := f.parseNormalizedID(jdstDirectoryID)
_, err = f.move(ctx, srcID, srcDirectoryID, srcLeaf, srcEtag, dstDirectoryID, dstLeaf)
if err != nil {
return err
}
srcFs.dirCache.FlushDir(srcRemote)
return nil
}
func (f *Fs) move(ctx context.Context, ID, srcDirectoryID, srcLeaf, srcEtag, dstDirectoryID, dstLeaf string) (*api.DriveItem, error) {
var resp *http.Response
var item *api.DriveItem
var err error
// move
if srcDirectoryID != dstDirectoryID {
if err = f.pacer.Call(func() (bool, error) {
id, _ := f.parseNormalizedID(ID)
item, resp, err = f.service.MoveItemByDriveID(ctx, id, srcEtag, dstDirectoryID, true)
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return nil, err
}
ID = item.Drivewsid
srcEtag = item.Etag
}
// rename
if srcLeaf != dstLeaf {
if err = f.pacer.Call(func() (bool, error) {
id, _ := f.parseNormalizedID(ID)
item, resp, err = f.service.RenameItemByDriveID(ctx, id, srcEtag, dstLeaf, true)
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return item, err
}
}
return item, err
}
// Move moves the src object to the specified remote.
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
srcObj, ok := src.(*Object)
if !ok {
fs.Debugf(src, "Can't move - not same remote type")
return nil, fs.ErrorCantMove
}
srcLeaf, srcDirectoryID, _, err := srcObj.fs.FindPath(ctx, srcObj.remote, true)
if err != nil {
return nil, err
}
dstLeaf, dstDirectoryID, _, err := f.FindPath(ctx, remote, true)
if err != nil {
return nil, err
}
item, err := f.move(ctx, srcObj.driveID, srcDirectoryID, srcLeaf, srcObj.etag, dstDirectoryID, dstLeaf)
if err != nil {
return src, err
}
return f.NewObjectFromDriveItem(ctx, remote, item)
}
// Creates from the parameters passed in a half finished Object which
// must have setMetaData called on it
//
// Returns the object, leaf, directoryID and error.
//
// Used to create new objects
func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, err error) {
// Create the directory for the object if it doesn't exist
_, _, _, err = f.FindPath(ctx, remote, true)
if err != nil {
return
}
// Temporary Object under construction
o = &Object{
fs: f,
remote: remote,
modTime: modTime,
size: size,
}
return o, nil
}
// ReadCookies parses the raw cookie string and returns an array of http.Cookie objects.
func ReadCookies(raw string) []*http.Cookie {
header := http.Header{}
header.Add("Cookie", raw)
request := http.Request{Header: header}
return request.Cookies()
}
var retryErrorCodes = []int{
400, // icloud is a mess, sometimes returns 400 on a perfectly fine request. So just retry
408, // Request Timeout
409, // Conflict, retry could fix it.
429, // Rate exceeded.
500, // Get occasional 500 Internal Server Error
502, // Server overload
503, // Service Unavailable
504, // Gateway Time-out
}
func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
if fserrors.ContextError(ctx, &err) {
return false, err
}
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
}
func ignoreResultUnknown(ctx context.Context, resp *http.Response, err error) (bool, error) {
if requestError, ok := err.(*api.RequestError); ok {
if requestError.Status == "unknown" {
fs.Debugf(requestError, " ignoring.")
return false, nil
}
}
return shouldRetry(ctx, resp, err)
}
func retryResultUnknown(ctx context.Context, resp *http.Response, err error) (bool, error) {
if requestError, ok := err.(*api.RequestError); ok {
if requestError.Status == "unknown" {
fs.Debugf(requestError, " retrying.")
return true, err
}
}
return shouldRetry(ctx, resp, err)
}
// NewFs constructs an Fs from the path, container:path
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
// Parse config into Options struct
opt := new(Options)
err := configstruct.Set(m, opt)
if err != nil {
return nil, err
}
if opt.Password != "" {
var err error
opt.Password, err = obscure.Reveal(opt.Password)
if err != nil {
return nil, fmt.Errorf("couldn't decrypt user password: %w", err)
}
}
if opt.TrustToken == "" {
return nil, fmt.Errorf("missing icloud trust token: try refreshing it with \"rclone config reconnect %s:\"", name)
}
cookies := ReadCookies(opt.Cookies)
callback := func(session *api.Session) {
m.Set(configCookies, session.GetCookieString())
}
icloud, err := api.New(
opt.AppleID,
opt.Password,
opt.TrustToken,
opt.ClientID,
cookies,
callback,
)
if err != nil {
return nil, err
}
if err := icloud.Authenticate(ctx); err != nil {
return nil, err
}
if icloud.Session.Requires2FA() {
return nil, errors.New("trust token expired, please reauth")
}
root = strings.Trim(root, "/")
f := &Fs{
name: name,
root: root,
icloud: icloud,
rootID: "FOLDER::com.apple.CloudDocs::root",
opt: *opt,
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
}
f.features = (&fs.Features{
CanHaveEmptyDirectories: true,
PartialUploads: false,
}).Fill(ctx, f)
rootID := f.rootID
f.service, err = icloud.DriveService()
if err != nil {
return nil, err
}
f.dirCache = dircache.New(
root,
rootID,
f,
)
err = f.dirCache.FindRoot(ctx, false)
if err != nil {
// Assume it is a file
newRoot, remote := dircache.SplitPath(root)
tempF := *f
tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
tempF.root = newRoot
// Make new Fs which is the parent
err = tempF.dirCache.FindRoot(ctx, false)
if err != nil {
// No root so return old f
return f, nil
}
_, err := tempF.NewObject(ctx, remote)
if err != nil {
if err == fs.ErrorObjectNotFound {
// File doesn't exist so return old f
return f, nil
}
return nil, err
}
f.dirCache = tempF.dirCache
f.root = tempF.root
// return an error with an fs which points to the parent
return f, fs.ErrorIsFile
}
return f, nil
}
// NewObject creates a new fs.Object from a given remote string.
//
// ctx: The context.Context for the function.
// remote: The remote string representing the object's location.
// Returns an fs.Object and an error.
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
return f.NewObjectFromDriveItem(ctx, remote, nil)
}
// NewObjectFromDriveItem creates a new fs.Object from a given remote string and DriveItem.
//
// ctx: The context.Context for the function.
// remote: The remote string representing the object's location.
// item: The optional DriveItem to use for initializing the Object. If nil, the function will read the metadata from the remote location.
// Returns an fs.Object and an error.
func (f *Fs) NewObjectFromDriveItem(ctx context.Context, remote string, item *api.DriveItem) (fs.Object, error) {
o := &Object{
fs: f,
remote: remote,
}
if item != nil {
err := o.setMetaData(item)
if err != nil {
return nil, err
}
} else {
item, err := f.readMetaData(ctx, remote)
if err != nil {
return nil, err
}
err = o.setMetaData(item)
if err != nil {
return nil, err
}
}
return o, nil
}
func (f *Fs) readMetaData(ctx context.Context, path string) (item *api.DriveItem, err error) {
leaf, ID, _, err := f.FindPath(ctx, path, false)
if err != nil {
if err == fs.ErrorDirNotFound {
return nil, fs.ErrorObjectNotFound
}
return nil, err
}
item, found, err := f.findLeafItem(ctx, ID, leaf)
if err != nil {
return nil, err
}
if !found {
return nil, fs.ErrorObjectNotFound
}
return item, nil
}
func (o *Object) setMetaData(item *api.DriveItem) (err error) {
if item.IsFolder() {
return fs.ErrorIsDir
}
o.size = item.Size
o.modTime = item.DateModified
o.createdTime = item.DateCreated
o.driveID = item.Drivewsid
o.docID = item.Docwsid
o.itemID = item.Itemid
o.etag = item.Etag
o.downloadURL = item.DownloadURL()
return nil
}
// ID returns the ID of the Object if known, or "" if not
func (o *Object) ID() string {
return o.driveID
}
// Fs implements fs.Object.
func (o *Object) Fs() fs.Info {
return o.fs
}
// Hash implements fs.Object.
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
return "", hash.ErrUnsupported
}
// ModTime implements fs.Object.
func (o *Object) ModTime(context.Context) time.Time {
return o.modTime
}
// Open implements fs.Object.
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
fs.FixRangeOption(options, o.size)
// Drive does not support empty files, so we cheat
if o.size == 0 {
return io.NopCloser(bytes.NewBufferString("")), nil
}
var resp *http.Response
var err error
if err = o.fs.pacer.Call(func() (bool, error) {
var url string
//var doc *api.Document
//if o.docID == "" {
//doc, resp, err = o.fs.service.GetDocByItemID(ctx, o.itemID)
//}
// Can not get the download url on a item to work, so do it the hard way.
url, _, err = o.fs.service.GetDownloadURLByDriveID(ctx, o.driveID)
resp, err = o.fs.service.DownloadFile(ctx, url, options)
return shouldRetry(ctx, resp, err)
}); err != nil {
return nil, err
}
return resp.Body, err
}
// Remote implements fs.Object.
func (o *Object) Remote() string {
return o.remote
}
// Remove implements fs.Object.
func (o *Object) Remove(ctx context.Context) error {
if o.itemID == "" {
return nil
}
var resp *http.Response
var err error
if err = o.fs.pacer.Call(func() (bool, error) {
_, resp, err = o.fs.service.MoveItemToTrashByID(ctx, o.driveID, o.etag, true)
return retryResultUnknown(ctx, resp, err)
}); err != nil {
return err
}
return nil
}
// SetModTime implements fs.Object.
func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
return fs.ErrorCantSetModTime
}
// Size implements fs.Object.
func (o *Object) Size() int64 {
return o.size
}
// Storable implements fs.Object.
func (o *Object) Storable() bool {
return true
}
// String implements fs.Object.
func (o *Object) String() string {
if o == nil {
return "<nil>"
}
return o.remote
}
// Update implements fs.Object.
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
size := src.Size()
if size < 0 {
return errors.New("file size unknown")
}
remote := o.Remote()
modTime := src.ModTime(ctx)
leaf, dirID, _, err := o.fs.FindPath(ctx, path.Clean(remote), true)
if err != nil {
return err
}
// Move current file to trash
if o.driveID != "" {
err = o.Remove(ctx)
if err != nil {
return err
}
}
name := o.fs.opt.Enc.FromStandardName(leaf)
var resp *http.Response
// Create document
var uploadInfo *api.UploadResponse
if err = o.fs.pacer.Call(func() (bool, error) {
uploadInfo, resp, err = o.fs.service.CreateUpload(ctx, size, name)
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return err
}
// Upload content
var upload *api.SingleFileResponse
if err = o.fs.pacer.Call(func() (bool, error) {
upload, resp, err = o.fs.service.Upload(ctx, in, size, name, uploadInfo.URL)
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return err
}
//var doc *api.Document
//if err = o.fs.pacer.Call(func() (bool, error) {
// doc, resp, err = o.fs.service.GetDocByItemID(ctx, dirID)
// return ignoreResultUnknown(ctx, resp, err)
//}); err != nil {
// return err
//}
r := api.NewUpdateFileInfo()
r.DocumentID = uploadInfo.DocumentID
r.Path.Path = name
r.Path.StartingDocumentID = api.GetDocIDFromDriveID(dirID)
//r.Path.StartingDocumentID = doc.DocumentID
r.Data.Receipt = upload.SingleFile.Receipt
r.Data.Signature = upload.SingleFile.Signature
r.Data.ReferenceSignature = upload.SingleFile.ReferenceSignature
r.Data.WrappingKey = upload.SingleFile.WrappingKey
r.Data.Size = upload.SingleFile.Size
r.Mtime = modTime.Unix() * 1000
r.Btime = modTime.Unix() * 1000
// Update metadata
var item *api.DriveItem
if err = o.fs.pacer.Call(func() (bool, error) {
item, resp, err = o.fs.service.UpdateFile(ctx, &r)
return ignoreResultUnknown(ctx, resp, err)
}); err != nil {
return err
}
err = o.setMetaData(item)
if err != nil {
return err
}
o.modTime = modTime
o.size = src.Size()
return nil
}
// Check interfaces are satisfied
var (
_ fs.Fs = &Fs{}
_ fs.Mover = (*Fs)(nil)
_ fs.Purger = (*Fs)(nil)
_ fs.DirMover = (*Fs)(nil)
_ fs.DirCacheFlusher = (*Fs)(nil)
_ fs.Copier = (*Fs)(nil)
_ fs.Object = &Object{}
_ fs.IDer = (*Object)(nil)
)