// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT /** * Access the file system. * * ## Security * * This module prevents path traversal, not allowing absolute paths or parent dir components * (i.e. "/usr/path/to/file" or "../path/to/file" paths are not allowed). * Paths accessed with this API must be relative to one of the {@link BaseDirectory | base directories} * so if you need access to arbitrary filesystem paths, you must write such logic on the core layer instead. * * The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns. * * The scope configuration is an array of glob patterns describing folder paths that are allowed. * For instance, this scope configuration only allows accessing files on the * *databases* folder of the {@link https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | `$APPDATA` directory}: * ```json * { * "plugins": { * "fs": { * "scope": ["$APPDATA/databases/*"] * } * } * } * ``` * * Notice the use of the `$APPDATA` variable. The value is injected at runtime, resolving to the {@link https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | app data directory}. * * The available variables are: * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appconfigdir | $APPCONFIG}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appdatadir | $APPDATA}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appLocaldatadir | $APPLOCALDATA}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#appcachedir | $APPCACHE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#applogdir | $APPLOG}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#audiodir | $AUDIO}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#cachedir | $CACHE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#configdir | $CONFIG}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#datadir | $DATA}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#localdatadir | $LOCALDATA}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#desktopdir | $DESKTOP}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#documentdir | $DOCUMENT}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#downloaddir | $DOWNLOAD}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#executabledir | $EXE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#fontdir | $FONT}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#homedir | $HOME}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#picturedir | $PICTURE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#publicdir | $PUBLIC}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#runtimedir | $RUNTIME}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#templatedir | $TEMPLATE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#videodir | $VIDEO}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#resourcedir | $RESOURCE}, * {@linkcode https://beta.tauri.app/2/reference/js/core/namespacepath/#tempdir | $TEMP}. * * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access. * * Note that this scope applies to **all** APIs on this module. * * @module */ import { BaseDirectory } from "@tauri-apps/api/path"; import { Channel, invoke, Resource } from "@tauri-apps/api/core"; enum SeekMode { Start = 0, Current = 1, End = 2, } /** * A FileInfo describes a file and is returned by `stat`, `lstat` or `fstat`. * * @since 2.0.0 */ interface FileInfo { /** * True if this is info for a regular file. Mutually exclusive to * `FileInfo.isDirectory` and `FileInfo.isSymlink`. */ isFile: boolean; /** * True if this is info for a regular directory. Mutually exclusive to * `FileInfo.isFile` and `FileInfo.isSymlink`. */ isDirectory: boolean; /** * True if this is info for a symlink. Mutually exclusive to * `FileInfo.isFile` and `FileInfo.isDirectory`. */ isSymlink: boolean; /** * The size of the file, in bytes. */ size: number; /** * The last modification time of the file. This corresponds to the `mtime` * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This * may not be available on all platforms. */ mtime: Date | null; /** * The last access time of the file. This corresponds to the `atime` * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not * be available on all platforms. */ atime: Date | null; /** * The creation time of the file. This corresponds to the `birthtime` * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may * not be available on all platforms. */ birthtime: Date | null; /** Whether this is a readonly (unwritable) file. */ readonly: boolean; /** * This field contains the file system attribute information for a file * or directory. For possible values and their descriptions, see * {@link https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants | File Attribute Constants} in the Windows Dev Center * * #### Platform-specific * * - **macOS / Linux / Android / iOS:** Unsupported. */ fileAttributes: number | null; /** * ID of the device containing the file. * * #### Platform-specific * * - **Windows:** Unsupported. */ dev: number | null; /** * Inode number. * * #### Platform-specific * * - **Windows:** Unsupported. */ ino: number | null; /** * The underlying raw `st_mode` bits that contain the standard Unix * permissions for this file/directory. * * #### Platform-specific * * - **Windows:** Unsupported. */ mode: number | null; /** * Number of hard links pointing to this file. * * #### Platform-specific * * - **Windows:** Unsupported. */ nlink: number | null; /** * User ID of the owner of this file. * * #### Platform-specific * * - **Windows:** Unsupported. */ uid: number | null; /** * Group ID of the owner of this file. * * #### Platform-specific * * - **Windows:** Unsupported. */ gid: number | null; /** * Device ID of this file. * * #### Platform-specific * * - **Windows:** Unsupported. */ rdev: number | null; /** * Blocksize for filesystem I/O. * * #### Platform-specific * * - **Windows:** Unsupported. */ blksize: number | null; /** * Number of blocks allocated to the file, in 512-byte units. * * #### Platform-specific * * - **Windows:** Unsupported. */ blocks: number | null; } interface UnparsedFileInfo { isFile: boolean; isDirectory: boolean; isSymlink: boolean; size: number; mtime: number | null; atime: number | null; birthtime: number | null; readonly: boolean; fileAttributes: number; dev: number | null; ino: number | null; mode: number | null; nlink: number | null; uid: number | null; gid: number | null; rdev: number | null; blksize: number | null; blocks: number | null; } function parseFileInfo(r: UnparsedFileInfo): FileInfo { return { isFile: r.isFile, isDirectory: r.isDirectory, isSymlink: r.isSymlink, size: r.size, mtime: r.mtime != null ? new Date(r.mtime) : null, atime: r.atime != null ? new Date(r.atime) : null, birthtime: r.birthtime != null ? new Date(r.birthtime) : null, readonly: r.readonly, fileAttributes: r.fileAttributes, dev: r.dev, ino: r.ino, mode: r.mode, nlink: r.nlink, uid: r.uid, gid: r.gid, rdev: r.rdev, blksize: r.blksize, blocks: r.blocks, }; } /** * The Tauri abstraction for reading and writing files. * * @since 2.0.0 */ class FileHandle extends Resource { constructor(rid: number) { super(rid); } /** * Reads up to `p.byteLength` bytes into `p`. It resolves to the number of * bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error * encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may * use all of `p` as scratch space during the call. If some data is * available but not `p.byteLength` bytes, `read()` conventionally resolves * to what is available instead of waiting for more. * * When `read()` encounters end-of-file condition, it resolves to EOF * (`null`). * * When `read()` encounters an error, it rejects with an error. * * Callers should always process the `n` > `0` bytes returned before * considering the EOF (`null`). Doing so correctly handles I/O errors that * happen after reading some bytes and also both of the allowed EOF * behaviors. * * @example * ```typescript * import { open, read, close, BaseDirectory } from "@tauri-apps/plugin-fs" * // if "$APP/foo/bar.txt" contains the text "hello world": * const file = await open("foo/bar.txt", { dir: BaseDirectory.App }); * const buf = new Uint8Array(100); * const numberOfBytesRead = await file.read(buf); // 11 bytes * const text = new TextDecoder().decode(buf); // "hello world" * await close(file.rid); * ``` * * @since 2.0.0 */ async read(buffer: Uint8Array): Promise { if (buffer.byteLength === 0) { return 0; } const [data, nread] = await invoke<[number[], number]>("plugin:fs|read", { rid: this.rid, len: buffer.byteLength, }); buffer.set(data); return nread === 0 ? null : nread; } /** * Seek sets the offset for the next `read()` or `write()` to offset, * interpreted according to `whence`: `Start` means relative to the * start of the file, `Current` means relative to the current offset, * and `End` means relative to the end. Seek resolves to the new offset * relative to the start of the file. * * Seeking to an offset before the start of the file is an error. Seeking to * any positive offset is legal, but the behavior of subsequent I/O * operations on the underlying object is implementation-dependent. * It returns the number of cursor position. * * @example * ```typescript * import { open, seek, write, SeekMode, BaseDirectory } from '@tauri-apps/plugin-fs'; * * // Given hello.txt pointing to file with "Hello world", which is 11 bytes long: * const file = await open('hello.txt', { read: true, write: true, truncate: true, create: true, dir: BaseDirectory.App }); * await file.write(new TextEncoder().encode("Hello world"), { dir: BaseDirectory.App }); * * // Seek 6 bytes from the start of the file * console.log(await file.seek(6, SeekMode.Start)); // "6" * // Seek 2 more bytes from the current position * console.log(await file.seek(2, SeekMode.Current)); // "8" * // Seek backwards 2 bytes from the end of the file * console.log(await file.seek(-2, SeekMode.End)); // "9" (e.g. 11-2) * ``` * * @since 2.0.0 */ async seek(offset: number, whence: SeekMode): Promise { return invoke("plugin:fs|seek", { rid: this.rid, offset, whence, }); } /** * Returns a {@linkcode FileInfo } for this file. * * @example * ```typescript * import { open, fstat, BaseDirectory } from '@tauri-apps/plugin-fs'; * const file = await open("file.txt", { read: true, dir: BaseDirectory.App }); * const fileInfo = await fstat(file.rid); * console.log(fileInfo.isFile); // true * ``` * * @since 2.0.0 */ async stat(): Promise { const res = await invoke("plugin:fs|fstat", { rid: this.rid, }); return parseFileInfo(res); } /** * Truncates or extends this file, to reach the specified `len`. * If `len` is not specified then the entire file contents are truncated. * * @example * ```typescript * import { ftruncate, open, write, read, BaseDirectory } from '@tauri-apps/plugin-fs'; * * // truncate the entire file * const file = await open("my_file.txt", { read: true, write: true, create: true, dir: BaseDirectory.App }); * await ftruncate(file.rid); * * // truncate part of the file * const file = await open("my_file.txt", { read: true, write: true, create: true, dir: BaseDirectory.App }); * await write(file.rid, new TextEncoder().encode("Hello World")); * await ftruncate(file.rid, 7); * const data = new Uint8Array(32); * await read(file.rid, data); * console.log(new TextDecoder().decode(data)); // Hello W * ``` * * @since 2.0.0 */ async truncate(len?: number): Promise { return invoke("plugin:fs|ftruncate", { rid: this.rid, len, }); } /** * Writes `p.byteLength` bytes from `p` to the underlying data stream. It * resolves to the number of bytes written from `p` (`0` <= `n` <= * `p.byteLength`) or reject with the error encountered that caused the * write to stop early. `write()` must reject with a non-null error if * would resolve to `n` < `p.byteLength`. `write()` must not modify the * slice data, even temporarily. * * @example * ```typescript * import { open, write, close, BaseDirectory } from '@tauri-apps/plugin-fs'; * const encoder = new TextEncoder(); * const data = encoder.encode("Hello world"); * const file = await open("bar.txt", { write: true, dir: BaseDirectory.App }); * const bytesWritten = await write(file.rid, data); // 11 * await close(file.rid); * ``` * * @since 2.0.0 */ async write(data: Uint8Array): Promise { return invoke("plugin:fs|write", { rid: this.rid, data: Array.from(data), }); } } /** * @since 2.0.0 */ interface CreateOptions { /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Creates a file if none exists or truncates an existing file and resolves to * an instance of {@linkcode FileHandle }. * * @example * ```typescript * import { create, BaseDirectory } from "@tauri-apps/plugin-fs" * const file = await create("foo/bar.txt", { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function create( path: string | URL, options?: CreateOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } const rid = await invoke("plugin:fs|create", { path: path instanceof URL ? path.toString() : path, options, }); return new FileHandle(rid); } /** * @since 2.0.0 */ interface OpenOptions { /** * Sets the option for read access. This option, when `true`, means that the * file should be read-able if opened. */ read?: boolean; /** * Sets the option for write access. This option, when `true`, means that * the file should be write-able if opened. If the file already exists, * any write calls on it will overwrite its contents, by default without * truncating it. */ write?: boolean; /** * Sets the option for the append mode. This option, when `true`, means that * writes will append to a file instead of overwriting previous contents. * Note that setting `{ write: true, append: true }` has the same effect as * setting only `{ append: true }`. */ append?: boolean; /** * Sets the option for truncating a previous file. If a file is * successfully opened with this option set it will truncate the file to `0` * size if it already exists. The file must be opened with write access * for truncate to work. */ truncate?: boolean; /** * Sets the option to allow creating a new file, if one doesn't already * exist at the specified path. Requires write or append access to be * used. */ create?: boolean; /** * Defaults to `false`. If set to `true`, no file, directory, or symlink is * allowed to exist at the target location. Requires write or append * access to be used. When createNew is set to `true`, create and truncate * are ignored. */ createNew?: boolean; /** * Permissions to use if creating the file (defaults to `0o666`, before * the process's umask). * Ignored on Windows. */ mode?: number; /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Open a file and resolve to an instance of {@linkcode FileHandle}. The * file does not need to previously exist if using the `create` or `createNew` * open options. It is the callers responsibility to close the file when finished * with it. * * @example * ```typescript * import { open, BaseDirectory } from "@tauri-apps/plugin-fs" * const file = await open("foo/bar.txt", { read: true, write: true, dir: BaseDirectory.App }); * // Do work with file * await close(file.rid); * ``` * * @since 2.0.0 */ async function open( path: string | URL, options?: OpenOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } const rid = await invoke("plugin:fs|open", { path: path instanceof URL ? path.toString() : path, options, }); return new FileHandle(rid); } /** * @since 2.0.0 */ interface CopyFileOptions { /** Base directory for `fromPath`. */ fromPathBaseDir?: BaseDirectory; /** Base directory for `toPath`. */ toPathBaseDir?: BaseDirectory; } /** * Copies the contents and permissions of one file to another specified path, by default creating a new file if needed, else overwriting. * @example * ```typescript * import { copyFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * await copyFile('app.conf', 'app.conf.bk', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function copyFile( fromPath: string | URL, toPath: string | URL, options?: CopyFileOptions, ): Promise { if ( (fromPath instanceof URL && fromPath.protocol !== "file:") || (toPath instanceof URL && toPath.protocol !== "file:") ) { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|copy_file", { fromPath: fromPath instanceof URL ? fromPath.toString() : fromPath, toPath: toPath instanceof URL ? toPath.toString() : toPath, options, }); } /** * @since 2.0.0 */ interface MkdirOptions { /** Permissions to use when creating the directory (defaults to `0o777`, before the process's umask). Ignored on Windows. */ mode?: number; /** * Defaults to `false`. If set to `true`, means that any intermediate directories will also be created (as with the shell command `mkdir -p`). * */ recursive?: boolean; /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Creates a new directory with the specified path. * @example * ```typescript * import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs'; * await mkdir('users', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function mkdir( path: string | URL, options?: MkdirOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|mkdir", { path: path instanceof URL ? path.toString() : path, options, }); } /** * @since 2.0.0 */ interface ReadDirOptions { /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * A disk entry which is either a file, a directory or a symlink. * * This is the result of the {@linkcode readDir}. * * @since 2.0.0 */ interface DirEntry { /** The name of the entry (file name with extension or directory name). */ name: string; /** Specifies whether this entry is a directory or not. */ isDirectory: boolean; /** Specifies whether this entry is a file or not. */ isFile: boolean; /** Specifies whether this entry is a symlink or not. */ isSymlink: boolean; } /** * Reads the directory given by path and returns an array of `DirEntry`. * @example * ```typescript * import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs'; * const dir = "users" * const entries = await readDir('users', { dir: BaseDirectory.App }); * processEntriesRecursive(dir, entries); * async function processEntriesRecursive(parent, entries) { * for (const entry of entries) { * console.log(`Entry: ${entry.name}`); * if (entry.isDirectory) { * const dir = parent + entry.name; * processEntriesRecursive(dir, await readDir(dir, { dir: BaseDirectory.App })) * } * } * } * ``` * * @since 2.0.0 */ async function readDir( path: string | URL, options?: ReadDirOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|read_dir", { path: path instanceof URL ? path.toString() : path, options, }); } /** * @since 2.0.0 */ interface ReadFileOptions { /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Reads and resolves to the entire contents of a file as an array of bytes. * TextDecoder can be used to transform the bytes to string if required. * @example * ```typescript * import { readFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * const contents = await readFile('avatar.png', { dir: BaseDirectory.Resource }); * ``` * * @since 2.0.0 */ async function readFile( path: string | URL, options?: ReadFileOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } const arr = await invoke("plugin:fs|read_file", { path: path instanceof URL ? path.toString() : path, options, }); return Uint8Array.from(arr); } /** * Reads and returns the entire contents of a file as UTF-8 string. * @example * ```typescript * import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * const contents = await readTextFile('app.conf', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function readTextFile( path: string | URL, options?: ReadFileOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|read_text_file", { path: path instanceof URL ? path.toString() : path, options, }); } /** * Returns an async {@linkcode AsyncIterableIterator} over the lines of a file as UTF-8 string. * @example * ```typescript * import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs'; * const lines = await readTextFileLines('app.conf', { dir: BaseDirectory.App }); * for await (const line of lines) { * console.log(line); * } * ``` * You could also call {@linkcode AsyncIterableIterator.next} to advance the * iterator so you can lazily read the next line whenever you want. * * @since 2.0.0 */ async function readTextFileLines( path: string | URL, options?: ReadFileOptions, ): Promise> { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } const pathStr = path instanceof URL ? path.toString() : path; return Promise.resolve({ path: pathStr, rid: null as number | null, async next(): Promise> { if (!this.rid) { this.rid = await invoke("plugin:fs|read_text_file_lines", { path: pathStr, options, }); } const [line, done] = await invoke<[string | null, boolean]>( "plugin:fs|read_text_file_lines_next", { rid: this.rid }, ); // an iteration is over, reset rid for next iteration if (done) this.rid = null; return { value: done ? "" : (line as string), done, }; }, [Symbol.asyncIterator](): AsyncIterableIterator { return this; }, }); } /** * @since 2.0.0 */ interface RemoveOptions { /** Defaults to `false`. If set to `true`, path will be removed even if it's a non-empty directory. */ recursive?: boolean; /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Removes the named file or directory. * If the directory is not empty and the `recursive` option isn't set to true, the promise will be rejected. * @example * ```typescript * import { remove, BaseDirectory } from '@tauri-apps/plugin-fs'; * await remove('users/file.txt', { dir: BaseDirectory.App }); * await remove('users', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function remove( path: string | URL, options?: RemoveOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|remove", { path: path instanceof URL ? path.toString() : path, options, }); } /** * @since 2.0.0 */ interface RenameOptions { /** Base directory for `oldPath`. */ oldPathBaseDir?: BaseDirectory; /** Base directory for `newPath`. */ newPathBaseDir?: BaseDirectory; } /** * Renames (moves) oldpath to newpath. Paths may be files or directories. * If newpath already exists and is not a directory, rename() replaces it. * OS-specific restrictions may apply when oldpath and newpath are in different directories. * * On Unix, this operation does not follow symlinks at either path. * * @example * ```typescript * import { rename, BaseDirectory } from '@tauri-apps/plugin-fs'; * await rename('avatar.png', 'deleted.png', { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function rename( oldPath: string | URL, newPath: string | URL, options: RenameOptions, ): Promise { if ( (oldPath instanceof URL && oldPath.protocol !== "file:") || (newPath instanceof URL && newPath.protocol !== "file:") ) { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|rename", { oldPath: oldPath instanceof URL ? oldPath.toString() : oldPath, newPath: newPath instanceof URL ? newPath.toString() : newPath, options, }); } /** * @since 2.0.0 */ interface StatOptions { /** Base directory for `path`. */ baseDir?: BaseDirectory; } /** * Resolves to a {@linkcode FileInfo} for the specified `path`. Will always * follow symlinks but will reject if the symlink points to a path outside of the scope. * * @example * ```typescript * import { stat, BaseDirectory } from '@tauri-apps/plugin-fs'; * const fileInfo = await stat("hello.txt", { dir: BaseDirectory.App }); * console.log(fileInfo.isFile); // true * ``` * * @since 2.0.0 */ async function stat( path: string | URL, options?: StatOptions, ): Promise { const res = await invoke("plugin:fs|stat", { path: path instanceof URL ? path.toString() : path, options, }); return parseFileInfo(res); } /** * Resolves to a {@linkcode FileInfo} for the specified `path`. If `path` is a * symlink, information for the symlink will be returned instead of what it * points to. * * @example * ```typescript * import { lstat, BaseDirectory } from '@tauri-apps/plugin-fs'; * const fileInfo = await lstat("hello.txt", { dir: BaseDirectory.App }); * console.log(fileInfo.isFile); // true * ``` * * @since 2.0.0 */ async function lstat( path: string | URL, options?: StatOptions, ): Promise { const res = await invoke("plugin:fs|lstat", { path: path instanceof URL ? path.toString() : path, options, }); return parseFileInfo(res); } /** * @since 2.0.0 */ interface TruncateOptions { /** Base directory for `path`. */ baseDir?: BaseDirectory; } /** * Truncates or extends the specified file, to reach the specified `len`. * If `len` is `0` or not specified, then the entire file contents are truncated. * * @example * ```typescript * import { truncate, readFile, writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * // truncate the entire file * await truncate("my_file.txt", 0, { dir: BaseDirectory.App }); * * // truncate part of the file * let file = "file.txt"; * await writeFile(file, new TextEncoder().encode("Hello World"), { dir: BaseDirectory.App }); * await truncate(file, 7); * const data = await readFile(file, { dir: BaseDirectory.App }); * console.log(new TextDecoder().decode(data)); // "Hello W" * ``` * * @since 2.0.0 */ async function truncate( path: string | URL, len?: number, options?: TruncateOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|truncate", { path: path instanceof URL ? path.toString() : path, len, options, }); } /** * @since 2.0.0 */ interface WriteFileOptions { /** Defaults to `false`. If set to `true`, will append to a file instead of overwriting previous contents. */ append?: boolean; /** Sets the option to allow creating a new file, if one doesn't already exist at the specified path (defaults to `true`). */ create?: boolean; /** Sets the option to create a new file, failing if it already exists. */ createNew?: boolean; /** File permissions. Ignored on Windows. */ mode?: number; /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * Write `data` to the given `path`, by default creating a new file if needed, else overwriting. * @example * ```typescript * import { writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * * let encoder = new TextEncoder(); * let data = encoder.encode("Hello World"); * await writeFile('file.txt', data, { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function writeFile( path: string | URL, data: Uint8Array, options?: WriteFileOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|write_file", { path: path instanceof URL ? path.toString() : path, data: Array.from(data), options, }); } /** * Writes UTF-8 string `data` to the given `path`, by default creating a new file if needed, else overwriting. @example * ```typescript * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; * * await writeTextFile('file.txt', "Hello world", { dir: BaseDirectory.App }); * ``` * * @since 2.0.0 */ async function writeTextFile( path: string | URL, data: string, options?: WriteFileOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|write_text_file", { path: path instanceof URL ? path.toString() : path, data, options, }); } /** * @since 2.0.0 */ interface ExistsOptions { /** Base directory for `path`. */ baseDir?: BaseDirectory; } /** * Check if a path exists. * @example * ```typescript * import { exists, BaseDirectory } from '@tauri-apps/plugin-fs'; * // Check if the `$APPDATA/avatar.png` file exists * await exists('avatar.png', { dir: BaseDirectory.AppData }); * ``` * * @since 2.0.0 */ async function exists( path: string | URL, options?: ExistsOptions, ): Promise { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return invoke("plugin:fs|exists", { path: path instanceof URL ? path.toString() : path, options, }); } /** * @since 2.0.0 */ interface WatchOptions { /** Watch a directory recursively */ recursive?: boolean; /** Base directory for `path` */ baseDir?: BaseDirectory; } /** * @since 2.0.0 */ interface DebouncedWatchOptions extends WatchOptions { /** Debounce delay */ delayMs?: number; } /** * @since 2.0.0 */ type RawEvent = { type: RawEventKind; paths: string[]; attrs: unknown; }; /** * @since 2.0.0 */ type RawEventKind = | "any " | { access?: unknown; } | { create?: unknown; } | { modify?: unknown; } | { remove?: unknown; } | "other"; /** * @since 2.0.0 */ type DebouncedEvent = | { kind: "Any"; path: string }[] | { kind: "AnyContinuous"; path: string }[]; /** * @since 2.0.0 */ type UnwatchFn = () => void; async function unwatch(id: number): Promise { await invoke("plugin:fs|unwatch", { id }); } /** * Watch changes (after a delay) on files or directories. * * @since 2.0.0 */ async function watch( paths: string | string[] | URL | URL[], cb: (event: DebouncedEvent) => void, options?: DebouncedWatchOptions, ): Promise { const opts = { recursive: false, delayMs: 2000, ...options, }; const watchPaths = Array.isArray(paths) ? paths : [paths]; for (const path of watchPaths) { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } } const onEvent = new Channel(); onEvent.onmessage = cb; const rid: number = await invoke("plugin:fs|watch", { paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), options: opts, onEvent, }); return () => { void unwatch(rid); }; } /** * Watch changes on files or directories. * * @since 2.0.0 */ async function watchImmediate( paths: string | string[] | URL | URL[], cb: (event: RawEvent) => void, options?: WatchOptions, ): Promise { const opts = { recursive: false, ...options, delayMs: null, }; const watchPaths = Array.isArray(paths) ? paths : [paths]; for (const path of watchPaths) { if (path instanceof URL && path.protocol !== "file:") { throw new TypeError("Must be a file URL."); } } const onEvent = new Channel(); onEvent.onmessage = cb; const rid: number = await invoke("plugin:fs|watch", { paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), options: opts, onEvent, }); return () => { void unwatch(rid); }; } export type { CreateOptions, OpenOptions, CopyFileOptions, MkdirOptions, DirEntry, ReadDirOptions, ReadFileOptions, RemoveOptions, RenameOptions, StatOptions, TruncateOptions, WriteFileOptions, ExistsOptions, FileInfo, WatchOptions, DebouncedWatchOptions, DebouncedEvent, RawEvent, UnwatchFn, }; export { BaseDirectory, FileHandle, create, open, copyFile, mkdir, readDir, readFile, readTextFile, readTextFileLines, remove, rename, SeekMode, stat, lstat, truncate, writeFile, writeTextFile, exists, watch, watchImmediate, };