2023-08-29 14:02:34 +02:00
// Package protondrive implements the Proton Drive backend
2023-07-22 11:46:21 +02:00
package protondrive
import (
"context"
"errors"
"fmt"
"io"
"path"
"strings"
"time"
protonDriveAPI "github.com/henrybear327/Proton-API-Bridge"
"github.com/henrybear327/go-proton-api"
"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/hash"
"github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/encoder"
"github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/readers"
)
/ *
- 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 (
minSleep = 10 * time . Millisecond
maxSleep = 2 * time . Second
decayConstant = 2 // bigger for slower decay, exponential
2023-09-11 16:48:14 +02:00
clientUIDKey = "client_uid"
clientAccessTokenKey = "client_access_token"
clientRefreshTokenKey = "client_refresh_token"
clientSaltedKeyPassKey = "client_salted_key_pass"
2023-07-22 11:46:21 +02:00
)
var (
2023-08-29 14:02:34 +02:00
errCanNotUploadFileWithUnknownSize = errors . New ( "proton Drive can't upload files with unknown size" )
errCanNotPurgeRootDirectory = errors . New ( "can't purge root directory" )
2023-07-22 11:46:21 +02:00
// for the auth/deauth handler
_mapper configmap . Mapper
_saltedKeyPass string
)
// Register with Fs
func init ( ) {
fs . Register ( & fs . RegInfo {
Name : "protondrive" ,
Description : "Proton Drive" ,
NewFs : NewFs ,
Options : [ ] fs . Option { {
Name : "username" ,
2023-09-08 22:54:46 +02:00
Help : ` The username of your proton account ` ,
2023-07-22 11:46:21 +02:00
Required : true ,
} , {
Name : "password" ,
2023-09-08 22:54:46 +02:00
Help : "The password of your proton account." ,
2023-07-22 11:46:21 +02:00
Required : true ,
IsPassword : true ,
2023-09-08 22:54:46 +02:00
} , {
2023-09-11 16:48:14 +02:00
Name : "mailbox_password" ,
2023-09-08 22:54:46 +02:00
Help : ` The mailbox password of your two - password proton account .
For more information regarding the mailbox password , please check the
following official knowledge base article :
https : //proton.me/support/the-difference-between-the-mailbox-password-and-login-password
` ,
IsPassword : true ,
Advanced : true ,
2023-07-22 11:46:21 +02:00
} , {
Name : "2fa" ,
Help : ` The 2 FA code
2023-09-06 19:50:28 +02:00
The value can also be provided with -- protondrive - 2 fa = 000000
2023-07-22 11:46:21 +02:00
The 2 FA code of your proton drive account if the account is set up with
two - factor authentication ` ,
Required : false ,
2023-09-11 16:48:14 +02:00
} , {
Name : clientUIDKey ,
Help : "Client uid key (internal use only)" ,
Required : false ,
Advanced : true ,
Sensitive : true ,
Hide : fs . OptionHideBoth ,
} , {
Name : clientAccessTokenKey ,
Help : "Client access token key (internal use only)" ,
Required : false ,
Advanced : true ,
Sensitive : true ,
Hide : fs . OptionHideBoth ,
} , {
Name : clientRefreshTokenKey ,
Help : "Client refresh token key (internal use only)" ,
Required : false ,
Advanced : true ,
Sensitive : true ,
Hide : fs . OptionHideBoth ,
} , {
Name : clientSaltedKeyPassKey ,
Help : "Client salted key pass key (internal use only)" ,
Required : false ,
Advanced : true ,
Sensitive : true ,
Hide : fs . OptionHideBoth ,
2023-07-22 11:46:21 +02:00
} , {
Name : config . ConfigEncoding ,
Help : config . ConfigEncodingHelp ,
Advanced : true ,
Default : ( encoder . Base |
encoder . EncodeInvalidUtf8 |
encoder . EncodeLeftSpace |
encoder . EncodeRightSpace ) ,
} , {
Name : "original_file_size" ,
Help : ` Return the file size before encryption
The size of the encrypted file will be different from ( bigger than ) the
original file size . Unless there is a reason to return the file size
after encryption is performed , otherwise , set this option to true , as
features like Open ( ) which will need to be supplied with original content
size , will fail to operate properly ` ,
Advanced : true ,
Default : true ,
} , {
Name : "app_version" ,
Help : ` The app version string
The app version string indicates the client that is currently performing
the API request . This information is required and will be sent with every
2023-07-24 08:56:21 +02:00
API request . ` ,
2023-07-22 11:46:21 +02:00
Advanced : true ,
2023-07-24 08:56:21 +02:00
Default : "macos-drive@1.0.0-alpha.1+rclone" ,
2023-07-22 11:46:21 +02:00
} , {
Name : "replace_existing_draft" ,
Help : ` Create a new revision when filename conflict is detected
When a file upload is cancelled or failed before completion , a draft will be
created and the subsequent upload of the same file to the same location will be
reported as a conflict .
2023-09-06 19:50:28 +02:00
The value can also be set by -- protondrive - replace - existing - draft = true
2023-07-22 11:46:21 +02:00
If the option is set to true , the draft will be replaced and then the upload
operation will restart . If there are other clients also uploading at the same
file location at the same time , the behavior is currently unknown . Need to set
to true for integration tests .
If the option is set to false , an error " a draft exist - usually this means a
file is being uploaded at another client , or , there was a failed upload attempt "
will be returned , and no upload will happen . ` ,
Advanced : true ,
Default : false ,
} , {
Name : "enable_caching" ,
Help : ` Caches the files and folders metadata to reduce API calls
2023-08-09 16:01:19 +02:00
Notice : If you are mounting ProtonDrive as a VFS , please disable this feature ,
as the current implementation doesn ' t update or clear the cache when there are
external changes .
2023-07-22 11:46:21 +02:00
The files and folders on ProtonDrive are represented as links with keyrings ,
which can be cached to improve performance and be friendly to the API server .
The cache is currently built for the case when the rclone is the only instance
performing operations to the mount point . The event system , which is the proton
API system that provides visibility of what has changed on the drive , is yet
to be implemented , so updates from other clients won ’ t be reflected in the
cache . Thus , if there are concurrent clients accessing the same mount point ,
then we might have a problem with caching the stale data . ` ,
Advanced : true ,
Default : true ,
} } ,
} )
}
// Options defines the configuration for this backend
type Options struct {
2023-09-08 22:54:46 +02:00
Username string ` config:"username" `
Password string ` config:"password" `
2023-09-11 16:48:14 +02:00
MailboxPassword string ` config:"mailbox_password" `
2023-09-08 22:54:46 +02:00
TwoFA string ` config:"2fa" `
2023-07-22 11:46:21 +02:00
// advanced
Enc encoder . MultiEncoder ` config:"encoding" `
ReportOriginalSize bool ` config:"original_file_size" `
AppVersion string ` config:"app_version" `
ReplaceExistingDraft bool ` config:"replace_existing_draft" `
EnableCaching bool ` config:"enable_caching" `
}
// Fs represents a remote proton drive
type Fs struct {
name string // name of this remote
// Notice that for ProtonDrive, it's attached under rootLink (usually /root)
root string // the path we are working on.
opt Options // parsed config options
ci * fs . ConfigInfo // global config
features * fs . Features // optional features
pacer * fs . Pacer // pacer for API calls
dirCache * dircache . DirCache // Map of directory path to directory id
protonDrive * protonDriveAPI . ProtonDrive // the Proton API bridging library
}
// Object describes an 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)
originalSize * int64 // size of the object (after decryption)
digests * string // object original content
blockSizes [ ] int64 // the block sizes of the encrypted file
modTime time . Time // modification time of the object
createdTime time . Time // creation time of the object
id string // ID of the object
mimetype string // mimetype of the file
link * proton . Link // link data on proton server
}
// shouldRetry returns a boolean as to whether this err deserves to be
// retried. It returns the err as a convenience
func shouldRetry ( ctx context . Context , err error ) ( bool , error ) {
return false , err
}
//------------------------------------------------------------------------------
// 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 ( "proton drive root link ID '%s'" , f . root )
}
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
// run all the dir/remote through this
func ( f * Fs ) sanitizePath ( _path string ) string {
_path = path . Clean ( _path )
if _path == "." || _path == "/" {
return ""
}
return f . opt . Enc . FromStandardPath ( _path )
}
func getConfigMap ( m configmap . Mapper ) ( uid , accessToken , refreshToken , saltedKeyPass string , ok bool ) {
if accessToken , ok = m . Get ( clientAccessTokenKey ) ; ! ok {
return
}
if uid , ok = m . Get ( clientUIDKey ) ; ! ok {
return
}
if refreshToken , ok = m . Get ( clientRefreshTokenKey ) ; ! ok {
return
}
if saltedKeyPass , ok = m . Get ( clientSaltedKeyPassKey ) ; ! ok {
return
}
_saltedKeyPass = saltedKeyPass
return
}
func setConfigMap ( m configmap . Mapper , uid , accessToken , refreshToken , saltedKeyPass string ) {
m . Set ( clientUIDKey , uid )
m . Set ( clientAccessTokenKey , accessToken )
m . Set ( clientRefreshTokenKey , refreshToken )
m . Set ( clientSaltedKeyPassKey , saltedKeyPass )
_saltedKeyPass = saltedKeyPass
}
func clearConfigMap ( m configmap . Mapper ) {
setConfigMap ( m , "" , "" , "" , "" )
_saltedKeyPass = ""
}
func authHandler ( auth proton . Auth ) {
2023-09-08 10:00:34 +02:00
// fs.Debugf("authHandler called")
2023-07-22 11:46:21 +02:00
setConfigMap ( _mapper , auth . UID , auth . AccessToken , auth . RefreshToken , _saltedKeyPass )
}
func deAuthHandler ( ) {
2023-09-08 10:00:34 +02:00
// fs.Debugf("deAuthHandler called")
2023-07-22 11:46:21 +02:00
clearConfigMap ( _mapper )
}
func newProtonDrive ( ctx context . Context , f * Fs , opt * Options , m configmap . Mapper ) ( * protonDriveAPI . ProtonDrive , error ) {
config := protonDriveAPI . NewDefaultConfig ( )
config . AppVersion = opt . AppVersion
config . UserAgent = f . ci . UserAgent // opt.UserAgent
config . ReplaceExistingDraft = opt . ReplaceExistingDraft
config . EnableCaching = opt . EnableCaching
// let's see if we have the cached access credential
uid , accessToken , refreshToken , saltedKeyPass , hasUseReusableLoginCredentials := getConfigMap ( m )
_saltedKeyPass = saltedKeyPass
if hasUseReusableLoginCredentials {
fs . Debugf ( f , "Has cached credentials" )
config . UseReusableLogin = true
config . ReusableCredential . UID = uid
config . ReusableCredential . AccessToken = accessToken
config . ReusableCredential . RefreshToken = refreshToken
config . ReusableCredential . SaltedKeyPass = saltedKeyPass
protonDrive /* credential will be nil since access credentials are passed in */ , _ , err := protonDriveAPI . NewProtonDrive ( ctx , config , authHandler , deAuthHandler )
if err != nil {
fs . Debugf ( f , "Cached credential doesn't work, clearing and using the fallback login method" )
// clear the access token on failure
clearConfigMap ( m )
fs . Debugf ( f , "couldn't initialize a new proton drive instance using cached credentials: %v" , err )
// we fallback to username+password login -> don't throw an error here
// return nil, fmt.Errorf("couldn't initialize a new proton drive instance: %w", err)
} else {
fs . Debugf ( f , "Used cached credential to initialize the ProtonDrive API" )
return protonDrive , nil
}
}
// if not, let's try to log the user in using username and password (and 2FA if required)
fs . Debugf ( f , "Using username and password to log in" )
config . UseReusableLogin = false
config . FirstLoginCredential . Username = opt . Username
config . FirstLoginCredential . Password = opt . Password
2023-09-08 22:54:46 +02:00
config . FirstLoginCredential . MailboxPassword = opt . MailboxPassword
2023-07-22 11:46:21 +02:00
config . FirstLoginCredential . TwoFA = opt . TwoFA
protonDrive , auth , err := protonDriveAPI . NewProtonDrive ( ctx , config , authHandler , deAuthHandler )
if err != nil {
return nil , fmt . Errorf ( "couldn't initialize a new proton drive instance: %w" , err )
}
fs . Debugf ( f , "Used username and password to initialize the ProtonDrive API" )
setConfigMap ( m , auth . UID , auth . AccessToken , auth . RefreshToken , auth . SaltedKeyPass )
return protonDrive , nil
}
// NewFs constructs an Fs from the path, container:path
func NewFs ( ctx context . Context , name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
// pacer is not used in NewFs()
_mapper = m
// 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 password: %w" , err )
}
}
2023-09-08 22:54:46 +02:00
if opt . MailboxPassword != "" {
var err error
opt . MailboxPassword , err = obscure . Reveal ( opt . MailboxPassword )
if err != nil {
return nil , fmt . Errorf ( "couldn't decrypt mailbox password: %w" , err )
}
}
2023-07-22 11:46:21 +02:00
ci := fs . GetConfig ( ctx )
root = strings . Trim ( root , "/" )
f := & Fs {
name : name ,
root : root ,
opt : * opt ,
ci : ci ,
pacer : fs . NewPacer ( ctx , pacer . NewDefault ( pacer . MinSleep ( minSleep ) , pacer . MaxSleep ( maxSleep ) , pacer . DecayConstant ( decayConstant ) ) ) ,
}
f . features = ( & fs . Features {
ReadMimeType : true ,
CanHaveEmptyDirectories : true ,
/ * can ' t have multiple threads downloading
The raw file is split into equally - sized ( currently 4 MB , but it might change in the future , say to 8 MB , 16 MB , etc . ) blocks , except the last one which might be smaller than 4 MB .
Each block is encrypted separately , where the size and sha1 after the encryption is performed on the block is added to the metadata of the block , but the original block size and sha1 is not in the metadata .
We can make assumption and implement the chunker , but for now , we would rather be safe about it , and let the block being concurrently downloaded and decrypted in the background , to speed up the download operation !
* /
NoMultiThreading : true ,
} ) . Fill ( ctx , f )
protonDrive , err := newProtonDrive ( ctx , f , opt , m )
if err != nil {
return nil , err
}
f . protonDrive = protonDrive
root = f . sanitizePath ( root )
f . dirCache = dircache . New (
root , /* root folder path */
protonDrive . MainShare . LinkID , /* real root ID is the root folder, since we can't go past this folder */
f ,
)
err = f . dirCache . FindRoot ( ctx , false )
if err != nil {
// if the root directory is not found, the initialization will still work
// but if it's other kinds of error, then we raise it
if err != fs . ErrorDirNotFound {
return nil , fmt . Errorf ( "couldn't initialize a new root remote: %w" , err )
}
// Assume it is a file (taken and modified from box.go)
newRoot , remote := dircache . SplitPath ( root )
tempF := * f
tempF . dirCache = dircache . New ( newRoot , protonDrive . MainShare . LinkID , & 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 . newObjectWithLink ( ctx , remote , nil )
if err != nil {
if err == fs . ErrorObjectNotFound {
// File doesn't exist so return old f
return f , nil
}
return nil , err
}
f . features . Fill ( ctx , & tempF )
// XXX: update the old f here instead of returning tempF, since
// `features` were already filled with functions having *f as a receiver.
// See https://github.com/rclone/rclone/issues/2182
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
}
//------------------------------------------------------------------------------
// CleanUp deletes all files currently in trash
func ( f * Fs ) CleanUp ( ctx context . Context ) error {
return f . pacer . Call ( func ( ) ( bool , error ) {
err := f . protonDrive . EmptyTrash ( ctx )
return shouldRetry ( ctx , err )
} )
}
// NewObject finds the Object at remote. If it can't be found
// it returns the error ErrorObjectNotFound.
//
// If remote points to a directory then it should return
// ErrorIsDir if possible without doing any extra work,
// otherwise ErrorObjectNotFound.
func ( f * Fs ) NewObject ( ctx context . Context , remote string ) ( fs . Object , error ) {
return f . newObjectWithLink ( ctx , remote , nil )
}
func ( f * Fs ) getObjectLink ( ctx context . Context , remote string ) ( * proton . Link , error ) {
// attempt to locate the file
leaf , folderLinkID , err := f . dirCache . FindPath ( ctx , f . sanitizePath ( remote ) , false )
if err != nil {
if err == fs . ErrorDirNotFound {
// parent folder of the file not found, we for sure can't find the file
return nil , fs . ErrorObjectNotFound
}
// other error has occurred
return nil , err
}
var link * proton . Link
if err = f . pacer . Call ( func ( ) ( bool , error ) {
link , err = f . protonDrive . SearchByNameInActiveFolderByID ( ctx , folderLinkID , leaf , true , false , proton . LinkStateActive )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , err
}
if link == nil { // both link and err are nil, file not found
return nil , fs . ErrorObjectNotFound
}
return link , nil
}
// readMetaDataForRemote reads the metadata from the remote
func ( f * Fs ) readMetaDataForRemote ( ctx context . Context , remote string , _link * proton . Link ) ( * proton . Link , * protonDriveAPI . FileSystemAttrs , error ) {
link , err := f . getObjectLink ( ctx , remote )
if err != nil {
return nil , nil , err
}
var fileSystemAttrs * protonDriveAPI . FileSystemAttrs
if err = f . pacer . Call ( func ( ) ( bool , error ) {
fileSystemAttrs , err = f . protonDrive . GetActiveRevisionAttrs ( ctx , link )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , nil , err
}
return link , fileSystemAttrs , nil
}
// readMetaData gets the metadata if it hasn't already been fetched
//
// it also sets the info
func ( o * Object ) readMetaData ( ctx context . Context , link * proton . Link ) ( err error ) {
if o . link != nil {
return nil
}
link , fileSystemAttrs , err := o . fs . readMetaDataForRemote ( ctx , o . remote , link )
if err != nil {
return err
}
o . id = link . LinkID
o . size = link . Size
o . modTime = time . Unix ( link . ModifyTime , 0 )
o . createdTime = time . Unix ( link . CreateTime , 0 )
o . mimetype = link . MIMEType
o . link = link
if fileSystemAttrs != nil {
o . modTime = fileSystemAttrs . ModificationTime
o . originalSize = & fileSystemAttrs . Size
o . blockSizes = fileSystemAttrs . BlockSizes
o . digests = & fileSystemAttrs . Digests
}
return nil
}
// Return an Object from a path
//
// If it can't be found it returns the error fs.ErrorObjectNotFound.
func ( f * Fs ) newObjectWithLink ( ctx context . Context , remote string , link * proton . Link ) ( fs . Object , error ) {
o := & Object {
fs : f ,
remote : remote ,
}
err := o . readMetaData ( ctx , link )
if err != nil {
return nil , err
}
return o , nil
}
// List the objects and directories in dir into entries. The
// entries can be returned in any order but should be for a
// complete directory.
//
// dir should be "" to list the root, and should not have
// trailing slashes.
//
// This should return ErrDirNotFound if the directory isn't
// found.
// Notice that this function is expensive since everything on proton is encrypted
// So having a remote with 10k files, during operations like sync, might take a while and lots of bandwidth!
func ( f * Fs ) List ( ctx context . Context , dir string ) ( fs . DirEntries , error ) {
folderLinkID , err := f . dirCache . FindDir ( ctx , f . sanitizePath ( dir ) , false ) // will handle ErrDirNotFound here
if err != nil {
return nil , err
}
var foldersAndFiles [ ] * protonDriveAPI . ProtonDirectoryData
if err = f . pacer . Call ( func ( ) ( bool , error ) {
foldersAndFiles , err = f . protonDrive . ListDirectory ( ctx , folderLinkID )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , err
}
entries := make ( fs . DirEntries , 0 )
for i := range foldersAndFiles {
remote := path . Join ( dir , f . opt . Enc . ToStandardName ( foldersAndFiles [ i ] . Name ) )
if foldersAndFiles [ i ] . IsFolder {
f . dirCache . Put ( remote , foldersAndFiles [ i ] . Link . LinkID )
d := fs . NewDir ( remote , time . Unix ( foldersAndFiles [ i ] . Link . ModifyTime , 0 ) ) . SetID ( foldersAndFiles [ i ] . Link . LinkID )
entries = append ( entries , d )
} else {
obj , err := f . newObjectWithLink ( ctx , remote , foldersAndFiles [ i ] . Link )
if err != nil {
return nil , err
}
entries = append ( entries , obj )
}
}
return entries , nil
}
2023-08-29 14:02:34 +02:00
// FindLeaf finds a directory of name leaf in the folder with ID pathID
2023-07-22 11:46:21 +02:00
//
// This should be implemented by the backend and will be called by the
// dircache package when appropriate.
func ( f * Fs ) FindLeaf ( ctx context . Context , pathID , leaf string ) ( string , bool , error ) {
/* f.opt.Enc.FromStandardName(leaf) not required since the DirCache only process sanitized path */
var link * proton . Link
var err error
if err = f . pacer . Call ( func ( ) ( bool , error ) {
link , err = f . protonDrive . SearchByNameInActiveFolderByID ( ctx , pathID , leaf , false , true , proton . LinkStateActive )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return "" , false , err
}
if link == nil {
return "" , false , nil
}
return link . LinkID , true , nil
}
2023-08-29 14:02:34 +02:00
// CreateDir makes a directory with pathID as parent and name leaf
2023-07-22 11:46:21 +02:00
//
// 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 ) {
/* f.opt.Enc.FromStandardName(leaf) not required since the DirCache only process sanitized path */
var newID string
var err error
if err = f . pacer . Call ( func ( ) ( bool , error ) {
newID , err = f . protonDrive . CreateNewFolderByID ( ctx , pathID , leaf )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return "" , err
}
return newID , err
}
// 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 {
2023-08-29 14:02:34 +02:00
return nil , errCanNotUploadFileWithUnknownSize
2023-07-22 11:46:21 +02:00
}
existingObj , err := f . NewObject ( ctx , src . Remote ( ) )
switch err {
case nil :
// object is found, we add an revision to it
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
}
}
// 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 ) ( * Object , error ) {
// ˇ-------ˇ filename
// e.g. /root/a/b/c/test.txt
// ^~~~~~~~~~~^ dirPath
// Create the directory for the object if it doesn't exist
_ , _ , err := f . dirCache . FindPath ( ctx , f . sanitizePath ( remote ) , true )
if err != nil {
return nil , err
}
// Temporary Object under construction
obj := & Object {
fs : f ,
remote : remote ,
size : size ,
originalSize : nil ,
id : "" ,
modTime : modTime ,
mimetype : "" ,
link : nil ,
}
return obj , nil
}
// Mkdir makes the directory (container, bucket)
//
// Shouldn't return an error if it already exists
func ( f * Fs ) Mkdir ( ctx context . Context , dir string ) error {
_ , err := f . dirCache . FindDir ( ctx , f . sanitizePath ( dir ) , true )
return err
}
// Rmdir removes the directory (container, bucket) if empty
//
// Return an error if it doesn't exist or isn't empty
func ( f * Fs ) Rmdir ( ctx context . Context , dir string ) error {
folderLinkID , err := f . dirCache . FindDir ( ctx , f . sanitizePath ( dir ) , false )
if err == fs . ErrorDirNotFound {
return fmt . Errorf ( "[Rmdir] cannot find LinkID for dir %s (%s)" , dir , f . sanitizePath ( dir ) )
} else if err != nil {
return err
}
if err = f . pacer . Call ( func ( ) ( bool , error ) {
err = f . protonDrive . MoveFolderToTrashByID ( ctx , folderLinkID , true )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return err
}
f . dirCache . FlushDir ( f . sanitizePath ( dir ) )
return nil
}
// Precision of the ModTimes in this Fs
func ( f * Fs ) Precision ( ) time . Duration {
return time . Second
}
// DirCacheFlush an optional interface to flush internal directory cache
// DirCacheFlush resets the directory cache - used in testing
// as an optional interface
func ( f * Fs ) DirCacheFlush ( ) {
f . dirCache . ResetRoot ( )
f . protonDrive . ClearCache ( )
}
2023-08-29 14:02:34 +02:00
// Hashes returns the supported hash types of the filesystem
2023-07-22 11:46:21 +02:00
func ( f * Fs ) Hashes ( ) hash . Set {
return hash . Set ( hash . SHA1 )
}
// About gets quota information
func ( f * Fs ) About ( ctx context . Context ) ( * fs . Usage , error ) {
var user * proton . User
var err error
if err = f . pacer . Call ( func ( ) ( bool , error ) {
user , err = f . protonDrive . About ( ctx )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , err
}
2023-07-28 11:03:23 +02:00
total := user . MaxSpace
used := user . UsedSpace
2023-07-22 11:46:21 +02:00
free := total - used
usage := & fs . Usage {
Total : & total ,
Used : & used ,
Free : & free ,
}
return usage , nil
}
// ------------------------------------------------------------
// Fs returns the parent Fs
func ( o * Object ) Fs ( ) fs . Info {
return o . fs
}
// Return a string version
func ( o * Object ) String ( ) string {
if o == nil {
return "<nil>"
}
return o . remote
}
// Remote returns the remote path
func ( o * Object ) Remote ( ) string {
return o . remote
}
// Hash returns the hashes of an object
func ( o * Object ) Hash ( ctx context . Context , t hash . Type ) ( string , error ) {
if t != hash . SHA1 {
return "" , hash . ErrUnsupported
}
if o . digests != nil {
return * o . digests , nil
}
2023-09-08 10:00:34 +02:00
// sha1 not cached: we fetch and try to obtain the sha1 of the link
2023-07-22 11:46:21 +02:00
fileSystemAttrs , err := o . fs . protonDrive . GetActiveRevisionAttrsByID ( ctx , o . ID ( ) )
if err != nil {
return "" , err
}
if fileSystemAttrs == nil || fileSystemAttrs . Digests == "" {
2023-07-24 08:56:21 +02:00
fs . Debugf ( o , "file sha1 digest missing" )
return "" , nil
2023-07-22 11:46:21 +02:00
}
return fileSystemAttrs . Digests , nil
}
// Size returns the size of an object in bytes
func ( o * Object ) Size ( ) int64 {
if o . fs . opt . ReportOriginalSize {
// if ReportOriginalSize is set, we will generate an error when the original size failed to be parsed
// this is crucial as features like Open() will need to use the proper size to operate the seek/range operator
if o . originalSize != nil {
return * o . originalSize
}
2023-09-08 10:00:34 +02:00
fs . Debugf ( o , "Original file size missing" )
2023-07-22 11:46:21 +02:00
}
return o . size
}
// 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 ( ctx context . Context ) time . Time {
return o . modTime
}
// SetModTime sets the modification time of the local fs object
func ( o * Object ) SetModTime ( ctx context . Context , modTime time . Time ) error {
return fs . ErrorCantSetModTime
}
// Storable returns a boolean showing whether this object storable
func ( o * Object ) Storable ( ) bool {
return true
}
// Open opens the file for read. Call Close() on the returned io.ReadCloser
func ( o * Object ) Open ( ctx context . Context , options ... fs . OpenOption ) ( io . ReadCloser , error ) {
fs . FixRangeOption ( options , * o . originalSize )
var offset , limit int64 = 0 , - 1
for _ , option := range options { // if the caller passes in nil for options, it will become array of nil
switch x := option . ( type ) {
case * fs . SeekOption :
offset = x . Offset
case * fs . RangeOption :
offset , limit = x . Decode ( o . Size ( ) )
default :
if option . Mandatory ( ) {
fs . Logf ( o , "Unsupported mandatory option: %v" , option )
}
}
}
// download and decrypt the file
var reader io . ReadCloser
var fileSystemAttrs * protonDriveAPI . FileSystemAttrs
var sizeOnServer int64
var err error
if err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
reader , sizeOnServer , fileSystemAttrs , err = o . fs . protonDrive . DownloadFileByID ( ctx , o . id , offset )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , err
}
if fileSystemAttrs != nil {
o . originalSize = & fileSystemAttrs . Size
o . modTime = fileSystemAttrs . ModificationTime
o . digests = & fileSystemAttrs . Digests
o . blockSizes = fileSystemAttrs . BlockSizes
} else {
fs . Debugf ( o , "fileSystemAttrs is nil: using fallback size, and now digests and blocksizes available" )
o . originalSize = & sizeOnServer
o . size = sizeOnServer
o . digests = nil
o . blockSizes = nil
}
retReader := io . NopCloser ( reader ) // the NewLimitedReadCloser will deal with the limit
// deal with limit
return readers . NewLimitedReadCloser ( retReader , limit ) , nil
}
// Update in to the object 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), Upload should either
// return an error or update the object properly (rather than e.g. calling panic).
func ( o * Object ) Update ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) error {
size := src . Size ( )
if size < 0 {
2023-08-29 14:02:34 +02:00
return errCanNotUploadFileWithUnknownSize
2023-07-22 11:46:21 +02:00
}
remote := o . Remote ( )
leaf , folderLinkID , err := o . fs . dirCache . FindPath ( ctx , o . fs . sanitizePath ( remote ) , true )
if err != nil {
return err
}
modTime := src . ModTime ( ctx )
var linkID string
var fileSystemAttrs * proton . RevisionXAttrCommon
if err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
linkID , fileSystemAttrs , err = o . fs . protonDrive . UploadFileByReader ( ctx , folderLinkID , leaf , modTime , in , 0 )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return err
}
2023-07-24 09:00:18 +02:00
var sha1Hash string
if val , ok := fileSystemAttrs . Digests [ "SHA1" ] ; ok {
sha1Hash = val
} else {
sha1Hash = ""
}
2023-07-22 11:46:21 +02:00
o . id = linkID
o . originalSize = & fileSystemAttrs . Size
o . modTime = modTime
o . blockSizes = fileSystemAttrs . BlockSizes
2023-07-24 09:00:18 +02:00
o . digests = & sha1Hash
2023-07-22 11:46:21 +02:00
return nil
}
// Remove an object
func ( o * Object ) Remove ( ctx context . Context ) error {
return o . fs . pacer . Call ( func ( ) ( bool , error ) {
err := o . fs . protonDrive . MoveFileToTrashByID ( ctx , o . id )
return shouldRetry ( ctx , err )
} )
}
// ID returns the ID of the Object if known, or "" if not
func ( o * Object ) ID ( ) string {
return o . id
}
// 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 {
root := path . Join ( f . root , dir )
if root == "" {
// we can't remove the root directory, but we can list the directory and delete every folder and file in here
2023-08-29 14:02:34 +02:00
return errCanNotPurgeRootDirectory
2023-07-22 11:46:21 +02:00
}
folderLinkID , err := f . dirCache . FindDir ( ctx , f . sanitizePath ( dir ) , false )
if err != nil {
return err
}
if err = f . pacer . Call ( func ( ) ( bool , error ) {
err = f . protonDrive . MoveFolderToTrashByID ( ctx , folderLinkID , false )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return err
}
f . dirCache . FlushDir ( dir )
return nil
}
// MimeType of an Object if known, "" otherwise
func ( o * Object ) MimeType ( ctx context . Context ) string {
return o . mimetype
}
// Disconnect the current user
func ( f * Fs ) Disconnect ( ctx context . Context ) error {
return f . pacer . Call ( func ( ) ( bool , error ) {
err := f . protonDrive . Logout ( ctx )
return shouldRetry ( ctx , err )
} )
}
// Move src to this remote using server-side move 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.ErrorCantMove
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
}
// check if the remote (dst) exists
_ , err := f . NewObject ( ctx , remote )
if err != nil {
if err != fs . ErrorObjectNotFound {
return nil , err
}
// object is indeed not found
} else {
// object at the dst exists
return nil , fs . ErrorCantMove
}
// attempt the move
dstLeaf , dstDirectoryID , err := f . dirCache . FindPath ( ctx , f . sanitizePath ( remote ) , true )
if err != nil {
return nil , err
}
if err = f . pacer . Call ( func ( ) ( bool , error ) {
err = f . protonDrive . MoveFileByID ( ctx , srcObj . id , dstDirectoryID , dstLeaf )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return nil , err
}
f . dirCache . FlushDir ( f . sanitizePath ( src . Remote ( ) ) )
return f . NewObject ( ctx , remote )
}
// 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 , _ , _ , dstDirectoryID , dstLeaf , err := f . dirCache . DirMove ( ctx , srcFs . dirCache , f . sanitizePath ( srcFs . root ) , f . sanitizePath ( srcRemote ) , f . sanitizePath ( f . root ) , f . sanitizePath ( dstRemote ) )
if err != nil {
return err
}
if err = f . pacer . Call ( func ( ) ( bool , error ) {
err = f . protonDrive . MoveFolderByID ( ctx , srcID , dstDirectoryID , dstLeaf )
return shouldRetry ( ctx , err )
} ) ; err != nil {
return err
}
srcFs . dirCache . FlushDir ( f . sanitizePath ( srcRemote ) )
return nil
}
// Check the interfaces are satisfied
var (
_ fs . Fs = ( * Fs ) ( nil )
_ fs . Mover = ( * Fs ) ( nil )
_ fs . DirMover = ( * Fs ) ( nil )
_ fs . DirCacheFlusher = ( * Fs ) ( nil )
_ fs . Abouter = ( * Fs ) ( nil )
_ fs . Object = ( * Object ) ( nil )
_ fs . MimeTyper = ( * Object ) ( nil )
_ fs . IDer = ( * Object ) ( nil )
)