@lhy-meta-server/meta-oss 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +378 -3
- package/dist/ali-oss/client.d.ts +185 -0
- package/dist/ali-oss/client.d.ts.map +1 -0
- package/dist/ali-oss/driver.d.ts +156 -0
- package/dist/ali-oss/driver.d.ts.map +1 -0
- package/dist/ali-oss/index.d.ts +11 -0
- package/dist/ali-oss/index.d.ts.map +1 -0
- package/dist/ali-oss/index.js +3 -0
- package/dist/ali-oss/index.js.map +11 -0
- package/dist/ali-oss/mapping.d.ts +25 -0
- package/dist/ali-oss/mapping.d.ts.map +1 -0
- package/dist/core/driver.d.ts +194 -0
- package/dist/core/driver.d.ts.map +1 -0
- package/dist/core/errors.d.ts +106 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/execution.d.ts +130 -0
- package/dist/core/execution.d.ts.map +1 -0
- package/dist/core/keys.d.ts +34 -0
- package/dist/core/keys.d.ts.map +1 -0
- package/dist/core/mime.d.ts +35 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/observability.d.ts +224 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/payload.d.ts +81 -0
- package/dist/core/payload.d.ts.map +1 -0
- package/dist/core/policy.d.ts +31 -0
- package/dist/core/policy.d.ts.map +1 -0
- package/dist/core/storage.d.ts +344 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/types.d.ts +301 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/validation.d.ts +73 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/elysia/index.d.ts +81 -0
- package/dist/elysia/index.d.ts.map +1 -0
- package/dist/elysia/index.js +3 -0
- package/dist/elysia/index.js.map +10 -0
- package/dist/filesystem/driver.d.ts +242 -0
- package/dist/filesystem/driver.d.ts.map +1 -0
- package/dist/filesystem/durability.d.ts +28 -0
- package/dist/filesystem/durability.d.ts.map +1 -0
- package/dist/filesystem/index.d.ts +11 -0
- package/dist/filesystem/index.d.ts.map +1 -0
- package/dist/filesystem/index.js +4 -0
- package/dist/filesystem/index.js.map +14 -0
- package/dist/filesystem/metadata.d.ts +56 -0
- package/dist/filesystem/metadata.d.ts.map +1 -0
- package/dist/filesystem/paths.d.ts +48 -0
- package/dist/filesystem/paths.d.ts.map +1 -0
- package/dist/filesystem/signer.d.ts +58 -0
- package/dist/filesystem/signer.d.ts.map +1 -0
- package/dist/index-5w45sfp6.js +4 -0
- package/dist/index-5w45sfp6.js.map +10 -0
- package/dist/index-7dpmhwry.js +4 -0
- package/dist/index-7dpmhwry.js.map +11 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +16 -0
- package/package.json +81 -6
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\filesystem\\driver.ts", "..\\src\\filesystem\\durability.ts", "..\\src\\filesystem\\metadata.ts", "..\\src\\filesystem\\paths.ts", "..\\src\\filesystem\\signer.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createHash, randomUUID } from 'node:crypto'\nimport type { Dirent } from 'node:fs'\nimport { createReadStream } from 'node:fs'\nimport {\n type FileHandle,\n link,\n lstat,\n mkdir,\n open,\n readdir,\n readFile,\n realpath,\n rename,\n rm,\n rmdir,\n stat,\n unlink,\n} from 'node:fs/promises'\nimport { dirname, join, resolve } from 'node:path'\nimport { Readable } from 'node:stream'\nimport type {\n DriverContext,\n DriverCopyOptions,\n DriverGetOptions,\n DriverListOptions,\n DriverPutOptions,\n DriverSignOptions,\n MultipartDriver,\n StorageDriver,\n} from '../core/driver'\nimport { StorageError, toStorageError } from '../core/errors'\nimport { createPayload, type StoragePayload } from '../core/payload'\nimport type {\n ListResult,\n MultipartUpload,\n ObjectInfo,\n ObjectStream,\n UploadedPart,\n VerifiedUrl,\n VerifyUrlRequest,\n} from '../core/types'\nimport { errorCode, removeStale, syncDirectory } from './durability'\nimport { metadataDirectoryName, metadataFileName, parseMetadata, type StoredMetadata } from './metadata'\nimport { assertFileKey, INTERNAL_DIRECTORY, internalPath, isInside, resolveInside, toKey } from './paths'\nimport { UrlSigner, type UrlSignerOptions } from './signer'\n\n/**\n * 本地文件系统适配器配置。\n */\nexport interface FileSystemDriverOptions {\n /**\n * 存储根目录;不存在时自动创建。\n */\n root: string\n /**\n * 签名 URL 配置;省略时 `signUrl` 抛出 `UNSUPPORTED`。\n */\n signing?: UrlSignerOptions\n /**\n * 新建目录的权限,默认为 `0o750`。\n */\n directoryMode?: number\n /**\n * 新建文件的权限,默认为 `0o640`。\n */\n fileMode?: number\n /**\n * 未完成分片会话的保留毫秒数,超时会在新建会话时清理,默认为 24 小时。\n */\n multipartTtl?: number\n /**\n * 崩溃残留临时文件的保留毫秒数,超时会在初始化时清理,默认为 1 小时。\n *\n * 该值应大于单次写入的最长耗时,避免误删仍在写入的临时文件。\n */\n temporaryTtl?: number\n /**\n * 是否对文件与目录执行 `fsync` 以保证断电持久性,默认为 `true`。\n * 关闭可提升吞吐,但断电时最近的写入可能丢失。\n */\n durable?: boolean\n}\n\ninterface MultipartManifest {\n key: string\n createdAt: number\n options: Omit<StoredMetadata, 'key' | 'size' | 'etag'> & { overwrite: boolean }\n}\n\ninterface WrittenFile {\n path: string\n etag: string\n size: number\n}\n\nconst uploadIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/\nconst notFoundCodes = new Set(['ENOENT', 'ENOTDIR'])\n\n/**\n * 生产级本地文件系统适配器。\n *\n * - 内容先写入内部临时目录并 `fsync`,再原子 `rename` / `link` 到目标位置,并同步父目录。\n * - 元数据以内容文件的 inode 命名并在内容提交前落盘,内容与元数据始终成对可见;\n * 并发写入同一键时,读取方看到的内容、ETag 与元数据总是来自同一次写入。\n * - 读取先打开文件句柄,再基于该句柄读取状态与元数据,句柄打开后对象被删除或覆盖也不影响本次读取。\n * - 拒绝路径穿越、符号链接逃逸、Windows 保留名与内部目录访问。\n * - 初始化时回收崩溃残留的临时文件,新建分片会话时回收过期会话。\n *\n * @example\n * ```ts\n * const driver = new FileSystemDriver({\n * root: '/data/oss',\n * signing: { secret: config.ossSecret, baseUrl: 'https://api.example.com/files' },\n * })\n * ```\n */\nexport class FileSystemDriver implements StorageDriver {\n /**\n * 适配器名称。\n */\n public readonly name = 'filesystem'\n /**\n * 分片上传能力。\n */\n public readonly multipart: MultipartDriver\n private readonly root: string\n private readonly signer?: UrlSigner\n private readonly directoryMode: number\n private readonly fileMode: number\n private readonly multipartTtl: number\n private readonly temporaryTtl: number\n private readonly durable: boolean\n private ready?: Promise<string>\n\n /**\n * 创建本地文件系统适配器。\n *\n * @param options 根目录、签名、权限与持久性配置。\n * @throws {@link StorageError} 配置非法时抛出 `INVALID_OPTION`。\n */\n public constructor(options: FileSystemDriverOptions) {\n if (typeof options?.root !== 'string' || options.root.length === 0) {\n throw new StorageError('INVALID_OPTION', 'root is required')\n }\n this.root = resolve(options.root)\n this.signer = options.signing ? new UrlSigner(options.signing) : undefined\n this.directoryMode = options.directoryMode ?? 0o750\n this.fileMode = options.fileMode ?? 0o640\n this.multipartTtl = options.multipartTtl ?? 24 * 60 * 60 * 1000\n this.temporaryTtl = options.temporaryTtl ?? 60 * 60 * 1000\n this.durable = options.durable ?? true\n this.multipart = {\n createMultipart: (key, putOptions) => this.createMultipart(key, putOptions),\n uploadPart: (upload, partNumber, payload, context) => this.uploadPart(upload, partNumber, payload, context),\n completeMultipart: (upload, parts, context) => this.completeMultipart(upload, parts, context),\n abortMultipart: (upload) => this.abortMultipart(upload),\n }\n }\n\n /**\n * 原子写入对象:内容、ETag 与元数据作为一个整体提交。\n *\n * 提交顺序为:内容写入临时文件并同步 → 以临时文件 inode 写入元数据并同步 →\n * 原子 `rename`(覆盖)或 `link`(禁止覆盖)到目标位置 → 同步父目录 → 回收被替换内容的元数据。\n * 任一步骤失败或进程崩溃时,目标位置要么保持旧对象,要么已是完整的新对象。\n *\n * @param key 完整对象键。\n * @param payload 上传内容。\n * @param options 已归一化的写入选项。\n * @returns 对象描述信息。\n */\n public async put(key: string, payload: StoragePayload, options: DriverPutOptions): Promise<ObjectInfo> {\n const target = await this.objectPath(key, true)\n const written = await this.writeTemporary(payload, options.signal)\n let metadataPath: string | undefined\n try {\n const ino = (await stat(written.path, { bigint: true })).ino\n metadataPath = await this.writeMetadata(key, ino, {\n key,\n size: written.size,\n contentType: options.contentType,\n metadata: options.metadata,\n etag: written.etag,\n ...(options.cacheControl ? { cacheControl: options.cacheControl } : {}),\n ...(options.contentDisposition ? { contentDisposition: options.contentDisposition } : {}),\n })\n if (options.signal.aborted) throw options.signal.reason\n const replaced = await this.commit(written.path, target, key, options.overwrite)\n metadataPath = undefined\n if (replaced !== undefined && replaced !== ino) await this.removeMetadata(key, replaced)\n } finally {\n await rm(written.path, { force: true })\n if (metadataPath) await rm(metadataPath, { force: true })\n }\n return this.head(key)\n }\n\n /**\n * 以流方式读取对象。\n *\n * 先打开文件句柄,再从同一句柄读取状态与元数据,因此句柄打开后对象被删除或覆盖,\n * 本次读取仍返回打开时刻的完整内容;返回的流结束或被取消时自动关闭句柄。\n *\n * @param key 完整对象键。\n * @param options 读取范围与取消信号。\n * @returns 对象描述信息与可读流。\n */\n public async get(key: string, options: DriverGetOptions): Promise<ObjectStream> {\n const handle = await this.openObject(key)\n let info: ObjectInfo\n let end: number | undefined\n try {\n info = await this.describe(key, handle)\n const range = options.range\n if (range && range.start >= info.size && !(info.size === 0 && range.start === 0)) {\n throw new StorageError('INVALID_OPTION', 'Requested range is not satisfiable', { key })\n }\n end = range && info.size > 0 ? Math.min(range.end ?? info.size - 1, info.size - 1) : undefined\n } catch (error) {\n await handle.close()\n throw error\n }\n const start = end === undefined ? undefined : (options.range?.start as number)\n const nodeStream = handle.createReadStream({ start, end, autoClose: true, signal: options.signal })\n const body = Readable.toWeb(nodeStream) as unknown as ReadableStream<Uint8Array>\n return { info, body, ...(end === undefined ? {} : { range: { start: start as number, end } }) }\n }\n\n /**\n * 读取对象描述信息。\n *\n * @param key 完整对象键。\n * @returns 对象描述信息。\n */\n public async head(key: string): Promise<ObjectInfo> {\n const handle = await this.openObject(key)\n try {\n return await this.describe(key, handle)\n } finally {\n await handle.close()\n }\n }\n\n /**\n * 删除对象与元数据,并清理随之变空的父目录。\n *\n * @param key 完整对象键。\n */\n public async delete(key: string): Promise<void> {\n const root = await this.ensureRoot()\n const path = await this.objectPath(key, false)\n let ino: bigint\n try {\n const details = await lstat(path, { bigint: true })\n if (!details.isFile()) return\n ino = details.ino\n await unlink(path)\n } catch (error) {\n if (notFoundCodes.has(errorCode(error) ?? '')) return\n throw toStorageError(error, 'delete', key)\n }\n await this.syncParent(path)\n await this.removeMetadata(key, ino)\n await this.pruneDirectories(root, dirname(path))\n }\n\n /**\n * 按键名字典序分页列举对象,支持分隔符折叠目录。\n *\n * 实现按目录逐层有序遍历,在凑满一页后立即停止,不会一次性加载整个前缀;\n * 设置分隔符时不会进入被折叠的子目录。每页只为当前页对象读取描述信息,\n * 并发数受限,适合大多数单机场景。海量对象建议使用云端 OSS。\n *\n * @param options 前缀、分隔符、游标与页大小。\n * @returns 当前页结果。\n */\n public async list(options: DriverListOptions): Promise<ListResult> {\n const root = await this.ensureRoot()\n const slash = options.prefix.lastIndexOf('/')\n const baseKey = slash >= 0 ? options.prefix.slice(0, slash) : ''\n const start = baseKey ? resolveInside(root, baseKey) : root\n const page: Array<{ name: string; prefix: boolean }> = []\n let truncated = false\n for await (const entry of this.entries(root, start, options)) {\n // 非 `/` 分隔符可能把不同目录折叠为同一公共前缀;输出全局有序,重复项必然相邻。\n if (entry.prefix && page.at(-1)?.name === entry.name) continue\n if (page.length === options.limit) {\n truncated = true\n break\n }\n page.push(entry)\n }\n const names = page.filter((entry) => !entry.prefix).map((entry) => entry.name)\n const objects: ObjectInfo[] = []\n for (let index = 0; index < names.length; index += 32) {\n const batch = await Promise.all(names.slice(index, index + 32).map((name) => this.headIfExists(name)))\n for (const info of batch) if (info) objects.push(info)\n }\n const last = page.at(-1)\n return {\n objects,\n prefixes: page.filter((entry) => entry.prefix).map((entry) => entry.name),\n ...(truncated && last ? { cursor: last.name } : {}),\n }\n }\n\n /**\n * 在本地复制对象及其元数据;读取基于源文件句柄,复制过程中源对象被修改也不影响结果一致性。\n *\n * @param source 完整源对象键。\n * @param destination 完整目标对象键。\n * @param options 覆盖策略与取消信号。\n * @returns 目标对象描述信息。\n */\n public async copy(source: string, destination: string, options: DriverCopyOptions): Promise<ObjectInfo> {\n const { info, body } = await this.get(source, { signal: options.signal })\n return this.put(destination, createPayload(body), {\n signal: options.signal,\n overwrite: options.overwrite,\n contentType: info.contentType as string,\n metadata: info.metadata,\n cacheControl: info.cacheControl,\n contentDisposition: info.contentDisposition,\n })\n }\n\n /**\n * 生成本地 HMAC 签名 URL。\n *\n * @param key 完整对象键。\n * @param options 方法、有效期与头部约束。\n * @returns 签名 URL。\n */\n public async signUrl(key: string, options: DriverSignOptions): Promise<string> {\n await this.objectPath(key, false)\n return this.requireSigner().sign(\n key,\n options.method,\n options.expiresIn,\n options.contentType,\n options.responseContentDisposition,\n )\n }\n\n /**\n * 校验本地签名 URL。\n *\n * @param request 请求信息。\n * @returns 授权信息。\n */\n public verifyUrl(request: VerifyUrlRequest): VerifiedUrl {\n const verified = this.requireSigner().verify(request)\n assertFileKey(verified.key)\n return verified\n }\n\n /**\n * 文件系统不持有长连接,关闭为空操作;读取流持有的句柄随流结束自动释放。\n */\n public async close(): Promise<void> {}\n\n /**\n * 回收崩溃残留的临时文件与孤立元数据。\n *\n * 初始化时会自动清理临时文件;孤立元数据(内容已不存在或已被替换)的回收需要\n * 遍历元数据目录,建议在低峰期由定时任务调用。\n *\n * @returns 删除的临时文件与元数据文件数量。\n */\n public async cleanup(): Promise<{ temporary: number; metadata: number }> {\n const root = await this.ensureRoot()\n const temporary = await removeStale(internalPath(root, 'tmp'), this.temporaryTtl)\n let metadata = 0\n const base = internalPath(root, 'meta')\n for (const group of await readdir(base, { withFileTypes: true })) {\n if (!group.isDirectory()) continue\n const directory = join(base, group.name)\n for (const file of await readdir(directory, { withFileTypes: true })) {\n if (!file.isFile()) continue\n if (await this.isOrphan(join(directory, file.name), file.name)) {\n await rm(join(directory, file.name), { force: true })\n metadata += 1\n }\n }\n try {\n await rmdir(directory)\n } catch {\n // 仍有有效元数据,或并发写入刚向该目录提交元数据,保留目录即可。\n }\n }\n return { temporary, metadata }\n }\n\n private async createMultipart(key: string, options: DriverPutOptions): Promise<MultipartUpload> {\n const root = await this.ensureRoot()\n await this.objectPath(key, true)\n await removeStale(internalPath(root, 'multipart'), this.multipartTtl)\n const uploadId = randomUUID()\n const directory = internalPath(root, 'multipart', uploadId)\n await mkdir(directory, { recursive: true, mode: this.directoryMode })\n const manifest: MultipartManifest = {\n key,\n createdAt: Date.now(),\n options: {\n contentType: options.contentType,\n metadata: options.metadata,\n overwrite: options.overwrite,\n ...(options.cacheControl ? { cacheControl: options.cacheControl } : {}),\n ...(options.contentDisposition ? { contentDisposition: options.contentDisposition } : {}),\n },\n }\n await this.writeFileAtomic(join(directory, 'manifest.json'), JSON.stringify(manifest))\n return { key, uploadId }\n }\n\n private async uploadPart(\n upload: MultipartUpload,\n partNumber: number,\n payload: StoragePayload,\n context: DriverContext,\n ): Promise<UploadedPart> {\n const directory = await this.uploadDirectory(upload)\n const written = await this.writeTemporary(payload, context.signal)\n try {\n if (context.signal.aborted) throw context.signal.reason\n await rename(written.path, join(directory, `${partNumber}.part`))\n } finally {\n await rm(written.path, { force: true })\n }\n return { partNumber, etag: written.etag }\n }\n\n private async completeMultipart(\n upload: MultipartUpload,\n parts: UploadedPart[],\n context: DriverContext,\n ): Promise<ObjectInfo> {\n const directory = await this.uploadDirectory(upload)\n const manifest = await this.readManifest(directory, upload)\n const files: string[] = []\n for (const part of parts) {\n const file = join(directory, `${part.partNumber}.part`)\n let actual: string\n try {\n actual = await this.digestFile(file)\n } catch (error) {\n throw new StorageError('INVALID_OPTION', `Part ${part.partNumber} has not been uploaded`, {\n key: upload.key,\n cause: error,\n })\n }\n if (actual !== part.etag) {\n throw new StorageError('INVALID_OPTION', `Part ${part.partNumber} etag mismatch`, { key: upload.key })\n }\n files.push(file)\n }\n const { overwrite, ...http } = manifest.options\n const info = await this.put(upload.key, createPayload(concatFiles(files)), {\n ...http,\n overwrite,\n signal: context.signal,\n })\n await rm(directory, { recursive: true, force: true })\n return info\n }\n\n private async abortMultipart(upload: MultipartUpload): Promise<void> {\n if (!uploadIdPattern.test(upload.uploadId)) return\n const root = await this.ensureRoot()\n await rm(internalPath(root, 'multipart', upload.uploadId), { recursive: true, force: true })\n }\n\n private ensureRoot(): Promise<string> {\n this.ready ??= (async () => {\n await mkdir(this.root, { recursive: true, mode: this.directoryMode })\n const root = await realpath(this.root)\n for (const name of ['tmp', 'meta', 'multipart']) {\n await mkdir(internalPath(root, name), { recursive: true, mode: this.directoryMode })\n }\n await removeStale(internalPath(root, 'tmp'), this.temporaryTtl)\n return root\n })().catch((error: unknown) => {\n this.ready = undefined\n throw toStorageError(error, 'init')\n })\n return this.ready\n }\n\n private async objectPath(key: string, createParents: boolean): Promise<string> {\n assertFileKey(key)\n const root = await this.ensureRoot()\n const target = resolveInside(root, key)\n const parent = dirname(target)\n if (createParents) await mkdir(parent, { recursive: true, mode: this.directoryMode })\n let realParent: string\n try {\n realParent = await realpath(parent)\n } catch (error) {\n if (notFoundCodes.has(errorCode(error) ?? '')) return target\n throw toStorageError(error, 'resolve', key)\n }\n if (realParent !== root && !isInside(root, realParent)) {\n throw new StorageError('INVALID_KEY', 'Invalid object key: escapes the storage root', { key })\n }\n return join(realParent, target.slice(parent.length + 1))\n }\n\n /**\n * 打开对象内容文件,拒绝符号链接、目录与特殊文件。\n *\n * 先以 `lstat` 排除符号链接,再打开并以 `fstat` 确认打开的正是该普通文件,\n * 消除检查与打开之间被替换为符号链接的竞态窗口。\n */\n private async openObject(key: string): Promise<FileHandle> {\n const path = await this.objectPath(key, false)\n let handle: FileHandle | undefined\n try {\n const linkDetails = await lstat(path, { bigint: true })\n if (!linkDetails.isFile()) throw notFound(key)\n handle = await open(path, 'r')\n const details = await handle.stat({ bigint: true })\n if (!details.isFile() || details.ino !== linkDetails.ino || details.dev !== linkDetails.dev) {\n throw notFound(key)\n }\n return handle\n } catch (error) {\n await handle?.close()\n if (notFoundCodes.has(errorCode(error) ?? '')) throw notFound(key, error)\n throw toStorageError(error, 'open', key)\n }\n }\n\n private async describe(key: string, handle: FileHandle): Promise<ObjectInfo> {\n const details = await handle.stat({ bigint: true })\n const size = Number(details.size)\n const stored = await this.readMetadata(key, details.ino, size)\n return {\n key,\n size,\n lastModified: details.mtime,\n contentType: stored?.contentType ?? 'application/octet-stream',\n metadata: stored?.metadata ?? {},\n ...(stored?.etag ? { etag: stored.etag } : {}),\n ...(stored?.cacheControl ? { cacheControl: stored.cacheControl } : {}),\n ...(stored?.contentDisposition ? { contentDisposition: stored.contentDisposition } : {}),\n }\n }\n\n private async headIfExists(key: string): Promise<ObjectInfo | undefined> {\n try {\n return await this.head(key)\n } catch (error) {\n // 列举与读取之间对象可能被删除,跳过即可。\n if ((error as StorageError).code === 'NOT_FOUND') return undefined\n throw error\n }\n }\n\n private async writeTemporary(payload: StoragePayload, signal: AbortSignal): Promise<WrittenFile> {\n const root = await this.ensureRoot()\n const path = internalPath(root, 'tmp', `${randomUUID()}.tmp`)\n const handle = await open(path, 'wx', this.fileMode)\n const hash = createHash('md5')\n let size = 0\n const reader = payload.stream().getReader()\n // 源流可能长期不产出数据:取消时通过读取器终止挂起的 read(),确保句柄及时释放。\n const onAbort = () => {\n reader.cancel(signal.reason).then(ignore, ignore)\n }\n signal.addEventListener('abort', onAbort, { once: true })\n try {\n if (signal.aborted) throw signal.reason\n for (;;) {\n const { done, value } = await reader.read()\n if (signal.aborted) throw signal.reason\n if (done) break\n hash.update(value)\n size += value.byteLength\n await handle.write(value)\n }\n if (this.durable) await handle.sync()\n } catch (error) {\n await handle.close()\n await rm(path, { force: true })\n throw error\n } finally {\n signal.removeEventListener('abort', onAbort)\n reader.releaseLock()\n }\n await handle.close()\n return { path, etag: hash.digest('hex'), size }\n }\n\n /**\n * 把临时文件原子提交到目标位置。\n *\n * @returns 被替换对象的 inode;目标此前不存在时返回 `undefined`。\n */\n private async commit(\n temporary: string,\n target: string,\n key: string,\n overwrite: boolean,\n ): Promise<bigint | undefined> {\n let replaced: bigint | undefined\n try {\n const existing = await lstat(target, { bigint: true })\n if (!existing.isFile()) {\n throw new StorageError('ALREADY_EXISTS', 'A directory or special file exists at this key', { key })\n }\n replaced = existing.ino\n } catch (error) {\n if (!notFoundCodes.has(errorCode(error) ?? '')) throw toStorageError(error, 'put', key)\n }\n try {\n if (overwrite) await renameWithRetry(temporary, target)\n else await link(temporary, target)\n } catch (error) {\n if (errorCode(error) === 'EEXIST') throw new StorageError('ALREADY_EXISTS', 'Object already exists', { key })\n if (lockedCodes.has(errorCode(error) ?? '')) {\n throw new StorageError('BACKEND', 'Object is locked by another reader or writer', {\n key,\n cause: error,\n retryable: true,\n })\n }\n throw toStorageError(error, 'put', key)\n }\n await this.syncParent(target)\n return overwrite ? replaced : undefined\n }\n\n private async syncParent(path: string): Promise<void> {\n if (this.durable) await syncDirectory(dirname(path))\n }\n\n private async metadataDirectory(key: string): Promise<string> {\n const root = await this.ensureRoot()\n return internalPath(root, 'meta', metadataDirectoryName(key))\n }\n\n private async writeMetadata(key: string, ino: bigint, metadata: StoredMetadata): Promise<string> {\n const directory = await this.metadataDirectory(key)\n await mkdir(directory, { recursive: true, mode: this.directoryMode })\n const path = join(directory, metadataFileName(ino))\n await this.writeFileAtomic(path, JSON.stringify(metadata))\n return path\n }\n\n private async readMetadata(key: string, ino: bigint, size: number): Promise<StoredMetadata | undefined> {\n const path = join(await this.metadataDirectory(key), metadataFileName(ino))\n try {\n return parseMetadata(await readFile(path, 'utf8'), key, size)\n } catch (error) {\n if (notFoundCodes.has(errorCode(error) ?? '')) return undefined\n throw toStorageError(error, 'head', key)\n }\n }\n\n /**\n * 删除指定 inode 的元数据文件。\n *\n * 不在此处删除空的分组目录:并发写入可能正在向同一目录提交元数据,空目录由\n * {@link FileSystemDriver.cleanup} 统一回收。\n */\n private async removeMetadata(key: string, ino: bigint): Promise<void> {\n const directory = await this.metadataDirectory(key)\n await rm(join(directory, metadataFileName(ino)), { force: true })\n }\n\n /**\n * 判断元数据文件是否已失去对应的内容文件(对象被删除或已被新写入替换)。\n */\n private async isOrphan(path: string, file: string): Promise<boolean> {\n let key: string\n try {\n const value = JSON.parse(await readFile(path, 'utf8')) as Partial<StoredMetadata> | null\n if (typeof value?.key !== 'string') return true\n key = value.key\n } catch {\n // 内容损坏或已被并发删除,均可安全回收。\n return true\n }\n try {\n const target = await this.objectPath(key, false)\n const details = await lstat(target, { bigint: true })\n return !details.isFile() || metadataFileName(details.ino) !== file\n } catch {\n return true\n }\n }\n\n private async writeFileAtomic(path: string, content: string): Promise<void> {\n const root = await this.ensureRoot()\n const temporary = internalPath(root, 'tmp', `${randomUUID()}.tmp`)\n const handle = await open(temporary, 'wx', this.fileMode)\n try {\n await handle.writeFile(content)\n if (this.durable) await handle.sync()\n } finally {\n await handle.close()\n }\n try {\n await rename(temporary, path)\n } finally {\n await rm(temporary, { force: true })\n }\n await this.syncParent(path)\n }\n\n private async pruneDirectories(root: string, directory: string): Promise<void> {\n let current = directory\n while (current !== root && isInside(root, current)) {\n try {\n await rmdir(current)\n } catch {\n return\n }\n current = dirname(current)\n }\n }\n\n /**\n * 按全局字典序惰性产生列举条目。\n *\n * 目录以 `名称/` 参与排序,使逐层深度优先遍历的输出顺序与“扁平键排序”完全一致;\n * 不与前缀或游标相交的目录会被整体跳过,设置分隔符时被折叠的目录不会被展开。\n */\n private async *entries(\n root: string,\n directory: string,\n options: DriverListOptions,\n ): AsyncGenerator<{ name: string; prefix: boolean }> {\n let dirents: Dirent[]\n try {\n dirents = await readdir(directory, { withFileTypes: true })\n } catch (error) {\n if (notFoundCodes.has(errorCode(error) ?? '')) return\n throw toStorageError(error, 'list')\n }\n const base = directory === root ? '' : `${toKey(root, directory)}/`\n const items = dirents\n .filter((entry) => !(directory === root && entry.name === INTERNAL_DIRECTORY))\n .filter((entry) => entry.isFile() || entry.isDirectory())\n .map((entry) => ({ entry, key: base + entry.name + (entry.isDirectory() ? '/' : '') }))\n .sort((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0))\n const { prefix, delimiter, cursor } = options\n for (const { entry, key } of items) {\n if (entry.isDirectory()) {\n // 目录内所有键都以 key 开头:与前缀不相交、或全部不大于游标时整体跳过。\n if (!key.startsWith(prefix) && !prefix.startsWith(key)) continue\n if (cursor !== undefined && cursor >= key && !cursor.startsWith(key)) continue\n // 目录键本身以 `/` 结尾,若在前缀之后出现分隔符,整个目录折叠为一个公共前缀。\n const folded = foldedPrefix(key, prefix, delimiter)\n if (folded === undefined) yield* this.entries(root, join(directory, entry.name), options)\n else if (cursor === undefined || folded > cursor) yield { name: folded, prefix: true }\n continue\n }\n if (!key.startsWith(prefix) || (cursor !== undefined && key <= cursor)) continue\n const folded = foldedPrefix(key, prefix, delimiter)\n if (folded === undefined) yield { name: key, prefix: false }\n else if (cursor === undefined || folded > cursor) yield { name: folded, prefix: true }\n }\n }\n\n private async uploadDirectory(upload: MultipartUpload): Promise<string> {\n if (!uploadIdPattern.test(upload.uploadId)) throw noSuchUpload(upload)\n const root = await this.ensureRoot()\n const directory = internalPath(root, 'multipart', upload.uploadId)\n await this.readManifest(directory, upload)\n return directory\n }\n\n private async readManifest(directory: string, upload: MultipartUpload): Promise<MultipartManifest> {\n let manifest: MultipartManifest\n try {\n manifest = JSON.parse(await readFile(join(directory, 'manifest.json'), 'utf8')) as MultipartManifest\n } catch (error) {\n throw noSuchUpload(upload, error)\n }\n if (manifest.key !== upload.key) throw noSuchUpload(upload)\n return manifest\n }\n\n private async digestFile(path: string): Promise<string> {\n const hash = createHash('md5')\n for await (const chunk of createReadStream(path)) hash.update(chunk as Buffer)\n return hash.digest('hex')\n }\n\n private requireSigner(): UrlSigner {\n if (!this.signer) {\n throw new StorageError('UNSUPPORTED', 'Signed URLs require the \"signing\" option on FileSystemDriver')\n }\n return this.signer\n }\n}\n\n/**\n * 计算键在前缀之后按分隔符折叠出的公共前缀。\n *\n * @param key 对象键或以 `/` 结尾的目录键。\n * @param prefix 列举前缀。\n * @param delimiter 分隔符;未设置时不折叠。\n * @returns 公共前缀;未设置分隔符、键不以前缀开头(前缀落在目录内部)或前缀之后\n * 不含分隔符时返回 `undefined`。\n */\nexport function foldedPrefix(key: string, prefix: string, delimiter: string | undefined): string | undefined {\n if (!delimiter || !key.startsWith(prefix)) return undefined\n const index = key.indexOf(delimiter, prefix.length)\n return index < 0 ? undefined : key.slice(0, index + delimiter.length)\n}\n\n/**\n * Windows 上目标文件被其他句柄打开时 `rename` 会返回的错误码。\n */\nconst lockedCodes = new Set(['EPERM', 'EACCES', 'EBUSY'])\n\n/**\n * 原子替换目标文件;目标被其他句柄短暂占用(Windows)时以退避重试,最长约 1 秒。\n *\n * POSIX 平台上 `rename` 从不因读取方占用而失败,因此只在首次失败时进入重试。\n *\n * @param from 源路径。\n * @param to 目标路径。\n * @param attempts 最大尝试次数。\n */\nexport async function renameWithRetry(from: string, to: string, attempts = 8): Promise<void> {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(from, to)\n return\n } catch (error) {\n if (attempt >= attempts || !lockedCodes.has(errorCode(error) ?? '')) throw error\n await new Promise((resolve) => setTimeout(resolve, Math.min(250, 10 * 2 ** attempt)))\n }\n }\n}\n\nasync function* concatFiles(files: string[]): AsyncGenerator<Uint8Array> {\n for (const file of files) {\n for await (const chunk of createReadStream(file)) yield chunk as Buffer\n }\n}\n\n/**\n * 吞掉 Promise 结果的空回调,用于无需关心结果的清理操作。\n */\nfunction ignore(): void {}\n\nfunction notFound(key: string, cause?: unknown): StorageError {\n return new StorageError('NOT_FOUND', 'Object not found', { key, cause })\n}\n\nfunction noSuchUpload(upload: MultipartUpload, cause?: unknown): StorageError {\n return new StorageError('NOT_FOUND', 'Multipart upload not found', { key: upload.key, cause })\n}\n",
|
|
6
|
+
"import { open, readdir, rm, stat } from 'node:fs/promises'\nimport { join } from 'node:path'\n\n/**\n * 读取 Node.js 系统错误码。\n *\n * @param error 任意错误值。\n * @returns 错误码;不存在时返回 `undefined`。\n */\nexport function errorCode(error: unknown): string | undefined {\n return (error as { code?: string } | null)?.code\n}\n\n/**\n * 对目录执行 `fsync`,确保其中的 `rename`、`link`、`unlink` 在断电后依然可见。\n *\n * Windows 与部分文件系统不允许打开或同步目录(返回 `EPERM`、`EISDIR`、`EINVAL`、\n * `EACCES` 等),这些平台上目录项更新由文件系统自身保证,此时静默跳过。\n *\n * @param directory 目录绝对路径。\n */\nexport async function syncDirectory(directory: string): Promise<void> {\n let handle: Awaited<ReturnType<typeof open>> | undefined\n try {\n handle = await open(directory, 'r')\n await handle.sync()\n } catch (error) {\n if (!unsupportedSync.has(errorCode(error) ?? '')) throw error\n } finally {\n await handle?.close()\n }\n}\n\nconst unsupportedSync = new Set(['EPERM', 'EISDIR', 'EINVAL', 'EACCES', 'EBADF', 'ENOTSUP'])\n\n/**\n * 删除目录中修改时间早于阈值的条目,用于回收崩溃残留的临时文件与过期会话。\n *\n * 条目可能被并发操作删除,单个条目失败不会中断清理。\n *\n * @param directory 待清理目录。\n * @param maxAge 保留的最长毫秒数。\n * @param now 当前时间。\n * @returns 实际删除的条目数。\n */\nexport async function removeStale(directory: string, maxAge: number, now = Date.now()): Promise<number> {\n let removed = 0\n for (const name of await readdir(directory)) {\n const path = join(directory, name)\n try {\n if (now - (await stat(path)).mtimeMs <= maxAge) continue\n await rm(path, { recursive: true, force: true })\n removed += 1\n } catch {\n // 条目已被并发删除或暂时不可访问,留待下次清理。\n }\n }\n return removed\n}\n",
|
|
7
|
+
"import { createHash } from 'node:crypto'\nimport type { ObjectHttpOptions } from '../core/types'\n\n/**\n * 持久化到旁路 JSON 文件中的对象属性。\n *\n * 元数据文件以内容文件的 inode 命名,与内容文件一一绑定:写入时先落盘元数据,\n * 再原子提交内容文件,因此读取方通过同一个文件句柄的 inode 总能找到与内容匹配的\n * 元数据,不会出现“新内容配旧元数据”的情况。\n */\nexport interface StoredMetadata extends ObjectHttpOptions {\n /**\n * 完整对象键,用于孤立元数据回收时定位对象。\n */\n key: string\n /**\n * 写入时的内容字节数,用于读取时二次校验。\n */\n size: number\n /**\n * 内容 MIME 类型。\n */\n contentType: string\n /**\n * 自定义元数据。\n */\n metadata: Record<string, string>\n /**\n * 写入时计算的内容 MD5 十六进制摘要,作为 ETag。\n */\n etag: string\n}\n\n/**\n * 计算对象键对应的元数据目录名。\n *\n * 使用 SHA-256 哈希而非原始键,避免长键或特殊字符超过文件系统限制。\n *\n * @param key 完整对象键。\n * @returns 元数据目录名。\n */\nexport function metadataDirectoryName(key: string): string {\n return createHash('sha256').update(key).digest('hex')\n}\n\n/**\n * 计算内容文件 inode 对应的元数据文件名。\n *\n * @param ino 内容文件的 inode(`bigint` 精度,避免超过安全整数范围)。\n * @returns 元数据文件名。\n */\nexport function metadataFileName(ino: bigint): string {\n return `${ino}.json`\n}\n\n/**\n * 解析元数据文件内容;内容损坏或与当前内容文件不匹配时返回 `undefined`,由调用方降级处理。\n *\n * @param text 元数据文件文本。\n * @param key 期望的完整对象键。\n * @param size 当前内容文件的字节数。\n * @returns 解析结果。\n */\nexport function parseMetadata(text: string, key: string, size: number): StoredMetadata | undefined {\n let value: Partial<StoredMetadata> | null\n try {\n value = JSON.parse(text) as Partial<StoredMetadata> | null\n } catch {\n return undefined\n }\n if (\n typeof value?.contentType !== 'string' ||\n typeof value.etag !== 'string' ||\n value.key !== key ||\n value.size !== size\n ) {\n return undefined\n }\n return {\n key,\n size,\n contentType: value.contentType,\n etag: value.etag,\n metadata: value.metadata && typeof value.metadata === 'object' ? value.metadata : {},\n ...(typeof value.cacheControl === 'string' ? { cacheControl: value.cacheControl } : {}),\n ...(typeof value.contentDisposition === 'string' ? { contentDisposition: value.contentDisposition } : {}),\n }\n}\n",
|
|
8
|
+
"import { isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { StorageError } from '../core/errors'\n\n/**\n * 存放元数据、临时文件与分片会话的内部目录名,对象键不得以此开头。\n */\nexport const INTERNAL_DIRECTORY = '.meta-oss'\n\nconst windowsReserved = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\\..*)?$/i\nconst windowsInvalid = /[<>:\"|?*]/\n\n/**\n * 校验对象键能否安全、跨平台地映射到文件系统路径。\n *\n * 在服务层通用校验之外,额外拒绝内部目录、Windows 保留名、Windows 非法字符,\n * 以及以点号或空格结尾的路径段,保证同一份数据可在 Linux、macOS 与 Windows 间迁移。\n *\n * @param key 已通过通用校验的对象键。\n * @throws {@link StorageError} 不可映射时抛出 `INVALID_KEY`。\n */\nexport function assertFileKey(key: string): void {\n const segments = key.split('/')\n if (segments[0] === INTERNAL_DIRECTORY) throw invalid(key, 'uses the reserved internal directory')\n for (const segment of segments) {\n if (windowsInvalid.test(segment)) throw invalid(key, 'contains characters not allowed on Windows')\n if (windowsReserved.test(segment)) throw invalid(key, 'uses a reserved device name')\n if (segment.endsWith('.') || segment.endsWith(' ')) throw invalid(key, 'segment ends with dot or space')\n }\n}\n\n/**\n * 将对象键解析为根目录下的绝对路径,并再次确认路径未逃逸根目录。\n *\n * @param root 存储根目录的绝对路径。\n * @param key 对象键。\n * @returns 对象文件的绝对路径。\n * @throws {@link StorageError} 路径逃逸根目录时抛出 `INVALID_KEY`。\n */\nexport function resolveInside(root: string, key: string): string {\n const target = resolve(root, ...key.split('/'))\n if (!isInside(root, target)) throw invalid(key, 'escapes the storage root')\n return target\n}\n\n/**\n * 判断目标路径是否严格位于根目录内部。\n *\n * @param root 根目录绝对路径。\n * @param target 目标绝对路径。\n * @returns 是否位于根目录内部且不等于根目录本身。\n */\nexport function isInside(root: string, target: string): boolean {\n const path = relative(root, target)\n return path !== '' && !path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)\n}\n\n/**\n * 将相对于根目录的系统路径转换为以 `/` 分隔的对象键。\n *\n * @param root 根目录绝对路径。\n * @param path 文件绝对路径。\n * @returns 对象键。\n */\nexport function toKey(root: string, path: string): string {\n return relative(root, path).split(sep).join('/')\n}\n\n/**\n * 拼接内部目录下的路径。\n *\n * @param root 存储根目录。\n * @param parts 内部目录下的路径段。\n * @returns 绝对路径。\n */\nexport function internalPath(root: string, ...parts: string[]): string {\n return join(root, INTERNAL_DIRECTORY, ...parts)\n}\n\nfunction invalid(key: string, reason: string): StorageError {\n return new StorageError('INVALID_KEY', `Invalid object key: ${reason}`, { key })\n}\n",
|
|
9
|
+
"import { createHmac, timingSafeEqual } from 'node:crypto'\nimport { StorageError } from '../core/errors'\nimport type { SignedUrlMethod, VerifiedUrl, VerifyUrlRequest } from '../core/types'\n\n/**\n * 本地签名 URL 的签名配置。\n */\nexport interface UrlSignerOptions {\n /**\n * HMAC-SHA256 密钥,至少 32 个字符;只能通过构造参数传入。\n */\n secret: string\n /**\n * 签名 URL 的基础地址,例如 `https://cdn.example.com/files`;对象键会追加在其后。\n */\n baseUrl: string\n /**\n * 当前时间来源,便于测试注入,默认为 `Date.now`。\n */\n now?: () => number\n}\n\n/**\n * 生成并校验本地文件存储的 HMAC 签名 URL。\n *\n * 插件不提供 HTTP 路由;业务可在自己的下载/上传路由中调用 {@link UrlSigner.verify}\n * 完成鉴权,再使用 `ObjectStorage` 读写对象。签名覆盖方法、对象键、过期时间、\n * `Content-Type` 与 `Content-Disposition`,并使用常量时间比较防止计时攻击。\n */\nexport class UrlSigner {\n private readonly secret: string\n private readonly base: URL\n private readonly now: () => number\n\n /**\n * 创建签名器。\n *\n * @param options 密钥、基础地址与时间来源。\n * @throws {@link StorageError} 密钥过短或基础地址非法时抛出 `INVALID_OPTION`。\n */\n public constructor(options: UrlSignerOptions) {\n if (typeof options?.secret !== 'string' || options.secret.length < 32) {\n throw new StorageError('INVALID_OPTION', 'signing secret must be at least 32 characters')\n }\n let base: URL\n try {\n base = new URL(options.baseUrl)\n } catch (error) {\n throw new StorageError('INVALID_OPTION', 'baseUrl must be an absolute URL', { cause: error })\n }\n if (!base.pathname.endsWith('/')) base.pathname += '/'\n this.secret = options.secret\n this.base = base\n this.now = options.now ?? Date.now\n }\n\n /**\n * 生成签名 URL。\n *\n * @param key 完整对象键。\n * @param method 授权的 HTTP 方法。\n * @param expiresIn 有效期秒数。\n * @param contentType `PUT` 要求的 MIME 类型。\n * @param responseContentDisposition `GET` 响应的 `Content-Disposition`。\n * @returns 签名 URL。\n */\n public sign(\n key: string,\n method: SignedUrlMethod,\n expiresIn: number,\n contentType?: string,\n responseContentDisposition?: string,\n ): string {\n const expires = Math.floor(this.now() / 1000) + expiresIn\n const url = new URL(key.split('/').map(encodeURIComponent).join('/'), this.base)\n url.searchParams.set('method', method)\n url.searchParams.set('expires', String(expires))\n if (contentType) url.searchParams.set('contentType', contentType)\n if (responseContentDisposition) url.searchParams.set('disposition', responseContentDisposition)\n url.searchParams.set('signature', this.digest(method, key, expires, contentType, responseContentDisposition))\n return url.href\n }\n\n /**\n * 校验签名 URL。\n *\n * @param request 请求 URL、方法与 `Content-Type`。\n * @returns 校验通过后的授权信息。\n * @throws {@link StorageError} 签名缺失、被篡改、已过期或与请求不匹配时抛出 `ACCESS_DENIED`。\n */\n public verify(request: VerifyUrlRequest): VerifiedUrl {\n const url = typeof request.url === 'string' ? new URL(request.url) : request.url\n if (url.origin !== this.base.origin || !url.pathname.startsWith(this.base.pathname)) {\n throw denied('URL does not match the signing base')\n }\n const params = url.searchParams\n const method = params.get('method')\n const signature = params.get('signature')\n const expiresText = params.get('expires') ?? ''\n if ((method !== 'GET' && method !== 'PUT') || !signature || !/^\\d+$/.test(expiresText)) {\n throw denied('Malformed signed URL')\n }\n let key: string\n try {\n key = url.pathname.slice(this.base.pathname.length).split('/').map(decodeURIComponent).join('/')\n } catch (error) {\n throw denied('Malformed object key', error)\n }\n const expires = Number(expiresText)\n const contentType = params.get('contentType') ?? undefined\n const disposition = params.get('disposition') ?? undefined\n const expected = Buffer.from(this.digest(method, key, expires, contentType, disposition))\n const actual = Buffer.from(signature)\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw denied('Invalid signature')\n if (expires * 1000 < this.now()) throw denied('Signed URL has expired')\n if ((request.method ?? 'GET').toUpperCase() !== method) throw denied('HTTP method does not match')\n if (method === 'PUT' && contentType && request.contentType !== contentType) {\n throw denied('Content-Type does not match')\n }\n return {\n key,\n method,\n expires,\n ...(contentType ? { contentType } : {}),\n ...(disposition ? { responseContentDisposition: disposition } : {}),\n }\n }\n\n private digest(\n method: string,\n key: string,\n expires: number,\n contentType: string | undefined,\n disposition: string | undefined,\n ): string {\n return createHmac('sha256', this.secret)\n .update([method, key, expires, contentType ?? '', disposition ?? ''].join('\\n'))\n .digest('base64url')\n }\n}\n\nfunction denied(message: string, cause?: unknown): StorageError {\n return new StorageError('ACCESS_DENIED', message, { cause })\n}\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "FD5C6DD2996003AE64756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ObjectHttpOptions } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* 持久化到旁路 JSON 文件中的对象属性。
|
|
4
|
+
*
|
|
5
|
+
* 元数据文件以内容文件的 inode 命名,与内容文件一一绑定:写入时先落盘元数据,
|
|
6
|
+
* 再原子提交内容文件,因此读取方通过同一个文件句柄的 inode 总能找到与内容匹配的
|
|
7
|
+
* 元数据,不会出现“新内容配旧元数据”的情况。
|
|
8
|
+
*/
|
|
9
|
+
export interface StoredMetadata extends ObjectHttpOptions {
|
|
10
|
+
/**
|
|
11
|
+
* 完整对象键,用于孤立元数据回收时定位对象。
|
|
12
|
+
*/
|
|
13
|
+
key: string;
|
|
14
|
+
/**
|
|
15
|
+
* 写入时的内容字节数,用于读取时二次校验。
|
|
16
|
+
*/
|
|
17
|
+
size: number;
|
|
18
|
+
/**
|
|
19
|
+
* 内容 MIME 类型。
|
|
20
|
+
*/
|
|
21
|
+
contentType: string;
|
|
22
|
+
/**
|
|
23
|
+
* 自定义元数据。
|
|
24
|
+
*/
|
|
25
|
+
metadata: Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* 写入时计算的内容 MD5 十六进制摘要,作为 ETag。
|
|
28
|
+
*/
|
|
29
|
+
etag: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 计算对象键对应的元数据目录名。
|
|
33
|
+
*
|
|
34
|
+
* 使用 SHA-256 哈希而非原始键,避免长键或特殊字符超过文件系统限制。
|
|
35
|
+
*
|
|
36
|
+
* @param key 完整对象键。
|
|
37
|
+
* @returns 元数据目录名。
|
|
38
|
+
*/
|
|
39
|
+
export declare function metadataDirectoryName(key: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* 计算内容文件 inode 对应的元数据文件名。
|
|
42
|
+
*
|
|
43
|
+
* @param ino 内容文件的 inode(`bigint` 精度,避免超过安全整数范围)。
|
|
44
|
+
* @returns 元数据文件名。
|
|
45
|
+
*/
|
|
46
|
+
export declare function metadataFileName(ino: bigint): string;
|
|
47
|
+
/**
|
|
48
|
+
* 解析元数据文件内容;内容损坏或与当前内容文件不匹配时返回 `undefined`,由调用方降级处理。
|
|
49
|
+
*
|
|
50
|
+
* @param text 元数据文件文本。
|
|
51
|
+
* @param key 期望的完整对象键。
|
|
52
|
+
* @param size 当前内容文件的字节数。
|
|
53
|
+
* @returns 解析结果。
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseMetadata(text: string, key: string, size: number): StoredMetadata | undefined;
|
|
56
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/filesystem/metadata.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAwBjG"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存放元数据、临时文件与分片会话的内部目录名,对象键不得以此开头。
|
|
3
|
+
*/
|
|
4
|
+
export declare const INTERNAL_DIRECTORY = ".meta-oss";
|
|
5
|
+
/**
|
|
6
|
+
* 校验对象键能否安全、跨平台地映射到文件系统路径。
|
|
7
|
+
*
|
|
8
|
+
* 在服务层通用校验之外,额外拒绝内部目录、Windows 保留名、Windows 非法字符,
|
|
9
|
+
* 以及以点号或空格结尾的路径段,保证同一份数据可在 Linux、macOS 与 Windows 间迁移。
|
|
10
|
+
*
|
|
11
|
+
* @param key 已通过通用校验的对象键。
|
|
12
|
+
* @throws {@link StorageError} 不可映射时抛出 `INVALID_KEY`。
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertFileKey(key: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* 将对象键解析为根目录下的绝对路径,并再次确认路径未逃逸根目录。
|
|
17
|
+
*
|
|
18
|
+
* @param root 存储根目录的绝对路径。
|
|
19
|
+
* @param key 对象键。
|
|
20
|
+
* @returns 对象文件的绝对路径。
|
|
21
|
+
* @throws {@link StorageError} 路径逃逸根目录时抛出 `INVALID_KEY`。
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveInside(root: string, key: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* 判断目标路径是否严格位于根目录内部。
|
|
26
|
+
*
|
|
27
|
+
* @param root 根目录绝对路径。
|
|
28
|
+
* @param target 目标绝对路径。
|
|
29
|
+
* @returns 是否位于根目录内部且不等于根目录本身。
|
|
30
|
+
*/
|
|
31
|
+
export declare function isInside(root: string, target: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* 将相对于根目录的系统路径转换为以 `/` 分隔的对象键。
|
|
34
|
+
*
|
|
35
|
+
* @param root 根目录绝对路径。
|
|
36
|
+
* @param path 文件绝对路径。
|
|
37
|
+
* @returns 对象键。
|
|
38
|
+
*/
|
|
39
|
+
export declare function toKey(root: string, path: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* 拼接内部目录下的路径。
|
|
42
|
+
*
|
|
43
|
+
* @param root 存储根目录。
|
|
44
|
+
* @param parts 内部目录下的路径段。
|
|
45
|
+
* @returns 绝对路径。
|
|
46
|
+
*/
|
|
47
|
+
export declare function internalPath(root: string, ...parts: string[]): string;
|
|
48
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/filesystem/paths.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,kBAAkB,cAAc,CAAA;AAK7C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAQ/C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAErE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SignedUrlMethod, VerifiedUrl, VerifyUrlRequest } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* 本地签名 URL 的签名配置。
|
|
4
|
+
*/
|
|
5
|
+
export interface UrlSignerOptions {
|
|
6
|
+
/**
|
|
7
|
+
* HMAC-SHA256 密钥,至少 32 个字符;只能通过构造参数传入。
|
|
8
|
+
*/
|
|
9
|
+
secret: string;
|
|
10
|
+
/**
|
|
11
|
+
* 签名 URL 的基础地址,例如 `https://cdn.example.com/files`;对象键会追加在其后。
|
|
12
|
+
*/
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
/**
|
|
15
|
+
* 当前时间来源,便于测试注入,默认为 `Date.now`。
|
|
16
|
+
*/
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 生成并校验本地文件存储的 HMAC 签名 URL。
|
|
21
|
+
*
|
|
22
|
+
* 插件不提供 HTTP 路由;业务可在自己的下载/上传路由中调用 {@link UrlSigner.verify}
|
|
23
|
+
* 完成鉴权,再使用 `ObjectStorage` 读写对象。签名覆盖方法、对象键、过期时间、
|
|
24
|
+
* `Content-Type` 与 `Content-Disposition`,并使用常量时间比较防止计时攻击。
|
|
25
|
+
*/
|
|
26
|
+
export declare class UrlSigner {
|
|
27
|
+
private readonly secret;
|
|
28
|
+
private readonly base;
|
|
29
|
+
private readonly now;
|
|
30
|
+
/**
|
|
31
|
+
* 创建签名器。
|
|
32
|
+
*
|
|
33
|
+
* @param options 密钥、基础地址与时间来源。
|
|
34
|
+
* @throws {@link StorageError} 密钥过短或基础地址非法时抛出 `INVALID_OPTION`。
|
|
35
|
+
*/
|
|
36
|
+
constructor(options: UrlSignerOptions);
|
|
37
|
+
/**
|
|
38
|
+
* 生成签名 URL。
|
|
39
|
+
*
|
|
40
|
+
* @param key 完整对象键。
|
|
41
|
+
* @param method 授权的 HTTP 方法。
|
|
42
|
+
* @param expiresIn 有效期秒数。
|
|
43
|
+
* @param contentType `PUT` 要求的 MIME 类型。
|
|
44
|
+
* @param responseContentDisposition `GET` 响应的 `Content-Disposition`。
|
|
45
|
+
* @returns 签名 URL。
|
|
46
|
+
*/
|
|
47
|
+
sign(key: string, method: SignedUrlMethod, expiresIn: number, contentType?: string, responseContentDisposition?: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* 校验签名 URL。
|
|
50
|
+
*
|
|
51
|
+
* @param request 请求 URL、方法与 `Content-Type`。
|
|
52
|
+
* @returns 校验通过后的授权信息。
|
|
53
|
+
* @throws {@link StorageError} 签名缺失、被篡改、已过期或与请求不匹配时抛出 `ACCESS_DENIED`。
|
|
54
|
+
*/
|
|
55
|
+
verify(request: VerifyUrlRequest): VerifiedUrl;
|
|
56
|
+
private digest;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/filesystem/signer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEnF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAElC;;;;;OAKG;IACH,YAAmB,OAAO,EAAE,gBAAgB,EAc3C;IAED;;;;;;;;;OASG;IACI,IAAI,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,0BAA0B,CAAC,EAAE,MAAM,GAClC,MAAM,CASR;IAED;;;;;;OAMG;IACI,MAAM,CAAC,OAAO,EAAE,gBAAgB,GAAG,WAAW,CAoCpD;IAED,OAAO,CAAC,MAAM;CAWf"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{h as F}from"./index-7dpmhwry.js";var $=new TextEncoder,W={test(j){for(let q=0;q<j.length;q+=1){let z=j.charCodeAt(q);if(z<32||z===127)return!0}return!1}},R=/^[a-z0-9][a-z0-9-]*$/,V=/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(\s*;.*)?$/i,G=1023,L=8192;function P(j){if(typeof j!=="string"||j.length===0)throw J(j,"must be a non-empty string");if($.encode(j).byteLength>G)throw J(j,`exceeds ${G} bytes`);if(W.test(j))throw J(j,"contains control characters");if(j.includes("\\"))throw J(j,"contains backslash");if(j.startsWith("/")||/^[a-z]:/i.test(j))throw J(j,"must be relative");for(let q of j.split("/"))if(q===""||q==="."||q==="..")throw J(j,"contains an invalid segment");return j}function D(j){if(j===void 0||j==="")return"";let q=j.endsWith("/")?j.slice(0,-1):j;return`${P(q)}/`}function w(j){if(j===void 0||j==="")return"";return P(j.endsWith("/")?j.slice(0,-1):j),j}function B(j){let q={},z=0;for(let[H,U]of Object.entries(j??{})){let Z=H.toLowerCase();if(!R.test(Z))throw Q(`Invalid metadata name: ${H}`);if(typeof U!=="string"||W.test(U))throw Q(`Invalid metadata value for: ${H}`);z+=$.encode(Z).byteLength+$.encode(U).byteLength,q[Z]=U}if(z>L)throw Q(`Metadata exceeds ${L} bytes`);return q}function C(j,q){if(q!==void 0&&(typeof q!=="string"||W.test(q)))throw Q(`Invalid ${j}`);return q}function I(j){if(!V.test(j)||W.test(j))throw Q(`Invalid content type: ${j}`);return j}function h(j,q,z,H=Number.MAX_SAFE_INTEGER){if(!Number.isSafeInteger(q)||q<z||q>H)throw Q(`${j} must be an integer between ${z} and ${H}`);return q}function J(j,q){return new F("INVALID_KEY",`Invalid object key: ${q}`,{key:typeof j==="string"?j:void 0})}function Q(j){return new F("INVALID_OPTION",j)}
|
|
2
|
+
export{P as a,D as b,w as c,B as d,C as e,I as f,h as g};
|
|
3
|
+
|
|
4
|
+
//# debugId=56A3FCD4618ED1FC64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\core\\validation.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { StorageError } from './errors'\nimport type { ObjectMetadata } from './types'\n\nconst encoder = new TextEncoder()\nconst controlCharacters = {\n test(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if (code < 0x20 || code === 0x7f) return true\n }\n return false\n },\n}\nconst metadataName = /^[a-z0-9][a-z0-9-]*$/\nconst mediaType = /^[a-z0-9][a-z0-9!#$&^_.+-]*\\/[a-z0-9][a-z0-9!#$&^_.+-]*(\\s*;.*)?$/i\n\n/**\n * 对象键允许的最大 UTF-8 字节数,与主流对象存储保持一致。\n */\nexport const MAX_KEY_BYTES = 1023\n\n/**\n * 自定义元数据序列化后允许的最大 UTF-8 字节数。\n */\nexport const MAX_METADATA_BYTES = 8192\n\n/**\n * 校验对象键并返回原值。\n *\n * 规则:非空、不超过 1023 字节、不含控制字符与反斜杠、不以 `/` 开头、\n * 不含空段以及 `.`、`..` 段、不以盘符开头。允许中文等 Unicode 字符。\n *\n * @param key 待校验的对象键。\n * @returns 校验通过的对象键。\n * @throws {@link StorageError} 键非法时抛出 `INVALID_KEY`。\n */\nexport function assertKey(key: string): string {\n if (typeof key !== 'string' || key.length === 0) throw invalidKey(key, 'must be a non-empty string')\n if (encoder.encode(key).byteLength > MAX_KEY_BYTES) throw invalidKey(key, `exceeds ${MAX_KEY_BYTES} bytes`)\n if (controlCharacters.test(key)) throw invalidKey(key, 'contains control characters')\n if (key.includes('\\\\')) throw invalidKey(key, 'contains backslash')\n if (key.startsWith('/') || /^[a-z]:/i.test(key)) throw invalidKey(key, 'must be relative')\n for (const segment of key.split('/')) {\n if (segment === '' || segment === '.' || segment === '..') throw invalidKey(key, 'contains an invalid segment')\n }\n return key\n}\n\n/**\n * 校验并归一化目录前缀;空值返回空字符串,否则保证以 `/` 结尾。\n *\n * @param prefix 待校验的前缀。\n * @returns 归一化后的前缀。\n * @throws {@link StorageError} 前缀非法时抛出 `INVALID_KEY`。\n */\nexport function normalizeDirectory(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return ''\n const trimmed = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix\n return `${assertKey(trimmed)}/`\n}\n\n/**\n * 校验列举前缀;允许以 `/` 结尾或为空,其余规则与对象键一致。\n *\n * @param prefix 待校验的前缀。\n * @returns 校验通过的前缀,未设置时为空字符串。\n * @throws {@link StorageError} 前缀非法时抛出 `INVALID_KEY`。\n */\nexport function assertPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return ''\n assertKey(prefix.endsWith('/') ? prefix.slice(0, -1) : prefix)\n return prefix\n}\n\n/**\n * 校验自定义元数据并返回小写键的新对象。\n *\n * @param metadata 待校验的元数据。\n * @returns 归一化后的元数据副本。\n * @throws {@link StorageError} 键名、值或总大小非法时抛出 `INVALID_OPTION`。\n */\nexport function normalizeMetadata(metadata: ObjectMetadata | undefined): Record<string, string> {\n const result: Record<string, string> = {}\n let bytes = 0\n for (const [rawName, value] of Object.entries(metadata ?? {})) {\n const name = rawName.toLowerCase()\n if (!metadataName.test(name)) throw invalidOption(`Invalid metadata name: ${rawName}`)\n if (typeof value !== 'string' || controlCharacters.test(value)) {\n throw invalidOption(`Invalid metadata value for: ${rawName}`)\n }\n bytes += encoder.encode(name).byteLength + encoder.encode(value).byteLength\n result[name] = value\n }\n if (bytes > MAX_METADATA_BYTES) throw invalidOption(`Metadata exceeds ${MAX_METADATA_BYTES} bytes`)\n return result\n}\n\n/**\n * 校验可选的 HTTP 头部取值,拒绝控制字符以防止头部注入。\n *\n * @param name 选项名称,用于错误信息。\n * @param value 待校验的取值。\n * @returns 校验通过的取值。\n * @throws {@link StorageError} 包含控制字符时抛出 `INVALID_OPTION`。\n */\nexport function assertHeaderValue(name: string, value: string | undefined): string | undefined {\n if (value !== undefined && (typeof value !== 'string' || controlCharacters.test(value))) {\n throw invalidOption(`Invalid ${name}`)\n }\n return value\n}\n\n/**\n * 校验 MIME 类型格式。\n *\n * @param value 待校验的 MIME 类型。\n * @returns 校验通过的 MIME 类型。\n * @throws {@link StorageError} 格式非法时抛出 `INVALID_OPTION`。\n */\nexport function assertContentType(value: string): string {\n if (!mediaType.test(value) || controlCharacters.test(value)) throw invalidOption(`Invalid content type: ${value}`)\n return value\n}\n\n/**\n * 校验整数配置项位于闭区间内。\n *\n * @param name 选项名称,用于错误信息。\n * @param value 待校验的值。\n * @param min 允许的最小值。\n * @param max 允许的最大值,默认为 `Number.MAX_SAFE_INTEGER`。\n * @returns 校验通过的值。\n * @throws {@link StorageError} 非整数或越界时抛出 `INVALID_OPTION`。\n */\nexport function assertInteger(name: string, value: number, min: number, max = Number.MAX_SAFE_INTEGER): number {\n if (!Number.isSafeInteger(value) || value < min || value > max) {\n throw invalidOption(`${name} must be an integer between ${min} and ${max}`)\n }\n return value\n}\n\nfunction invalidKey(key: unknown, reason: string): StorageError {\n return new StorageError('INVALID_KEY', `Invalid object key: ${reason}`, {\n key: typeof key === 'string' ? key : undefined,\n })\n}\n\nfunction invalidOption(message: string): StorageError {\n return new StorageError('INVALID_OPTION', message)\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "wCAGA,IAAM,EAAU,IAAI,YACd,EAAoB,CACxB,IAAI,CAAC,EAAwB,CAC3B,QAAS,EAAQ,EAAG,EAAQ,EAAM,OAAQ,GAAS,EAAG,CACpD,IAAM,EAAO,EAAM,WAAW,CAAK,EACnC,GAAI,EAAO,IAAQ,IAAS,IAAM,MAAO,GAE3C,MAAO,GAEX,EACM,EAAe,uBACf,EAAY,qEAKL,EAAgB,KAKhB,EAAqB,KAY3B,SAAS,CAAS,CAAC,EAAqB,CAC7C,GAAI,OAAO,IAAQ,UAAY,EAAI,SAAW,EAAG,MAAM,EAAW,EAAK,4BAA4B,EACnG,GAAI,EAAQ,OAAO,CAAG,EAAE,WAAa,EAAe,MAAM,EAAW,EAAK,WAAW,SAAqB,EAC1G,GAAI,EAAkB,KAAK,CAAG,EAAG,MAAM,EAAW,EAAK,6BAA6B,EACpF,GAAI,EAAI,SAAS,IAAI,EAAG,MAAM,EAAW,EAAK,oBAAoB,EAClE,GAAI,EAAI,WAAW,GAAG,GAAK,WAAW,KAAK,CAAG,EAAG,MAAM,EAAW,EAAK,kBAAkB,EACzF,QAAW,KAAW,EAAI,MAAM,GAAG,EACjC,GAAI,IAAY,IAAM,IAAY,KAAO,IAAY,KAAM,MAAM,EAAW,EAAK,6BAA6B,EAEhH,OAAO,EAUF,SAAS,CAAkB,CAAC,EAAoC,CACrE,GAAI,IAAW,QAAa,IAAW,GAAI,MAAO,GAClD,IAAM,EAAU,EAAO,SAAS,GAAG,EAAI,EAAO,MAAM,EAAG,EAAE,EAAI,EAC7D,MAAO,GAAG,EAAU,CAAO,KAUtB,SAAS,CAAY,CAAC,EAAoC,CAC/D,GAAI,IAAW,QAAa,IAAW,GAAI,MAAO,GAElD,OADA,EAAU,EAAO,SAAS,GAAG,EAAI,EAAO,MAAM,EAAG,EAAE,EAAI,CAAM,EACtD,EAUF,SAAS,CAAiB,CAAC,EAA8D,CAC9F,IAAM,EAAiC,CAAC,EACpC,EAAQ,EACZ,QAAY,EAAS,KAAU,OAAO,QAAQ,GAAY,CAAC,CAAC,EAAG,CAC7D,IAAM,EAAO,EAAQ,YAAY,EACjC,GAAI,CAAC,EAAa,KAAK,CAAI,EAAG,MAAM,EAAc,0BAA0B,GAAS,EACrF,GAAI,OAAO,IAAU,UAAY,EAAkB,KAAK,CAAK,EAC3D,MAAM,EAAc,+BAA+B,GAAS,EAE9D,GAAS,EAAQ,OAAO,CAAI,EAAE,WAAa,EAAQ,OAAO,CAAK,EAAE,WACjE,EAAO,GAAQ,EAEjB,GAAI,EAAQ,EAAoB,MAAM,EAAc,oBAAoB,SAA0B,EAClG,OAAO,EAWF,SAAS,CAAiB,CAAC,EAAc,EAA+C,CAC7F,GAAI,IAAU,SAAc,OAAO,IAAU,UAAY,EAAkB,KAAK,CAAK,GACnF,MAAM,EAAc,WAAW,GAAM,EAEvC,OAAO,EAUF,SAAS,CAAiB,CAAC,EAAuB,CACvD,GAAI,CAAC,EAAU,KAAK,CAAK,GAAK,EAAkB,KAAK,CAAK,EAAG,MAAM,EAAc,yBAAyB,GAAO,EACjH,OAAO,EAaF,SAAS,CAAa,CAAC,EAAc,EAAe,EAAa,EAAM,OAAO,iBAA0B,CAC7G,GAAI,CAAC,OAAO,cAAc,CAAK,GAAK,EAAQ,GAAO,EAAQ,EACzD,MAAM,EAAc,GAAG,gCAAmC,SAAW,GAAK,EAE5E,OAAO,EAGT,SAAS,CAAU,CAAC,EAAc,EAA8B,CAC9D,OAAO,IAAI,EAAa,cAAe,uBAAuB,IAAU,CACtE,IAAK,OAAO,IAAQ,SAAW,EAAM,MACvC,CAAC,EAGH,SAAS,CAAa,CAAC,EAA+B,CACpD,OAAO,IAAI,EAAa,iBAAkB,CAAO",
|
|
8
|
+
"debugId": "56A3FCD4618ED1FC64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
class V extends Error{code;operation;key;retryable;constructor(q,w,J={}){super(w,J.cause===void 0?void 0:{cause:J.cause});this.name="StorageError",this.code=q,this.operation=J.operation,this.key=J.key,this.retryable=J.retryable??!1}}function T(q,w){return q instanceof V&&(w===void 0||q.code===w)}var Y=new Set(["ECONNRESET","ECONNREFUSED","ECONNABORTED","ETIMEDOUT","EAI_AGAIN","EPIPE","ENETUNREACH","EHOSTUNREACH","EBUSY","EAGAIN"]);function R(q){let w=q,J=w?.status??w?.statusCode??w?.response?.status;return typeof J==="number"?J:void 0}function X(q){let w=q?.code;return typeof w==="string"?w:void 0}function f(q,w,J){if(q instanceof V){if(q.operation!==void 0||w===void 0)return q;return new V(q.code,q.message,{cause:q.cause,operation:w,key:q.key??J,retryable:q.retryable})}let M=R(q),Q=X(q),Z=q instanceof Error?q.message:String(q),$={cause:q,operation:w,key:J};if(M===404||Q==="NoSuchKey"||Q==="ENOENT")return new V("NOT_FOUND","Object not found",$);if(M===401||M===403||Q==="AccessDenied"||Q==="EACCES"||Q==="EPERM")return new V("ACCESS_DENIED","Access denied",$);if(M===409||M===412||Q==="EEXIST"||Q==="FileAlreadyExists")return new V("ALREADY_EXISTS","Object already exists",$);let U=M===408||M===429||M!==void 0&&M>=500||Q!==void 0&&Y.has(Q);return new V("BACKEND",Z||"Storage backend error",{...$,retryable:U})}import{Readable as j}from"node:stream";var L=new TextEncoder;function H(q){if(q instanceof Uint8Array)return q;if(typeof q==="string")return L.encode(q);if(q instanceof ArrayBuffer)return new Uint8Array(q);if(ArrayBuffer.isView(q))return new Uint8Array(q.buffer,q.byteOffset,q.byteLength);throw new V("INVALID_OPTION","Unsupported body chunk type")}async function W(q){let w=[],J=0;for await(let Z of _(q))w.push(Z),J+=Z.byteLength;let M=new Uint8Array(J),Q=0;for(let Z of w)M.set(Z,Q),Q+=Z.byteLength;return M}async function*_(q){let w=q.getReader();try{for(;;){let{done:J,value:M}=await w.read();if(J)return;yield M}}finally{w.releaseLock()}}function A(q){if(typeof q==="string"||q instanceof ArrayBuffer||ArrayBuffer.isView(q)){let w=H(q);return G(w.byteLength,()=>new Blob([w]).stream(),async()=>w)}if(q instanceof Blob)return G(q.size,()=>q.stream(),async()=>new Uint8Array(await q.arrayBuffer()));if(K(q))return F(()=>q);if(I(q))return F(()=>O(q));throw new V("INVALID_OPTION","Unsupported object body type")}function N(q,w){if(q.size!==void 0){if(q.size>w)throw P(w);return q}return F(()=>q.stream().pipeThrough(B(w)))}function P(q){return new V("UPLOAD_REJECTED",`Object exceeds the maximum size of ${q} bytes`)}function B(q){let w=0;return new TransformStream({transform(J,M){if(w+=J.byteLength,w>q)M.error(P(q));else M.enqueue(J)}})}function G(q,w,J){return{size:q,replayable:!0,stream:w,bytes:J}}function F(q){let w=!1,J=()=>{if(w)throw new V("INVALID_OPTION","Streaming body can only be consumed once");return w=!0,q()};return{size:void 0,replayable:!1,stream:J,bytes:()=>W(J())}}function K(q){return typeof q?.getReader==="function"}function I(q){return typeof q?.[Symbol.asyncIterator]==="function"}function O(q){let w=q[Symbol.asyncIterator]();return new ReadableStream({async pull(J){try{let{done:M,value:Q}=await w.next();if(M)J.close();else J.enqueue(H(Q))}catch(M){J.error(M)}},async cancel(J){await w.return?.(J)}})}function z(q){return j.fromWeb(q)}
|
|
2
|
+
export{V as h,T as i,f as j,W as k,_ as l,A as m,N as n,z as o};
|
|
3
|
+
|
|
4
|
+
//# debugId=D2C36066959D8E2064756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\core\\errors.ts", "..\\src\\core\\payload.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * 稳定的对象存储错误码。\n *\n * 业务代码应依据错误码而不是错误消息进行分支判断,错误消息可能随版本调整。\n */\nexport type StorageErrorCode =\n | 'INVALID_KEY'\n | 'INVALID_OPTION'\n | 'NOT_FOUND'\n | 'ALREADY_EXISTS'\n | 'UPLOAD_REJECTED'\n | 'UNSUPPORTED'\n | 'TIMEOUT'\n | 'ABORTED'\n | 'ACCESS_DENIED'\n | 'CLOSED'\n | 'BACKEND'\n\n/**\n * 创建对象存储错误时可附加的上下文。\n */\nexport interface StorageErrorOptions {\n /**\n * 原始错误,便于排障时追溯底层 SDK 或系统调用。\n */\n cause?: unknown\n /**\n * 发生错误的对象存储操作名称。\n */\n operation?: string\n /**\n * 与错误关联的对象键。\n */\n key?: string\n /**\n * 重试同一请求是否可能成功,默认为 `false`。\n */\n retryable?: boolean\n}\n\n/**\n * 对象存储模块唯一的错误类型。\n *\n * 所有适配器都应把底层异常归一化为该类型,使业务层可以通过 `code`\n * 获得一致的判断依据,并通过 `cause` 保留原始异常。\n *\n * @example\n * ```ts\n * try {\n * await storage.head('avatars/1.png')\n * } catch (error) {\n * if (isStorageError(error, 'NOT_FOUND')) return null\n * throw error\n * }\n * ```\n */\nexport class StorageError extends Error {\n /**\n * 机器可读的稳定错误码。\n */\n public readonly code: StorageErrorCode\n /**\n * 发生错误的操作名称。\n */\n public readonly operation?: string\n /**\n * 与错误关联的对象键。\n */\n public readonly key?: string\n /**\n * 重试同一请求是否可能成功。\n */\n public readonly retryable: boolean\n\n /**\n * 创建对象存储错误。\n *\n * @param code 稳定错误码。\n * @param message 可读错误描述,不应包含密钥或签名等敏感信息。\n * @param options 原始错误、操作、对象键及可重试标记。\n */\n public constructor(code: StorageErrorCode, message: string, options: StorageErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'StorageError'\n this.code = code\n this.operation = options.operation\n this.key = options.key\n this.retryable = options.retryable ?? false\n }\n}\n\n/**\n * 判断任意值是否为对象存储错误,并可选地匹配错误码。\n *\n * @param error 待判断的值。\n * @param code 期望的错误码;省略时只判断类型。\n * @returns 是否为匹配的 {@link StorageError}。\n */\nexport function isStorageError(error: unknown, code?: StorageErrorCode): error is StorageError {\n return error instanceof StorageError && (code === undefined || error.code === code)\n}\n\nconst transientCodes = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n 'EPIPE',\n 'ENETUNREACH',\n 'EHOSTUNREACH',\n 'EBUSY',\n 'EAGAIN',\n])\n\n/**\n * 读取错误对象上的 HTTP 状态码,兼容 `status`、`statusCode` 与 `response.status`。\n *\n * @param error 任意错误值。\n * @returns 状态码;不存在时返回 `undefined`。\n */\nexport function statusOf(error: unknown): number | undefined {\n const value = error as { status?: unknown; statusCode?: unknown; response?: { status?: unknown } } | null\n const status = value?.status ?? value?.statusCode ?? value?.response?.status\n return typeof status === 'number' ? status : undefined\n}\n\n/**\n * 读取错误对象上的字符串错误码。\n *\n * @param error 任意错误值。\n * @returns 错误码;不存在时返回 `undefined`。\n */\nexport function codeOf(error: unknown): string | undefined {\n const code = (error as { code?: unknown } | null)?.code\n return typeof code === 'string' ? code : undefined\n}\n\n/**\n * 将任意底层异常归一化为 {@link StorageError}。\n *\n * 规则:已是存储错误则补充上下文后原样返回;HTTP 404 / `NoSuchKey` / `ENOENT`\n * 映射为 `NOT_FOUND`;401、403 / `AccessDenied` / `EACCES` 映射为 `ACCESS_DENIED`;\n * 409、412 / `EEXIST` 映射为 `ALREADY_EXISTS`;408、429、5xx 与常见瞬时网络错误\n * 映射为可重试的 `BACKEND`;其余映射为不可重试的 `BACKEND`。\n *\n * @param error 底层异常。\n * @param operation 操作名称。\n * @param key 关联对象键。\n * @returns 归一化后的存储错误。\n */\nexport function toStorageError(error: unknown, operation?: string, key?: string): StorageError {\n if (error instanceof StorageError) {\n if (error.operation !== undefined || operation === undefined) return error\n return new StorageError(error.code, error.message, {\n cause: error.cause,\n operation,\n key: error.key ?? key,\n retryable: error.retryable,\n })\n }\n const status = statusOf(error)\n const code = codeOf(error)\n const message = error instanceof Error ? error.message : String(error)\n const context = { cause: error, operation, key }\n if (status === 404 || code === 'NoSuchKey' || code === 'ENOENT') {\n return new StorageError('NOT_FOUND', 'Object not found', context)\n }\n if (status === 401 || status === 403 || code === 'AccessDenied' || code === 'EACCES' || code === 'EPERM') {\n return new StorageError('ACCESS_DENIED', 'Access denied', context)\n }\n if (status === 409 || status === 412 || code === 'EEXIST' || code === 'FileAlreadyExists') {\n return new StorageError('ALREADY_EXISTS', 'Object already exists', context)\n }\n const retryable =\n status === 408 ||\n status === 429 ||\n (status !== undefined && status >= 500) ||\n (code !== undefined && transientCodes.has(code))\n return new StorageError('BACKEND', message || 'Storage backend error', { ...context, retryable })\n}\n",
|
|
6
|
+
"import { Readable } from 'node:stream'\nimport type { ReadableStream as NodeWebReadableStream } from 'node:stream/web'\nimport { StorageError } from './errors'\nimport type { ObjectBody } from './types'\n\nconst encoder = new TextEncoder()\n\n/**\n * 统一后的上传内容,由服务层创建并交给适配器消费。\n *\n * 适配器无需关心原始输入是字符串、`Blob` 还是 Node.js 流,只需按需调用\n * {@link StoragePayload.stream} 或 {@link StoragePayload.bytes}。\n */\nexport interface StoragePayload {\n /**\n * 已知的内容字节数;流式输入在读取前无法确定时为 `undefined`。\n */\n readonly size: number | undefined\n /**\n * 内容能否被重复读取;不可重复读取的内容不会被自动重试。\n */\n readonly replayable: boolean\n /**\n * 打开内容的 Web 标准可读流。\n *\n * 可重复读取的内容每次调用都返回新的流;不可重复读取的内容只能调用一次。\n *\n * @throws {@link StorageError} 不可重复读取的内容被二次打开时抛出 `INVALID_OPTION`。\n */\n stream(): ReadableStream<Uint8Array>\n /**\n * 将全部内容读取为连续字节数组。\n */\n bytes(): Promise<Uint8Array>\n}\n\n/**\n * 将任意分块统一转换为字节数组。\n *\n * @param chunk 流或异步迭代器产生的分块。\n * @returns 对应的字节视图,不复制底层内存。\n * @throws {@link StorageError} 分块类型不受支持时抛出 `INVALID_OPTION`。\n */\nexport function toBytes(chunk: unknown): Uint8Array {\n if (chunk instanceof Uint8Array) return chunk\n if (typeof chunk === 'string') return encoder.encode(chunk)\n if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk)\n if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)\n throw new StorageError('INVALID_OPTION', 'Unsupported body chunk type')\n}\n\n/**\n * 将可读流完整读取为连续字节数组。\n *\n * @param stream Web 标准可读流。\n * @returns 合并后的字节数组。\n */\nexport async function readAll(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {\n const chunks: Uint8Array[] = []\n let length = 0\n for await (const chunk of iterate(stream)) {\n chunks.push(chunk)\n length += chunk.byteLength\n }\n const result = new Uint8Array(length)\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.byteLength\n }\n return result\n}\n\n/**\n * 以异步迭代方式逐块读取 Web 可读流,并在提前退出时释放读取锁。\n *\n * @param stream Web 标准可读流。\n * @returns 逐块产生字节的异步迭代器。\n */\nexport async function* iterate(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {\n const reader = stream.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) return\n yield value\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * 将业务输入统一为 {@link StoragePayload}。\n *\n * 字符串、二进制与 `Blob` 是可重复读取的;Web 流、Node.js 流与异步迭代器\n * 只能被消费一次。Node.js 流按异步迭代器读取,因此对象模式下产生的字符串\n * 或 Buffer 分块同样会被正确转换。\n *\n * @param body 业务提供的对象内容。\n * @returns 统一后的上传内容。\n * @throws {@link StorageError} 输入类型不受支持时抛出 `INVALID_OPTION`。\n */\nexport function createPayload(body: ObjectBody): StoragePayload {\n if (typeof body === 'string' || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n const bytes = toBytes(body)\n return replayable(\n bytes.byteLength,\n () => new Blob([bytes as Uint8Array<ArrayBuffer>]).stream(),\n async () => bytes,\n )\n }\n if (body instanceof Blob) {\n return replayable(\n body.size,\n () => body.stream(),\n async () => new Uint8Array(await body.arrayBuffer()),\n )\n }\n if (isWebStream(body)) return oneShot(() => body)\n if (isAsyncIterable(body)) return oneShot(() => fromIterable(body))\n throw new StorageError('INVALID_OPTION', 'Unsupported object body type')\n}\n\n/**\n * 为上传内容附加字节数上限,超限时以 `UPLOAD_REJECTED` 终止读取。\n *\n * @param payload 原始上传内容。\n * @param maxSize 允许的最大字节数。\n * @returns 带上限检查的上传内容。\n * @throws {@link StorageError} 已知大小超过上限时立即抛出 `UPLOAD_REJECTED`。\n */\nexport function limitPayload(payload: StoragePayload, maxSize: number): StoragePayload {\n if (payload.size !== undefined) {\n if (payload.size > maxSize) throw tooLarge(maxSize)\n return payload\n }\n const open = () => payload.stream().pipeThrough(sizeGuard(maxSize))\n return oneShot(open)\n}\n\nfunction tooLarge(maxSize: number): StorageError {\n return new StorageError('UPLOAD_REJECTED', `Object exceeds the maximum size of ${maxSize} bytes`)\n}\n\nfunction sizeGuard(maxSize: number): TransformStream<Uint8Array, Uint8Array> {\n let total = 0\n return new TransformStream({\n transform(chunk, controller) {\n total += chunk.byteLength\n if (total > maxSize) controller.error(tooLarge(maxSize))\n else controller.enqueue(chunk)\n },\n })\n}\n\nfunction replayable(\n size: number,\n stream: () => ReadableStream<Uint8Array>,\n bytes: () => Promise<Uint8Array>,\n): StoragePayload {\n return { size, replayable: true, stream, bytes }\n}\n\nfunction oneShot(open: () => ReadableStream<Uint8Array>): StoragePayload {\n let consumed = false\n const stream = () => {\n if (consumed) throw new StorageError('INVALID_OPTION', 'Streaming body can only be consumed once')\n consumed = true\n return open()\n }\n return { size: undefined, replayable: false, stream, bytes: () => readAll(stream()) }\n}\n\nfunction isWebStream(value: unknown): value is ReadableStream<Uint8Array> {\n return typeof (value as ReadableStream | null)?.getReader === 'function'\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return typeof (value as AsyncIterable<unknown> | null)?.[Symbol.asyncIterator] === 'function'\n}\n\nfunction fromIterable(source: AsyncIterable<unknown>): ReadableStream<Uint8Array> {\n const iterator = source[Symbol.asyncIterator]()\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done) controller.close()\n else controller.enqueue(toBytes(value))\n } catch (error) {\n controller.error(error)\n }\n },\n async cancel(reason) {\n await iterator.return?.(reason)\n },\n })\n}\n\n/**\n * 将 Web 可读流转换为 Node.js 可读流,供只接受 Node.js 流的 SDK 使用。\n *\n * @param stream Web 标准可读流。\n * @returns Node.js 可读流。\n */\nexport function toNodeReadable(stream: ReadableStream<Uint8Array>): Readable {\n return Readable.fromWeb(stream as unknown as NodeWebReadableStream<Uint8Array>)\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "AAwDO,MAAM,UAAqB,KAAM,CAItB,KAIA,UAIA,IAIA,UAST,WAAW,CAAC,EAAwB,EAAiB,EAA+B,CAAC,EAAG,CAC7F,MAAM,EAAS,EAAQ,QAAU,OAAY,OAAY,CAAE,MAAO,EAAQ,KAAM,CAAC,EACjF,KAAK,KAAO,eACZ,KAAK,KAAO,EACZ,KAAK,UAAY,EAAQ,UACzB,KAAK,IAAM,EAAQ,IACnB,KAAK,UAAY,EAAQ,WAAa,GAE1C,CASO,SAAS,CAAc,CAAC,EAAgB,EAAgD,CAC7F,OAAO,aAAiB,IAAiB,IAAS,QAAa,EAAM,OAAS,GAGhF,IAAM,EAAiB,IAAI,IAAI,CAC7B,aACA,eACA,eACA,YACA,YACA,QACA,cACA,eACA,QACA,QACF,CAAC,EAQM,SAAS,CAAQ,CAAC,EAAoC,CAC3D,IAAM,EAAQ,EACR,EAAS,GAAO,QAAU,GAAO,YAAc,GAAO,UAAU,OACtE,OAAO,OAAO,IAAW,SAAW,EAAS,OASxC,SAAS,CAAM,CAAC,EAAoC,CACzD,IAAM,EAAQ,GAAqC,KACnD,OAAO,OAAO,IAAS,SAAW,EAAO,OAgBpC,SAAS,CAAc,CAAC,EAAgB,EAAoB,EAA4B,CAC7F,GAAI,aAAiB,EAAc,CACjC,GAAI,EAAM,YAAc,QAAa,IAAc,OAAW,OAAO,EACrE,OAAO,IAAI,EAAa,EAAM,KAAM,EAAM,QAAS,CACjD,MAAO,EAAM,MACb,YACA,IAAK,EAAM,KAAO,EAClB,UAAW,EAAM,SACnB,CAAC,EAEH,IAAM,EAAS,EAAS,CAAK,EACvB,EAAO,EAAO,CAAK,EACnB,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAC/D,EAAU,CAAE,MAAO,EAAO,YAAW,KAAI,EAC/C,GAAI,IAAW,KAAO,IAAS,aAAe,IAAS,SACrD,OAAO,IAAI,EAAa,YAAa,mBAAoB,CAAO,EAElE,GAAI,IAAW,KAAO,IAAW,KAAO,IAAS,gBAAkB,IAAS,UAAY,IAAS,QAC/F,OAAO,IAAI,EAAa,gBAAiB,gBAAiB,CAAO,EAEnE,GAAI,IAAW,KAAO,IAAW,KAAO,IAAS,UAAY,IAAS,oBACpE,OAAO,IAAI,EAAa,iBAAkB,wBAAyB,CAAO,EAE5E,IAAM,EACJ,IAAW,KACX,IAAW,KACV,IAAW,QAAa,GAAU,KAClC,IAAS,QAAa,EAAe,IAAI,CAAI,EAChD,OAAO,IAAI,EAAa,UAAW,GAAW,wBAAyB,IAAK,EAAS,WAAU,CAAC,ECnLlG,mBAAS,oBAKT,IAAM,EAAU,IAAI,YAsCb,SAAS,CAAO,CAAC,EAA4B,CAClD,GAAI,aAAiB,WAAY,OAAO,EACxC,GAAI,OAAO,IAAU,SAAU,OAAO,EAAQ,OAAO,CAAK,EAC1D,GAAI,aAAiB,YAAa,OAAO,IAAI,WAAW,CAAK,EAC7D,GAAI,YAAY,OAAO,CAAK,EAAG,OAAO,IAAI,WAAW,EAAM,OAAQ,EAAM,WAAY,EAAM,UAAU,EACrG,MAAM,IAAI,EAAa,iBAAkB,6BAA6B,EASxE,eAAsB,CAAO,CAAC,EAAyD,CACrF,IAAM,EAAuB,CAAC,EAC1B,EAAS,EACb,cAAiB,KAAS,EAAQ,CAAM,EACtC,EAAO,KAAK,CAAK,EACjB,GAAU,EAAM,WAElB,IAAM,EAAS,IAAI,WAAW,CAAM,EAChC,EAAS,EACb,QAAW,KAAS,EAClB,EAAO,IAAI,EAAO,CAAM,EACxB,GAAU,EAAM,WAElB,OAAO,EAST,eAAuB,CAAO,CAAC,EAAgE,CAC7F,IAAM,EAAS,EAAO,UAAU,EAChC,GAAI,CACF,OAAS,CACP,IAAQ,OAAM,SAAU,MAAM,EAAO,KAAK,EAC1C,GAAI,EAAM,OACV,MAAM,UAER,CACA,EAAO,YAAY,GAehB,SAAS,CAAa,CAAC,EAAkC,CAC9D,GAAI,OAAO,IAAS,UAAY,aAAgB,aAAe,YAAY,OAAO,CAAI,EAAG,CACvF,IAAM,EAAQ,EAAQ,CAAI,EAC1B,OAAO,EACL,EAAM,WACN,IAAM,IAAI,KAAK,CAAC,CAAgC,CAAC,EAAE,OAAO,EAC1D,SAAY,CACd,EAEF,GAAI,aAAgB,KAClB,OAAO,EACL,EAAK,KACL,IAAM,EAAK,OAAO,EAClB,SAAY,IAAI,WAAW,MAAM,EAAK,YAAY,CAAC,CACrD,EAEF,GAAI,EAAY,CAAI,EAAG,OAAO,EAAQ,IAAM,CAAI,EAChD,GAAI,EAAgB,CAAI,EAAG,OAAO,EAAQ,IAAM,EAAa,CAAI,CAAC,EAClE,MAAM,IAAI,EAAa,iBAAkB,8BAA8B,EAWlE,SAAS,CAAY,CAAC,EAAyB,EAAiC,CACrF,GAAI,EAAQ,OAAS,OAAW,CAC9B,GAAI,EAAQ,KAAO,EAAS,MAAM,EAAS,CAAO,EAClD,OAAO,EAGT,OAAO,EADM,IAAM,EAAQ,OAAO,EAAE,YAAY,EAAU,CAAO,CAAC,CAC/C,EAGrB,SAAS,CAAQ,CAAC,EAA+B,CAC/C,OAAO,IAAI,EAAa,kBAAmB,sCAAsC,SAAe,EAGlG,SAAS,CAAS,CAAC,EAA0D,CAC3E,IAAI,EAAQ,EACZ,OAAO,IAAI,gBAAgB,CACzB,SAAS,CAAC,EAAO,EAAY,CAE3B,GADA,GAAS,EAAM,WACX,EAAQ,EAAS,EAAW,MAAM,EAAS,CAAO,CAAC,EAClD,OAAW,QAAQ,CAAK,EAEjC,CAAC,EAGH,SAAS,CAAU,CACjB,EACA,EACA,EACgB,CAChB,MAAO,CAAE,OAAM,WAAY,GAAM,SAAQ,OAAM,EAGjD,SAAS,CAAO,CAAC,EAAwD,CACvE,IAAI,EAAW,GACT,EAAS,IAAM,CACnB,GAAI,EAAU,MAAM,IAAI,EAAa,iBAAkB,0CAA0C,EAEjG,OADA,EAAW,GACJ,EAAK,GAEd,MAAO,CAAE,KAAM,OAAW,WAAY,GAAO,SAAQ,MAAO,IAAM,EAAQ,EAAO,CAAC,CAAE,EAGtF,SAAS,CAAW,CAAC,EAAqD,CACxE,OAAO,OAAQ,GAAiC,YAAc,WAGhE,SAAS,CAAe,CAAC,EAAiD,CACxE,OAAO,OAAQ,IAA0C,OAAO,iBAAmB,WAGrF,SAAS,CAAY,CAAC,EAA4D,CAChF,IAAM,EAAW,EAAO,OAAO,eAAe,EAC9C,OAAO,IAAI,eAA2B,MAC9B,KAAI,CAAC,EAAY,CACrB,GAAI,CACF,IAAQ,OAAM,SAAU,MAAM,EAAS,KAAK,EAC5C,GAAI,EAAM,EAAW,MAAM,EACtB,OAAW,QAAQ,EAAQ,CAAK,CAAC,EACtC,MAAO,EAAO,CACd,EAAW,MAAM,CAAK,SAGpB,OAAM,CAAC,EAAQ,CACnB,MAAM,EAAS,SAAS,CAAM,EAElC,CAAC,EASI,SAAS,CAAc,CAAC,EAA8C,CAC3E,OAAO,EAAS,QAAQ,CAAsD",
|
|
9
|
+
"debugId": "D2C36066959D8E2064756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面向 Bun 与 Node.js 的对象存储核心入口。
|
|
3
|
+
*
|
|
4
|
+
* - `@lhy-meta-server/meta-oss`:框架无关的 `ObjectStorage` 服务与适配器契约。
|
|
5
|
+
* - `@lhy-meta-server/meta-oss/filesystem`:本地文件系统适配器。
|
|
6
|
+
* - `@lhy-meta-server/meta-oss/ali-oss`:阿里云 OSS 适配器。
|
|
7
|
+
* - `@lhy-meta-server/meta-oss/elysia`:Elysia 注入插件。
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { type DriverContext, type DriverCopyOptions, type DriverGetOptions, type DriverListOptions, type DriverPutOptions, type DriverSignOptions, defineDriver, type MultipartDriver, type StorageDriver, } from './core/driver';
|
|
12
|
+
export { isStorageError, StorageError, type StorageErrorCode, type StorageErrorOptions, toStorageError, } from './core/errors';
|
|
13
|
+
export type { RetryOptions } from './core/execution';
|
|
14
|
+
export { type CreateObjectKeyOptions, createObjectKey, type KeyGenerator } from './core/keys';
|
|
15
|
+
export { DEFAULT_CONTENT_TYPE, lookupContentType } from './core/mime';
|
|
16
|
+
export { type StorageErrorSummary, type StorageEvent, StorageMetrics, type StorageMetricsSnapshot, type StorageObserver, type StorageOperation, type StorageOutcome, type StorageRetryEvent, } from './core/observability';
|
|
17
|
+
export { createPayload, readAll, type StoragePayload, toNodeReadable } from './core/payload';
|
|
18
|
+
export type { UploadPolicy } from './core/policy';
|
|
19
|
+
export { ObjectStorage, type ObjectStorageOptions } from './core/storage';
|
|
20
|
+
export type { DeleteManyResult, GetOptions, ListOptions, ListResult, MultipartUpload, MultipartUploadOptions, ObjectBody, ObjectHttpOptions, ObjectInfo, ObjectMetadata, ObjectRange, ObjectStream, OperationOptions, PutOptions, SignedUrlMethod, SignedUrlOptions, UploadedPart, VerifiedUrl, VerifyUrlRequest, } from './core/types';
|
|
21
|
+
export { assertKey } from './core/validation';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAC7F,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACrE,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC5F,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AACzE,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as H,b as i,c as a,d as s,e as K,f as I,g as $}from"./index-5w45sfp6.js";import{h as W,i as S,j as Z,k as b,l as p,m as X,n as w,o as GO}from"./index-7dpmhwry.js";function r(O){return O}import{randomUUID as x}from"node:crypto";var t="application/octet-stream",o={aac:"audio/aac",avif:"image/avif",bmp:"image/bmp",css:"text/css; charset=utf-8",csv:"text/csv; charset=utf-8",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",gif:"image/gif",gz:"application/gzip",htm:"text/html; charset=utf-8",html:"text/html; charset=utf-8",ico:"image/x-icon",jpeg:"image/jpeg",jpg:"image/jpeg",js:"text/javascript; charset=utf-8",json:"application/json",m4a:"audio/mp4",md:"text/markdown; charset=utf-8",mjs:"text/javascript; charset=utf-8",mov:"video/quicktime",mp3:"audio/mpeg",mp4:"video/mp4",ogg:"audio/ogg",pdf:"application/pdf",png:"image/png",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",svg:"image/svg+xml",tar:"application/x-tar",txt:"text/plain; charset=utf-8",wav:"audio/wav",webm:"video/webm",webp:"image/webp",woff:"font/woff",woff2:"font/woff2",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xml:"application/xml",zip:"application/zip"};function q(O){let G=O.slice(O.lastIndexOf("/")+1),L=G.lastIndexOf(".");return L<=0?"":G.slice(L+1).toLowerCase()}function M(O,G={}){let L=q(O);return G[L]??o[L]??"application/octet-stream"}function z(O){return O.split(";",1)[0]?.trim().toLowerCase()??""}function f(O,G){let L=z(O);return G.some((B)=>{let F=B.toLowerCase();return F.endsWith("/*")?L.startsWith(F.slice(0,-1)):L===F})}function e(O,G={}){let L=G.date??new Date,B=String(L.getUTCFullYear()),F=String(L.getUTCMonth()+1).padStart(2,"0"),U=String(L.getUTCDate()).padStart(2,"0"),Q=q(O).replace(/[^a-z0-9]/g,"").slice(0,16),Y=Q?`${x()}.${Q}`:x();return[G.directory?.replace(/^\/+|\/+$/g,""),B,F,U,Y].filter(Boolean).join("/")}class N{operations=0;errors=0;retries=0;inFlight=0;bytesWritten=0;totalDuration=0;byOperation={};byErrorCode={};constructor(){}start(){this.inFlight+=1}record(O){if(this.inFlight=Math.max(0,this.inFlight-1),this.operations+=1,this.totalDuration+=O.duration,this.byOperation[O.operation]=(this.byOperation[O.operation]??0)+1,O.outcome==="error"){this.errors+=1;let G=O.errorCode??"BACKEND";this.byErrorCode[G]=(this.byErrorCode[G]??0)+1}else if(O.operation==="put"||O.operation==="uploadPart")this.bytesWritten+=O.bytes??0}retry(){this.retries+=1}snapshot(){return Object.freeze({operations:this.operations,errors:this.errors,retries:this.retries,inFlight:this.inFlight,bytesWritten:this.bytesWritten,totalDuration:this.totalDuration,byOperation:Object.freeze({...this.byOperation}),byErrorCode:Object.freeze({...this.byErrorCode})})}reset(){this.operations=0,this.errors=0,this.retries=0,this.bytesWritten=0,this.totalDuration=0,this.byOperation={},this.byErrorCode={}}}function A(O){let G=O.cause,L=typeof G?.status==="number"?G.status:typeof G?.statusCode==="number"?G.statusCode:void 0,B=typeof G?.code==="string"?G.code:void 0,F=typeof G?.requestId==="string"?G.requestId:void 0;return Object.freeze({code:O.code,message:O.code==="BACKEND"?"Storage backend error":O.message,retryable:O.retryable,...L===void 0?{}:{status:L},...B===void 0?{}:{causeCode:B},...F===void 0?{}:{requestId:F}})}function g(O,G,L,B,F=Math.random){let U=Math.min(L,G*2**(O-1));return B?Math.min(L,Math.round(U*(1+F()*0.5))):U}function m(O){return S(O)&&O.retryable}function k(O,G){let L=new AbortController,B=()=>L.abort(G?.reason);if(G?.aborted)B();else G?.addEventListener("abort",B,{once:!0});let F=O>0?setTimeout(()=>L.abort(new W("TIMEOUT",`Operation timed out after ${O} ms`,{retryable:!0})),O):void 0;return{signal:L.signal,dispose(){if(F)clearTimeout(F);G?.removeEventListener("abort",B)}}}function c(O,G){if(G.aborted)return Promise.reject(D(G));return new Promise((L,B)=>{let F=()=>B(D(G));G.addEventListener("abort",F,{once:!0}),O.then((U)=>{G.removeEventListener("abort",F),L(U)},(U)=>{G.removeEventListener("abort",F),B(U)})})}function D(O){let G=O.reason;if(S(G))return G;return new W("ABORTED","Operation was aborted",{cause:G})}function d(O,G){if(G?.aborted)return Promise.reject(D(G));return new Promise((L,B)=>{let F=()=>{clearTimeout(U),B(D(G))},U=setTimeout(()=>{G?.removeEventListener("abort",F),L()},O);G?.addEventListener("abort",F,{once:!0})})}class h{limit;active=0;queue=[];constructor(O){this.limit=O}async run(O){if(this.active>=this.limit)await new Promise((G)=>this.queue.push(G));else this.active+=1;try{return await O()}finally{let G=this.queue.shift();if(G)G();else this.active-=1}}}async function y(O,G,L){let B=Array(O.length),F=0,U=async()=>{while(F<O.length){let Q=F;F+=1,B[Q]=await L(O[Q],Q)}};return await Promise.all(Array.from({length:Math.min(G,O.length)},U)),B}async function l(O,G){let L=O.then(()=>!0,()=>!0);if(G<=0)return!1;let B,F=new Promise((U)=>{B=setTimeout(()=>U(!1),G)});try{return await Promise.race([L,F])}finally{clearTimeout(B)}}function E(O,G,L){let B=q(G),F=(U)=>U.map((Q)=>Q.toLowerCase());if(O.deniedExtensions&&F(O.deniedExtensions).includes(B))throw T(G,`Extension is not allowed: ${B}`);if(O.allowedExtensions&&!F(O.allowedExtensions).includes(B))throw T(G,`Extension is not allowed: ${B||"(none)"}`);if(O.allowedContentTypes&&!f(L,O.allowedContentTypes))throw T(G,`Content type is not allowed: ${z(L)}`)}function T(O,G){return new W("UPLOAD_REJECTED",G,{key:O})}var u=102400,P=1e4;class n{driverName;driver;prefix;timeout;retries;baseDelay;maxDelay;jitter;shouldRetry;semaphore;concurrency;policy;maxSize;overwrite;contentTypes;partSize;partConcurrency;abortGrace;multipartThreshold;observer;metrics;closing;constructor(O){if(!O?.driver)throw new W("INVALID_OPTION","driver is required");this.driver=O.driver,this.driverName=O.driver.name,this.prefix=i(O.prefix),this.timeout=$("timeout",O.timeout??30000,0);let G=O.retry??{};if(this.retries=$("retry.retries",G.retries??2,0,10),this.baseDelay=$("retry.baseDelay",G.baseDelay??100,0),this.maxDelay=$("retry.maxDelay",G.maxDelay??2000,this.baseDelay),this.jitter=G.jitter??!0,this.shouldRetry=G.shouldRetry??m,this.concurrency=$("concurrency",O.concurrency??32,1),this.semaphore=new h(this.concurrency),this.policy=O.upload??{},this.maxSize=$("upload.maxSize",this.policy.maxSize??Number.MAX_SAFE_INTEGER,0),this.overwrite=O.overwrite??!0,this.contentTypes=O.contentTypes??{},this.partSize=$("partSize",O.partSize??8388608,u),this.partConcurrency=$("partConcurrency",O.partConcurrency??4,1),this.abortGrace=$("abortGrace",O.abortGrace??5000,0),this.multipartThreshold=$("multipartThreshold",O.multipartThreshold??67108864,0),this.multipartThreshold>0&&this.multipartThreshold<this.partSize)throw new W("INVALID_OPTION","multipartThreshold must be 0 or not less than partSize");this.observer=O.observer,this.metrics=O.metrics??new N}async put(O,G,L={}){H(O);let B=this.putOptions(O,L),F=w(X(G),this.maxSize);if(this.shouldUseMultipart(F,B.overwrite))return this.uploadMultipartPayload(O,F,L);let U=await this.run("put",L,{key:O,bytes:F.size,idempotent:F.replayable},(Q)=>this.driver.put(this.fullKey(O),F,{...B,...Q}));return this.publicInfo(U)}async get(O,G={}){H(O);let L=G.range;if(L){if($("range.start",L.start,0),L.end!==void 0)$("range.end",L.end,L.start)}let B=await this.run("get",G,{key:O,idempotent:!0},(F)=>this.driver.get(this.fullKey(O),{...F,range:L}));return{...B,info:this.publicInfo(B.info)}}async getBytes(O,G={}){return b((await this.get(O,G)).body)}async getText(O,G={}){return new TextDecoder().decode(await this.getBytes(O,G))}async head(O,G={}){H(O);let L=await this.run("head",G,{key:O,idempotent:!0},(B)=>this.driver.head(this.fullKey(O),B));return this.publicInfo(L)}async exists(O,G={}){return H(O),this.run("exists",G,{key:O,idempotent:!0},async(L)=>{try{return await this.driver.head(this.fullKey(O),L),!0}catch(B){if(Z(B).code==="NOT_FOUND")return!1;throw B}})}async delete(O,G={}){H(O),await this.run("delete",G,{key:O,idempotent:!0},(L)=>this.driver.delete(this.fullKey(O),L))}async deleteMany(O,G={}){let L=[...new Set(O)];if(L.forEach(H),L.length===0)return{deleted:[],failed:[]};return this.run("deleteMany",G,{idempotent:!0},async(B)=>{let F={deleted:[],failed:[]},U=this.driver.deleteMany?.bind(this.driver);if(U){for(let Y=0;Y<L.length;Y+=1000){let j=L.slice(Y,Y+1000),V=await U(j.map((J)=>this.fullKey(J)),B);F.deleted.push(...V.deleted.map((J)=>this.publicKey(J))),F.failed.push(...V.failed.map((J)=>({...J,key:this.publicKey(J.key)})))}return F}return(await y(L,Math.min(this.concurrency,16),async(Y)=>{try{await this.driver.delete(this.fullKey(Y),B);return}catch(j){return Z(j,"delete",Y)}})).forEach((Y,j)=>{let V=L[j];if(Y)F.failed.push({key:V,error:Y});else F.deleted.push(V)}),F})}async list(O={}){let G=a(O.prefix),L=$("limit",O.limit??1000,1,1000);if(O.delimiter!==void 0&&O.delimiter.length===0)throw new W("INVALID_OPTION","delimiter must not be empty");let B=this.capability("list",this.driver.list),F=await this.run("list",O,{idempotent:!0},(U)=>B({...U,prefix:this.prefix+G,delimiter:O.delimiter,cursor:O.cursor,limit:L}));return{objects:F.objects.map((U)=>this.publicInfo(U)),prefixes:F.prefixes.map((U)=>this.publicKey(U)),cursor:F.cursor}}async*listAll(O={}){let G;do{let L=await this.list({...O,cursor:G,delimiter:void 0});yield*L.objects,G=L.cursor}while(G)}async copy(O,G,L={}){H(O),H(G);let B=L.overwrite??this.overwrite,F=await this.run("copy",L,{key:G,idempotent:B},(U)=>this.copyObject(O,G,B,U));return this.publicInfo(F)}async move(O,G,L={}){if(H(O),H(G),O===G)throw new W("INVALID_OPTION","source and destination must differ");let B=L.overwrite??this.overwrite,F=await this.run("move",L,{key:G,idempotent:!1},async(U)=>{let Q=await this.copyObject(O,G,B,U);return await this.driver.delete(this.fullKey(O),U),Q});return this.publicInfo(F)}async copyObject(O,G,L,B){let F=this.fullKey(O),U=this.fullKey(G);if(this.driver.copy)return this.driver.copy(F,U,{...B,overwrite:L});let{info:Q,body:Y}=await this.driver.get(F,B),j=X(Y);return this.driver.put(U,j,{...B,contentType:Q.contentType??M(G,this.contentTypes),cacheControl:Q.cacheControl,contentDisposition:Q.contentDisposition,metadata:Q.metadata,overwrite:L})}async signUrl(O,G={}){H(O);let L=G.method??"GET";if(L!=="GET"&&L!=="PUT")throw new W("INVALID_OPTION",`Unsupported method: ${L}`);let B=$("expiresIn",G.expiresIn??900,1,604800),F;if(L==="PUT")F=I(G.contentType??M(O,this.contentTypes)),E(this.policy,O,F);let U=K("responseContentDisposition",G.responseContentDisposition),Q=this.capability("signUrl",this.driver.signUrl);return this.run("signUrl",G,{key:O,idempotent:!0},(Y)=>Q(this.fullKey(O),{...Y,method:L,expiresIn:B,contentType:F,responseContentDisposition:U}))}verifyUrl(O){if(this.closing)throw new W("CLOSED","Object storage has been closed",{operation:"verifyUrl"});let L=this.capability("verifyUrl",this.driver.verifyUrl)(O);if(!L.key.startsWith(this.prefix))throw new W("ACCESS_DENIED","Signed URL is outside of the storage prefix");return{...L,key:H(this.publicKey(L.key))}}async createMultipart(O,G={}){H(O);let L=this.multipartDriver(),B=this.putOptions(O,G),F=await this.run("createMultipart",G,{key:O,idempotent:!1},(U)=>L.createMultipart(this.fullKey(O),{...B,...U}));return{key:O,uploadId:F.uploadId}}async uploadPart(O,G,L,B={}){this.assertUpload(O),$("partNumber",G,1,P);let F=this.multipartDriver(),U=await w(X(L),this.maxSize).bytes(),Q=X(U);return this.run("uploadPart",B,{key:O.key,bytes:U.byteLength,idempotent:!0},(Y)=>F.uploadPart(this.driverUpload(O),G,Q,Y))}async completeMultipart(O,G,L={}){if(this.assertUpload(O),G.length===0)throw new W("INVALID_OPTION","parts must not be empty");let B=new Map;for(let Y of G){if($("partNumber",Y.partNumber,1,P),typeof Y.etag!=="string"||Y.etag.length===0)throw new W("INVALID_OPTION","part etag is required");B.set(Y.partNumber,Y)}let F=[...B.values()].sort((Y,j)=>Y.partNumber-j.partNumber),U=this.multipartDriver(),Q=await this.run("completeMultipart",L,{key:O.key,idempotent:!1},(Y)=>U.completeMultipart(this.driverUpload(O),F,Y));return this.publicInfo(Q)}async abortMultipart(O,G={}){this.assertUpload(O);let L=this.multipartDriver();await this.run("abortMultipart",G,{key:O.key,idempotent:!0},(B)=>L.abortMultipart(this.driverUpload(O),B))}async uploadMultipart(O,G,L={}){return H(O),this.multipartDriver(),this.uploadMultipartPayload(O,w(X(G),this.maxSize),L)}shouldUseMultipart(O,G){return this.multipartThreshold>0&&G&&this.driver.multipart!==void 0&&O.size!==void 0&&O.size>this.multipartThreshold}async uploadMultipartPayload(O,G,L){let B=$("partSize",L.partSize??this.partSize,u),F=$("concurrency",L.concurrency??this.partConcurrency,1),U=await this.createMultipart(O,L),Q=new Set,Y=[],j,V=(J,_)=>{let C=this.uploadPart(U,J,_,L).then((R)=>{Y.push(R)},(R)=>{j??=R}).finally(()=>Q.delete(C));Q.add(C)};try{let J=0;for await(let _ of OO(G,B)){if(j)break;if(J+=1,J>P)throw new W("UPLOAD_REJECTED",`Object exceeds ${P} parts`,{key:O});V(J,_);while(Q.size>=F)await Promise.race(Q)}if(await Promise.all(Q),j)throw j;if(J===0)return await this.abortMultipart(U,{...L,retries:0}),await this.put(O,new Uint8Array(0),L);return await this.completeMultipart(U,Y,L)}catch(J){throw await Promise.allSettled(Q),await this.abortMultipart(U,{timeout:L.timeout,retries:0}).catch(()=>{return}),J}}getMetrics(){return this.metrics.snapshot()}resetMetrics(){this.metrics.reset()}close(){return this.closing??=(async()=>{let O=performance.now();this.metrics.start();try{await this.driver.close?.(),this.emit({operation:"close",outcome:"success",started:O,attempts:1})}catch(G){let L=Z(G,"close");throw this.emit({operation:"close",outcome:"error",started:O,attempts:1,error:L}),L}})(),this.closing}putOptions(O,G){let L=I(G.contentType??M(O,this.contentTypes));return E(this.policy,O,L),{contentType:L,cacheControl:K("cacheControl",G.cacheControl),contentDisposition:K("contentDisposition",G.contentDisposition),metadata:s(G.metadata),overwrite:G.overwrite??this.overwrite}}capability(O,G){if(!G)throw new W("UNSUPPORTED",`Driver "${this.driverName}" does not support ${O}`,{operation:O});return G.bind(this.driver)}multipartDriver(){let O=this.driver.multipart;if(!O)throw new W("UNSUPPORTED",`Driver "${this.driverName}" does not support multipart upload`);return O}assertUpload(O){if(H(O?.key),typeof O.uploadId!=="string"||O.uploadId.length===0)throw new W("INVALID_OPTION","uploadId is required",{key:O.key})}driverUpload(O){return{key:this.fullKey(O.key),uploadId:O.uploadId}}fullKey(O){return this.prefix+O}publicKey(O){return O.startsWith(this.prefix)?O.slice(this.prefix.length):O}publicInfo(O){return{...O,key:this.publicKey(O.key)}}async run(O,G,L,B){if(this.closing)throw new W("CLOSED","Object storage has been closed",{operation:O,key:L.key});let F=$("timeout",G.timeout??this.timeout,0),U=L.idempotent?$("retries",G.retries??this.retries,0,10):0,Q=performance.now();this.metrics.start();let Y=0;try{let j=await this.semaphore.run(async()=>{for(;;){Y+=1;let V=k(F,G.signal),J=Promise.resolve().then(()=>B({signal:V.signal}));try{return await c(J,V.signal)}catch(_){if(V.signal.aborted)await l(J,this.abortGrace);let v=V.signal.aborted?D(V.signal):_,C=Z(v,O,L.key);if(G.signal?.aborted||Y>U||!this.shouldRetry(C,Y))throw C;let R=g(Y,this.baseDelay,this.maxDelay,this.jitter);this.metrics.retry(),this.notify("onRetry",{operation:O,driver:this.driverName,key:L.key,attempt:Y+1,delay:R,error:A(C)}),await d(R,G.signal)}finally{V.dispose()}}});return this.emit({operation:O,outcome:"success",started:Q,attempts:Y,key:L.key,bytes:L.bytes}),j}catch(j){let V=Z(j,O,L.key);throw this.emit({operation:O,outcome:"error",started:Q,attempts:Y,key:L.key,error:V}),V}}emit(O){let G=Object.freeze({operation:O.operation,outcome:O.outcome,driver:this.driverName,...O.key===void 0?{}:{key:O.key},duration:Math.max(0,performance.now()-O.started),attempts:O.attempts,...O.bytes===void 0?{}:{bytes:O.bytes},...O.error===void 0?{}:{errorCode:O.error.code,error:A(O.error)},timestamp:Date.now()});this.metrics.record(G),this.notify("onOperation",G)}notify(O,G){try{this.observer?.[O]?.call(this.observer,Object.freeze(G))}catch{}}}async function*OO(O,G){let L=new Uint8Array(G),B=0;for await(let F of p(O.stream())){let U=0;while(U<F.byteLength){let Q=Math.min(G-B,F.byteLength-U);if(L.set(F.subarray(U,U+Q),B),B+=Q,U+=Q,B===G)yield L,L=new Uint8Array(G),B=0}}if(B>0)yield L.subarray(0,B)}export{Z as toStorageError,GO as toNodeReadable,b as readAll,M as lookupContentType,S as isStorageError,r as defineDriver,X as createPayload,e as createObjectKey,H as assertKey,N as StorageMetrics,W as StorageError,n as ObjectStorage,t as DEFAULT_CONTENT_TYPE};
|
|
2
|
+
|
|
3
|
+
//# debugId=C8A2B002EC4BF53A64756E2164756E21
|