fs.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. // Copyright 2019-2022 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. /**
  5. * Access the file system.
  6. *
  7. * This package is also accessible with `window.__TAURI__.fs` when [`build.withGlobalTauri`](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri) in `tauri.conf.json` is set to `true`.
  8. *
  9. * The APIs must be added to [`tauri.allowlist.fs`](https://tauri.app/v1/api/config/#allowlistconfig.fs) in `tauri.conf.json`:
  10. * ```json
  11. * {
  12. * "tauri": {
  13. * "allowlist": {
  14. * "fs": {
  15. * "all": true, // enable all FS APIs
  16. * "readFile": true,
  17. * "writeFile": true,
  18. * "readDir": true,
  19. * "copyFile": true,
  20. * "createDir": true,
  21. * "removeDir": true,
  22. * "removeFile": true,
  23. * "renameFile": true,
  24. * "exists": true
  25. * }
  26. * }
  27. * }
  28. * }
  29. * ```
  30. * It is recommended to allowlist only the APIs you use for optimal bundle size and security.
  31. *
  32. * ## Security
  33. *
  34. * This module prevents path traversal, not allowing absolute paths or parent dir components
  35. * (i.e. "/usr/path/to/file" or "../path/to/file" paths are not allowed).
  36. * Paths accessed with this API must be relative to one of the {@link BaseDirectory | base directories}
  37. * so if you need access to arbitrary filesystem paths, you must write such logic on the core layer instead.
  38. *
  39. * The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns.
  40. *
  41. * The scope configuration is an array of glob patterns describing folder paths that are allowed.
  42. * For instance, this scope configuration only allows accessing files on the
  43. * *databases* folder of the {@link path.appDataDir | $APPDATA directory}:
  44. * ```json
  45. * {
  46. * "tauri": {
  47. * "allowlist": {
  48. * "fs": {
  49. * "scope": ["$APPDATA/databases/*"]
  50. * }
  51. * }
  52. * }
  53. * }
  54. * ```
  55. *
  56. * Notice the use of the `$APPDATA` variable. The value is injected at runtime, resolving to the {@link path.appDataDir | app data directory}.
  57. * The available variables are:
  58. * {@link path.appConfigDir | `$APPCONFIG`}, {@link path.appDataDir | `$APPDATA`}, {@link path.appLocalDataDir | `$APPLOCALDATA`},
  59. * {@link path.appCacheDir | `$APPCACHE`}, {@link path.appLogDir | `$APPLOG`},
  60. * {@link path.audioDir | `$AUDIO`}, {@link path.cacheDir | `$CACHE`}, {@link path.configDir | `$CONFIG`}, {@link path.dataDir | `$DATA`},
  61. * {@link path.localDataDir | `$LOCALDATA`}, {@link path.desktopDir | `$DESKTOP`}, {@link path.documentDir | `$DOCUMENT`},
  62. * {@link path.downloadDir | `$DOWNLOAD`}, {@link path.executableDir | `$EXE`}, {@link path.fontDir | `$FONT`}, {@link path.homeDir | `$HOME`},
  63. * {@link path.pictureDir | `$PICTURE`}, {@link path.publicDir | `$PUBLIC`}, {@link path.runtimeDir | `$RUNTIME`},
  64. * {@link path.templateDir | `$TEMPLATE`}, {@link path.videoDir | `$VIDEO`}, {@link path.resourceDir | `$RESOURCE`}, {@link path.appDir | `$APP`},
  65. * {@link path.logDir | `$LOG`}, {@link os.tempdir | `$TEMP`}.
  66. *
  67. * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
  68. *
  69. * Note that this scope applies to **all** APIs on this module.
  70. *
  71. * @module
  72. */
  73. import { invokeTauriCommand } from './helpers/tauri'
  74. /**
  75. * @since 1.0.0
  76. */
  77. export enum BaseDirectory {
  78. Audio = 1,
  79. Cache,
  80. Config,
  81. Data,
  82. LocalData,
  83. Desktop,
  84. Document,
  85. Download,
  86. Executable,
  87. Font,
  88. Home,
  89. Picture,
  90. Public,
  91. Runtime,
  92. Template,
  93. Video,
  94. Resource,
  95. App,
  96. Log,
  97. Temp,
  98. AppConfig,
  99. AppData,
  100. AppLocalData,
  101. AppCache,
  102. AppLog
  103. }
  104. /**
  105. * @since 1.0.0
  106. */
  107. interface FsOptions {
  108. dir?: BaseDirectory
  109. // note that adding fields here needs a change in the writeBinaryFile check
  110. }
  111. /**
  112. * @since 1.0.0
  113. */
  114. interface FsDirOptions {
  115. dir?: BaseDirectory
  116. recursive?: boolean
  117. }
  118. /**
  119. * Options object used to write a UTF-8 string to a file.
  120. *
  121. * @since 1.0.0
  122. */
  123. interface FsTextFileOption {
  124. /** Path to the file to write. */
  125. path: string
  126. /** The UTF-8 string to write to the file. */
  127. contents: string
  128. }
  129. type BinaryFileContents = Iterable<number> | ArrayLike<number> | ArrayBuffer
  130. /**
  131. * Options object used to write a binary data to a file.
  132. *
  133. * @since 1.0.0
  134. */
  135. interface FsBinaryFileOption {
  136. /** Path to the file to write. */
  137. path: string
  138. /** The byte array contents. */
  139. contents: BinaryFileContents
  140. }
  141. /**
  142. * @since 1.0.0
  143. */
  144. interface FileEntry {
  145. path: string
  146. /**
  147. * Name of the directory/file
  148. * can be null if the path terminates with `..`
  149. */
  150. name?: string
  151. /** Children of this entry if it's a directory; null otherwise */
  152. children?: FileEntry[]
  153. }
  154. /**
  155. * Reads a file as an UTF-8 encoded string.
  156. * @example
  157. * ```typescript
  158. * import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs';
  159. * // Read the text file in the `$APPCONFIG/app.conf` path
  160. * const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig });
  161. * ```
  162. *
  163. * @since 1.0.0
  164. */
  165. async function readTextFile(
  166. filePath: string,
  167. options: FsOptions = {}
  168. ): Promise<string> {
  169. return invokeTauriCommand<string>({
  170. __tauriModule: 'Fs',
  171. message: {
  172. cmd: 'readTextFile',
  173. path: filePath,
  174. options
  175. }
  176. })
  177. }
  178. /**
  179. * Reads a file as byte array.
  180. * @example
  181. * ```typescript
  182. * import { readBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
  183. * // Read the image file in the `$RESOURCEDIR/avatar.png` path
  184. * const contents = await readBinaryFile('avatar.png', { dir: BaseDirectory.Resource });
  185. * ```
  186. *
  187. * @since 1.0.0
  188. */
  189. async function readBinaryFile(
  190. filePath: string,
  191. options: FsOptions = {}
  192. ): Promise<Uint8Array> {
  193. const arr = await invokeTauriCommand<number[]>({
  194. __tauriModule: 'Fs',
  195. message: {
  196. cmd: 'readFile',
  197. path: filePath,
  198. options
  199. }
  200. })
  201. return Uint8Array.from(arr)
  202. }
  203. /**
  204. * Writes a UTF-8 text file.
  205. * @example
  206. * ```typescript
  207. * import { writeTextFile, BaseDirectory } from '@tauri-apps/api/fs';
  208. * // Write a text file to the `$APPCONFIG/app.conf` path
  209. * await writeTextFile('app.conf', 'file contents', { dir: BaseDirectory.AppConfig });
  210. * ```
  211. *
  212. * @since 1.0.0
  213. */
  214. async function writeTextFile(
  215. path: string,
  216. contents: string,
  217. options?: FsOptions
  218. ): Promise<void>
  219. /**
  220. * Writes a UTF-8 text file.
  221. * @example
  222. * ```typescript
  223. * import { writeTextFile, BaseDirectory } from '@tauri-apps/api/fs';
  224. * // Write a text file to the `$APPCONFIG/app.conf` path
  225. * await writeTextFile({ path: 'app.conf', contents: 'file contents' }, { dir: BaseDirectory.AppConfig });
  226. * ```
  227. * @returns A promise indicating the success or failure of the operation.
  228. *
  229. * @since 1.0.0
  230. */
  231. async function writeTextFile(
  232. file: FsTextFileOption,
  233. options?: FsOptions
  234. ): Promise<void>
  235. /**
  236. * Writes a UTF-8 text file.
  237. *
  238. * @returns A promise indicating the success or failure of the operation.
  239. *
  240. * @since 1.0.0
  241. */
  242. async function writeTextFile(
  243. path: string | FsTextFileOption,
  244. contents?: string | FsOptions,
  245. options?: FsOptions
  246. ): Promise<void> {
  247. if (typeof options === 'object') {
  248. Object.freeze(options)
  249. }
  250. if (typeof path === 'object') {
  251. Object.freeze(path)
  252. }
  253. const file: FsTextFileOption = { path: '', contents: '' }
  254. let fileOptions: FsOptions | undefined = options
  255. if (typeof path === 'string') {
  256. file.path = path
  257. } else {
  258. file.path = path.path
  259. file.contents = path.contents
  260. }
  261. if (typeof contents === 'string') {
  262. file.contents = contents ?? ''
  263. } else {
  264. fileOptions = contents
  265. }
  266. return invokeTauriCommand({
  267. __tauriModule: 'Fs',
  268. message: {
  269. cmd: 'writeFile',
  270. path: file.path,
  271. contents: Array.from(new TextEncoder().encode(file.contents)),
  272. options: fileOptions
  273. }
  274. })
  275. }
  276. /**
  277. * Writes a byte array content to a file.
  278. * @example
  279. * ```typescript
  280. * import { writeBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
  281. * // Write a binary file to the `$APPDATA/avatar.png` path
  282. * await writeBinaryFile('avatar.png', new Uint8Array([]), { dir: BaseDirectory.AppData });
  283. * ```
  284. *
  285. * @param options Configuration object.
  286. * @returns A promise indicating the success or failure of the operation.
  287. *
  288. * @since 1.0.0
  289. */
  290. async function writeBinaryFile(
  291. path: string,
  292. contents: BinaryFileContents,
  293. options?: FsOptions
  294. ): Promise<void>
  295. /**
  296. * Writes a byte array content to a file.
  297. * @example
  298. * ```typescript
  299. * import { writeBinaryFile, BaseDirectory } from '@tauri-apps/api/fs';
  300. * // Write a binary file to the `$APPDATA/avatar.png` path
  301. * await writeBinaryFile({ path: 'avatar.png', contents: new Uint8Array([]) }, { dir: BaseDirectory.AppData });
  302. * ```
  303. *
  304. * @param file The object containing the file path and contents.
  305. * @param options Configuration object.
  306. * @returns A promise indicating the success or failure of the operation.
  307. *
  308. * @since 1.0.0
  309. */
  310. async function writeBinaryFile(
  311. file: FsBinaryFileOption,
  312. options?: FsOptions
  313. ): Promise<void>
  314. /**
  315. * Writes a byte array content to a file.
  316. *
  317. * @returns A promise indicating the success or failure of the operation.
  318. *
  319. * @since 1.0.0
  320. */
  321. async function writeBinaryFile(
  322. path: string | FsBinaryFileOption,
  323. contents?: BinaryFileContents | FsOptions,
  324. options?: FsOptions
  325. ): Promise<void> {
  326. if (typeof options === 'object') {
  327. Object.freeze(options)
  328. }
  329. if (typeof path === 'object') {
  330. Object.freeze(path)
  331. }
  332. const file: FsBinaryFileOption = { path: '', contents: [] }
  333. let fileOptions: FsOptions | undefined = options
  334. if (typeof path === 'string') {
  335. file.path = path
  336. } else {
  337. file.path = path.path
  338. file.contents = path.contents
  339. }
  340. if (contents && 'dir' in contents) {
  341. fileOptions = contents
  342. } else if (typeof path === 'string') {
  343. // @ts-expect-error
  344. file.contents = contents ?? []
  345. }
  346. return invokeTauriCommand({
  347. __tauriModule: 'Fs',
  348. message: {
  349. cmd: 'writeFile',
  350. path: file.path,
  351. contents: Array.from(
  352. file.contents instanceof ArrayBuffer
  353. ? new Uint8Array(file.contents)
  354. : file.contents
  355. ),
  356. options: fileOptions
  357. }
  358. })
  359. }
  360. /**
  361. * List directory files.
  362. * @example
  363. * ```typescript
  364. * import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
  365. * // Reads the `$APPDATA/users` directory recursively
  366. * const entries = await readDir('users', { dir: BaseDirectory.AppData, recursive: true });
  367. *
  368. * function processEntries(entries) {
  369. * for (const entry of entries) {
  370. * console.log(`Entry: ${entry.path}`);
  371. * if (entry.children) {
  372. * processEntries(entry.children)
  373. * }
  374. * }
  375. * }
  376. * ```
  377. *
  378. * @since 1.0.0
  379. */
  380. async function readDir(
  381. dir: string,
  382. options: FsDirOptions = {}
  383. ): Promise<FileEntry[]> {
  384. return invokeTauriCommand({
  385. __tauriModule: 'Fs',
  386. message: {
  387. cmd: 'readDir',
  388. path: dir,
  389. options
  390. }
  391. })
  392. }
  393. /**
  394. * Creates a directory.
  395. * If one of the path's parent components doesn't exist
  396. * and the `recursive` option isn't set to true, the promise will be rejected.
  397. * @example
  398. * ```typescript
  399. * import { createDir, BaseDirectory } from '@tauri-apps/api/fs';
  400. * // Create the `$APPDATA/users` directory
  401. * await createDir('users', { dir: BaseDirectory.AppData, recursive: true });
  402. * ```
  403. *
  404. * @returns A promise indicating the success or failure of the operation.
  405. *
  406. * @since 1.0.0
  407. */
  408. async function createDir(
  409. dir: string,
  410. options: FsDirOptions = {}
  411. ): Promise<void> {
  412. return invokeTauriCommand({
  413. __tauriModule: 'Fs',
  414. message: {
  415. cmd: 'createDir',
  416. path: dir,
  417. options
  418. }
  419. })
  420. }
  421. /**
  422. * Removes a directory.
  423. * If the directory is not empty and the `recursive` option isn't set to true, the promise will be rejected.
  424. * @example
  425. * ```typescript
  426. * import { removeDir, BaseDirectory } from '@tauri-apps/api/fs';
  427. * // Remove the directory `$APPDATA/users`
  428. * await removeDir('users', { dir: BaseDirectory.AppData });
  429. * ```
  430. *
  431. * @returns A promise indicating the success or failure of the operation.
  432. *
  433. * @since 1.0.0
  434. */
  435. async function removeDir(
  436. dir: string,
  437. options: FsDirOptions = {}
  438. ): Promise<void> {
  439. return invokeTauriCommand({
  440. __tauriModule: 'Fs',
  441. message: {
  442. cmd: 'removeDir',
  443. path: dir,
  444. options
  445. }
  446. })
  447. }
  448. /**
  449. * Copies a file to a destination.
  450. * @example
  451. * ```typescript
  452. * import { copyFile, BaseDirectory } from '@tauri-apps/api/fs';
  453. * // Copy the `$APPCONFIG/app.conf` file to `$APPCONFIG/app.conf.bk`
  454. * await copyFile('app.conf', 'app.conf.bk', { dir: BaseDirectory.AppConfig });
  455. * ```
  456. *
  457. * @returns A promise indicating the success or failure of the operation.
  458. *
  459. * @since 1.0.0
  460. */
  461. async function copyFile(
  462. source: string,
  463. destination: string,
  464. options: FsOptions = {}
  465. ): Promise<void> {
  466. return invokeTauriCommand({
  467. __tauriModule: 'Fs',
  468. message: {
  469. cmd: 'copyFile',
  470. source,
  471. destination,
  472. options
  473. }
  474. })
  475. }
  476. /**
  477. * Removes a file.
  478. * @example
  479. * ```typescript
  480. * import { removeFile, BaseDirectory } from '@tauri-apps/api/fs';
  481. * // Remove the `$APPConfig/app.conf` file
  482. * await removeFile('app.conf', { dir: BaseDirectory.AppConfig });
  483. * ```
  484. *
  485. * @returns A promise indicating the success or failure of the operation.
  486. *
  487. * @since 1.0.0
  488. */
  489. async function removeFile(
  490. file: string,
  491. options: FsOptions = {}
  492. ): Promise<void> {
  493. return invokeTauriCommand({
  494. __tauriModule: 'Fs',
  495. message: {
  496. cmd: 'removeFile',
  497. path: file,
  498. options
  499. }
  500. })
  501. }
  502. /**
  503. * Renames a file.
  504. * @example
  505. * ```typescript
  506. * import { renameFile, BaseDirectory } from '@tauri-apps/api/fs';
  507. * // Rename the `$APPDATA/avatar.png` file
  508. * await renameFile('avatar.png', 'deleted.png', { dir: BaseDirectory.AppData });
  509. * ```
  510. *
  511. * @returns A promise indicating the success or failure of the operation.
  512. *
  513. * @since 1.0.0
  514. */
  515. async function renameFile(
  516. oldPath: string,
  517. newPath: string,
  518. options: FsOptions = {}
  519. ): Promise<void> {
  520. return invokeTauriCommand({
  521. __tauriModule: 'Fs',
  522. message: {
  523. cmd: 'renameFile',
  524. oldPath,
  525. newPath,
  526. options
  527. }
  528. })
  529. }
  530. /**
  531. * Check if a path exists.
  532. * @example
  533. * ```typescript
  534. * import { exists, BaseDirectory } from '@tauri-apps/api/fs';
  535. * // Check if the `$APPDATA/avatar.png` file exists
  536. * await exists('avatar.png', { dir: BaseDirectory.AppData });
  537. * ```
  538. *
  539. * @since 1.1.0
  540. */
  541. async function exists(path: string, options: FsOptions = {}): Promise<boolean> {
  542. return invokeTauriCommand({
  543. __tauriModule: 'Fs',
  544. message: {
  545. cmd: 'exists',
  546. path,
  547. options
  548. }
  549. })
  550. }
  551. export type {
  552. FsOptions,
  553. FsDirOptions,
  554. FsTextFileOption,
  555. BinaryFileContents,
  556. FsBinaryFileOption,
  557. FileEntry
  558. }
  559. export {
  560. BaseDirectory as Dir,
  561. readTextFile,
  562. readBinaryFile,
  563. writeTextFile,
  564. writeTextFile as writeFile,
  565. writeBinaryFile,
  566. readDir,
  567. createDir,
  568. removeDir,
  569. copyFile,
  570. removeFile,
  571. renameFile,
  572. exists
  573. }