UNPKG

happy-opfs

Version:

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

1 lines 104 kB
{"version":3,"file":"main.cjs","sources":["../src/fs/constants.ts","../src/fs/assertions.ts","../src/fs/helpers.ts","../src/fs/utils.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/worker/helpers.ts","../src/worker/shared.ts","../src/worker/opfs_worker.ts","../src/worker/opfs_worker_adapter.ts"],"sourcesContent":["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 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}","import { SEPARATOR, basename, dirname } from '@std/path/posix';\nimport { Err, Ok, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult } from 'happy-rusty';\nimport { ABORT_ERROR, CURRENT_DIR, NOT_FOUND_ERROR, ROOT_DIR } from './constants.ts';\n\n/**\n * The root directory handle of the file system.\n */\nlet fsRoot: FileSystemDirectoryHandle;\n\n/**\n * Retrieves the root directory handle of the file system.\n *\n * @returns A promise that resolves to the `FileSystemDirectoryHandle` of the root directory.\n */\nasync function getFsRoot(): Promise<FileSystemDirectoryHandle> {\n fsRoot ??= await navigator.storage.getDirectory();\n return fsRoot;\n}\n\n/**\n * Checks if the provided path is the root directory path.\n *\n * @param path - The path to check.\n * @returns A boolean indicating whether the path is the root directory path.\n */\nexport function isRootPath(path: string): boolean {\n return path === ROOT_DIR;\n}\n\n/**\n * Checks if the provided directory path is the current directory.\n *\n * @param dirPath - The directory path to check.\n * @returns A boolean indicating whether the directory path is the current directory.\n */\nexport function isCurrentDir(dirPath: string): boolean {\n return dirPath === CURRENT_DIR;\n}\n\n/**\n * Asynchronously obtains a handle to a child directory from the given parent directory handle.\n *\n * @param dirHandle - The handle to the parent directory.\n * @param dirName - The name of the child directory to retrieve.\n * @param options - Optional parameters that specify options such as whether to create the directory if it does not exist.\n * @returns A promise that resolves to an `AsyncIOResult` containing the `FileSystemDirectoryHandle` for the child directory.\n */\nasync function getChildDirHandle(dirHandle: FileSystemDirectoryHandle, dirName: string, options?: FileSystemGetDirectoryOptions): AsyncIOResult<FileSystemDirectoryHandle> {\n try {\n const handle = await dirHandle.getDirectoryHandle(dirName, options);\n\n return Ok(handle);\n } catch (e) {\n const err = e as DOMException;\n const error = new Error(`${ err.name }: ${ err.message } When get child directory '${ dirName }' from directory '${ dirHandle.name || ROOT_DIR }'.`);\n error.name = err.name;\n\n return Err(error);\n }\n}\n\n/**\n * Retrieves a file handle for a child file within a directory.\n *\n * @param dirHandle - The directory handle to search within.\n * @param fileName - The name of the file to retrieve.\n * @param options - Optional parameters for getting the file handle.\n * @returns A promise that resolves to an `AsyncIOResult` containing the `FileSystemFileHandle`.\n */\nasync function getChildFileHandle(dirHandle: FileSystemDirectoryHandle, fileName: string, options?: FileSystemGetFileOptions): AsyncIOResult<FileSystemFileHandle> {\n try {\n const handle = await dirHandle.getFileHandle(fileName, options);\n\n return Ok(handle);\n } catch (e) {\n const err = e as DOMException;\n const error = new Error(`${ err.name }: ${ err.message } When get child file '${ fileName }' from directory '${ dirHandle.name || ROOT_DIR }'.`);\n error.name = err.name;\n\n return Err(error);\n }\n}\n\n/**\n * Retrieves a directory handle given a path.\n *\n * @param dirPath - The path of the directory to retrieve.\n * @param options - Optional parameters for getting the directory handle.\n * @returns A promise that resolves to an `AsyncIOResult` containing the `FileSystemDirectoryHandle`.\n */\nexport async function getDirHandle(dirPath: string, options?: FileSystemGetDirectoryOptions): AsyncIOResult<FileSystemDirectoryHandle> {\n // create from root\n let dirHandle = await getFsRoot();\n\n if (isRootPath(dirPath)) {\n // root is already the a handle\n return Ok(dirHandle);\n }\n\n // start with /\n let childDirPath = dirPath.slice(1);\n\n while (childDirPath) {\n let dirName = '';\n const index = childDirPath.indexOf(SEPARATOR);\n\n if (index === -1) {\n dirName = childDirPath;\n childDirPath = '';\n } else {\n dirName = childDirPath.slice(0, index);\n childDirPath = childDirPath.slice(index + 1);\n\n // skip //\n if (index === 0) {\n continue;\n }\n }\n\n const dirHandleRes = await getChildDirHandle(dirHandle, dirName, options);\n if (dirHandleRes.isErr()) {\n // stop\n return dirHandleRes;\n }\n\n dirHandle = dirHandleRes.unwrap();\n }\n\n return Ok(dirHandle);\n}\n\n/**\n * Retrieves a file handle given a file path.\n *\n * @param filePath - The path of the file to retrieve.\n * @param options - Optional parameters for getting the file handle.\n * @returns A promise that resolves to an `AsyncIOResult` containing the `FileSystemFileHandle`.\n */\nexport async function getFileHandle(filePath: string, options?: FileSystemGetFileOptions): AsyncIOResult<FileSystemFileHandle> {\n const isCreate = options?.create ?? false;\n\n const dirPath = dirname(filePath);\n const fileName = basename(filePath);\n\n const dirHandleRes = await getDirHandle(dirPath, {\n create: isCreate,\n });\n\n return dirHandleRes.andThenAsync(dirHandle => {\n return getChildFileHandle(dirHandle, fileName, {\n create: isCreate,\n });\n });\n}\n\n/**\n * Whether the error is a `NotFoundError`.\n * @param err - The error to check.\n * @returns `true` if the error is a `NotFoundError`, otherwise `false`.\n */\nexport function isNotFoundError(err: Error): boolean {\n return err.name === NOT_FOUND_ERROR;\n}\n\n/**\n * Gets the final result from a list of AsyncVoidIOResult tasks.\n * @param tasks - The list of tasks to get the final result from.\n * @returns The final result from the list of tasks.\n */\nexport async function getFinalResult(tasks: AsyncVoidIOResult[]): AsyncVoidIOResult {\n const allRes = await Promise.all(tasks);\n // anyone failed?\n const fail = allRes.find(x => x.isErr());\n\n return fail ?? RESULT_VOID;\n}\n\n/**\n * Creates an `AbortError` Error.\n * @returns An `AbortError` Error.\n */\nexport function createAbortError(): Error {\n const error = new Error();\n error.name = ABORT_ERROR;\n\n return error;\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 { 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 { TIMEOUT_ERROR } from '../fs/constants.ts';\nimport type { ErrorLike, FileLike } from '../fs/defines.ts';\n\n/**\n * Serialize an `Error` to plain object.\n * @param error - `Error` object.\n * @returns Serializable version of Error.\n */\nexport function serializeError(error: Error | null): ErrorLike | null {\n return error ? {\n name: error.name,\n message: error.message,\n } : error;\n}\n\n/**\n * Deserialize an `Error` from plain object.\n * @param error - Serializable version of Error.\n * @returns `Error` object.\n */\nexport function deserializeError(error: ErrorLike): Error {\n const err = new Error(error.message);\n err.name = error.name;\n\n return err;\n}\n\n/**\n * Serialize a `File` to plain object.\n * @param file - `File` object.\n * @returns Serializable version of File.\n */\nexport async function serializeFile(file: File): Promise<FileLike> {\n const ab = await file.arrayBuffer();\n return {\n name: file.name,\n type: file.type,\n lastModified: file.lastModified,\n size: ab.byteLength,\n data: ab,\n };\n}\n\n/**\n * Deserialize a `File` from plain object.\n * @param file - Serializable version of File.\n * @returns `File` object.\n */\nexport function deserializeFile(file: FileLike): File {\n const blob = new Blob([file.data]);\n\n return new File([blob], file.name, {\n type: file.type,\n lastModified: file.lastModified,\n });\n}\n\n/**\n * Global timeout of per sync I/O operation.\n */\nlet globalOpTimeout = 1000;\n\n/**\n * Set global timeout of per sync I/O operation.\n * @param timeout - Timeout in milliseconds.\n */\nexport function setGlobalOpTimeout(timeout: number): void {\n globalOpTimeout = timeout;\n}\n\n/**\n * Sleep until a condition is met.\n * @param condition - Condition to be met.\n */\nexport function sleepUntil(condition: () => boolean) {\n const start = Date.now();\n while (!condition()) {\n if (Date.now() - start > globalOpTimeout) {\n const error = new Error('Operating Timeout');\n error.name = TIMEOUT_ERROR;\n\n throw error;\n }\n }\n}","import { sleepUntil } from './helpers.ts';\n\n/**\n * Async I/O operations called from main thread to worker thread.\n */\nexport const enum WorkerAsyncOp {\n // core\n createFile,\n mkdir,\n move,\n readDir,\n remove,\n stat,\n writeFile,\n // ext\n appendFile,\n copy,\n emptyDir,\n exists,\n deleteTemp,\n mkTemp,\n pruneTemp,\n readBlobFile,\n unzip,\n zip,\n}\n\n/**\n * Main thread lock index used in Int32Array.\n */\nconst MAIN_LOCK_INDEX = 0;\n\n/**\n * Worker thread lock index used in Int32Array.\n */\nconst WORKER_LOCK_INDEX = 1;\n\n/**\n * Data index used in Int32Array.\n */\nconst DATA_INDEX = 2;\n\n/**\n * Main thread locked value.\n */\nconst MAIN_LOCKED = 1;\n\n/**\n * Main thread unlocked value.\n * Default.\n */\nconst MAIN_UNLOCKED = 0;\n\n/**\n * Worker thread locked value.\n * Default.\n */\nconst WORKER_LOCKED = MAIN_UNLOCKED;\n\n/**\n * Worker thread unlocked value.\n */\nconst WORKER_UNLOCKED = MAIN_LOCKED;\n\n/**\n * Cache the `TextEncoder` instance.\n */\nlet encoder: TextEncoder;\n\n/**\n * Cache the `TextDecoder` instance.\n */\nlet decoder: TextDecoder;\n\n/**\n * Get the cached `TextEncoder` instance.\n * @returns Ins