UNPKG

minigame-std

Version:

Mini Game Standard Development Library.

1 lines 232 kB
{"version":3,"file":"main.cjs","sources":["../src/std/assert/assertions.ts","../src/macros/env.ts","../src/std/fs/fs_helpers.ts","../src/std/utils/promisify.ts","../src/std/utils/mod.ts","../src/std/fs/mina_fs_shared.ts","../src/std/fs/mina_fs_async.ts","../src/std/fs/fs_async.ts","../src/std/fs/mina_fs_sync.ts","../src/std/fs/fs_sync.ts","../src/std/audio/web_audio.ts","../src/std/codec/mina_codec.ts","../src/std/codec/web_codec.ts","../src/std/codec/mod.ts","../src/std/base64/base64.ts","../src/std/base64/mina_base64.ts","../src/std/base64/web_base64.ts","../src/std/base64/mod.ts","../src/std/clipboard/mina_clipboard.ts","../src/std/clipboard/web_clipboard.ts","../src/std/clipboard/mod.ts","../src/std/crypto/hmac/mina_hmac.ts","../src/std/crypto/hmac/web_hmac.ts","../src/std/crypto/hmac/mod.ts","../src/std/crypto/md/md5.ts","../src/std/crypto/md/mod.ts","../src/std/crypto/random/mina_random.ts","../src/std/crypto/random/web_random.ts","../src/std/crypto/random/mod.ts","../src/std/crypto/rsa/mina_rsa.ts","../src/std/crypto/rsa/web_rsa.ts","../src/std/crypto/rsa/mod.ts","../src/std/crypto/sha/mina_sha.ts","../src/std/crypto/sha/web_sha.ts","../src/std/crypto/sha/mod.ts","../src/std/event/mina_event.ts","../src/std/event/web_event.ts","../src/std/event/mod.ts","../src/std/fetch/mina_fetch.ts","../src/std/fetch/mod.ts","../src/std/image/mina_image.ts","../src/std/image/web_image.ts","../src/std/image/mod.ts","../src/std/lbs/mina_lbs.ts","../src/std/lbs/web_lbs.ts","../src/std/lbs/mod.ts","../src/std/network/mina_network.ts","../src/std/network/web_network.ts","../src/std/network/mod.ts","../src/std/platform/base.ts","../src/std/platform/device.ts","../src/std/platform/target.ts","../src/std/platform/mod.ts","../src/std/socket/socket_define.ts","../src/std/socket/mina_socket.ts","../src/std/socket/web_socket.ts","../src/std/socket/mod.ts","../src/std/storage/mina_storage.ts","../src/std/storage/web_storage.ts","../src/std/storage/mod.ts"],"sourcesContent":["import invariant from 'tiny-invariant';\n\n/**\n * 断言传入的是一个字符串。\n * @param str - 需要断言的字符串。\n */\nexport function assertString(str: string): void {\n invariant(typeof str === 'string', () => `Param must be a string but received ${ str }`);\n}\n\n/**\n * 断言传入的 URL 是否为 `https` 协议。\n * @param url - 需要断言的 URL 字符串。\n */\nexport function assertSafeUrl(url: string): void {\n invariant(typeof url === 'string', () => `Url must be a string but received ${ url }`);\n invariant(url.startsWith('https://'), () => `Url must start with https:// but received ${ url }`);\n}\n\n/**\n * 断言传入的 WebSocket URL 是否为 `wss` 协议。\n * @param url - 需要断言的 WebSocket URL 字符串。\n */\nexport function assertSafeSocketUrl(url: string): void {\n invariant(typeof url === 'string', () => `SocketUrl must be a string but received ${ url }`);\n invariant(url.startsWith('wss://'), () => `SocketUrl must start with wss:// but received ${ url }`);\n}","/**\n * 小游戏环境宏。\n *\n * 可通过打包工具在build时修改,如esbuild、webpack等。\n */\ndeclare const __MINIGAME_STD_MINA__ = false;\n\n/**\n * 判断当前环境是否为小游戏环境。\n * @returns 如果在小游戏环境中返回 true,否则返回 false。\n */\nexport function isMinaEnv(): boolean {\n return __MINIGAME_STD_MINA__;\n}","import { ABORT_ERROR, toFileSystemHandleLike, type FileSystemFileHandleLike, type FileSystemHandleLike } from 'happy-opfs';\n\n/**\n * 将 `FileSystemHandleLike` 转换为小游戏 `Stats`。\n * @param handleLike - FileSystemHandleLike\n * @returns\n */\nexport function convertFileSystemHandleLikeToStats(handleLike: FileSystemHandleLike): WechatMinigame.Stats {\n const { kind } = handleLike;\n const isFile = kind === 'file';\n const isDirectory = kind === 'directory';\n\n let size = 0;\n let lastModifiedTime = 0;\n\n if (isFile) {\n const file = handleLike as FileSystemFileHandleLike;\n\n size = file.size\n lastModifiedTime = file.lastModified;\n }\n\n const stats: WechatMinigame.Stats = {\n isFile: (): boolean => isFile,\n isDirectory: (): boolean => isDirectory,\n size,\n lastModifiedTime,\n lastAccessedTime: 0,\n mode: 0,\n };\n\n return stats;\n}\n\n/**\n * 将`FileSystemHandle`转换为小游戏 `Stats`。\n * @param handle - FileSystemHandle\n * @returns\n */\nexport async function convertFileSystemHandleToStats(handle: FileSystemHandle): Promise<WechatMinigame.Stats> {\n const handleLike = await toFileSystemHandleLike(handle);\n return convertFileSystemHandleLikeToStats(handleLike);\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}","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Err, Ok, promiseToAsyncResult, type AsyncResult, type Result } from 'happy-rusty';\nimport { Future } from 'tiny-future';\n\n/**\n * 类型工具:判断 API 是否符合 promisify 条件。\n */\nexport type ValidAPI<T> = T extends (params: infer P) => infer R\n ? R extends void | Promise<any>\n ? P extends { success?: any; } | undefined\n ? true\n : P extends { fail?: any; } | undefined\n ? true\n : false\n : false\n : false;\n\n/**\n * 类型工具:提取成功回调参数类型。\n */\nexport type SuccessType<T> = T extends (params: infer P) => any\n ? P extends { success?: (res: infer S) => any }\n ? S\n : never\n : never;\n\n/**\n * 类型工具:提取失败回调参数类型。\n */\nexport type FailType<T> = T extends (params: infer P) => any\n ? P extends { fail?: (err: infer E) => any }\n ? E\n : never\n : never;\n\n/**\n * 将小游戏异步 API 转换为返回 `AsyncResult<T, E>` 的新函数,需要转换的 API 必须是接受可选 `success` 和 `fail` 回调的函数,并且其返回值必须是 `void` 或 `Promise`。\n *\n * 其中 `T` 为 `success` 回调的参数类型,`E` 为 `fail` 回调的参数类型。\n *\n * @param api - 小游戏异步 API。\n * @returns 返回一个新的函数,该函数返回 `AsyncResult<T, E>`。\n */\nexport function promisifyWithResult<F extends (...args: any[]) => any, T = SuccessType<F>, E = FailType<F>>(api: F) : ValidAPI<F> extends true\n ? (...args: Parameters<F>) => AsyncResult<T, E>\n : never {\n // @ts-expect-error 跳过运行时是否满足转换条件的检查\n return (...args: Parameters<F>): AsyncResult<T, E> => {\n const future = new Future<Result<T, E>>();\n\n const options = args[0] ?? {};\n const { success, fail } = options;\n\n // 强制使用callback的方式调用,即使支持Promise\n options.success = (res: T) => {\n // success 回调依旧执行\n success?.(res);\n future.resolve(Ok(res));\n };\n options.fail = (err: E) => {\n // fail 回调依旧执行\n fail?.(err);\n future.resolve(Err(err));\n };\n\n const res = api(options);\n\n // 也支持其他返回Promise的API\n if (res instanceof Promise) {\n return promiseToAsyncResult(res);\n } else if (res != null) {\n throw new Error('API must return void or Promise. Otherwise the return value will be discarded.');\n }\n\n return future.promise;\n };\n}","import { Err, Ok, type AsyncIOResult, type IOResult } from 'happy-rusty';\nexport * from './promisify.ts';\n\n/**\n * 将小游戏失败回调的结果转换为 `Error` 类型。\n *\n * 如果是异步 API 的 `fail` 回调返回的结果通常是 `WechatMinigame.GeneralCallbackResult` 或者变体类型,\n * 如果是同步 API throw 的异常通常是一个类似 `Error` 的类型。\n * @param err - 小游戏错误对象。\n * @returns 转换后的 `Error` 对象。\n */\nexport function miniGameFailureToError(err: WechatMinigame.GeneralCallbackResult | Error): Error {\n return new Error((err as WechatMinigame.GeneralCallbackResult).errMsg ?? (err as Error).message);\n}\n\n/**\n * 将错误对象转换为 IOResult 类型。\n * @typeParam T - Result 的 Ok 类型。\n * @param err - 错误对象。\n * @returns 转换后的 IOResult 对象。\n */\nexport function miniGameFailureToResult<T>(err: WechatMinigame.GeneralCallbackResult): IOResult<T> {\n return Err(miniGameFailureToError(err));\n}\n\n/**\n * 执行同步函数,预期异常都是 `WechatMinigame.GeneralCallbackResult`。\n * @param op - 需要执行的同步函数。\n * @returns IOResult。\n */\nexport function tryGeneralSyncOp<T>(op: () => T): IOResult<T> {\n try {\n return Ok(op());\n } catch (e) {\n return miniGameFailureToResult(e as WechatMinigame.GeneralCallbackResult);\n }\n}\n\n/**\n * 执行异步函数,预期异常都是 `WechatMinigame.GeneralCallbackResult`。\n * @param op - 需要执行的异步函数。\n * @returns AsyncIOResult。\n */\nexport async function tryGeneralAsyncOp<T>(op: () => Promise<T>): AsyncIOResult<T> {\n try {\n return Ok(await op());\n } catch (e) {\n return miniGameFailureToResult(e as WechatMinigame.GeneralCallbackResult);\n }\n}\n\n/**\n * 执行同步函数,预期异常都是 `DOMException`。\n * @param op - 需要执行的同步函数。\n * @returns IOResult。\n */\nexport function tryDOMSyncOp<T>(op: () => T): IOResult<T> {\n try {\n return Ok(op());\n } catch (e) {\n return Err(e as DOMException);\n }\n}\n\n/**\n * 执行异步函数,预期异常都是 `DOMException`。\n * @param op - 需要执行的异步函数。\n * @returns AsyncIOResult。\n */\nexport async function tryDOMAsyncOp<T>(op: () => Promise<T>): AsyncIOResult<T> {\n try {\n return Ok(await op());\n } catch (e) {\n return Err(e as DOMException);\n }\n}\n\n/**\n * 将 BufferSource 转换为 Uint8Array。\n * @param data - 需要转换的 BufferSource。\n * @returns Uint8Array。\n */\nexport function bufferSource2U8a(data: BufferSource): Uint8Array {\n if (data instanceof Uint8Array) {\n return data;\n }\n\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n\n if (ArrayBuffer.isView(data)) {\n return new Uint8Array(data.byteOffset === 0 ? data.buffer : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));\n }\n\n throw new TypeError(`BufferSource is not ArrayBuffer or ArrayBufferView`);\n}\n\n/**\n * 将 BufferSource 转换为 ArrayBuffer。\n * @param data - 需要转换的 BufferSource。\n * @returns ArrayBuffer。\n */\nexport function bufferSource2Ab(data: BufferSource): ArrayBuffer {\n if (data instanceof ArrayBuffer) {\n return data;\n }\n\n if (ArrayBuffer.isView(data)) {\n return (data.byteOffset === 0 ? data.buffer : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)) as ArrayBuffer;\n }\n\n throw new TypeError(`BufferSource is not ArrayBuffer or ArrayBufferView`);\n}","/**\n * 同步/异步的公共代码。\n */\n\nimport { assertAbsolutePath, NOT_FOUND_ERROR, type ExistsOptions } from 'happy-opfs';\nimport { Err, Ok, RESULT_FALSE, RESULT_VOID, type IOResult, type VoidIOResult } from 'happy-rusty';\nimport { assertString } from '../assert/assertions.ts';\nimport { bufferSource2Ab, miniGameFailureToError } from '../utils/mod.ts';\nimport type { FileEncoding, ReadOptions, WriteFileContent } from './fs_define.ts';\n\n/**\n * 小游戏文件系统管理器实例。\n *\n * for tree shake\n */\nlet fs: WechatMinigame.FileSystemManager;\n\n/**\n * 获取小游戏文件系统管理器实例。\n * @returns 文件系统管理器实例。\n */\nexport function getFs(): WechatMinigame.FileSystemManager {\n fs ??= wx.getFileSystemManager();\n return fs;\n}\n\n/**\n * 根路径,`wxfile://` 或 `http://`。\n *\n * for tree shake\n */\nlet rootPath: string;\n\n/**\n * 用户可写的根路径, `wxfile://usr` 或 `http://usr`。\n *\n * for tree shake\n */\nlet rootUsrPath: string;\n\n/**\n * 获取文件系统的根路径。\n * @returns 文件系统的根路径。\n */\nexport function getRootUsrPath(): string {\n rootUsrPath ??= wx.env.USER_DATA_PATH;\n // trim `usr`\n rootPath ??= rootUsrPath.slice(0, rootUsrPath.indexOf('usr'));\n\n return rootUsrPath;\n}\n\n/**\n * 获取给定路径的绝对路径。\n * @param path - 相对USER_DATA_PATH的相对路径,也必须以`/`开头。\n * @returns 转换后的绝对路径。\n */\nexport function getAbsolutePath(path: string): string {\n assertString(path);\n\n const usrPath = getRootUsrPath();\n\n // usr or tmp\n if (path.startsWith(rootPath)) {\n return path;\n }\n\n assertAbsolutePath(path);\n return usrPath + path;\n}\n\n/**\n * 判断是否文件或者文件夹不存在。\n * @param err - 错误对象。\n */\nexport function isNotFoundIOError(err: WechatMinigame.FileError): boolean {\n // 1300002\tno such file or directory ${path}\n // 可能没有errCode\n // 同步接口抛出异常是 `Error`,但 instanceof Error 却是 false\n return err.errCode === 1300002 || (err.errMsg ?? (err as unknown as Error).message).includes('no such file or directory');\n}\n\n/**\n * 判断是否文件或者文件夹已存在。\n * @param err - 错误对象。\n */\nexport function isAlreadyExistsIOError(err: WechatMinigame.FileError): boolean {\n // 1301005\tfile already exists ${dirPath}\t已有同名文件或目录\n // 可能没有errCode\n // 同步接口抛出异常是 `Error`,但 instanceof Error 却是 false\n return err.errCode === 1301005 || (err.errMsg ?? (err as unknown as Error).message).includes('already exists');\n}\n\n/**\n * 将错误对象转换为 IOResult 类型。\n * @typeParam T - Result 的 Ok 类型。\n * @param err - 错误对象。\n * @returns 转换后的 IOResult 对象。\n */\nexport function fileErrorToResult<T>(err: WechatMinigame.FileError): IOResult<T> {\n const error = miniGameFailureToError(err);\n\n if (isNotFoundIOError(err)) {\n error.name = NOT_FOUND_ERROR;\n }\n\n return Err(error);\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 * 处理 `mkdir` 的错误。\n */\nexport function errToMkdirResult(err: WechatMinigame.FileError): VoidIOResult {\n // 已存在当做成功\n return isAlreadyExistsIOError(err) ? RESULT_VOID : fileErrorToResult(err);\n}\n\n/**\n * 获取读取文件的编码。\n */\nexport function getReadFileEncoding(options?: ReadOptions): FileEncoding | undefined {\n // NOTE: 想要读取 ArrayBuffer 就不能传 encoding\n // 如果传了 'binary',读出来的是字符串\n let encoding: FileEncoding | undefined = options?.encoding;\n if (!encoding || encoding === 'binary') {\n encoding = undefined;\n }\n\n return encoding;\n}\n\n/**\n * 处理 `remove` 的错误。\n */\nexport function errToRemoveResult(err: WechatMinigame.FileError): VoidIOResult {\n // 目标 path 本就不存在,当做成功\n return isNotFoundIOError(err) ? RESULT_VOID : fileErrorToResult(err);\n}\n\ninterface GetWriteFileContents {\n data: string | ArrayBuffer;\n encoding: FileEncoding | undefined;\n}\n/**\n * 获取写入文件的参数。\n */\nexport function getWriteFileContents(contents: WriteFileContent): GetWriteFileContents {\n const isBin = typeof contents !== 'string';\n\n const encoding = isBin ? undefined : 'utf8'\n const data = isBin ? bufferSource2Ab(contents) : contents;\n\n const res: GetWriteFileContents = {\n data,\n encoding,\n };\n\n return res;\n}\n\n/**\n * 获取 `exists` 的结果。\n */\nexport function getExistsResult(statsResult: IOResult<WechatMinigame.Stats>, options?: ExistsOptions): IOResult<boolean> {\n return statsResult.andThen(stats => {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n if (isDirectory && isFile) {\n throw new TypeError('ExistsOptions.isDirectory and ExistsOptions.isFile must not be true together.');\n }\n\n const notExist =\n (isDirectory && stats.isFile())\n || (isFile && stats.isDirectory());\n\n return Ok(!notExist);\n }).orElse((err): IOResult<boolean> => {\n return isNotFoundError(err) ? RESULT_FALSE : Err(err);\n });\n}","import type { FetchResponse, FetchTask } from '@happy-ts/fetch-t';\nimport { basename, dirname, join } from '@std/path/posix';\nimport * as fflate from 'fflate/browser';\nimport type { ExistsOptions, WriteOptions, ZipOptions } from 'happy-opfs';\nimport { Err, Ok, RESULT_VOID, type AsyncIOResult, type AsyncVoidIOResult, type IOResult, type VoidIOResult } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { assertSafeUrl } from '../assert/assertions.ts';\nimport { miniGameFailureToResult } from '../utils/mod.ts';\nimport type { DownloadFileOptions, ReadFileContent, ReadOptions, StatOptions, UploadFileOptions, WriteFileContent } from './fs_define.ts';\nimport { createAbortError } from './fs_helpers.ts';\nimport { errToMkdirResult, errToRemoveResult, fileErrorToResult, getAbsolutePath, getExistsResult, getFs, getReadFileEncoding, getRootUsrPath, getWriteFileContents, isNotFoundError } from './mina_fs_shared.ts';\n\n/**\n * 递归创建文件夹,相当于`mkdir -p`。\n * @param dirPath - 需要创建的目录路径。\n * @returns 创建结果的异步操作,成功时返回 true。\n */\nexport async function mkdir(dirPath: string): AsyncVoidIOResult {\n const absPath = getAbsolutePath(dirPath);\n\n // 根目录无需创建\n if (absPath === getRootUsrPath()) {\n return RESULT_VOID;\n }\n\n const statRes = await stat(absPath);\n\n if (statRes.isOk()) {\n // 存在则不创建\n return RESULT_VOID;\n }\n\n const future = new Future<VoidIOResult>();\n\n getFs().mkdir({\n dirPath: absPath,\n recursive: true,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(errToMkdirResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 重命名文件或目录。\n * @param srcPath - 原路径。\n * @param destPath - 新路径。\n * @returns 重命名操作的异步结果,成功时返回 true。\n */\nexport function move(srcPath: string, destPath: string): AsyncVoidIOResult {\n const absSrcPath = getAbsolutePath(srcPath);\n const absDestPath = getAbsolutePath(destPath);\n\n const future = new Future<VoidIOResult>();\n\n getFs().rename({\n oldPath: absSrcPath,\n newPath: absDestPath,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 读取目录下的所有文件和子目录。\n * @param dirPath - 目录路径。\n * @returns 包含目录内容的字符串数组的异步操作。\n */\nexport function readDir(dirPath: string): AsyncIOResult<string[]> {\n const absPath = getAbsolutePath(dirPath);\n\n const future = new Future<IOResult<string[]>>();\n\n getFs().readdir({\n dirPath: absPath,\n success(res): void {\n future.resolve(Ok(res.files));\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 以 UTF-8 格式读取文件。\n * @param filePath - 文件路径。\n * @param options - 读取选项,指定编码为 'utf8'。\n * @returns 包含文件内容的字符串的异步操作。\n */\nexport function readFile(filePath: string, options: ReadOptions & {\n encoding: 'utf8',\n}): AsyncIOResult<string>;\n\n/**\n * 以二进制格式读取文件。\n * @param filePath - 文件路径。\n * @param options - 读取选项,指定编码为 'binary'。\n * @returns 包含文件内容的 ArrayBuffer 的异步操作。\n */\nexport function readFile(filePath: string, options?: ReadOptions & {\n encoding: 'binary',\n}): AsyncIOResult<ArrayBuffer>;\n\n/**\n * 读取文件内容,可选地指定编码和返回类型。\n * @template T - 返回内容的类型。\n * @param filePath - 文件路径。\n * @param options - 可选的读取选项。\n * @returns 包含文件内容的异步操作。\n */\nexport function readFile<T extends ReadFileContent>(filePath: string, options?: ReadOptions): AsyncIOResult<T> {\n const absPath = getAbsolutePath(filePath);\n const encoding = getReadFileEncoding(options);\n\n const future = new Future<IOResult<T>>();\n\n getFs().readFile({\n filePath: absPath,\n encoding,\n success(res): void {\n future.resolve(Ok(res.data as T));\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 删除指定路径的文件或目录。\n * @param path - 需要删除的文件或目录的路径。\n * @returns 删除操作的异步结果,成功时返回 true。\n */\nexport async function remove(path: string): AsyncVoidIOResult {\n const statRes = await stat(path);\n\n if (statRes.isErr()) {\n // 不存在当做成功\n return isNotFoundError(statRes.unwrapErr()) ? RESULT_VOID : statRes.asErr();\n }\n\n const absPath = getAbsolutePath(path);\n\n const future = new Future<VoidIOResult>();\n\n // 文件夹还是文件\n if (statRes.unwrap().isDirectory()) {\n getFs().rmdir({\n dirPath: absPath,\n recursive: true,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(errToRemoveResult(err));\n },\n });\n } else {\n getFs().unlink({\n filePath: absPath,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(errToRemoveResult(err));\n },\n });\n }\n\n return future.promise;\n}\n\n/**\n * 获取文件或目录的状态信息。\n * @param path - 文件或目录的路径。\n * @param options - 可选选项。\n * @returns 包含状态信息的异步操作。\n */\nexport function stat(path: string): AsyncIOResult<WechatMinigame.Stats>;\nexport function stat(path: string, options: StatOptions & {\n recursive: true;\n}): AsyncIOResult<WechatMinigame.FileStats[]>;\nexport function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]>;\nexport function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]> {\n type T = WechatMinigame.Stats | WechatMinigame.FileStats[];\n\n const absPath = getAbsolutePath(path);\n\n const future = new Future<IOResult<T>>();\n\n getFs().stat({\n path: absPath,\n recursive: options?.recursive ?? false,\n success(res): void {\n future.resolve(Ok(res.stats));\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 将内容写入文件。\n * @param filePath - 文件路径。\n * @param contents - 要写入的内容。\n * @param options - 可选的写入选项。\n * @returns 写入操作的异步结果,成功时返回 true。\n */\nexport async function writeFile(filePath: string, contents: WriteFileContent, options?: WriteOptions): AsyncVoidIOResult {\n const absPath = getAbsolutePath(filePath);\n\n // 默认创建\n const { append = false, create = true } = options ?? {};\n\n if (create) {\n const res = await mkdir(dirname(absPath));\n if (res.isErr()) {\n return res;\n }\n }\n\n const fs = getFs();\n let method: typeof fs.appendFile | typeof fs.writeFile = fs.writeFile;\n\n if (append) {\n // append先判断文件是否存在\n const res = await exists(absPath);\n if (res.isErr()) {\n return res.asErr();\n }\n\n if (res.unwrap()) {\n // 文件存在才能使用appendFile\n method = fs.appendFile;\n }\n }\n\n const { data, encoding } = getWriteFileContents(contents);\n\n const future = new Future<VoidIOResult>();\n\n method({\n filePath: absPath,\n data,\n encoding,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 向文件追加内容。\n * @param filePath - 文件路径。\n * @param contents - 要追加的内容。\n * @returns 追加操作的异步结果,成功时返回 true。\n */\nexport function appendFile(filePath: string, contents: WriteFileContent): AsyncVoidIOResult {\n return writeFile(filePath, contents, {\n append: true,\n });\n}\n\nfunction copyFile(srcPath: string, destPath: string): AsyncVoidIOResult {\n const future = new Future<VoidIOResult>();\n\n getFs().copyFile({\n srcPath,\n destPath,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 复制文件或文件夹。\n *\n * @param srcPath - 源文件或文件夹路径。\n * @param destPath - 目标文件或文件夹路径。\n * @returns 操作的异步结果。\n */\nexport async function copy(srcPath: string, destPath: string): AsyncVoidIOResult {\n const absSrcPath = getAbsolutePath(srcPath);\n const absDestPath = getAbsolutePath(destPath);\n\n return (await stat(absSrcPath, {\n recursive: true,\n })).andThenAsync(async statsArray => {\n // directory\n if (Array.isArray(statsArray)) {\n for (const { path, stats } of statsArray) {\n // 不能用join\n const srcEntryPath = absSrcPath + path;\n const destEntryPath = absDestPath + path;\n\n const res = await (stats.isDirectory()\n ? mkdir(destEntryPath)\n : copyFile(srcEntryPath, destEntryPath));\n\n if (res.isErr()) {\n return res;\n }\n }\n\n return RESULT_VOID;\n } else {\n // file\n return (await mkdir(dirname(absDestPath))).andThenAsync(() => {\n return copyFile(absSrcPath, absDestPath);\n });\n }\n });\n}\n\n/**\n * 检查指定路径的文件或目录是否存在。\n * @param path - 文件或目录的路径。\n * @param options - 可选的检查选项。\n * @returns 检查存在性的异步结果,存在时返回 true。\n */\nexport async function exists(path: string, options?: ExistsOptions): AsyncIOResult<boolean> {\n const res = await stat(path);\n return getExistsResult(res, options);\n}\n\n/**\n * 清空目录中的所有文件和子目录。\n * @param dirPath - 目录路径。\n * @returns 清空操作的异步结果,成功时返回 true。\n */\nexport async function emptyDir(dirPath: string): AsyncVoidIOResult {\n const res = await readDir(dirPath);\n if (res.isErr()) {\n // 不存在则创建\n return isNotFoundError(res.unwrapErr()) ? mkdir(dirPath) : res.asErr();\n }\n\n const tasks = res.unwrap().map(name => remove(join(dirPath, name)));\n\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 * 读取文件并解析为 JSON。\n * @param filePath - 文件路径。\n * @returns 读取结果。\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 * 读取文本文件的内容。\n * @param filePath - 文件路径。\n * @returns 包含文件文本内容的异步操作。\n */\nexport function readTextFile(filePath: string): AsyncIOResult<string> {\n return readFile(filePath, {\n encoding: 'utf8',\n });\n}\n\n/**\n * 下载文件并保存到临时文件。\n * @param fileUrl - 文件的网络 URL。\n * @param options - 可选参数。\n * @returns 下载操作的异步结果,成功时返回 true。\n */\nexport function downloadFile(fileUrl: string, options?: DownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult>;\n/**\n * 下载文件。\n * @param fileUrl - 文件的网络 URL。\n * @param filePath - 可选的下载后文件存储的路径,没传则存到临时文件。\n * @param options - 可选参数。\n * @returns 下载操作的异步结果,成功时返回 true。\n */\nexport function downloadFile(fileUrl: string, filePath: string, options?: DownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult>;\nexport function downloadFile(fileUrl: string, filePath?: string | DownloadFileOptions, options?: DownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult> {\n type T = WechatMinigame.DownloadFileSuccessCallbackResult;\n\n assertSafeUrl(fileUrl);\n\n let absFilePath: string | undefined = undefined;\n if (typeof filePath === 'string') {\n absFilePath = getAbsolutePath(filePath);\n } else {\n options = filePath;\n }\n\n const {\n onProgress,\n ...rest\n } = options ?? {};\n\n let aborted = false;\n\n const future = new Future<IOResult<T>>();\n\n let task: WechatMinigame.DownloadTask;\n\n const download = () => {\n task = wx.downloadFile({\n ...rest,\n url: fileUrl,\n filePath: absFilePath,\n async success(res): Promise<void> {\n if (aborted) {\n future.resolve(Err(createAbortError()));\n return;\n }\n\n const { statusCode } = res;\n\n if (statusCode >= 200 && statusCode < 300) {\n future.resolve(Ok(res));\n return;\n }\n\n // remove the not expected file but no need to actively delete the temporary file\n if (res.filePath) {\n await remove(res.filePath);\n }\n\n future.resolve(Err(new Error(statusCode.toString())));\n },\n fail(err): void {\n future.resolve(aborted ? Err(createAbortError()) : miniGameFailureToResult(err));\n },\n });\n\n if (typeof onProgress === 'function') {\n task.onProgressUpdate(res => {\n const { totalBytesExpectedToWrite, totalBytesWritten } = res;\n onProgress(typeof totalBytesExpectedToWrite === 'number' && typeof totalBytesWritten === 'number' ? Ok({\n totalByteLength: totalBytesExpectedToWrite,\n completedByteLength: totalBytesWritten,\n }) : Err(new Error(`Unknown download progress ${ totalBytesWritten }/${ totalBytesExpectedToWrite }`)));\n });\n }\n };\n\n // maybe download to a temp file\n if (typeof absFilePath === 'string' && absFilePath) {\n // create the directory if not exists\n mkdir(dirname(absFilePath)).then(res => {\n if (aborted) {\n future.resolve(Err(createAbortError()));\n return;\n }\n\n if (res.isErr()) {\n future.resolve(res.asErr());\n return;\n }\n\n download();\n });\n } else {\n download();\n }\n\n return {\n abort(): void {\n aborted = true;\n task?.abort();\n },\n\n get aborted(): boolean {\n return aborted;\n },\n\n get response(): FetchResponse<T> {\n return future.promise;\n },\n };\n}\n\n/**\n * 文件上传。\n * @param filePath - 需要上传的文件路径。\n * @param fileUrl - 目标网络 URL。\n * @param options - 可选参数。\n * @returns 上传操作的异步结果,成功时返回 true。\n */\nexport function uploadFile(filePath: string, fileUrl: string, options?: UploadFileOptions): FetchTask<WechatMinigame.UploadFileSuccessCallbackResult> {\n type T = WechatMinigame.UploadFileSuccessCallbackResult;\n\n assertSafeUrl(fileUrl);\n const absPath = getAbsolutePath(filePath);\n\n let aborted = false;\n\n const future = new Future<IOResult<T>>();\n\n const task = wx.uploadFile({\n name: basename(filePath),\n ...options,\n url: fileUrl,\n filePath: absPath,\n success(res): void {\n future.resolve(Ok(res));\n },\n fail(err): void {\n future.resolve(miniGameFailureToResult(err));\n },\n });\n\n return {\n abort(): void {\n aborted = true;\n task?.abort();\n },\n\n get aborted(): boolean {\n return aborted;\n },\n\n get response(): FetchResponse<T> {\n return future.promise;\n },\n };\n}\n\n/**\n * 解压 zip 文件。\n * @param zipFilePath - 要解压的 zip 文件路径。\n * @param targetPath - 要解压到的目标文件夹路径。\n * @returns 解压操作的异步结果。\n */\nexport function unzip(zipFilePath: string, targetPath: string): AsyncVoidIOResult {\n const absZipPath = getAbsolutePath(zipFilePath);\n const absTargetPath = getAbsolutePath(targetPath);\n\n const future = new Future<VoidIOResult>();\n\n getFs().unzip({\n zipFilePath: absZipPath,\n targetPath: absTargetPath,\n success(): void {\n future.resolve(RESULT_VOID);\n },\n fail(err): void {\n future.resolve(fileErrorToResult(err));\n },\n });\n\n return future.promise;\n}\n\n/**\n * 从网络下载 zip 文件并解压。\n * @param zipFileUrl - Zip 文件的网络地址。\n * @param targetPath - 要解压到的目标文件夹路径。\n * @param options - 可选的下载参数。\n * @returns 下载并解压操作的异步结果。\n */\nexport async function unzipFromUrl(zipFileUrl: string, targetPath: string, options?: DownloadFileOptions): AsyncVoidIOResult {\n return (await downloadFile(zipFileUrl, options).response).andThenAsync(({ tempFilePath }) => {\n return unzip(tempFilePath, targetPath);\n });\n}\n\n/**\n * 压缩文件到内存。\n * @param sourcePath - 需要压缩的文件(夹)路径。\n * @param options - 可选的压缩参数。\n * @returns 压缩成功的异步结果。\n */\nexport async function zip(sourcePath: string, options?: ZipOptions): AsyncIOResult<Uint8Array>;\n/**\n * 压缩文件。\n * @param sourcePath - 需要压缩的文件(夹)路径。\n * @param zipFilePath - 压缩后的 zip 文件路径。\n * @param options - 可选的压缩参数。\n * @returns 压缩成功的异步结果。\n */\nexport async function zip(sourcePath: string, zipFilePath: string, options?: ZipOptions): AsyncVoidIOResult\nexport async function zip<T>(sourcePath: string, zipFilePath?: string | ZipOptions, options?: ZipOptions): AsyncIOResult<T> {\n const absSourcePath = getAbsolutePath(sourcePath);\n\n let absZipFilePath: string;\n if (typeof zipFilePath === 'string') {\n absZipFilePath = getAbsolutePath(zipFilePath);\n } else {\n options = zipFilePath;\n }\n\n return (await stat(absSourcePath)).andThenAsync(async stats => {\n const zipped: fflate.AsyncZippable = {};\n\n const sourceName = basename(absSourcePath);\n\n if (stats.isFile()) {\n // file\n const res = await readFile(absSourcePath);\n if (res.isErr()) {\n return res.asErr();\n }\n\n zipped[sourceName] = new Uint8Array(res.unwrap());\n } else {\n // directory\n const res = await stat(absSourcePath, {\n recursive: true,\n });\n if (res.isErr()) {\n return res.asErr();\n }\n\n // default to preserve root\n const preserveRoot = options?.preserveRoot ?? true;\n\n for (const { path, stats } of res.unwrap()) {\n if (stats.isFile()) {\n const entryName = preserveRoot ? join(sourceName, path) : path;\n // 不能用 join,否则 http://usr 会变成 http:/usr\n const res = await readFile(absSourcePath + path);\n if (res.isErr()) {\n return res.asErr();\n }\n\n zipped[entryName] = new Uint8Array(res.unwrap());\n }\n }\n }\n\n const future = new Future<IOResult<T>>();\n\n fflate.zip(zipped, {\n consume: true,\n }, async (err, u8a) => {\n if (err) {\n future.resolve(Err(err));\n return;\n }\n\n if (absZipFilePath) {\n const res = await writeFile(absZipFilePath, 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\ntype ZipFromUrlOptions = DownloadFileOptions & ZipOptions;\n/**\n * 下载文件并压缩到内存。\n * @param sourceUrl - 要下载的文件 URL。\n * @param options - 合并的下载和压缩选项。\n */\nexport async function zipFromUrl(sourceUrl: string, options?: ZipFromUrlOptions): AsyncIOResult<Uint8Array>;\n/**\n * 下载文件并压缩为 zip 文件。\n * @param sourceUrl - 要下载的文件 URL。\n * @param zipFilePath - 要输出的 zip 文件路径。\n * @param options - 合并的下载和压缩选项。\n */\nexport async function zipFromUrl(sourceUrl: string, zipFilePath: string, options?: ZipFromUrlOptions): AsyncVoidIOResult;\nexport async function zipFromUrl<T>(sourceUrl: string, zipFilePath?: string | ZipFromUrlOptions, options?: ZipFromUrlOptions): AsyncIOResult<T> {\n if (typeof zipFilePath !== 'string') {\n options = zipFilePath;\n zipFilePath = undefined;\n }\n\n return (await downloadFile(sourceUrl, options).response).andThenAsync(async ({ tempFilePath }) => {\n return await (zipFilePath\n ? zip(tempFilePath, zipFilePath, options)\n : zip(tempFilePath, options)) as IOResult<T>;\n });\n}","import type { FetchTask } from '@happy-ts/fetch-t';\nimport {\n appendFile as webAppendFile,\n copy as webCopy,\n downloadFile as webDownloadFile,\n emptyDir as webEmptyDir,\n exists as webExists,\n mkdir as webMkdir,\n move as webMove,\n readDir as webReadDir,\n readFile as webReadFile,\n readJsonFile as webReadJsonFile,\n readTextFile as webReadTextFile,\n remove as webRemove,\n stat as webStat,\n unzip as webUnzip,\n unzipFromUrl as webUnzipFromUrl,\n uploadFile as webUploadFile,\n writeFile as webWriteFile,\n zip as webZip,\n zipFromUrl as webZipFromUrl,\n type DownloadFileTempResponse,\n type WriteOptions,\n type ZipOptions,\n} from 'happy-opfs';\nimport { Ok, type AsyncIOResult, type AsyncVoidIOResult } from 'happy-rusty';\nimport { isMinaEnv } from '../../macros/env.ts';\nimport type { StatOptions, UnionDownloadFileOptions, UnionUploadFileOptions, WriteFileContent, ZipFromUrlOptions } from './fs_define.ts';\nimport { convertFileSystemHandleToStats } from './fs_helpers.ts';\nimport {\n appendFile as minaAppendFile,\n copy as minaCopy,\n downloadFile as minaDownloadFile,\n emptyDir as minaEmptyDir,\n exists as minaExists,\n mkdir as minaMkdir,\n move as minaMove,\n readDir as minaReadDir,\n readFile as minaReadFile,\n readJsonFile as minaReadJsonFile,\n readTextFile as minaReadTextFile,\n remove as minaRemove,\n stat as minaStat,\n unzip as minaUnzip,\n unzipFromUrl as minaUnzipFromUrl,\n uploadFile as minaUploadFile,\n writeFile as minaWriteFile,\n zip as minaZip,\n zipFromUrl as minaZipFromUrl,\n} from './mina_fs_async.ts';\n\n/**\n * 递归创建文件夹,相当于`mkdir -p`。\n * @param dirPath - 将要创建的目录的路径。\n * @returns 创建成功返回 true 的异步操作结果。\n */\nexport function mkdir(dirPath: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaMkdir : webMkdir)(dirPath);\n}\n\n/**\n * 重命名文件或目录。\n * @param srcPath - 原始路径。\n * @param destPath - 新路径。\n * @returns 重命名成功返回 true 的异步操作结果。\n */\nexport function move(srcPath: string, destPath: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaMove : webMove)(srcPath, destPath);\n}\n\n/**\n * 异步读取指定目录下的所有文件和子目录。\n * @param dirPath - 需要读取的目录路径。\n * @returns 包含目录内容的字符串数组的异步操作结果。\n */\nexport async function readDir(dirPath: string): AsyncIOResult<string[]> {\n if (isMinaEnv()) {\n return minaReadDir(dirPath);\n }\n\n return (await webReadDir(dirPath)).andThenAsync(async entries => {\n const items: string[] = [];\n for await (const { path } of entries) {\n items.push(path);\n }\n return Ok(items);\n });\n}\n\n/**\n * 读取文件内容。\n * @param filePath - 文件的路径。\n * @returns 包含文件内容的 ArrayBuffer 的异步操作结果。\n */\nexport function readFile(filePath: string): AsyncIOResult<ArrayBuffer> {\n return (isMinaEnv() ? minaReadFile : webReadFile)(filePath);\n}\n\n/**\n * 删除文件或目录。\n * @param path - 要删除的文件或目录的路径。\n * @returns 删除成功返回 true 的异步操作结果。\n */\nexport function remove(path: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaRemove : webRemove)(path);\n}\n\nexport async function stat(path: string): AsyncIOResult<WechatMinigame.Stats>;\nexport async function stat(path: string, options: StatOptions & {\n recursive: true;\n}): AsyncIOResult<WechatMinigame.FileStats[]>;\nexport async function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]>\n/**\n * 获取文件或目录的状态信息。\n * @param path - 文件或目录的路径。\n * @param options - 可选选项。\n * @returns 包含状态信息的异步操作结果。\n */\nexport async function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]> {\n if (isMinaEnv()) {\n return await minaStat(path, options);\n }\n\n return (await webStat(path)).andThenAsync(async (handle): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]> => {\n const entryStats = await convertFileSystemHandleToStats(handle);\n\n if (entryStats.isFile() || !options?.recursive) {\n return Ok(entryStats);\n }\n\n // 递归读取目录\n return (await webReadDir(path)).andThenAsync(async entries => {\n const statsArr: WechatMinigame.FileStats[] = [{\n path,\n stats: entryStats,\n }];\n\n for await (const { path, handle } of entries) {\n statsArr.push({\n path,\n stats: await convertFileSystemHandleToStats(handle),\n })\n }\n\n return Ok(statsArr);\n });\n });\n}\n\n/**\n * 写入文件,不存在则创建,同时创建对应目录,contents只支持ArrayBuffer和string,并且需要确保string一定是utf8编码的。\n * @param filePath - 文件路径。\n * @param contents - 要写入的内容。\n * @param options - 可选选项。\n * @returns 写入成功返回 true 的异步操作结果。\n */\nexport function writeFile(filePath: string, contents: WriteFileContent, options?: WriteOptions): AsyncVoidIOResult {\n return (isMinaEnv() ? minaWriteFile : webWriteFile)(filePath, contents, options);\n}\n\n/**\n * 向文件追加内容。\n * @param filePath - 文件路径。\n * @param contents - 要追加的内容。\n * @returns 追加成功返回 true 的异步操作结果。\n */\nexport function appendFile(filePath: string, contents: WriteFileContent): AsyncVoidIOResult {\n return (isMinaEnv() ? minaAppendFile : webAppendFile)(filePath, contents);\n}\n\n/**\n * 复制文件或文件夹。\n *\n * @param srcPath - 源文件或文件夹路径。\n * @param destPath - 目标文件或文件夹路径。\n * @returns 操作的异步结果。\n */\nexport function copy(srcPath: string, destPath: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaCopy : webCopy)(srcPath, destPath);\n}\n\n/**\n * 检查指定路径的文件或目录是否存在。\n * @param path - 文件或目录的路径。\n * @returns 存在返回 true 的异步操作结果。\n */\nexport function exists(path: string): AsyncIOResult<boolean> {\n return (isMinaEnv() ? minaExists : webExists)(path);\n}\n\n/**\n * 清空指定目录下的所有文件和子目录。\n * @param dirPath - 目录路径。\n * @returns 清空成功返回 true 的异步操作结果。\n */\nexport function emptyDir(dirPath: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaEmptyDir : webEmptyDir)(dirPath);\n}\n\n/**\n * 读取文件并解析为 JSON。\n * @param filePath - 文件路径。\n * @returns 读取结果。\n */\nexport function readJsonFile<T>(filePath: string): AsyncIOResult<T> {\n return (isMinaEnv() ? minaReadJsonFile : webReadJsonFile)(filePath);\n}\n\n/**\n * 读取文本文件的内容。\n * @param filePath - 文件路径。\n * @returns 包含文件文本内容的异步操作结果。\n */\nexport function readTextFile(filePath: string): AsyncIOResult<string> {\n return (isMinaEnv() ? minaReadTextFile : webReadTextFile)(filePath);\n}\n\n/**\n * 下载文件并保存到临时文件。\n * @param fileUrl - 文件的网络 URL。\n * @param options - 可选参数。\n * @returns 下载操作的异步结果,成功时返回 true。\n */\nexport function downloadFile(fileUrl: string, options?: UnionDownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult | DownloadFileTempResponse>;\n/**\n * 下载文件。\n * @param fileUrl - 文件的网络 URL。\n * @param filePath - 可选的下载后文件存储的路径,没传则存到临时文件。\n * @param options - 可选的请求初始化参数。\n * @returns 下载成功返回原始结果。\n */\nexport function downloadFile(fileUrl: string, filePath: string, options?: UnionDownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult | Response>\nexport function downloadFile(fileUrl: string, filePath?: string | UnionDownloadFileOptions, options?: UnionDownloadFileOptions): FetchTask<WechatMinigame.DownloadFileSuccessCallbackResult | DownloadFileTempResponse | Response> {\n if (typeof filePath === 'string') {\n return isMinaEnv()\n ? minaDownloadFile(fileUrl, filePath, options)\n : webDownloadFile(fileUrl, filePath, options);\n } else {\n return isMinaEnv()\n ? minaDownloadFile(fileUrl, filePath)\n : webDownloadFile(fileUrl, filePath);\n }\n}\n\n/**\n * 上传本地文件。\n * @param filePath - 需要上传的文件路径。\n * @param fileUrl - 目标服务器的 URL。\n * @param options - 可选的请求初始化参数。\n * @returns 上传成功返回原始结果。\n */\nexport function uploadFile(filePath: string, fileUrl: string, options?: UnionUploadFileOptions): FetchTask<WechatMinigame.UploadFileSuccessCallbackResult | Response> {\n return (isMinaEnv() ? minaUploadFile : webUploadFile)(filePath, fileUrl, options);\n}\n\n/**\n * 解压 zip 文件。\n * @param zipFilePath - 要解压的 zip 文件路径。\n * @param targetPath - 要解压到的目标文件夹路径。\n * @returns 解压操作的异步结果。\n */\nexport function unzip(zipFilePath: string, targetPath: string): AsyncVoidIOResult {\n return (isMinaEnv() ? minaUnzip : webUnzip)(zipFilePath, targetPath);\n}\n\n/**\n * 从网络下载 zip 文件并解压。\n * @param zipFileUrl - Zip 文件的网络地址。\n * @param targetPath - 要解压到的目标文件夹路径。\n * @param options - 可选的下载参数。\n * @returns 下载并解压操作的异步结果。\n */\nexport async function unzipFromUrl(zipFileUrl: string, targetPath: string, options?: UnionDownloadFileOptions): AsyncVoidIOResult {\n return (isMinaEnv() ? minaUnzipFromUrl : webUnzipFromUrl)(zipFileUrl, targetPath, options);\n}\n\n/**\n * 压缩文件到内存。\n * @param sourcePath - 需要压缩的文件(夹)路径。\n * @param options - 可选的压缩参数。\n * @returns 压缩成功的异步结果。\n */\nexport function zip(sourcePath: string, options?: ZipOptions): AsyncIOResult<Uint8Array>;\n/**\n * 压缩文件。\n * @param sourcePath - 需要压缩的文件(夹)路径。\n * @param zipFilePath - 压缩后的 zip 文件路径。\n * @param options - 可选的压缩参数。\n * @returns 压缩成功的异步结果。\n */\nexport function zip(sourcePath: string, zipFilePath: string, options?: ZipOptions): AsyncVoidIOResult;\nexport function zip(sourcePath: string, zipFilePath?: string | ZipOptions, options?: ZipOptions): AsyncVoidIOResult | AsyncIOResult<Uint8Array> {\n if (typeof zipFilePath === 'string') {\n return isMinaEnv()\n ? minaZip(sourcePath, zipFilePath, options)\n : webZip(sourcePath, zipFilePath, options);\n } else {\n return isMinaEnv()\n ? minaZip(sourcePath, zipFilePath)\n : webZip(sourcePath, zipFilePath);\n }\n}\n\n/**\n * 下载文件并压缩到内存。\n * @param sourceUrl - 要下载的文件 URL。\n * @param options - 合并的下载和压缩选项。\n */\nexport function zipFromUrl(sourceUrl: string, options?: ZipFromUrlOptions): AsyncIOResult<Uint8Array>;\n/**\n * 下载文件并压缩为 zip 文件。\n * @param sourceUrl - 要下载的文件 URL。\n * @param zipFilePath - 要输出的 zip 文件路径。\n * @param options - 合并的下载和压缩选项。\n */\nexport function zipFromUrl(sourceUrl: string, zipFilePath: string, options?: ZipFromUrlOptions): AsyncVoidIOResult;\nexport function zipFromUrl(sourceUrl: string, zipFilePath?: string | ZipFromUrlOptions, options?: ZipFromUrlOptions): AsyncVoidIOResult | AsyncIOResult<Uint8Array> {\n if (typeof zipFilePath === 'string') {\n return isMinaEnv()\n ? minaZipFromUrl(sourceUrl, zipFilePath, options)\n : webZipFromUrl(sourceUrl, zipFilePath, options);\n } else {\n return isMinaEnv()\n ? minaZipFromUrl(sourceUrl, zipFilePath)\n : webZipFromUrl(sourceUrl, zipFilePath);\n }\n}","import { basename, dirname, join, SEPARATOR } from '@std/path/posix';\nimport * as fflate from 'fflate/browser';\nimport { type ExistsOptions, type WriteOptions, type ZipOptions } from 'happy-opfs';\nimport { Err, Ok, RESULT_VOID, type IOResult, type VoidIOResult } from 'happy-rusty';\nimport type { ReadFileContent, ReadOptions, StatOptions, WriteFileContent } from './fs_define.ts';\nimport { errToMkdirResult, errToRemoveResult, fileErrorToResult, getAbsolutePath, getExistsResult, getFs, getReadFileEncoding, getWriteFileContents, isNotFoundError } from './mina_fs_shared.ts';\n\n/**\n * 安全地调用同步接口。\n * @param op - 同步操作。\n * @param errToResult - 错误处理函数。\n * @returns\n */\nfunction trySyncOp<T>(op: () => T, errToResult: (err: WechatMinigame.FileError) => IOResult<T> = fileErrorToResult): IOResult<T> {\n try {\n const res = op();\n return Ok(res);\n } catch (e: unknown) {\n return errToResult(e as WechatMinigame.FileError);\n }\n}\n\n/**\n * `mkdir` 的同步版本。\n */\nexport function mkdirSync(dirPath: string): VoidIOResult {\n const absPath = getAbsolutePath(dirPath);\n\n return trySyncOp(() => getFs().mkdirSync(absPath, true), errToMkdirResult);\n}\n\n/**\n * `move` 的同步版本。\n */\nexport function moveSync(srcPath: string, destPath: string): VoidIOResult {\n const absSrcPath = getAbsolutePath(srcPath);\n const absDestPath = getAbsolutePath(destPath);\n\n return trySyncOp(() => getFs().renameSync(absSrcPath, absDestPath));\n}\n\n/**\n * `readDir` 的同步版本。\n */\nexport function readDirSync(dirPath: string): IOResult<string[]> {\n const absPath = getAbsolutePath(dirPath);\n\n return trySyncOp(() => getFs().readdirSync(absPath));\n}\n\n/**\n * `readFile` 的同步版本。\n */\nexport function readFileSync(filePath: string, options: ReadOptions & {\n encoding: 'utf8',\n}): IOResult<string>;\nexport function readFileSync(filePath: string, options?: ReadOptions & {\n encoding: 'binary',\n}): IOResult<ArrayBuffer>;\nexport function readFileSync<T extends ReadFileContent>(filePath: string, options?: ReadOptions): IOResult<T> {\n const absPath = getAbsolutePath(filePath);\n const encoding = getReadFileEncoding(options);\n\n return trySyncOp(() => getFs().readFileSync(absPath, encoding) as T);\n}\n\n/**\n * `remove` 的同步版本。\n */\nexport function removeSync(path: string): VoidIOResult {\n const statRes = statSync(path);\n\n if (statRes.isErr()) {\n // 不存在当做成功\n return isNotFoundError(statRes.unwrapErr()) ? RESULT_VOID : statRes.asErr();\n }\n\n const absPath = getAbsolutePath(path);\n\n return trySyncOp(() => {\n // 文件夹还是文件\n if (statRes.unwrap().isDirectory()) {\n getFs().rmdirSync(absPath, true);\n } else {\n getFs().unlinkSync(absPath);\n }\n }, errToRemoveResult);\n}\n\n/**\n * `stat` 的同步版本。\n */\nexport function statSync(path: string): IOResult<WechatMinigame.Stats>;\nexport function statSync(path: string, options: StatOptions & {\n recursive: true;\n}): IOResult<WechatMinigame.FileStats[]>;\nexport function statSync(path: string, options?: StatOptions): IOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]>;\nexport function statSync(path: string, options?: StatOptions): IOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]> {\n const absPath = getAbsolutePath(path);\n\n return trySyncOp(() => getFs().statSync(abs