protondrive.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  1. // Package protondrive implements the Proton Drive backend
  2. package protondrive
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "path"
  9. "strings"
  10. "time"
  11. protonDriveAPI "github.com/henrybear327/Proton-API-Bridge"
  12. "github.com/henrybear327/go-proton-api"
  13. "github.com/rclone/rclone/fs"
  14. "github.com/rclone/rclone/fs/config"
  15. "github.com/rclone/rclone/fs/config/configmap"
  16. "github.com/rclone/rclone/fs/config/configstruct"
  17. "github.com/rclone/rclone/fs/config/obscure"
  18. "github.com/rclone/rclone/fs/hash"
  19. "github.com/rclone/rclone/lib/dircache"
  20. "github.com/rclone/rclone/lib/encoder"
  21. "github.com/rclone/rclone/lib/pacer"
  22. "github.com/rclone/rclone/lib/readers"
  23. )
  24. /*
  25. - dirCache operates on relative path to root
  26. - path sanitization
  27. - rule of thumb: sanitize before use, but store things as-is
  28. - the paths cached in dirCache are after sanitizing
  29. - the remote/dir passed in aren't, and are stored as-is
  30. */
  31. const (
  32. minSleep = 10 * time.Millisecond
  33. maxSleep = 2 * time.Second
  34. decayConstant = 2 // bigger for slower decay, exponential
  35. clientUIDKey = "client_uid"
  36. clientAccessTokenKey = "client_access_token"
  37. clientRefreshTokenKey = "client_refresh_token"
  38. clientSaltedKeyPassKey = "client_salted_key_pass"
  39. )
  40. var (
  41. errCanNotUploadFileWithUnknownSize = errors.New("proton Drive can't upload files with unknown size")
  42. errCanNotPurgeRootDirectory = errors.New("can't purge root directory")
  43. // for the auth/deauth handler
  44. _mapper configmap.Mapper
  45. _saltedKeyPass string
  46. )
  47. // Register with Fs
  48. func init() {
  49. fs.Register(&fs.RegInfo{
  50. Name: "protondrive",
  51. Description: "Proton Drive",
  52. NewFs: NewFs,
  53. Options: []fs.Option{{
  54. Name: "username",
  55. Help: `The username of your proton account`,
  56. Required: true,
  57. }, {
  58. Name: "password",
  59. Help: "The password of your proton account.",
  60. Required: true,
  61. IsPassword: true,
  62. }, {
  63. Name: "mailbox_password",
  64. Help: `The mailbox password of your two-password proton account.
  65. For more information regarding the mailbox password, please check the
  66. following official knowledge base article:
  67. https://proton.me/support/the-difference-between-the-mailbox-password-and-login-password
  68. `,
  69. IsPassword: true,
  70. Advanced: true,
  71. }, {
  72. Name: "2fa",
  73. Help: `The 2FA code
  74. The value can also be provided with --protondrive-2fa=000000
  75. The 2FA code of your proton drive account if the account is set up with
  76. two-factor authentication`,
  77. Required: false,
  78. }, {
  79. Name: clientUIDKey,
  80. Help: "Client uid key (internal use only)",
  81. Required: false,
  82. Advanced: true,
  83. Sensitive: true,
  84. Hide: fs.OptionHideBoth,
  85. }, {
  86. Name: clientAccessTokenKey,
  87. Help: "Client access token key (internal use only)",
  88. Required: false,
  89. Advanced: true,
  90. Sensitive: true,
  91. Hide: fs.OptionHideBoth,
  92. }, {
  93. Name: clientRefreshTokenKey,
  94. Help: "Client refresh token key (internal use only)",
  95. Required: false,
  96. Advanced: true,
  97. Sensitive: true,
  98. Hide: fs.OptionHideBoth,
  99. }, {
  100. Name: clientSaltedKeyPassKey,
  101. Help: "Client salted key pass key (internal use only)",
  102. Required: false,
  103. Advanced: true,
  104. Sensitive: true,
  105. Hide: fs.OptionHideBoth,
  106. }, {
  107. Name: config.ConfigEncoding,
  108. Help: config.ConfigEncodingHelp,
  109. Advanced: true,
  110. Default: (encoder.Base |
  111. encoder.EncodeInvalidUtf8 |
  112. encoder.EncodeLeftSpace |
  113. encoder.EncodeRightSpace),
  114. }, {
  115. Name: "original_file_size",
  116. Help: `Return the file size before encryption
  117. The size of the encrypted file will be different from (bigger than) the
  118. original file size. Unless there is a reason to return the file size
  119. after encryption is performed, otherwise, set this option to true, as
  120. features like Open() which will need to be supplied with original content
  121. size, will fail to operate properly`,
  122. Advanced: true,
  123. Default: true,
  124. }, {
  125. Name: "app_version",
  126. Help: `The app version string
  127. The app version string indicates the client that is currently performing
  128. the API request. This information is required and will be sent with every
  129. API request.`,
  130. Advanced: true,
  131. Default: "macos-drive@1.0.0-alpha.1+rclone",
  132. }, {
  133. Name: "replace_existing_draft",
  134. Help: `Create a new revision when filename conflict is detected
  135. When a file upload is cancelled or failed before completion, a draft will be
  136. created and the subsequent upload of the same file to the same location will be
  137. reported as a conflict.
  138. The value can also be set by --protondrive-replace-existing-draft=true
  139. If the option is set to true, the draft will be replaced and then the upload
  140. operation will restart. If there are other clients also uploading at the same
  141. file location at the same time, the behavior is currently unknown. Need to set
  142. to true for integration tests.
  143. If the option is set to false, an error "a draft exist - usually this means a
  144. file is being uploaded at another client, or, there was a failed upload attempt"
  145. will be returned, and no upload will happen.`,
  146. Advanced: true,
  147. Default: false,
  148. }, {
  149. Name: "enable_caching",
  150. Help: `Caches the files and folders metadata to reduce API calls
  151. Notice: If you are mounting ProtonDrive as a VFS, please disable this feature,
  152. as the current implementation doesn't update or clear the cache when there are
  153. external changes.
  154. The files and folders on ProtonDrive are represented as links with keyrings,
  155. which can be cached to improve performance and be friendly to the API server.
  156. The cache is currently built for the case when the rclone is the only instance
  157. performing operations to the mount point. The event system, which is the proton
  158. API system that provides visibility of what has changed on the drive, is yet
  159. to be implemented, so updates from other clients won’t be reflected in the
  160. cache. Thus, if there are concurrent clients accessing the same mount point,
  161. then we might have a problem with caching the stale data.`,
  162. Advanced: true,
  163. Default: true,
  164. }},
  165. })
  166. }
  167. // Options defines the configuration for this backend
  168. type Options struct {
  169. Username string `config:"username"`
  170. Password string `config:"password"`
  171. MailboxPassword string `config:"mailbox_password"`
  172. TwoFA string `config:"2fa"`
  173. // advanced
  174. Enc encoder.MultiEncoder `config:"encoding"`
  175. ReportOriginalSize bool `config:"original_file_size"`
  176. AppVersion string `config:"app_version"`
  177. ReplaceExistingDraft bool `config:"replace_existing_draft"`
  178. EnableCaching bool `config:"enable_caching"`
  179. }
  180. // Fs represents a remote proton drive
  181. type Fs struct {
  182. name string // name of this remote
  183. // Notice that for ProtonDrive, it's attached under rootLink (usually /root)
  184. root string // the path we are working on.
  185. opt Options // parsed config options
  186. ci *fs.ConfigInfo // global config
  187. features *fs.Features // optional features
  188. pacer *fs.Pacer // pacer for API calls
  189. dirCache *dircache.DirCache // Map of directory path to directory id
  190. protonDrive *protonDriveAPI.ProtonDrive // the Proton API bridging library
  191. }
  192. // Object describes an object
  193. type Object struct {
  194. fs *Fs // what this object is part of
  195. remote string // The remote path (relative to the fs.root)
  196. size int64 // size of the object (on server, after encryption)
  197. originalSize *int64 // size of the object (after decryption)
  198. digests *string // object original content
  199. blockSizes []int64 // the block sizes of the encrypted file
  200. modTime time.Time // modification time of the object
  201. createdTime time.Time // creation time of the object
  202. id string // ID of the object
  203. mimetype string // mimetype of the file
  204. link *proton.Link // link data on proton server
  205. }
  206. // shouldRetry returns a boolean as to whether this err deserves to be
  207. // retried. It returns the err as a convenience
  208. func shouldRetry(ctx context.Context, err error) (bool, error) {
  209. return false, err
  210. }
  211. //------------------------------------------------------------------------------
  212. // Name of the remote (as passed into NewFs)
  213. func (f *Fs) Name() string {
  214. return f.name
  215. }
  216. // Root of the remote (as passed into NewFs)
  217. func (f *Fs) Root() string {
  218. return f.opt.Enc.ToStandardPath(f.root)
  219. }
  220. // String converts this Fs to a string
  221. func (f *Fs) String() string {
  222. return fmt.Sprintf("proton drive root link ID '%s'", f.root)
  223. }
  224. // Features returns the optional features of this Fs
  225. func (f *Fs) Features() *fs.Features {
  226. return f.features
  227. }
  228. // run all the dir/remote through this
  229. func (f *Fs) sanitizePath(_path string) string {
  230. _path = path.Clean(_path)
  231. if _path == "." || _path == "/" {
  232. return ""
  233. }
  234. return f.opt.Enc.FromStandardPath(_path)
  235. }
  236. func getConfigMap(m configmap.Mapper) (uid, accessToken, refreshToken, saltedKeyPass string, ok bool) {
  237. if accessToken, ok = m.Get(clientAccessTokenKey); !ok {
  238. return
  239. }
  240. if uid, ok = m.Get(clientUIDKey); !ok {
  241. return
  242. }
  243. if refreshToken, ok = m.Get(clientRefreshTokenKey); !ok {
  244. return
  245. }
  246. if saltedKeyPass, ok = m.Get(clientSaltedKeyPassKey); !ok {
  247. return
  248. }
  249. _saltedKeyPass = saltedKeyPass
  250. return
  251. }
  252. func setConfigMap(m configmap.Mapper, uid, accessToken, refreshToken, saltedKeyPass string) {
  253. m.Set(clientUIDKey, uid)
  254. m.Set(clientAccessTokenKey, accessToken)
  255. m.Set(clientRefreshTokenKey, refreshToken)
  256. m.Set(clientSaltedKeyPassKey, saltedKeyPass)
  257. _saltedKeyPass = saltedKeyPass
  258. }
  259. func clearConfigMap(m configmap.Mapper) {
  260. setConfigMap(m, "", "", "", "")
  261. _saltedKeyPass = ""
  262. }
  263. func authHandler(auth proton.Auth) {
  264. // fs.Debugf("authHandler called")
  265. setConfigMap(_mapper, auth.UID, auth.AccessToken, auth.RefreshToken, _saltedKeyPass)
  266. }
  267. func deAuthHandler() {
  268. // fs.Debugf("deAuthHandler called")
  269. clearConfigMap(_mapper)
  270. }
  271. func newProtonDrive(ctx context.Context, f *Fs, opt *Options, m configmap.Mapper) (*protonDriveAPI.ProtonDrive, error) {
  272. config := protonDriveAPI.NewDefaultConfig()
  273. config.AppVersion = opt.AppVersion
  274. config.UserAgent = f.ci.UserAgent // opt.UserAgent
  275. config.ReplaceExistingDraft = opt.ReplaceExistingDraft
  276. config.EnableCaching = opt.EnableCaching
  277. // let's see if we have the cached access credential
  278. uid, accessToken, refreshToken, saltedKeyPass, hasUseReusableLoginCredentials := getConfigMap(m)
  279. _saltedKeyPass = saltedKeyPass
  280. if hasUseReusableLoginCredentials {
  281. fs.Debugf(f, "Has cached credentials")
  282. config.UseReusableLogin = true
  283. config.ReusableCredential.UID = uid
  284. config.ReusableCredential.AccessToken = accessToken
  285. config.ReusableCredential.RefreshToken = refreshToken
  286. config.ReusableCredential.SaltedKeyPass = saltedKeyPass
  287. protonDrive /* credential will be nil since access credentials are passed in */, _, err := protonDriveAPI.NewProtonDrive(ctx, config, authHandler, deAuthHandler)
  288. if err != nil {
  289. fs.Debugf(f, "Cached credential doesn't work, clearing and using the fallback login method")
  290. // clear the access token on failure
  291. clearConfigMap(m)
  292. fs.Debugf(f, "couldn't initialize a new proton drive instance using cached credentials: %v", err)
  293. // we fallback to username+password login -> don't throw an error here
  294. // return nil, fmt.Errorf("couldn't initialize a new proton drive instance: %w", err)
  295. } else {
  296. fs.Debugf(f, "Used cached credential to initialize the ProtonDrive API")
  297. return protonDrive, nil
  298. }
  299. }
  300. // if not, let's try to log the user in using username and password (and 2FA if required)
  301. fs.Debugf(f, "Using username and password to log in")
  302. config.UseReusableLogin = false
  303. config.FirstLoginCredential.Username = opt.Username
  304. config.FirstLoginCredential.Password = opt.Password
  305. config.FirstLoginCredential.MailboxPassword = opt.MailboxPassword
  306. config.FirstLoginCredential.TwoFA = opt.TwoFA
  307. protonDrive, auth, err := protonDriveAPI.NewProtonDrive(ctx, config, authHandler, deAuthHandler)
  308. if err != nil {
  309. return nil, fmt.Errorf("couldn't initialize a new proton drive instance: %w", err)
  310. }
  311. fs.Debugf(f, "Used username and password to initialize the ProtonDrive API")
  312. setConfigMap(m, auth.UID, auth.AccessToken, auth.RefreshToken, auth.SaltedKeyPass)
  313. return protonDrive, nil
  314. }
  315. // NewFs constructs an Fs from the path, container:path
  316. func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
  317. // pacer is not used in NewFs()
  318. _mapper = m
  319. // Parse config into Options struct
  320. opt := new(Options)
  321. err := configstruct.Set(m, opt)
  322. if err != nil {
  323. return nil, err
  324. }
  325. if opt.Password != "" {
  326. var err error
  327. opt.Password, err = obscure.Reveal(opt.Password)
  328. if err != nil {
  329. return nil, fmt.Errorf("couldn't decrypt password: %w", err)
  330. }
  331. }
  332. if opt.MailboxPassword != "" {
  333. var err error
  334. opt.MailboxPassword, err = obscure.Reveal(opt.MailboxPassword)
  335. if err != nil {
  336. return nil, fmt.Errorf("couldn't decrypt mailbox password: %w", err)
  337. }
  338. }
  339. ci := fs.GetConfig(ctx)
  340. root = strings.Trim(root, "/")
  341. f := &Fs{
  342. name: name,
  343. root: root,
  344. opt: *opt,
  345. ci: ci,
  346. pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
  347. }
  348. f.features = (&fs.Features{
  349. ReadMimeType: true,
  350. CanHaveEmptyDirectories: true,
  351. /* can't have multiple threads downloading
  352. The raw file is split into equally-sized (currently 4MB, but it might change in the future, say to 8MB, 16MB, etc.) blocks, except the last one which might be smaller than 4MB.
  353. 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.
  354. 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!
  355. */
  356. NoMultiThreading: true,
  357. }).Fill(ctx, f)
  358. protonDrive, err := newProtonDrive(ctx, f, opt, m)
  359. if err != nil {
  360. return nil, err
  361. }
  362. f.protonDrive = protonDrive
  363. root = f.sanitizePath(root)
  364. f.dirCache = dircache.New(
  365. root, /* root folder path */
  366. protonDrive.MainShare.LinkID, /* real root ID is the root folder, since we can't go past this folder */
  367. f,
  368. )
  369. err = f.dirCache.FindRoot(ctx, false)
  370. if err != nil {
  371. // if the root directory is not found, the initialization will still work
  372. // but if it's other kinds of error, then we raise it
  373. if err != fs.ErrorDirNotFound {
  374. return nil, fmt.Errorf("couldn't initialize a new root remote: %w", err)
  375. }
  376. // Assume it is a file (taken and modified from box.go)
  377. newRoot, remote := dircache.SplitPath(root)
  378. tempF := *f
  379. tempF.dirCache = dircache.New(newRoot, protonDrive.MainShare.LinkID, &tempF)
  380. tempF.root = newRoot
  381. // Make new Fs which is the parent
  382. err = tempF.dirCache.FindRoot(ctx, false)
  383. if err != nil {
  384. // No root so return old f
  385. return f, nil
  386. }
  387. _, err := tempF.newObjectWithLink(ctx, remote, nil)
  388. if err != nil {
  389. if err == fs.ErrorObjectNotFound {
  390. // File doesn't exist so return old f
  391. return f, nil
  392. }
  393. return nil, err
  394. }
  395. f.features.Fill(ctx, &tempF)
  396. // XXX: update the old f here instead of returning tempF, since
  397. // `features` were already filled with functions having *f as a receiver.
  398. // See https://github.com/rclone/rclone/issues/2182
  399. f.dirCache = tempF.dirCache
  400. f.root = tempF.root
  401. // return an error with an fs which points to the parent
  402. return f, fs.ErrorIsFile
  403. }
  404. return f, nil
  405. }
  406. //------------------------------------------------------------------------------
  407. // CleanUp deletes all files currently in trash
  408. func (f *Fs) CleanUp(ctx context.Context) error {
  409. return f.pacer.Call(func() (bool, error) {
  410. err := f.protonDrive.EmptyTrash(ctx)
  411. return shouldRetry(ctx, err)
  412. })
  413. }
  414. // NewObject finds the Object at remote. If it can't be found
  415. // it returns the error ErrorObjectNotFound.
  416. //
  417. // If remote points to a directory then it should return
  418. // ErrorIsDir if possible without doing any extra work,
  419. // otherwise ErrorObjectNotFound.
  420. func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  421. return f.newObjectWithLink(ctx, remote, nil)
  422. }
  423. func (f *Fs) getObjectLink(ctx context.Context, remote string) (*proton.Link, error) {
  424. // attempt to locate the file
  425. leaf, folderLinkID, err := f.dirCache.FindPath(ctx, f.sanitizePath(remote), false)
  426. if err != nil {
  427. if err == fs.ErrorDirNotFound {
  428. // parent folder of the file not found, we for sure can't find the file
  429. return nil, fs.ErrorObjectNotFound
  430. }
  431. // other error has occurred
  432. return nil, err
  433. }
  434. var link *proton.Link
  435. if err = f.pacer.Call(func() (bool, error) {
  436. link, err = f.protonDrive.SearchByNameInActiveFolderByID(ctx, folderLinkID, leaf, true, false, proton.LinkStateActive)
  437. return shouldRetry(ctx, err)
  438. }); err != nil {
  439. return nil, err
  440. }
  441. if link == nil { // both link and err are nil, file not found
  442. return nil, fs.ErrorObjectNotFound
  443. }
  444. return link, nil
  445. }
  446. // readMetaDataForRemote reads the metadata from the remote
  447. func (f *Fs) readMetaDataForRemote(ctx context.Context, remote string, _link *proton.Link) (*proton.Link, *protonDriveAPI.FileSystemAttrs, error) {
  448. link, err := f.getObjectLink(ctx, remote)
  449. if err != nil {
  450. return nil, nil, err
  451. }
  452. var fileSystemAttrs *protonDriveAPI.FileSystemAttrs
  453. if err = f.pacer.Call(func() (bool, error) {
  454. fileSystemAttrs, err = f.protonDrive.GetActiveRevisionAttrs(ctx, link)
  455. return shouldRetry(ctx, err)
  456. }); err != nil {
  457. return nil, nil, err
  458. }
  459. return link, fileSystemAttrs, nil
  460. }
  461. // readMetaData gets the metadata if it hasn't already been fetched
  462. //
  463. // it also sets the info
  464. func (o *Object) readMetaData(ctx context.Context, link *proton.Link) (err error) {
  465. if o.link != nil {
  466. return nil
  467. }
  468. link, fileSystemAttrs, err := o.fs.readMetaDataForRemote(ctx, o.remote, link)
  469. if err != nil {
  470. return err
  471. }
  472. o.id = link.LinkID
  473. o.size = link.Size
  474. o.modTime = time.Unix(link.ModifyTime, 0)
  475. o.createdTime = time.Unix(link.CreateTime, 0)
  476. o.mimetype = link.MIMEType
  477. o.link = link
  478. if fileSystemAttrs != nil {
  479. o.modTime = fileSystemAttrs.ModificationTime
  480. o.originalSize = &fileSystemAttrs.Size
  481. o.blockSizes = fileSystemAttrs.BlockSizes
  482. o.digests = &fileSystemAttrs.Digests
  483. }
  484. return nil
  485. }
  486. // Return an Object from a path
  487. //
  488. // If it can't be found it returns the error fs.ErrorObjectNotFound.
  489. func (f *Fs) newObjectWithLink(ctx context.Context, remote string, link *proton.Link) (fs.Object, error) {
  490. o := &Object{
  491. fs: f,
  492. remote: remote,
  493. }
  494. err := o.readMetaData(ctx, link)
  495. if err != nil {
  496. return nil, err
  497. }
  498. return o, nil
  499. }
  500. // List the objects and directories in dir into entries. The
  501. // entries can be returned in any order but should be for a
  502. // complete directory.
  503. //
  504. // dir should be "" to list the root, and should not have
  505. // trailing slashes.
  506. //
  507. // This should return ErrDirNotFound if the directory isn't
  508. // found.
  509. // Notice that this function is expensive since everything on proton is encrypted
  510. // So having a remote with 10k files, during operations like sync, might take a while and lots of bandwidth!
  511. func (f *Fs) List(ctx context.Context, dir string) (fs.DirEntries, error) {
  512. folderLinkID, err := f.dirCache.FindDir(ctx, f.sanitizePath(dir), false) // will handle ErrDirNotFound here
  513. if err != nil {
  514. return nil, err
  515. }
  516. var foldersAndFiles []*protonDriveAPI.ProtonDirectoryData
  517. if err = f.pacer.Call(func() (bool, error) {
  518. foldersAndFiles, err = f.protonDrive.ListDirectory(ctx, folderLinkID)
  519. return shouldRetry(ctx, err)
  520. }); err != nil {
  521. return nil, err
  522. }
  523. entries := make(fs.DirEntries, 0)
  524. for i := range foldersAndFiles {
  525. remote := path.Join(dir, f.opt.Enc.ToStandardName(foldersAndFiles[i].Name))
  526. if foldersAndFiles[i].IsFolder {
  527. f.dirCache.Put(remote, foldersAndFiles[i].Link.LinkID)
  528. d := fs.NewDir(remote, time.Unix(foldersAndFiles[i].Link.ModifyTime, 0)).SetID(foldersAndFiles[i].Link.LinkID)
  529. entries = append(entries, d)
  530. } else {
  531. obj, err := f.newObjectWithLink(ctx, remote, foldersAndFiles[i].Link)
  532. if err != nil {
  533. return nil, err
  534. }
  535. entries = append(entries, obj)
  536. }
  537. }
  538. return entries, nil
  539. }
  540. // FindLeaf finds a directory of name leaf in the folder with ID pathID
  541. //
  542. // This should be implemented by the backend and will be called by the
  543. // dircache package when appropriate.
  544. func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (string, bool, error) {
  545. /* f.opt.Enc.FromStandardName(leaf) not required since the DirCache only process sanitized path */
  546. var link *proton.Link
  547. var err error
  548. if err = f.pacer.Call(func() (bool, error) {
  549. link, err = f.protonDrive.SearchByNameInActiveFolderByID(ctx, pathID, leaf, false, true, proton.LinkStateActive)
  550. return shouldRetry(ctx, err)
  551. }); err != nil {
  552. return "", false, err
  553. }
  554. if link == nil {
  555. return "", false, nil
  556. }
  557. return link.LinkID, true, nil
  558. }
  559. // CreateDir makes a directory with pathID as parent and name leaf
  560. //
  561. // This should be implemented by the backend and will be called by the
  562. // dircache package when appropriate.
  563. func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (string, error) {
  564. /* f.opt.Enc.FromStandardName(leaf) not required since the DirCache only process sanitized path */
  565. var newID string
  566. var err error
  567. if err = f.pacer.Call(func() (bool, error) {
  568. newID, err = f.protonDrive.CreateNewFolderByID(ctx, pathID, leaf)
  569. return shouldRetry(ctx, err)
  570. }); err != nil {
  571. return "", err
  572. }
  573. return newID, err
  574. }
  575. // Put in to the remote path with the modTime given of the given size
  576. //
  577. // When called from outside an Fs by rclone, src.Size() will always be >= 0.
  578. // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
  579. // return an error or upload it properly (rather than e.g. calling panic).
  580. //
  581. // May create the object even if it returns an error - if so
  582. // will return the object and the error, otherwise will return
  583. // nil and the error
  584. func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  585. size := src.Size()
  586. if size < 0 {
  587. return nil, errCanNotUploadFileWithUnknownSize
  588. }
  589. existingObj, err := f.NewObject(ctx, src.Remote())
  590. switch err {
  591. case nil:
  592. // object is found, we add an revision to it
  593. return existingObj, existingObj.Update(ctx, in, src, options...)
  594. case fs.ErrorObjectNotFound:
  595. // object not found, so we need to create it
  596. remote := src.Remote()
  597. size := src.Size()
  598. modTime := src.ModTime(ctx)
  599. obj, err := f.createObject(ctx, remote, modTime, size)
  600. if err != nil {
  601. return nil, err
  602. }
  603. return obj, obj.Update(ctx, in, src, options...)
  604. default:
  605. // real error caught
  606. return nil, err
  607. }
  608. }
  609. // Creates from the parameters passed in a half finished Object which
  610. // must have setMetaData called on it
  611. //
  612. // Returns the object, leaf, directoryID and error.
  613. //
  614. // Used to create new objects
  615. func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (*Object, error) {
  616. // ˇ-------ˇ filename
  617. // e.g. /root/a/b/c/test.txt
  618. // ^~~~~~~~~~~^ dirPath
  619. // Create the directory for the object if it doesn't exist
  620. _, _, err := f.dirCache.FindPath(ctx, f.sanitizePath(remote), true)
  621. if err != nil {
  622. return nil, err
  623. }
  624. // Temporary Object under construction
  625. obj := &Object{
  626. fs: f,
  627. remote: remote,
  628. size: size,
  629. originalSize: nil,
  630. id: "",
  631. modTime: modTime,
  632. mimetype: "",
  633. link: nil,
  634. }
  635. return obj, nil
  636. }
  637. // Mkdir makes the directory (container, bucket)
  638. //
  639. // Shouldn't return an error if it already exists
  640. func (f *Fs) Mkdir(ctx context.Context, dir string) error {
  641. _, err := f.dirCache.FindDir(ctx, f.sanitizePath(dir), true)
  642. return err
  643. }
  644. // Rmdir removes the directory (container, bucket) if empty
  645. //
  646. // Return an error if it doesn't exist or isn't empty
  647. func (f *Fs) Rmdir(ctx context.Context, dir string) error {
  648. folderLinkID, err := f.dirCache.FindDir(ctx, f.sanitizePath(dir), false)
  649. if err == fs.ErrorDirNotFound {
  650. return fmt.Errorf("[Rmdir] cannot find LinkID for dir %s (%s)", dir, f.sanitizePath(dir))
  651. } else if err != nil {
  652. return err
  653. }
  654. if err = f.pacer.Call(func() (bool, error) {
  655. err = f.protonDrive.MoveFolderToTrashByID(ctx, folderLinkID, true)
  656. return shouldRetry(ctx, err)
  657. }); err != nil {
  658. return err
  659. }
  660. f.dirCache.FlushDir(f.sanitizePath(dir))
  661. return nil
  662. }
  663. // Precision of the ModTimes in this Fs
  664. func (f *Fs) Precision() time.Duration {
  665. return time.Second
  666. }
  667. // DirCacheFlush an optional interface to flush internal directory cache
  668. // DirCacheFlush resets the directory cache - used in testing
  669. // as an optional interface
  670. func (f *Fs) DirCacheFlush() {
  671. f.dirCache.ResetRoot()
  672. f.protonDrive.ClearCache()
  673. }
  674. // Hashes returns the supported hash types of the filesystem
  675. func (f *Fs) Hashes() hash.Set {
  676. return hash.Set(hash.SHA1)
  677. }
  678. // About gets quota information
  679. func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
  680. var user *proton.User
  681. var err error
  682. if err = f.pacer.Call(func() (bool, error) {
  683. user, err = f.protonDrive.About(ctx)
  684. return shouldRetry(ctx, err)
  685. }); err != nil {
  686. return nil, err
  687. }
  688. total := user.MaxSpace
  689. used := user.UsedSpace
  690. free := total - used
  691. usage := &fs.Usage{
  692. Total: &total,
  693. Used: &used,
  694. Free: &free,
  695. }
  696. return usage, nil
  697. }
  698. // ------------------------------------------------------------
  699. // Fs returns the parent Fs
  700. func (o *Object) Fs() fs.Info {
  701. return o.fs
  702. }
  703. // Return a string version
  704. func (o *Object) String() string {
  705. if o == nil {
  706. return "<nil>"
  707. }
  708. return o.remote
  709. }
  710. // Remote returns the remote path
  711. func (o *Object) Remote() string {
  712. return o.remote
  713. }
  714. // Hash returns the hashes of an object
  715. func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
  716. if t != hash.SHA1 {
  717. return "", hash.ErrUnsupported
  718. }
  719. if o.digests != nil {
  720. return *o.digests, nil
  721. }
  722. // sha1 not cached: we fetch and try to obtain the sha1 of the link
  723. fileSystemAttrs, err := o.fs.protonDrive.GetActiveRevisionAttrsByID(ctx, o.ID())
  724. if err != nil {
  725. return "", err
  726. }
  727. if fileSystemAttrs == nil || fileSystemAttrs.Digests == "" {
  728. fs.Debugf(o, "file sha1 digest missing")
  729. return "", nil
  730. }
  731. return fileSystemAttrs.Digests, nil
  732. }
  733. // Size returns the size of an object in bytes
  734. func (o *Object) Size() int64 {
  735. if o.fs.opt.ReportOriginalSize {
  736. // if ReportOriginalSize is set, we will generate an error when the original size failed to be parsed
  737. // this is crucial as features like Open() will need to use the proper size to operate the seek/range operator
  738. if o.originalSize != nil {
  739. return *o.originalSize
  740. }
  741. fs.Debugf(o, "Original file size missing")
  742. }
  743. return o.size
  744. }
  745. // ModTime returns the modification time of the object
  746. //
  747. // It attempts to read the objects mtime and if that isn't present the
  748. // LastModified returned in the http headers
  749. func (o *Object) ModTime(ctx context.Context) time.Time {
  750. return o.modTime
  751. }
  752. // SetModTime sets the modification time of the local fs object
  753. func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  754. return fs.ErrorCantSetModTime
  755. }
  756. // Storable returns a boolean showing whether this object storable
  757. func (o *Object) Storable() bool {
  758. return true
  759. }
  760. // Open opens the file for read. Call Close() on the returned io.ReadCloser
  761. func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
  762. fs.FixRangeOption(options, *o.originalSize)
  763. var offset, limit int64 = 0, -1
  764. for _, option := range options { // if the caller passes in nil for options, it will become array of nil
  765. switch x := option.(type) {
  766. case *fs.SeekOption:
  767. offset = x.Offset
  768. case *fs.RangeOption:
  769. offset, limit = x.Decode(o.Size())
  770. default:
  771. if option.Mandatory() {
  772. fs.Logf(o, "Unsupported mandatory option: %v", option)
  773. }
  774. }
  775. }
  776. // download and decrypt the file
  777. var reader io.ReadCloser
  778. var fileSystemAttrs *protonDriveAPI.FileSystemAttrs
  779. var sizeOnServer int64
  780. var err error
  781. if err = o.fs.pacer.Call(func() (bool, error) {
  782. reader, sizeOnServer, fileSystemAttrs, err = o.fs.protonDrive.DownloadFileByID(ctx, o.id, offset)
  783. return shouldRetry(ctx, err)
  784. }); err != nil {
  785. return nil, err
  786. }
  787. if fileSystemAttrs != nil {
  788. o.originalSize = &fileSystemAttrs.Size
  789. o.modTime = fileSystemAttrs.ModificationTime
  790. o.digests = &fileSystemAttrs.Digests
  791. o.blockSizes = fileSystemAttrs.BlockSizes
  792. } else {
  793. fs.Debugf(o, "fileSystemAttrs is nil: using fallback size, and now digests and blocksizes available")
  794. o.originalSize = &sizeOnServer
  795. o.size = sizeOnServer
  796. o.digests = nil
  797. o.blockSizes = nil
  798. }
  799. retReader := io.NopCloser(reader) // the NewLimitedReadCloser will deal with the limit
  800. // deal with limit
  801. return readers.NewLimitedReadCloser(retReader, limit), nil
  802. }
  803. // Update in to the object with the modTime given of the given size
  804. //
  805. // When called from outside an Fs by rclone, src.Size() will always be >= 0.
  806. // But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
  807. // return an error or update the object properly (rather than e.g. calling panic).
  808. func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
  809. size := src.Size()
  810. if size < 0 {
  811. return errCanNotUploadFileWithUnknownSize
  812. }
  813. remote := o.Remote()
  814. leaf, folderLinkID, err := o.fs.dirCache.FindPath(ctx, o.fs.sanitizePath(remote), true)
  815. if err != nil {
  816. return err
  817. }
  818. modTime := src.ModTime(ctx)
  819. var linkID string
  820. var fileSystemAttrs *proton.RevisionXAttrCommon
  821. if err = o.fs.pacer.Call(func() (bool, error) {
  822. linkID, fileSystemAttrs, err = o.fs.protonDrive.UploadFileByReader(ctx, folderLinkID, leaf, modTime, in, 0)
  823. return shouldRetry(ctx, err)
  824. }); err != nil {
  825. return err
  826. }
  827. var sha1Hash string
  828. if val, ok := fileSystemAttrs.Digests["SHA1"]; ok {
  829. sha1Hash = val
  830. } else {
  831. sha1Hash = ""
  832. }
  833. o.id = linkID
  834. o.originalSize = &fileSystemAttrs.Size
  835. o.modTime = modTime
  836. o.blockSizes = fileSystemAttrs.BlockSizes
  837. o.digests = &sha1Hash
  838. return nil
  839. }
  840. // Remove an object
  841. func (o *Object) Remove(ctx context.Context) error {
  842. return o.fs.pacer.Call(func() (bool, error) {
  843. err := o.fs.protonDrive.MoveFileToTrashByID(ctx, o.id)
  844. return shouldRetry(ctx, err)
  845. })
  846. }
  847. // ID returns the ID of the Object if known, or "" if not
  848. func (o *Object) ID() string {
  849. return o.id
  850. }
  851. // Purge all files in the directory specified
  852. //
  853. // Implement this if you have a way of deleting all the files
  854. // quicker than just running Remove() on the result of List()
  855. //
  856. // Return an error if it doesn't exist
  857. func (f *Fs) Purge(ctx context.Context, dir string) error {
  858. root := path.Join(f.root, dir)
  859. if root == "" {
  860. // we can't remove the root directory, but we can list the directory and delete every folder and file in here
  861. return errCanNotPurgeRootDirectory
  862. }
  863. folderLinkID, err := f.dirCache.FindDir(ctx, f.sanitizePath(dir), false)
  864. if err != nil {
  865. return err
  866. }
  867. if err = f.pacer.Call(func() (bool, error) {
  868. err = f.protonDrive.MoveFolderToTrashByID(ctx, folderLinkID, false)
  869. return shouldRetry(ctx, err)
  870. }); err != nil {
  871. return err
  872. }
  873. f.dirCache.FlushDir(dir)
  874. return nil
  875. }
  876. // MimeType of an Object if known, "" otherwise
  877. func (o *Object) MimeType(ctx context.Context) string {
  878. return o.mimetype
  879. }
  880. // Disconnect the current user
  881. func (f *Fs) Disconnect(ctx context.Context) error {
  882. return f.pacer.Call(func() (bool, error) {
  883. err := f.protonDrive.Logout(ctx)
  884. return shouldRetry(ctx, err)
  885. })
  886. }
  887. // Move src to this remote using server-side move operations.
  888. //
  889. // This is stored with the remote path given.
  890. //
  891. // It returns the destination Object and a possible error.
  892. //
  893. // Will only be called if src.Fs().Name() == f.Name()
  894. //
  895. // If it isn't possible then return fs.ErrorCantMove
  896. func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  897. srcObj, ok := src.(*Object)
  898. if !ok {
  899. fs.Debugf(src, "Can't move - not same remote type")
  900. return nil, fs.ErrorCantMove
  901. }
  902. // check if the remote (dst) exists
  903. _, err := f.NewObject(ctx, remote)
  904. if err != nil {
  905. if err != fs.ErrorObjectNotFound {
  906. return nil, err
  907. }
  908. // object is indeed not found
  909. } else {
  910. // object at the dst exists
  911. return nil, fs.ErrorCantMove
  912. }
  913. // attempt the move
  914. dstLeaf, dstDirectoryID, err := f.dirCache.FindPath(ctx, f.sanitizePath(remote), true)
  915. if err != nil {
  916. return nil, err
  917. }
  918. if err = f.pacer.Call(func() (bool, error) {
  919. err = f.protonDrive.MoveFileByID(ctx, srcObj.id, dstDirectoryID, dstLeaf)
  920. return shouldRetry(ctx, err)
  921. }); err != nil {
  922. return nil, err
  923. }
  924. f.dirCache.FlushDir(f.sanitizePath(src.Remote()))
  925. return f.NewObject(ctx, remote)
  926. }
  927. // DirMove moves src, srcRemote to this remote at dstRemote
  928. // using server-side move operations.
  929. //
  930. // Will only be called if src.Fs().Name() == f.Name()
  931. //
  932. // If it isn't possible then return fs.ErrorCantDirMove
  933. //
  934. // If destination exists then return fs.ErrorDirExists
  935. func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
  936. srcFs, ok := src.(*Fs)
  937. if !ok {
  938. fs.Debugf(srcFs, "Can't move directory - not same remote type")
  939. return fs.ErrorCantDirMove
  940. }
  941. srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, f.sanitizePath(srcFs.root), f.sanitizePath(srcRemote), f.sanitizePath(f.root), f.sanitizePath(dstRemote))
  942. if err != nil {
  943. return err
  944. }
  945. if err = f.pacer.Call(func() (bool, error) {
  946. err = f.protonDrive.MoveFolderByID(ctx, srcID, dstDirectoryID, dstLeaf)
  947. return shouldRetry(ctx, err)
  948. }); err != nil {
  949. return err
  950. }
  951. srcFs.dirCache.FlushDir(f.sanitizePath(srcRemote))
  952. return nil
  953. }
  954. // Check the interfaces are satisfied
  955. var (
  956. _ fs.Fs = (*Fs)(nil)
  957. _ fs.Mover = (*Fs)(nil)
  958. _ fs.DirMover = (*Fs)(nil)
  959. _ fs.DirCacheFlusher = (*Fs)(nil)
  960. _ fs.Abouter = (*Fs)(nil)
  961. _ fs.Object = (*Object)(nil)
  962. _ fs.MimeTyper = (*Object)(nil)
  963. _ fs.IDer = (*Object)(nil)
  964. )