UNPKG

happy-opfs

Version:

A browser-compatible fs module inspired by the Deno fs and @std/fs APIs, based on OPFS implementation.

1 lines 71.7 kB
{"version":3,"file":"types.d.ts","sources":["../src/fs/assertions.ts","../src/fs/constants.ts","../src/fs/defines.ts","../src/fs/opfs_core.ts","../src/fs/opfs_download.ts","../src/fs/opfs_ext.ts","../src/fs/opfs_tmp.ts","../src/fs/opfs_unzip.ts","../src/fs/opfs_upload.ts","../src/fs/opfs_zip.ts","../src/fs/support.ts","../src/fs/utils.ts","../src/worker/opfs_worker.ts","../src/worker/shared.ts","../src/worker/opfs_worker_adapter.ts"],"sourcesContent":["import invariant from 'tiny-invariant';\nimport { ROOT_DIR } from './constants.ts';\n\n/**\n * Asserts that the provided path is an absolute path.\n *\n * @param path - The file path to validate.\n * @throws Will throw an error if the path is not an absolute path.\n */\nexport function assertAbsolutePath(path: string): void {\n invariant(typeof path === 'string', () => `Path must be a string but received ${ path }`);\n invariant(path[0] === ROOT_DIR, () => `Path must start with / but received ${ path }`);\n}\n\n/**\n * Asserts that the provided URL is a valid file URL.\n *\n * @param fileUrl - The file URL to validate.\n * @throws Will throw an error if the URL is not a valid file URL.\n */\nexport function assertFileUrl(fileUrl: string): void {\n invariant(typeof fileUrl === 'string', () => `File url must be a string but received ${ fileUrl }`);\n}","export { ABORT_ERROR, TIMEOUT_ERROR } from '@happy-ts/fetch-t';\n\n/**\n * A constant representing the error thrown when a file or directory is not found.\n * Name of DOMException.NOT_FOUND_ERR.\n */\nexport const NOT_FOUND_ERROR = 'NotFoundError' as const;\n\n/**\n * A constant representing the root directory path.\n */\nexport const ROOT_DIR = '/' as const;\n\n/**\n * A constant representing the current directory path.\n */\nexport const CURRENT_DIR = '.' as const;\n\n/**\n * A constant representing the temporary directory path.\n */\nexport const TMP_DIR = '/tmp' as const;","import type { FetchInit } from '@happy-ts/fetch-t';\n\n/**\n * Represents the possible content types that can be written to a file.\n */\nexport type WriteFileContent = BufferSource | Blob | string;\n\n/**\n * Represents the possible content types that can be written synchronously to a file.\n */\nexport type WriteSyncFileContent = BufferSource | string;\n\n/**\n * Represents the possible content types that can be read from a file.\n */\nexport type ReadFileContent = ArrayBuffer | File | string;\n\n/**\n * Options for reading files with specified encoding.\n */\nexport interface ReadOptions {\n /**\n * The encoding to use for reading the file's content.\n * @defaultValue `'binary'`\n */\n encoding?: FileEncoding;\n}\n\n/**\n * Options for writing files, including flags for creation and appending.\n */\nexport interface WriteOptions {\n /**\n * Whether to create the file if it does not exist.\n * @defaultValue `true`\n */\n create?: boolean;\n\n /**\n * Whether to append to the file if it already exists.\n * @defaultValue `false`\n */\n append?: boolean;\n}\n\n/**\n * Options to determine the existence of a file or directory.\n */\nexport interface ExistsOptions {\n /**\n * Whether to check for the existence of a directory.\n * @defaultValue `false`\n */\n isDirectory?: boolean;\n\n /**\n * Whether to check for the existence of a file.\n * @defaultValue `false`\n */\n isFile?: boolean;\n}\n\n/**\n * Supported file encodings for reading and writing files.\n */\nexport type FileEncoding = 'binary' | 'utf8' | 'blob';\n\n/**\n * fetch-t options for download and upload.\n */\nexport type FsRequestInit = Omit<FetchInit, 'abortable' | 'responseType'>\n\n/**\n * fetch-t request options for uploading files.\n */\nexport interface UploadRequestInit extends FsRequestInit {\n /**\n * The filename to use when uploading the file.\n */\n filename?: string;\n}\n\n/**\n * Options for reading directories.\n */\nexport interface ReadDirOptions {\n /**\n * Whether to recursively read the contents of directories.\n */\n recursive: boolean;\n}\n\n/**\n * An entry returned by `readDir`.\n */\nexport interface ReadDirEntry {\n /**\n * The relative path of the entry from readDir the path parameter.\n */\n path: string;\n\n /**\n * The handle of the entry.\n */\n handle: FileSystemHandle;\n}\n\n/**\n * An entry returned by `readDirSync`.\n */\nexport interface ReadDirEntrySync {\n /**\n * The relative path of the entry from readDir the path parameter.\n */\n path: string;\n\n /**\n * The handle of the entry.\n */\n handle: FileSystemHandleLike;\n}\n\n/**\n * A handle to a file or directory returned by `statSync`.\n */\nexport interface FileSystemHandleLike {\n /**\n * The name of the entry.\n */\n name: string;\n\n /**\n * The kind of the entry.\n */\n kind: FileSystemHandleKind;\n}\n\nexport interface FileSystemFileHandleLike extends FileSystemHandleLike {\n /**\n * The type of the file.\n */\n type: string;\n\n /**\n * The size of the file.\n */\n size: number;\n\n /**\n * The last modified time of the file.\n */\n lastModified: number;\n}\n\n/**\n * Serializable version of Error.\n */\nexport interface ErrorLike {\n /**\n * The name of the error.\n */\n name: string;\n\n /**\n * The message of the error.\n */\n message: string;\n}\n\n/**\n * Serializable version of File.\n */\nexport interface FileLike {\n /**\n * The name of the file.\n */\n name: string;\n\n /**\n * The type of the file.\n */\n type: string;\n\n /**\n * The last modified time of the file.\n */\n lastModified: number;\n\n /**\n * The size of the file.\n */\n size: number;\n\n /**\n * The binary data of the file.\n */\n data: ArrayBuffer;\n}\n\n/**\n * Setup options of `connectSyncAgent`.\n */\nexport interface SyncAgentOptions {\n /**\n * The worker to communicate with.\n */\n worker: Worker | URL | string;\n\n /**\n * The length of the buffer to use for communication.\n */\n bufferLength?: number;\n\n /**\n * The timeout for operations.\n */\n opTimeout?: number;\n}\n\n/**\n * Options for `zip`.\n */\nexport interface ZipOptions {\n /**\n * Whether to preserve the root directory in the zip file.\n * @defaultValue `true`\n */\n preserveRoot: boolean;\n}\n\n/**\n * Options for `mkTemp`.\n */\nexport interface TempOptions {\n /**\n * Whether to create a directory.\n * eg: `mktemp -d`\n * @defaultValue `false`\n */\n isDirectory?: boolean;\n\n /**\n * The basename of the file or directory.\n * eg: `mktemp -t basename.XXX`\n * @defaultValue `tmp`\n */\n basename?: string;\n\n /**\n * The extension of the file.\n * eg: `mktemp --suffix .txt`\n */\n extname?: string;\n}\n\n/**\n * Options for `copy`.\n */\nexport interface CopyOptions {\n /**\n * Whether to overwrite the destination file if it already exists.\n * @defaultValue `true`\n */\n overwrite?: boolean;\n}\n\n/**\n * Result of `downloadFile` when the file is saved to a temporary path.\n */\nexport interface DownloadFileTempResponse {\n /**\n * The temporary path of the downloaded file to be saved.\n */\n tempFilePath: string;\n\n /**\n * The raw response.\n */\n rawResponse: Response;\n}\n\n/**\n * Options for `move`.\n */\nexport interface MoveOptions {\n /**\n * Whether to overwrite the destination file if it already exists.\n * @defaultValue `true`\n */\n overwrite?: boolean;\n}","import { basename, dirname, join } from '@std/path/posix';\nimport { Err, Ok, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult } from 'happy-rusty';\nimport { assertAbsolutePath } from './assertions.ts';\nimport { NOT_FOUND_ERROR } from './constants.ts';\nimport type { ReadDirEntry, ReadDirOptions, ReadFileContent, ReadOptions, WriteFileContent, WriteOptions } from './defines.ts';\nimport { getDirHandle, getFileHandle, isNotFoundError, isRootPath } from './helpers.ts';\nimport { isDirectoryHandle } from './utils.ts';\n\n/**\n * Creates a new file at the specified path same as `touch`.\n *\n * @param filePath - The path of the file to create.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file was successfully created.\n */\nexport async function createFile(filePath: string): AsyncVoidIOResult {\n assertAbsolutePath(filePath);\n\n const fileHandleRes = await getFileHandle(filePath, {\n create: true,\n });\n\n return fileHandleRes.and(RESULT_VOID);\n}\n\n/**\n * Creates a new directory at the specified path same as `mkdir -p`.\n *\n * @param dirPath - The path where the new directory will be created.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the directory was successfully created.\n */\nexport async function mkdir(dirPath: string): AsyncVoidIOResult {\n assertAbsolutePath(dirPath);\n\n const dirHandleRes = await getDirHandle(dirPath, {\n create: true,\n });\n\n return dirHandleRes.and(RESULT_VOID);\n}\n\n/**\n * Reads the contents of a directory at the specified path.\n *\n * @param dirPath - The path of the directory to read.\n * @param options - Options of readdir.\n * @returns A promise that resolves to an `AsyncIOResult` containing an async iterable iterator over the entries of the directory.\n */\nexport async function readDir(dirPath: string, options?: ReadDirOptions): AsyncIOResult<AsyncIterableIterator<ReadDirEntry>> {\n assertAbsolutePath(dirPath);\n\n const dirHandleRes = await getDirHandle(dirPath);\n\n async function* read(dirHandle: FileSystemDirectoryHandle, subDirPath: string): AsyncIterableIterator<ReadDirEntry> {\n const entries = dirHandle.entries();\n\n for await (const [name, handle] of entries) {\n // relative path from `dirPath`\n const path = subDirPath === dirPath ? name : join(subDirPath, name);\n yield {\n path,\n handle,\n };\n\n if (isDirectoryHandle(handle) && options?.recursive) {\n yield* read(await dirHandle.getDirectoryHandle(name), path);\n }\n }\n }\n\n return dirHandleRes.andThen(x => Ok(read(x, dirPath)));\n}\n\n/**\n * Reads the content of a file at the specified path as a File.\n *\n * @param filePath - The path of the file to read.\n * @param options - Read options specifying the 'blob' encoding.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as a File.\n */\nexport function readFile(filePath: string, options: ReadOptions & {\n encoding: 'blob';\n}): AsyncIOResult<File>;\n\n/**\n * Reads the content of a file at the specified path as a string.\n *\n * @param filePath - The path of the file to read.\n * @param options - Read options specifying the 'utf8' encoding.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as a string.\n */\nexport function readFile(filePath: string, options: ReadOptions & {\n encoding: 'utf8';\n}): AsyncIOResult<string>;\n\n/**\n * Reads the content of a file at the specified path as an ArrayBuffer by default.\n *\n * @param filePath - The path of the file to read.\n * @param options - Read options specifying the 'binary' encoding.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as an ArrayBuffer.\n */\nexport function readFile(filePath: string, options?: ReadOptions & {\n encoding: 'binary';\n}): AsyncIOResult<ArrayBuffer>;\n\n/**\n * Reads the content of a file at the specified path with the specified options.\n *\n * @template T The type of the content to read from the file.\n * @param filePath - The path of the file to read.\n * @param options - Optional read options.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content.\n */\nexport async function readFile<T extends ReadFileContent>(filePath: string, options?: ReadOptions): AsyncIOResult<T> {\n assertAbsolutePath(filePath);\n\n const fileHandleRes = await getFileHandle(filePath);\n\n return fileHandleRes.andThenAsync(async fileHandle => {\n const file = await fileHandle.getFile();\n switch (options?.encoding) {\n case 'blob': {\n return Ok(file as unknown as T);\n }\n case 'utf8': {\n const text = await file.text();\n return Ok(text as unknown as T);\n }\n default: {\n const data = await file.arrayBuffer();\n return Ok(data as unknown as T);\n }\n }\n });\n}\n\n/**\n * Removes a file or directory at the specified path same as `rm -rf`.\n *\n * @param path - The path of the file or directory to remove.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file or directory was successfully removed.\n */\nexport async function remove(path: string): AsyncVoidIOResult {\n assertAbsolutePath(path);\n\n const dirPath = dirname(path);\n const childName = basename(path);\n\n const dirHandleRes = await getDirHandle(dirPath);\n\n return (await dirHandleRes.andThenAsync(async (dirHandle): AsyncVoidIOResult => {\n try {\n // root\n if (isRootPath(dirPath) && isRootPath(childName)) {\n // TODO ts not support yet\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (dirHandle as any).remove({\n recursive: true,\n });\n } else {\n await dirHandle.removeEntry(childName, {\n recursive: true,\n });\n }\n } catch (e) {\n return Err(e as DOMException);\n }\n\n return RESULT_VOID;\n })).orElse<Error>(err => {\n // not found as success\n return isNotFoundError(err) ? RESULT_VOID : Err(err);\n });\n}\n\n/**\n * Retrieves the status of a file or directory at the specified path.\n *\n * @param path - The path of the file or directory to retrieve status for.\n * @returns A promise that resolves to an `AsyncIOResult` containing the `FileSystemHandle`.\n */\nexport async function stat(path: string): AsyncIOResult<FileSystemHandle> {\n assertAbsolutePath(path);\n\n const dirPath = dirname(path);\n const childName = basename(path);\n\n const dirHandleRes = await getDirHandle(dirPath);\n if (!childName) {\n // root\n return dirHandleRes;\n }\n\n return dirHandleRes.andThenAsync(async dirHandle => {\n // currently only rely on traversal inspection\n for await (const [name, handle] of dirHandle.entries()) {\n if (name === childName) {\n return Ok(handle);\n }\n }\n\n const err = new Error(`${ NOT_FOUND_ERROR }: '${ childName }' does not exist. Full path is '${ path }'.`);\n err.name = NOT_FOUND_ERROR;\n\n return Err(err);\n });\n}\n\n/**\n * Writes content to a file at the specified path.\n *\n * @param filePath - The path of the file to write to.\n * @param contents - The content to write to the file.\n * @param options - Optional write options.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file was successfully written.\n */\nexport async function writeFile(filePath: string, contents: WriteFileContent, options?: WriteOptions): AsyncVoidIOResult {\n assertAbsolutePath(filePath);\n\n // create as default\n const { append = false, create = true } = options ?? {};\n\n const fileHandleRes = await getFileHandle(filePath, {\n create,\n });\n\n return fileHandleRes.andThenAsync(async fileHandle => {\n const writable = await fileHandle.createWritable({\n keepExistingData: append,\n });\n const params: WriteParams = {\n type: 'write',\n data: contents,\n };\n\n // append?\n if (append) {\n const { size } = await fileHandle.getFile();\n params.position = size;\n }\n\n await writable.write(params);\n await writable.close();\n\n return RESULT_VOID;\n });\n}","import { fetchT, type FetchResponse, type FetchTask } from '@happy-ts/fetch-t';\nimport { extname } from '@std/path/posix';\nimport { Err, Ok } from 'happy-rusty';\nimport { assertAbsolutePath, assertFileUrl } from './assertions.ts';\nimport type { DownloadFileTempResponse, FsRequestInit } from './defines.ts';\nimport { createAbortError } from './helpers.ts';\nimport { writeFile } from './opfs_core.ts';\nimport { generateTempPath } from './utils.ts';\n\n/**\n * Downloads a file from a URL and saves it to a temporary file.\n * The returned response will contain the temporary file path.\n *\n * @param fileUrl - The URL of the file to download.\n * @param requestInit - Optional request initialization parameters.\n * @returns A task that can be aborted and contains the result of the download.\n */\nexport function downloadFile(fileUrl: string, requestInit?: FsRequestInit): FetchTask<DownloadFileTempResponse>;\n/**\n * Downloads a file from a URL and saves it to the specified path.\n *\n * @param fileUrl - The URL of the file to download.\n * @param filePath - The path where the downloaded file will be saved.\n * @param requestInit - Optional request initialization parameters.\n * @returns A task that can be aborted and contains the result of the download.\n */\nexport function downloadFile(fileUrl: string, filePath: string, requestInit?: FsRequestInit): FetchTask<Response>;\nexport function downloadFile(fileUrl: string, filePath?: string | FsRequestInit, requestInit?: FsRequestInit): FetchTask<Response | DownloadFileTempResponse> {\n assertFileUrl(fileUrl);\n\n let saveToTemp = false;\n\n if (typeof filePath === 'string') {\n assertAbsolutePath(filePath);\n } else {\n requestInit = filePath;\n // save to a temporary file, reserve the extension\n filePath = generateTempPath({\n extname: extname(fileUrl),\n });\n saveToTemp = true;\n }\n\n let aborted = false;\n\n const fetchTask = fetchT(fileUrl, {\n redirect: 'follow',\n ...requestInit,\n abortable: true,\n });\n\n const response = (async (): FetchResponse<Response> => {\n const responseRes = await fetchTask.response;\n\n return responseRes.andThenAsync(async response => {\n const blob = await response.blob();\n\n // maybe aborted\n if (aborted) {\n return Err(createAbortError());\n }\n\n const writeRes = await writeFile(filePath, blob);\n\n return writeRes.and(Ok(response));\n });\n })();\n\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abort(reason?: any): void {\n aborted = true;\n fetchTask.abort(reason);\n },\n\n get aborted(): boolean {\n return aborted;\n },\n\n get response(): FetchResponse<Response | DownloadFileTempResponse> {\n return saveToTemp\n ? response.then(res => {\n return res.map<DownloadFileTempResponse>(rawResponse => {\n return {\n tempFilePath: filePath,\n rawResponse,\n };\n });\n })\n : response;\n },\n };\n}","import { basename, dirname, join } from '@std/path/posix';\nimport { Err, Ok, RESULT_FALSE, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult, type IOResult } from 'happy-rusty';\nimport invariant from 'tiny-invariant';\nimport { assertAbsolutePath } from './assertions.ts';\nimport type { CopyOptions, ExistsOptions, MoveOptions, WriteFileContent } from './defines.ts';\nimport { getDirHandle, getFinalResult, isNotFoundError } from './helpers.ts';\nimport { mkdir, readDir, readFile, remove, stat, writeFile } from './opfs_core.ts';\nimport { isDirectoryHandle, isFileHandle } from './utils.ts';\n\n/**\n * Moves a file handle to a new path.\n *\n * @param fileHandle - The file handle to move.\n * @param newPath - The new path of the file handle.\n * @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file handle was successfully moved.\n */\nasync function moveHandle(fileHandle: FileSystemFileHandle, newPath: string): AsyncVoidIOResult {\n const newDirPath = dirname(newPath);\n\n return (await getDirHandle(newDirPath, {\n create: true,\n })).andThenAsync(async newDirHandle => {\n const newName = basename(newPath);\n\n try {\n // TODO ts not support yet\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (fileHandle as any).move(newDirHandle, newName);\n return RESULT_VOID;\n } catch (e) {\n return Err(e as DOMException);\n }\n });\n}\n\n/**\n * @param srcFileHandle - The source file handle to move or copy.\n * @param destFilePath - The destination file path.\n */\ntype handleSrcFileToDest = (srcFileHandle: FileSystemFileHandle, destFilePath: string) => AsyncVoidIOResult;\n/**\n * Copy or move a file or directory from one path to another.\n * @param srcPath - The source file/directory path.\n * @param destPath - The destination file/directory path.\n * @param handler - How to handle the file handle to the destination.\n * @param overwrite - Whether to overwrite the destination file if it exists.\n * @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file was successfully copied/moved.\n */\nasync function mkDestFromSrc(srcPath: string, destPath: string, handler: handleSrcFileToDest, overwrite = true): AsyncVoidIOResult {\n assertAbsolutePath(destPath);\n\n return (await stat(srcPath)).andThenAsync(async srcHandle => {\n // if overwrite is false, we need this flag to determine whether to write file.\n let destExists = false;\n const destHandleRes = await stat(destPath);\n\n if (destHandleRes.isErr()) {\n if (!isNotFoundError(destHandleRes.unwrapErr())) {\n return destHandleRes.asErr();\n }\n } else {\n destExists = true;\n // check\n const destHandle = destHandleRes.unwrap();\n if (!((isFileHandle(srcHandle) && isFileHandle(destHandle))\n || (isDirectoryHandle(srcHandle) && isDirectoryHandle(destHandle)))) {\n return Err(new Error(`Both 'srcPath' and 'destPath' must both be a file or directory.`));\n }\n }\n\n // both are files\n if (isFileHandle(srcHandle)) {\n return (overwrite || !destExists) ? await handler(srcHandle, destPath) : RESULT_VOID;\n }\n\n // both are directories\n const readDirRes = await readDir(srcPath, {\n recursive: true,\n });\n return readDirRes.andThenAsync(async entries => {\n const tasks: AsyncVoidIOResult[] = [\n // make sure new dir created\n mkdir(destPath),\n ];\n\n for await (const { path, handle } of entries) {\n const newEntryPath = join(destPath, path);\n\n let newPathExists = false;\n if (destExists) {\n // should check every file\n const existsRes = await exists(newEntryPath);\n if (existsRes.isErr()) {\n tasks.push(Promise.resolve(existsRes.asErr()));\n continue;\n }\n\n newPathExists = existsRes.unwrap();\n }\n\n const res: AsyncVoidIOResult = isFileHandle(handle)\n ? (overwrite || !newPathExists ? handler(handle, newEntryPath) : Promise.resolve(RESULT_VOID))\n : mkdir(newEntryPath);\n\n tasks.push(res);\n }\n\n return getFinalResult(tasks);\n });\n });\n}\n\n/**\n * Appends content to a file at the specified path.\n *\n * @param filePath - The path of the file to append to.\n * @param contents - The content to append to the file.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the content was successfully appended.\n */\nexport function appendFile(filePath: string, contents: WriteFileContent): AsyncVoidIOResult {\n return writeFile(filePath, contents, {\n append: true,\n });\n}\n\n/**\n * Copies a file or directory from one location to another same as `cp -r`.\n *\n * Both `srcPath` and `destPath` must both be a file or directory.\n *\n * @param srcPath - The source file/directory path.\n * @param destPath - The destination file/directory path.\n * @param options - The copy options.\n * @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the file was successfully copied.\n */\nexport async function copy(srcPath: string, destPath: string, options?: CopyOptions): AsyncVoidIOResult {\n const {\n overwrite = true,\n } = options ?? {};\n\n return mkDestFromSrc(srcPath, destPath, async (srcHandle, destPath) => {\n return await writeFile(destPath, await srcHandle.getFile());\n }, overwrite);\n}\n\n/**\n * Empties the contents of a directory at the specified path.\n *\n * @param dirPath - The path of the directory to empty.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the directory was successfully emptied.\n */\nexport async function emptyDir(dirPath: string): AsyncVoidIOResult {\n const readDirRes = await readDir(dirPath);\n\n if (readDirRes.isErr()) {\n // create if not exist\n return isNotFoundError(readDirRes.unwrapErr()) ? mkdir(dirPath) : readDirRes.asErr();\n }\n\n const tasks: AsyncVoidIOResult[] = [];\n\n for await (const { path } of readDirRes.unwrap()) {\n tasks.push(remove(join(dirPath, path)));\n }\n\n return getFinalResult(tasks);\n}\n\n/**\n * Checks whether a file or directory exists at the specified path.\n *\n * @param path - The path of the file or directory to check for existence.\n * @param options - Optional existence options.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file or directory exists.\n */\nexport async function exists(path: string, options?: ExistsOptions): AsyncIOResult<boolean> {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n invariant(!(isDirectory && isFile), () => 'ExistsOptions.isDirectory and ExistsOptions.isFile must not be true together.');\n\n const statRes = await stat(path);\n\n return statRes.andThen(handle => {\n const notExist =\n (isDirectory && isFileHandle(handle))\n || (isFile && isDirectoryHandle(handle));\n\n return Ok(!notExist);\n }).orElse((err): IOResult<boolean> => {\n return isNotFoundError(err) ? RESULT_FALSE : statRes.asErr();\n });\n}\n\n/**\n * Move a file or directory from an old path to a new path.\n *\n * @param srcPath - The current path of the file or directory.\n * @param destPath - The new path of the file or directory.\n * @param options - Options of move.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file or directory was successfully moved.\n */\nexport async function move(srcPath: string, destPath: string, options?: MoveOptions): AsyncVoidIOResult {\n const {\n overwrite = true,\n } = options ?? {};\n\n return (await mkDestFromSrc(srcPath, destPath, moveHandle, overwrite)).andThenAsync(() => {\n // finally remove src\n return remove(srcPath);\n });\n}\n\n/**\n * Reads the content of a file at the specified path as a File.\n *\n * @param filePath - The path of the file to read.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as a File.\n */\nexport function readBlobFile(filePath: string): AsyncIOResult<File> {\n return readFile(filePath, {\n encoding: 'blob',\n });\n}\n\n/**\n * Reads the content of a file at the specified path as a string and returns it as a JSON object.\n *\n * @param filePath - The path of the file to read.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as a JSON object.\n */\nexport async function readJsonFile<T>(filePath: string): AsyncIOResult<T> {\n return (await readTextFile(filePath)).andThenAsync(async contents => {\n try {\n return Ok(JSON.parse(contents));\n } catch (e) {\n return Err(e as Error);\n }\n });\n}\n\n/**\n * Reads the content of a file at the specified path as a string.\n *\n * @param filePath - The path of the file to read.\n * @returns A promise that resolves to an `AsyncIOResult` containing the file content as a string.\n */\nexport function readTextFile(filePath: string): AsyncIOResult<string> {\n return readFile(filePath, {\n encoding: 'utf8',\n });\n}","import { Err, Ok, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult } from 'happy-rusty';\nimport invariant from 'tiny-invariant';\nimport { TMP_DIR } from './constants.ts';\nimport type { TempOptions } from './defines.ts';\nimport { createFile, mkdir, readDir, remove } from './opfs_core.ts';\nimport { generateTempPath, isFileHandle } from './utils.ts';\n\n/**\n * Create a temporary file or directory.\n *\n * @param options - Options and flags.\n * @returns A promise that resolves the result of the temporary file or directory path.\n */\nexport async function mkTemp(options?: TempOptions): AsyncIOResult<string> {\n const {\n isDirectory = false,\n } = options ?? {};\n\n const path = generateTempPath(options);\n const res = await (isDirectory ? mkdir : createFile)(path);\n\n return res.and(Ok(path));\n}\n\n/**\n * Delete the temporary directory and all its contents.\n * @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the temporary directory was successfully deleted.\n */\nexport function deleteTemp(): AsyncVoidIOResult {\n return remove(TMP_DIR);\n}\n\n/**\n * Prune the temporary directory and delete all expired files.\n * @param expired - The date to determine whether a file is expired.\n * @returns A promise that resolves to an `AsyncVoidIOResult` indicating whether the temporary directory was successfully pruned.\n */\nexport async function pruneTemp(expired: Date): AsyncVoidIOResult {\n invariant(expired instanceof Date, () => `Expired must be a Date but received ${ expired }`);\n\n const readDirRes = await readDir(TMP_DIR, {\n recursive: true,\n });\n\n return readDirRes.andThenAsync(async entries => {\n try {\n for await (const { handle } of entries) {\n if (isFileHandle(handle) && (await handle.getFile()).lastModified <= expired.getTime()) {\n // TODO ts not support yet\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (handle as any).remove();\n }\n }\n } catch (e) {\n return Err(e as DOMException);\n }\n\n return RESULT_VOID;\n });\n}","import { fetchT } from '@happy-ts/fetch-t';\nimport { join, SEPARATOR } from '@std/path/posix';\nimport * as fflate from 'fflate/browser';\nimport { Err, type AsyncVoidIOResult, type VoidIOResult } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { assertAbsolutePath, assertFileUrl } from './assertions.ts';\nimport type { FsRequestInit } from './defines.ts';\nimport { getFinalResult } from './helpers.ts';\nimport { readFile, writeFile } from './opfs_core.ts';\n\n/**\n * Unzip a buffer then write to the target path.\n * @param buffer - Zipped ArrayBuffer.\n * @param targetPath - Target directory path.\n */\nasync function unzipBufferToTarget(buffer: ArrayBuffer, targetPath: string): AsyncVoidIOResult {\n const data = new Uint8Array(buffer);\n\n const future = new Future<VoidIOResult>();\n\n fflate.unzip(data, async (err, unzipped) => {\n if (err) {\n future.resolve(Err(err));\n return;\n }\n\n const tasks: AsyncVoidIOResult[] = [];\n\n for (const path in unzipped) {\n // ignore directory\n if (path.at(-1) !== SEPARATOR) {\n tasks.push(writeFile(join(targetPath, path), unzipped[path]));\n }\n }\n\n future.resolve(getFinalResult(tasks));\n });\n\n return await future.promise;\n}\n\n/**\n * Unzip a zip file to a directory.\n * Equivalent to `unzip -o <zipFilePath> -d <targetPath>\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the unzip backend.\n * @param zipFilePath - Zip file path.\n * @param targetPath - The directory to unzip to.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the zip file was successfully unzipped.\n */\nexport async function unzip(zipFilePath: string, targetPath: string): AsyncVoidIOResult {\n assertAbsolutePath(targetPath);\n\n const fileRes = await readFile(zipFilePath);\n\n return fileRes.andThenAsync(buffer => {\n return unzipBufferToTarget(buffer, targetPath);\n });\n}\n\n/**\n * Unzip a remote zip file to a directory.\n * Equivalent to `unzip -o <zipFilePath> -d <targetPath>\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the unzip backend.\n * @param zipFileUrl - Zip file url.\n * @param targetPath - The directory to unzip to.\n * @param requestInit - Optional request initialization parameters.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the zip file was successfully unzipped.\n */\nexport async function unzipFromUrl(zipFileUrl: string, targetPath: string, requestInit?: FsRequestInit): AsyncVoidIOResult {\n assertFileUrl(zipFileUrl);\n assertAbsolutePath(targetPath);\n\n const fetchRes = await fetchT(zipFileUrl, {\n redirect: 'follow',\n ...requestInit,\n responseType: 'arraybuffer',\n abortable: false,\n });\n\n return fetchRes.andThenAsync(buffer => {\n return unzipBufferToTarget(buffer, targetPath);\n });\n}","import { fetchT, type FetchResponse, type FetchTask } from '@happy-ts/fetch-t';\nimport { basename } from '@std/path/posix';\nimport { Err } from 'happy-rusty';\nimport { assertFileUrl } from './assertions.ts';\nimport type { UploadRequestInit } from './defines.ts';\nimport { createAbortError } from './helpers.ts';\nimport { readBlobFile } from './opfs_ext.ts';\n\n/**\n * Uploads a file from the specified path to a URL.\n *\n * @param filePath - The path of the file to upload.\n * @param fileUrl - The URL where the file will be uploaded.\n * @param requestInit - Optional request initialization parameters.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the file was successfully uploaded.\n */\nexport function uploadFile(filePath: string, fileUrl: string, requestInit?: UploadRequestInit): FetchTask<Response> {\n type T = Response;\n\n assertFileUrl(fileUrl);\n\n let aborted = false;\n\n let fetchTask: FetchTask<T>;\n\n const response = (async (): FetchResponse<T> => {\n const fileRes = await readBlobFile(filePath)\n\n return fileRes.andThenAsync(async file => {\n // maybe aborted\n if (aborted) {\n return Err(createAbortError());\n }\n\n const {\n // default file name\n filename = basename(filePath),\n ...rest\n } = requestInit ?? {};\n\n const formData = new FormData();\n formData.append(filename, file, filename);\n\n fetchTask = fetchT(fileUrl, {\n method: 'POST',\n ...rest,\n abortable: true,\n body: formData,\n });\n\n return fetchTask.response;\n });\n })();\n\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n abort(reason?: any): void {\n aborted = true;\n fetchTask?.abort(reason);\n },\n\n get aborted(): boolean {\n return aborted;\n },\n\n get response(): FetchResponse<T> {\n return response;\n },\n };\n}","import { fetchT } from '@happy-ts/fetch-t';\nimport { basename, join } from '@std/path/posix';\nimport * as fflate from 'fflate/browser';\nimport { Err, Ok, type AsyncIOResult, type AsyncVoidIOResult, type IOResult } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { assertAbsolutePath, assertFileUrl } from './assertions.ts';\nimport type { FsRequestInit, ZipOptions } from './defines.ts';\nimport { readDir, stat, writeFile } from './opfs_core.ts';\nimport { getFileDataByHandle, isFileHandle } from './utils.ts';\n\n/**\n * Zip a zippable data then write to the target path.\n * @param zippable - Zippable data.\n * @param zipFilePath - Target zip file path.\n */\nasync function zipTo<T>(zippable: fflate.AsyncZippable, zipFilePath?: string): AsyncIOResult<T> {\n const future = new Future<IOResult<T>>();\n\n fflate.zip(zippable, {\n consume: true,\n }, async (err, u8a) => {\n if (err) {\n future.resolve(Err(err));\n return;\n }\n\n // whether to write to file\n if (zipFilePath) {\n const res = await writeFile(zipFilePath, u8a);\n future.resolve(res as IOResult<T>);\n } else {\n future.resolve(Ok(u8a as T));\n }\n });\n\n return await future.promise;\n}\n\n/**\n * Zip a file or directory and write to a zip file.\n * Equivalent to `zip -r <zipFilePath> <targetPath>`.\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the zip backend.\n * @param sourcePath - The path to be zipped.\n * @param zipFilePath - The path to the zip file.\n * @param options - Options of zip.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the source was successfully zipped.\n */\nexport async function zip(sourcePath: string, zipFilePath: string, options?: ZipOptions): AsyncVoidIOResult;\n\n/**\n * Zip a file or directory and return the zip file data.\n * Equivalent to `zip -r <zipFilePath> <targetPath>`.\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the zip backend.\n * @param sourcePath - The path to be zipped.\n * @param options - Options of zip.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the source was successfully zipped.\n */\nexport async function zip(sourcePath: string, options?: ZipOptions): AsyncIOResult<Uint8Array>;\nexport async function zip<T>(sourcePath: string, zipFilePath?: string | ZipOptions, options?: ZipOptions): AsyncIOResult<T> {\n if (typeof zipFilePath === 'string') {\n assertAbsolutePath(zipFilePath);\n } else {\n options = zipFilePath;\n zipFilePath = undefined;\n }\n\n const statRes = await stat(sourcePath);\n\n return statRes.andThenAsync(async handle => {\n const sourceName = basename(sourcePath);\n const zippable: fflate.AsyncZippable = {};\n\n if (isFileHandle(handle)) {\n // file\n const data = await getFileDataByHandle(handle);\n zippable[sourceName] = data;\n } else {\n // directory\n const readDirRes = await readDir(sourcePath, {\n recursive: true,\n });\n if (readDirRes.isErr()) {\n return readDirRes.asErr();\n }\n\n // default to preserve root\n const preserveRoot = options?.preserveRoot ?? true;\n\n for await (const { path, handle } of readDirRes.unwrap()) {\n // path\n if (isFileHandle(handle)) {\n const entryName = preserveRoot ? join(sourceName, path) : path;\n const data = await getFileDataByHandle(handle);\n zippable[entryName] = data;\n }\n }\n }\n\n return zipTo(zippable, zipFilePath);\n });\n}\n\n/**\n * Zip a remote file and write to a zip file.\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the zip backend.\n * @param sourceUrl - The url to be zipped.\n * @param zipFilePath - The path to the zip file.\n * @param requestInit - Optional request initialization parameters.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the source was successfully zipped.\n */\nexport async function zipFromUrl(sourceUrl: string, zipFilePath: string, requestInit?: FsRequestInit): AsyncVoidIOResult;\n\n/**\n * Zip a remote file and return the zip file data.\n *\n * Use [fflate](https://github.com/101arrowz/fflate) as the zip backend.\n * @param sourceUrl - The url to be zipped.\n * @param requestInit - Optional request initialization parameters.\n * @returns A promise that resolves to an `AsyncIOResult` indicating whether the source was successfully zipped.\n */\nexport async function zipFromUrl(sourceUrl: string, requestInit?: FsRequestInit): AsyncIOResult<Uint8Array>;\nexport async function zipFromUrl<T>(sourceUrl: string, zipFilePath?: string | FsRequestInit, requestInit?: FsRequestInit): AsyncIOResult<T> {\n assertFileUrl(sourceUrl);\n\n if (typeof zipFilePath === 'string') {\n assertAbsolutePath(zipFilePath);\n } else {\n requestInit = zipFilePath;\n zipFilePath = undefined;\n }\n\n const fetchRes = await fetchT(sourceUrl, {\n redirect: 'follow',\n ...requestInit,\n responseType: 'arraybuffer',\n abortable: false,\n });\n\n return fetchRes.andThenAsync(buffer => {\n const sourceName = basename(sourceUrl);\n const zippable: fflate.AsyncZippable = {};\n\n zippable[sourceName] = new Uint8Array(buffer);\n\n return zipTo(zippable, zipFilePath);\n });\n}","/**\n * Checks if the Origin Private File System (OPFS) is supported in the current environment.\n *\n * @returns A boolean indicating whether OPFS is supported.\n */\nexport function isOPFSSupported(): boolean {\n return typeof navigator?.storage?.getDirectory === 'function';\n}","import { join, SEPARATOR } from '@std/path/posix';\nimport { TMP_DIR } from './constants.ts';\nimport type { FileSystemFileHandleLike, FileSystemHandleLike, TempOptions } from './defines.ts';\n\n/**\n * Generate a temporary path but not create it.\n *\n * @param options - Options and flags.\n * @returns The temporary path.\n */\nexport function generateTempPath(options?: TempOptions): string {\n const {\n isDirectory = false,\n basename = 'tmp',\n extname = '',\n } = options ?? {};\n\n const base = basename ? `${ basename }-` : '';\n const ext = isDirectory ? '' : extname;\n\n // use uuid to generate a unique name\n return join(TMP_DIR, `${ base }${ crypto.randomUUID() }${ ext }`);\n}\n\n/**\n * Check whether the path is a temporary path.\n * @param path - The path to check.\n * @returns `true` if the path is a temporary path otherwise `false`.\n */\nexport function isTempPath(path: string): boolean {\n return path.startsWith(`${ TMP_DIR }${ SEPARATOR }`);\n}\n\n/**\n * Serialize a `FileSystemHandle` to plain object.\n * @param handle - `FileSystemHandle` object.\n * @returns Serializable version of FileSystemHandle that is FileSystemHandleLike.\n */\nexport async function toFileSystemHandleLike(handle: FileSystemHandle): Promise<FileSystemHandleLike> {\n const { name, kind } = handle;\n\n if (isFileHandle(handle)) {\n const file = await handle.getFile();\n const { size, lastModified, type } = file;\n\n const fileHandle: FileSystemFileHandleLike = {\n name,\n kind,\n type,\n size,\n lastModified,\n };\n\n return fileHandle;\n }\n\n const handleLike: FileSystemHandleLike= {\n name,\n kind,\n };\n\n return handleLike;\n}\n\n/**\n * Whether the handle is a file.\n * @param handle - The handle which is a FileSystemHandle.\n * @returns `true` if the handle is a file, otherwise `false`.\n */\nexport function isFileHandle(handle: FileSystemHandle): handle is FileSystemFileHandle {\n return handle.kind === 'file';\n}\n\n/**\n * Whether the handle is a directory.\n * @param handle - The handle which is a FileSystemHandle.\n * @returns `true` if the handle is a directory, otherwise `false`.\n */\nexport function isDirectoryHandle(handle: FileSystemHandle): handle is FileSystemDirectoryHandle {\n return handle.kind === 'directory';\n}\n\n/**\n * Whether the handle is a file-like.\n * @param handle - The handle which is a FileSystemHandleLike.\n * @returns `true` if the handle is a file, otherwise `false`.\n */\nexport function isFileHandleLike(handle: FileSystemHandleLike): handle is FileSystemFileHandleLike {\n return handle.kind === 'file';\n}\n\n/**\n * Gets the data of a file handle.\n * @param handle - The file handle.\n * @returns A promise that resolves to the data of the file.\n */\nexport async function getFileDataByHandle(handle: FileSystemFileHandle): Promise<Uint8Array> {\n const file = await handle.getFile();\n const ab = await file.arrayBuffer();\n return new Uint8Array(ab);\n}","import type { IOResult } from 'happy-rusty';\nimport type { ReadDirEntry, ReadDirEntrySync } from '../fs/defines.ts';\nimport { createFile, mkdir, readDir, remove, stat, writeFile } from '../fs/opfs_core.ts';\nimport { appendFile, copy, emptyDir, exists, move, readBlobFile, } from '../fs/opfs_ext.ts';\nimport { deleteTemp, mkTemp, pruneTemp } from '../fs/opfs_tmp.ts';\nimport { unzip } from '../fs/opfs_unzip.ts';\nimport { zip } from '../fs/opfs_zip.ts';\nimport { toFileSystemHandleLike } from '../fs/utils.ts';\nimport { serializeError, serializeFile } from './helpers.ts';\nimport { decodeFromBuffer, encodeToBuffer, respondToMainFromWorker, SyncMessenger, WorkerAsyncOp } from './shared.ts';\n\n/**\n * Async I/O operations which allow to call from main thread.\n */\nconst asyncOps = {\n [WorkerAsyncOp.createFile]: createFile,\n [WorkerAsyncOp.mkdir]: mkdir,\n [WorkerAsyncOp.move]: move,\n [WorkerAsyncOp.readDir]: readDir,\n [WorkerAsyncOp.remove]: remove,\n [WorkerAsyncOp.stat]: stat,\n [WorkerAsyncOp.writeFile]: writeFile,\n [WorkerAsyncOp.appendFile]: appendFile,\n [WorkerAsyncOp.copy]: copy,\n [WorkerAsyncOp.emptyDir]: emptyDir,\n [WorkerAsyncOp.exists]: exists,\n [WorkerAsyncOp.deleteTemp]: deleteTemp,\n [WorkerAsyncOp.mkTemp]: mkTemp,\n [WorkerAsyncOp.pruneTemp]: pruneTemp,\n [WorkerAsyncOp.readBlobFile]: readBlobFile,\n [WorkerAsyncOp.unzip]: unzip,\n [WorkerAsyncOp.zip]: zip,\n};\n\n/**\n * Cache the messenger instance.\n */\nlet messenger: SyncMessenger;\n\n/**\n * Start worker agent.\n * Listens to postMessage from main thread.\n * Start runner loop.\n */\nexport function startSyncAgent() {\n if (typeof window !== 'undefined') {\n throw new Error('Only can use in worker');\n }\n\n if (messenger) {\n throw new Error('Worker messenger already started');\n }\n\n addEventListener('message', (event: MessageEvent<SharedArrayBuffer>) => {\n // created at main thread and transfer to worker\n const sab = event.data;\n\n if (!(sab instanceof SharedArrayBuffer)) {\n throw new TypeError('Only can post SharedArrayBuffer to Worker');\n }\n\n messenger = new SyncMessenger(sab);\n\n // notify main thread that worker is ready\n postMessage(true);\n\n // start waiting for request\n runWorkerLoop();\n });\n}\n\n/**\n * Run worker loop.\n */\nasync function runWorkerLoop(): Promise<void> {\n // loop forever\n while (true) {\n try {\n await respondToMainFromWorker(messenger, async (data) => {\n const [op, ...args] = decodeFromBuffer(data) as [WorkerAsyncOp, ...Parameters<typeof asyncOps[WorkerAsyncOp]>];\n\n // handling unequal parameters for serialization and deserialization\n if (op === WorkerAsyncOp.writeFile || op === WorkerAsyncOp.appendFile) {\n // actually is an byte array\n if (Array.isArray(args[1])) {\n args[1] = new Uint8Array(args[1]);\n }\n } else if (op === WorkerAsyncOp.pruneTemp) {\n // actually is a Date string\n args[0] = new Date(args[0] as Date);\n }\n\n let response: Uint8Array;\n\n const handle = asyncOps[op];\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const res: IOResult<any> = await (handle as any)(...args);\n\n if (res.isErr()) {\n // without result success\n response = encodeToBuffer([serializeError(res.unwrapErr())]);\n } else {\n // manually serialize response\n let rawResponse;\n\n if (op === WorkerAsyncOp.readBlobFile) {\n const file: File = res.unwrap();\n\n