fichier.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. // Package fichier provides an interface to the 1Fichier storage system.
  2. package fichier
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/rclone/rclone/fs"
  13. "github.com/rclone/rclone/fs/config"
  14. "github.com/rclone/rclone/fs/config/configmap"
  15. "github.com/rclone/rclone/fs/config/configstruct"
  16. "github.com/rclone/rclone/fs/fshttp"
  17. "github.com/rclone/rclone/fs/hash"
  18. "github.com/rclone/rclone/lib/dircache"
  19. "github.com/rclone/rclone/lib/encoder"
  20. "github.com/rclone/rclone/lib/pacer"
  21. "github.com/rclone/rclone/lib/rest"
  22. )
  23. const (
  24. rootID = "0"
  25. apiBaseURL = "https://api.1fichier.com/v1"
  26. minSleep = 400 * time.Millisecond // api is extremely rate limited now
  27. maxSleep = 5 * time.Second
  28. decayConstant = 2 // bigger for slower decay, exponential
  29. attackConstant = 0 // start with max sleep
  30. )
  31. func init() {
  32. fs.Register(&fs.RegInfo{
  33. Name: "fichier",
  34. Description: "1Fichier",
  35. NewFs: NewFs,
  36. Options: []fs.Option{{
  37. Help: "Your API Key, get it from https://1fichier.com/console/params.pl.",
  38. Name: "api_key",
  39. Sensitive: true,
  40. }, {
  41. Help: "If you want to download a shared folder, add this parameter.",
  42. Name: "shared_folder",
  43. Advanced: true,
  44. }, {
  45. Help: "If you want to download a shared file that is password protected, add this parameter.",
  46. Name: "file_password",
  47. Advanced: true,
  48. IsPassword: true,
  49. }, {
  50. Help: "If you want to list the files in a shared folder that is password protected, add this parameter.",
  51. Name: "folder_password",
  52. Advanced: true,
  53. IsPassword: true,
  54. }, {
  55. Help: "Set if you wish to use CDN download links.",
  56. Name: "cdn",
  57. Default: false,
  58. Advanced: true,
  59. }, {
  60. Name: config.ConfigEncoding,
  61. Help: config.ConfigEncodingHelp,
  62. Advanced: true,
  63. // Characters that need escaping
  64. //
  65. // '\\': '\', // FULLWIDTH REVERSE SOLIDUS
  66. // '<': '<', // FULLWIDTH LESS-THAN SIGN
  67. // '>': '>', // FULLWIDTH GREATER-THAN SIGN
  68. // '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
  69. // '\'': ''', // FULLWIDTH APOSTROPHE
  70. // '$': '$', // FULLWIDTH DOLLAR SIGN
  71. // '`': '`', // FULLWIDTH GRAVE ACCENT
  72. //
  73. // Leading space and trailing space
  74. Default: (encoder.Display |
  75. encoder.EncodeBackSlash |
  76. encoder.EncodeSingleQuote |
  77. encoder.EncodeBackQuote |
  78. encoder.EncodeDoubleQuote |
  79. encoder.EncodeLtGt |
  80. encoder.EncodeDollar |
  81. encoder.EncodeLeftSpace |
  82. encoder.EncodeRightSpace |
  83. encoder.EncodeInvalidUtf8),
  84. }},
  85. })
  86. }
  87. // Options defines the configuration for this backend
  88. type Options struct {
  89. APIKey string `config:"api_key"`
  90. SharedFolder string `config:"shared_folder"`
  91. FilePassword string `config:"file_password"`
  92. FolderPassword string `config:"folder_password"`
  93. CDN bool `config:"cdn"`
  94. Enc encoder.MultiEncoder `config:"encoding"`
  95. }
  96. // Fs is the interface a cloud storage system must provide
  97. type Fs struct {
  98. root string
  99. name string
  100. features *fs.Features
  101. opt Options
  102. dirCache *dircache.DirCache
  103. baseClient *http.Client
  104. pacer *fs.Pacer
  105. rest *rest.Client
  106. }
  107. // FindLeaf finds a directory of name leaf in the folder with ID pathID
  108. func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
  109. folderID, err := strconv.Atoi(pathID)
  110. if err != nil {
  111. return "", false, err
  112. }
  113. folders, err := f.listFolders(ctx, folderID)
  114. if err != nil {
  115. return "", false, err
  116. }
  117. for _, folder := range folders.SubFolders {
  118. if folder.Name == leaf {
  119. pathIDOut := strconv.Itoa(folder.ID)
  120. return pathIDOut, true, nil
  121. }
  122. }
  123. return "", false, nil
  124. }
  125. // CreateDir makes a directory with pathID as parent and name leaf
  126. func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
  127. folderID, err := strconv.Atoi(pathID)
  128. if err != nil {
  129. return "", err
  130. }
  131. resp, err := f.makeFolder(ctx, leaf, folderID)
  132. if err != nil {
  133. return "", err
  134. }
  135. return strconv.Itoa(resp.FolderID), err
  136. }
  137. // Name of the remote (as passed into NewFs)
  138. func (f *Fs) Name() string {
  139. return f.name
  140. }
  141. // Root of the remote (as passed into NewFs)
  142. func (f *Fs) Root() string {
  143. return f.root
  144. }
  145. // String returns a description of the FS
  146. func (f *Fs) String() string {
  147. return fmt.Sprintf("1Fichier root '%s'", f.root)
  148. }
  149. // Precision of the ModTimes in this Fs
  150. func (f *Fs) Precision() time.Duration {
  151. return fs.ModTimeNotSupported
  152. }
  153. // Hashes returns the supported hash types of the filesystem
  154. func (f *Fs) Hashes() hash.Set {
  155. return hash.Set(hash.Whirlpool)
  156. }
  157. // Features returns the optional features of this Fs
  158. func (f *Fs) Features() *fs.Features {
  159. return f.features
  160. }
  161. // NewFs makes a new Fs object from the path
  162. //
  163. // The path is of the form remote:path
  164. //
  165. // Remotes are looked up in the config file. If the remote isn't
  166. // found then NotFoundInConfigFile will be returned.
  167. //
  168. // On Windows avoid single character remote names as they can be mixed
  169. // up with drive letters.
  170. func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
  171. opt := new(Options)
  172. err := configstruct.Set(config, opt)
  173. if err != nil {
  174. return nil, err
  175. }
  176. // If using a Shared Folder override root
  177. if opt.SharedFolder != "" {
  178. root = ""
  179. }
  180. //workaround for wonky parser
  181. root = strings.Trim(root, "/")
  182. f := &Fs{
  183. name: name,
  184. root: root,
  185. opt: *opt,
  186. pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
  187. baseClient: &http.Client{},
  188. }
  189. f.features = (&fs.Features{
  190. DuplicateFiles: true,
  191. CanHaveEmptyDirectories: true,
  192. ReadMimeType: true,
  193. }).Fill(ctx, f)
  194. client := fshttp.NewClient(ctx)
  195. f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
  196. f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey)
  197. f.dirCache = dircache.New(root, rootID, f)
  198. // Find the current root
  199. err = f.dirCache.FindRoot(ctx, false)
  200. if err != nil {
  201. // Assume it is a file
  202. newRoot, remote := dircache.SplitPath(root)
  203. tempF := *f
  204. tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
  205. tempF.root = newRoot
  206. // Make new Fs which is the parent
  207. err = tempF.dirCache.FindRoot(ctx, false)
  208. if err != nil {
  209. // No root so return old f
  210. return f, nil
  211. }
  212. _, err := tempF.NewObject(ctx, remote)
  213. if err != nil {
  214. if err == fs.ErrorObjectNotFound {
  215. // File doesn't exist so return old f
  216. return f, nil
  217. }
  218. return nil, err
  219. }
  220. f.features.Fill(ctx, &tempF)
  221. // XXX: update the old f here instead of returning tempF, since
  222. // `features` were already filled with functions having *f as a receiver.
  223. // See https://github.com/rclone/rclone/issues/2182
  224. f.dirCache = tempF.dirCache
  225. f.root = tempF.root
  226. // return an error with an fs which points to the parent
  227. return f, fs.ErrorIsFile
  228. }
  229. return f, nil
  230. }
  231. // List the objects and directories in dir into entries. The
  232. // entries can be returned in any order but should be for a
  233. // complete directory.
  234. //
  235. // dir should be "" to list the root, and should not have
  236. // trailing slashes.
  237. //
  238. // This should return ErrDirNotFound if the directory isn't
  239. // found.
  240. func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  241. if f.opt.SharedFolder != "" {
  242. return f.listSharedFiles(ctx, f.opt.SharedFolder)
  243. }
  244. dirContent, err := f.listDir(ctx, dir)
  245. if err != nil {
  246. return nil, err
  247. }
  248. return dirContent, nil
  249. }
  250. // NewObject finds the Object at remote. If it can't be found
  251. // it returns the error ErrorObjectNotFound.
  252. func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  253. leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false)
  254. if err != nil {
  255. if err == fs.ErrorDirNotFound {
  256. return nil, fs.ErrorObjectNotFound
  257. }
  258. return nil, err
  259. }
  260. folderID, err := strconv.Atoi(directoryID)
  261. if err != nil {
  262. return nil, err
  263. }
  264. files, err := f.listFiles(ctx, folderID)
  265. if err != nil {
  266. return nil, err
  267. }
  268. for _, file := range files.Items {
  269. if file.Filename == leaf {
  270. path, ok := f.dirCache.GetInv(directoryID)
  271. if !ok {
  272. return nil, errors.New("cannot find dir in dircache")
  273. }
  274. return f.newObjectFromFile(ctx, path, file), nil
  275. }
  276. }
  277. return nil, fs.ErrorObjectNotFound
  278. }
  279. // Put in to the remote path with the modTime given of the given size
  280. //
  281. // When called from outside an Fs by rclone, src.Size() will always be >= 0.
  282. // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
  283. // return an error or upload it properly (rather than e.g. calling panic).
  284. //
  285. // May create the object even if it returns an error - if so
  286. // will return the object and the error, otherwise will return
  287. // nil and the error
  288. func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  289. existingObj, err := f.NewObject(ctx, src.Remote())
  290. switch err {
  291. case nil:
  292. return existingObj, existingObj.Update(ctx, in, src, options...)
  293. case fs.ErrorObjectNotFound:
  294. // Not found so create it
  295. return f.PutUnchecked(ctx, in, src, options...)
  296. default:
  297. return nil, err
  298. }
  299. }
  300. // putUnchecked uploads the object with the given name and size
  301. //
  302. // This will create a duplicate if we upload a new file without
  303. // checking to see if there is one already - use Put() for that.
  304. func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) {
  305. if size > int64(300e9) {
  306. return nil, errors.New("File too big, can't upload")
  307. } else if size == 0 {
  308. return nil, fs.ErrorCantUploadEmptyFiles
  309. }
  310. nodeResponse, err := f.getUploadNode(ctx)
  311. if err != nil {
  312. return nil, err
  313. }
  314. leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, true)
  315. if err != nil {
  316. return nil, err
  317. }
  318. _, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL, options...)
  319. if err != nil {
  320. return nil, err
  321. }
  322. fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL)
  323. if err != nil {
  324. return nil, err
  325. }
  326. if len(fileUploadResponse.Links) == 0 {
  327. return nil, errors.New("upload response not found")
  328. } else if len(fileUploadResponse.Links) > 1 {
  329. fs.Debugf(remote, "Multiple upload responses found, using the first")
  330. }
  331. link := fileUploadResponse.Links[0]
  332. fileSize, err := strconv.ParseInt(link.Size, 10, 64)
  333. if err != nil {
  334. return nil, err
  335. }
  336. return &Object{
  337. fs: f,
  338. remote: remote,
  339. file: File{
  340. CDN: 0,
  341. Checksum: link.Whirlpool,
  342. ContentType: "",
  343. Date: time.Now().Format("2006-01-02 15:04:05"),
  344. Filename: link.Filename,
  345. Pass: 0,
  346. Size: fileSize,
  347. URL: link.Download,
  348. },
  349. }, nil
  350. }
  351. // PutUnchecked uploads the object
  352. //
  353. // This will create a duplicate if we upload a new file without
  354. // checking to see if there is one already - use Put() for that.
  355. func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  356. return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
  357. }
  358. // Mkdir makes the directory (container, bucket)
  359. //
  360. // Shouldn't return an error if it already exists
  361. func (f *Fs) Mkdir(ctx context.Context, dir string) error {
  362. _, err := f.dirCache.FindDir(ctx, dir, true)
  363. return err
  364. }
  365. // Rmdir removes the directory (container, bucket) if empty
  366. //
  367. // Return an error if it doesn't exist or isn't empty
  368. func (f *Fs) Rmdir(ctx context.Context, dir string) error {
  369. directoryID, err := f.dirCache.FindDir(ctx, dir, false)
  370. if err != nil {
  371. return err
  372. }
  373. folderID, err := strconv.Atoi(directoryID)
  374. if err != nil {
  375. return err
  376. }
  377. _, err = f.removeFolder(ctx, dir, folderID)
  378. if err != nil {
  379. return err
  380. }
  381. f.dirCache.FlushDir(dir)
  382. return nil
  383. }
  384. // Move src to this remote using server side move operations.
  385. func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  386. srcObj, ok := src.(*Object)
  387. if !ok {
  388. fs.Debugf(src, "Can't move - not same remote type")
  389. return nil, fs.ErrorCantMove
  390. }
  391. srcFs := srcObj.fs
  392. // Find current directory ID
  393. srcLeaf, srcDirectoryID, err := srcFs.dirCache.FindPath(ctx, srcObj.remote, false)
  394. if err != nil {
  395. return nil, err
  396. }
  397. // Create temporary object
  398. dstObj, dstLeaf, dstDirectoryID, err := f.createObject(ctx, remote)
  399. if err != nil {
  400. return nil, err
  401. }
  402. // If it is in the correct directory, just rename it
  403. var url string
  404. if srcDirectoryID == dstDirectoryID {
  405. // No rename needed
  406. if srcLeaf == dstLeaf {
  407. return src, nil
  408. }
  409. resp, err := f.renameFile(ctx, srcObj.file.URL, dstLeaf)
  410. if err != nil {
  411. return nil, fmt.Errorf("couldn't rename file: %w", err)
  412. }
  413. if resp.Status != "OK" {
  414. return nil, fmt.Errorf("couldn't rename file: %s", resp.Message)
  415. }
  416. url = resp.URLs[0].URL
  417. } else {
  418. dstFolderID, err := strconv.Atoi(dstDirectoryID)
  419. if err != nil {
  420. return nil, err
  421. }
  422. rename := dstLeaf
  423. // No rename needed
  424. if srcLeaf == dstLeaf {
  425. rename = ""
  426. }
  427. resp, err := f.moveFile(ctx, srcObj.file.URL, dstFolderID, rename)
  428. if err != nil {
  429. return nil, fmt.Errorf("couldn't move file: %w", err)
  430. }
  431. if resp.Status != "OK" {
  432. return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
  433. }
  434. url = resp.URLs[0]
  435. }
  436. file, err := f.readFileInfo(ctx, url)
  437. if err != nil {
  438. return nil, errors.New("couldn't read file data")
  439. }
  440. dstObj.setMetaData(*file)
  441. return dstObj, nil
  442. }
  443. // DirMove moves src, srcRemote to this remote at dstRemote
  444. // using server-side move operations.
  445. //
  446. // Will only be called if src.Fs().Name() == f.Name()
  447. //
  448. // If it isn't possible then return fs.ErrorCantDirMove.
  449. //
  450. // If destination exists then return fs.ErrorDirExists.
  451. //
  452. // This is complicated by the fact that we can't use moveDir to move
  453. // to a different directory AND rename at the same time as it can
  454. // overwrite files in the source directory.
  455. func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
  456. srcFs, ok := src.(*Fs)
  457. if !ok {
  458. fs.Debugf(srcFs, "Can't move directory - not same remote type")
  459. return fs.ErrorCantDirMove
  460. }
  461. srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
  462. if err != nil {
  463. return err
  464. }
  465. srcIDnumeric, err := strconv.Atoi(srcID)
  466. if err != nil {
  467. return err
  468. }
  469. dstDirectoryIDnumeric, err := strconv.Atoi(dstDirectoryID)
  470. if err != nil {
  471. return err
  472. }
  473. var resp *MoveDirResponse
  474. resp, err = f.moveDir(ctx, srcIDnumeric, dstLeaf, dstDirectoryIDnumeric)
  475. if err != nil {
  476. return fmt.Errorf("couldn't rename leaf: %w", err)
  477. }
  478. if resp.Status != "OK" {
  479. return fmt.Errorf("couldn't rename leaf: %s", resp.Message)
  480. }
  481. srcFs.dirCache.FlushDir(srcRemote)
  482. return nil
  483. }
  484. // Copy src to this remote using server side move operations.
  485. func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  486. srcObj, ok := src.(*Object)
  487. if !ok {
  488. fs.Debugf(src, "Can't move - not same remote type")
  489. return nil, fs.ErrorCantMove
  490. }
  491. // Create temporary object
  492. dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
  493. if err != nil {
  494. return nil, err
  495. }
  496. folderID, err := strconv.Atoi(directoryID)
  497. if err != nil {
  498. return nil, err
  499. }
  500. resp, err := f.copyFile(ctx, srcObj.file.URL, folderID, leaf)
  501. if err != nil {
  502. return nil, fmt.Errorf("couldn't move file: %w", err)
  503. }
  504. if resp.Status != "OK" {
  505. return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
  506. }
  507. file, err := f.readFileInfo(ctx, resp.URLs[0].ToURL)
  508. if err != nil {
  509. return nil, errors.New("couldn't read file data")
  510. }
  511. dstObj.setMetaData(*file)
  512. return dstObj, nil
  513. }
  514. // About gets quota information
  515. func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
  516. opts := rest.Opts{
  517. Method: "POST",
  518. Path: "/user/info.cgi",
  519. ContentType: "application/json",
  520. }
  521. var accountInfo AccountInfo
  522. var resp *http.Response
  523. err = f.pacer.Call(func() (bool, error) {
  524. resp, err = f.rest.CallJSON(ctx, &opts, nil, &accountInfo)
  525. return shouldRetry(ctx, resp, err)
  526. })
  527. if err != nil {
  528. return nil, fmt.Errorf("failed to read user info: %w", err)
  529. }
  530. // FIXME max upload size would be useful to use in Update
  531. usage = &fs.Usage{
  532. Used: fs.NewUsageValue(accountInfo.ColdStorage), // bytes in use
  533. Total: fs.NewUsageValue(accountInfo.AvailableColdStorage), // bytes total
  534. Free: fs.NewUsageValue(accountInfo.AvailableColdStorage - accountInfo.ColdStorage), // bytes free
  535. }
  536. return usage, nil
  537. }
  538. // PublicLink adds a "readable by anyone with link" permission on the given file or folder.
  539. func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
  540. o, err := f.NewObject(ctx, remote)
  541. if err != nil {
  542. return "", err
  543. }
  544. return o.(*Object).file.URL, nil
  545. }
  546. // Check the interfaces are satisfied
  547. var (
  548. _ fs.Fs = (*Fs)(nil)
  549. _ fs.Mover = (*Fs)(nil)
  550. _ fs.DirMover = (*Fs)(nil)
  551. _ fs.Copier = (*Fs)(nil)
  552. _ fs.PublicLinker = (*Fs)(nil)
  553. _ fs.PutUncheckeder = (*Fs)(nil)
  554. _ dircache.DirCacher = (*Fs)(nil)
  555. )