@lhy-meta-server/meta-oss 0.0.0-stage → 0.0.2
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,73 @@
|
|
|
1
|
+
import type { ObjectMetadata } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 对象键允许的最大 UTF-8 字节数,与主流对象存储保持一致。
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_KEY_BYTES = 1023;
|
|
6
|
+
/**
|
|
7
|
+
* 自定义元数据序列化后允许的最大 UTF-8 字节数。
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_METADATA_BYTES = 8192;
|
|
10
|
+
/**
|
|
11
|
+
* 校验对象键并返回原值。
|
|
12
|
+
*
|
|
13
|
+
* 规则:非空、不超过 1023 字节、不含控制字符与反斜杠、不以 `/` 开头、
|
|
14
|
+
* 不含空段以及 `.`、`..` 段、不以盘符开头。允许中文等 Unicode 字符。
|
|
15
|
+
*
|
|
16
|
+
* @param key 待校验的对象键。
|
|
17
|
+
* @returns 校验通过的对象键。
|
|
18
|
+
* @throws {@link StorageError} 键非法时抛出 `INVALID_KEY`。
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertKey(key: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* 校验并归一化目录前缀;空值返回空字符串,否则保证以 `/` 结尾。
|
|
23
|
+
*
|
|
24
|
+
* @param prefix 待校验的前缀。
|
|
25
|
+
* @returns 归一化后的前缀。
|
|
26
|
+
* @throws {@link StorageError} 前缀非法时抛出 `INVALID_KEY`。
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeDirectory(prefix: string | undefined): string;
|
|
29
|
+
/**
|
|
30
|
+
* 校验列举前缀;允许以 `/` 结尾或为空,其余规则与对象键一致。
|
|
31
|
+
*
|
|
32
|
+
* @param prefix 待校验的前缀。
|
|
33
|
+
* @returns 校验通过的前缀,未设置时为空字符串。
|
|
34
|
+
* @throws {@link StorageError} 前缀非法时抛出 `INVALID_KEY`。
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertPrefix(prefix: string | undefined): string;
|
|
37
|
+
/**
|
|
38
|
+
* 校验自定义元数据并返回小写键的新对象。
|
|
39
|
+
*
|
|
40
|
+
* @param metadata 待校验的元数据。
|
|
41
|
+
* @returns 归一化后的元数据副本。
|
|
42
|
+
* @throws {@link StorageError} 键名、值或总大小非法时抛出 `INVALID_OPTION`。
|
|
43
|
+
*/
|
|
44
|
+
export declare function normalizeMetadata(metadata: ObjectMetadata | undefined): Record<string, string>;
|
|
45
|
+
/**
|
|
46
|
+
* 校验可选的 HTTP 头部取值,拒绝控制字符以防止头部注入。
|
|
47
|
+
*
|
|
48
|
+
* @param name 选项名称,用于错误信息。
|
|
49
|
+
* @param value 待校验的取值。
|
|
50
|
+
* @returns 校验通过的取值。
|
|
51
|
+
* @throws {@link StorageError} 包含控制字符时抛出 `INVALID_OPTION`。
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertHeaderValue(name: string, value: string | undefined): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* 校验 MIME 类型格式。
|
|
56
|
+
*
|
|
57
|
+
* @param value 待校验的 MIME 类型。
|
|
58
|
+
* @returns 校验通过的 MIME 类型。
|
|
59
|
+
* @throws {@link StorageError} 格式非法时抛出 `INVALID_OPTION`。
|
|
60
|
+
*/
|
|
61
|
+
export declare function assertContentType(value: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* 校验整数配置项位于闭区间内。
|
|
64
|
+
*
|
|
65
|
+
* @param name 选项名称,用于错误信息。
|
|
66
|
+
* @param value 待校验的值。
|
|
67
|
+
* @param min 允许的最小值。
|
|
68
|
+
* @param max 允许的最大值,默认为 `Number.MAX_SAFE_INTEGER`。
|
|
69
|
+
* @returns 校验通过的值。
|
|
70
|
+
* @throws {@link StorageError} 非整数或越界时抛出 `INVALID_OPTION`。
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertInteger(name: string, value: number, min: number, max?: number): number;
|
|
73
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/core/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAe7C;;GAEG;AACH,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC;;GAEG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAEtC;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU7C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAIrE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI/D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAc9F;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAK7F;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,SAA0B,GAAG,MAAM,CAK7G"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elysia 集成入口。
|
|
3
|
+
*
|
|
4
|
+
* 通过 `@lhy-meta-server/meta-oss/elysia` 引入,只负责把 `ObjectStorage` 注入请求上下文
|
|
5
|
+
* 并管理生命周期,不注册任何 HTTP 路由。
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { Elysia } from 'elysia';
|
|
10
|
+
import type { ObjectStorage } from '../core/storage';
|
|
11
|
+
/**
|
|
12
|
+
* Elysia 对象存储插件配置。
|
|
13
|
+
*
|
|
14
|
+
* @typeParam Name 注入 Elysia Context 的属性名称。
|
|
15
|
+
*/
|
|
16
|
+
export interface ElysiaOssOptions<Name extends string = 'oss'> {
|
|
17
|
+
/**
|
|
18
|
+
* 要注入的对象存储服务实例。
|
|
19
|
+
*/
|
|
20
|
+
storage: ObjectStorage;
|
|
21
|
+
/**
|
|
22
|
+
* 注入 Context 的属性名称,默认为 `oss`。
|
|
23
|
+
*/
|
|
24
|
+
propertyName?: Name;
|
|
25
|
+
/**
|
|
26
|
+
* Elysia 插件实例名称,用于去重;同一应用注册多个存储时需要不同名称。
|
|
27
|
+
*/
|
|
28
|
+
name?: string;
|
|
29
|
+
/**
|
|
30
|
+
* 应用停止时是否关闭存储服务,默认为 `true`。
|
|
31
|
+
*/
|
|
32
|
+
closeOnStop?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 创建 Elysia 对象存储插件。
|
|
36
|
+
*
|
|
37
|
+
* 插件通过 `decorate` 将同一个 `ObjectStorage` 实例注入所有请求上下文,
|
|
38
|
+
* 并可在应用停止时释放适配器资源。
|
|
39
|
+
*
|
|
40
|
+
* @typeParam Name 注入 Elysia Context 的属性名称。
|
|
41
|
+
* @param options 存储服务、属性名与生命周期配置。
|
|
42
|
+
* @returns 可传给 `app.use()` 的 Elysia 插件。
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const app = new Elysia()
|
|
47
|
+
* .use(oss({ storage }))
|
|
48
|
+
* .post('/avatar', ({ oss, body }) => oss.put('avatars/1.png', body.file), {
|
|
49
|
+
* body: t.Object({ file: t.File() }),
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function oss<const Name extends string = 'oss'>(options: ElysiaOssOptions<Name>): Elysia<"", {
|
|
54
|
+
decorator: { [name in Name]: ObjectStorage; };
|
|
55
|
+
store: {};
|
|
56
|
+
derive: {};
|
|
57
|
+
resolve: {};
|
|
58
|
+
}, {
|
|
59
|
+
typebox: {};
|
|
60
|
+
error: {};
|
|
61
|
+
}, {
|
|
62
|
+
schema: {};
|
|
63
|
+
standaloneSchema: {};
|
|
64
|
+
macro: {};
|
|
65
|
+
macroFn: {};
|
|
66
|
+
parser: {};
|
|
67
|
+
response: {};
|
|
68
|
+
}, {}, {
|
|
69
|
+
derive: {};
|
|
70
|
+
resolve: {};
|
|
71
|
+
schema: {};
|
|
72
|
+
standaloneSchema: {};
|
|
73
|
+
response: {};
|
|
74
|
+
}, {
|
|
75
|
+
derive: {};
|
|
76
|
+
resolve: {};
|
|
77
|
+
schema: {};
|
|
78
|
+
standaloneSchema: {};
|
|
79
|
+
response: {};
|
|
80
|
+
}>;
|
|
81
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/elysia/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,KAAK;IAC3D;;OAEG;IACH,OAAO,EAAE,aAAa,CAAA;IACtB;;OAEG;IACH,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,GAAG,CAAC,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG,KAAK,EAAE,OAAO,EAAE,gBAAgB,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;GAMrF"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{Elysia as D}from"elysia";function H(h){let{storage:k,closeOnStop:A=!0}=h,q=h.propertyName??"oss",B=h.name??`@lhy-meta-server/meta-oss:${q}`,z=new D({name:B}).decorate(q,k);return A?z.onStop(()=>k.close()):z}export{H as oss};
|
|
2
|
+
|
|
3
|
+
//# debugId=81FF7A25C049DE2E64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\elysia\\index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Elysia 集成入口。\n *\n * 通过 `@lhy-meta-server/meta-oss/elysia` 引入,只负责把 `ObjectStorage` 注入请求上下文\n * 并管理生命周期,不注册任何 HTTP 路由。\n *\n * @packageDocumentation\n */\nimport { Elysia } from 'elysia'\nimport type { ObjectStorage } from '../core/storage'\n\n/**\n * Elysia 对象存储插件配置。\n *\n * @typeParam Name 注入 Elysia Context 的属性名称。\n */\nexport interface ElysiaOssOptions<Name extends string = 'oss'> {\n /**\n * 要注入的对象存储服务实例。\n */\n storage: ObjectStorage\n /**\n * 注入 Context 的属性名称,默认为 `oss`。\n */\n propertyName?: Name\n /**\n * Elysia 插件实例名称,用于去重;同一应用注册多个存储时需要不同名称。\n */\n name?: string\n /**\n * 应用停止时是否关闭存储服务,默认为 `true`。\n */\n closeOnStop?: boolean\n}\n\n/**\n * 创建 Elysia 对象存储插件。\n *\n * 插件通过 `decorate` 将同一个 `ObjectStorage` 实例注入所有请求上下文,\n * 并可在应用停止时释放适配器资源。\n *\n * @typeParam Name 注入 Elysia Context 的属性名称。\n * @param options 存储服务、属性名与生命周期配置。\n * @returns 可传给 `app.use()` 的 Elysia 插件。\n *\n * @example\n * ```ts\n * const app = new Elysia()\n * .use(oss({ storage }))\n * .post('/avatar', ({ oss, body }) => oss.put('avatars/1.png', body.file), {\n * body: t.Object({ file: t.File() }),\n * })\n * ```\n */\nexport function oss<const Name extends string = 'oss'>(options: ElysiaOssOptions<Name>) {\n const { storage, closeOnStop = true } = options\n const propertyName = options.propertyName ?? ('oss' as Name)\n const name = options.name ?? `@lhy-meta-server/meta-oss:${propertyName}`\n const plugin = new Elysia({ name }).decorate(propertyName, storage)\n return closeOnStop ? plugin.onStop(() => storage.close()) : plugin\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "AAQA,iBAAS,eA8CF,SAAS,CAAsC,CAAC,EAAiC,CACtF,IAAQ,UAAS,cAAc,IAAS,EAClC,EAAe,EAAQ,cAAiB,MACxC,EAAO,EAAQ,MAAQ,6BAA6B,IACpD,EAAS,IAAI,EAAO,CAAE,MAAK,CAAC,EAAE,SAAS,EAAc,CAAO,EAClE,OAAO,EAAc,EAAO,OAAO,IAAM,EAAQ,MAAM,CAAC,EAAI",
|
|
8
|
+
"debugId": "81FF7A25C049DE2E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { DriverCopyOptions, DriverGetOptions, DriverListOptions, DriverPutOptions, DriverSignOptions, MultipartDriver, StorageDriver } from '../core/driver';
|
|
2
|
+
import { type StoragePayload } from '../core/payload';
|
|
3
|
+
import type { ListObjectsResult, ObjectInfo, ObjectStream, VerifiedUrl, VerifyUrlRequest } from '../core/types';
|
|
4
|
+
import { type UrlSignerOptions } from './signer';
|
|
5
|
+
/**
|
|
6
|
+
* 本地文件系统适配器配置。
|
|
7
|
+
*/
|
|
8
|
+
export interface FileSystemDriverOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 存储根目录;不存在时自动创建。
|
|
11
|
+
*/
|
|
12
|
+
root: string;
|
|
13
|
+
/**
|
|
14
|
+
* 签名 URL 配置;省略时 `signUrl` 抛出 `UNSUPPORTED`。
|
|
15
|
+
*/
|
|
16
|
+
signing?: UrlSignerOptions;
|
|
17
|
+
/**
|
|
18
|
+
* 新建目录的权限,默认为 `0o750`。
|
|
19
|
+
*/
|
|
20
|
+
directoryMode?: number;
|
|
21
|
+
/**
|
|
22
|
+
* 新建文件的权限,默认为 `0o640`。
|
|
23
|
+
*/
|
|
24
|
+
fileMode?: number;
|
|
25
|
+
/**
|
|
26
|
+
* 未完成分片会话的保留毫秒数,超时会在新建会话时清理,默认为 24 小时。
|
|
27
|
+
*/
|
|
28
|
+
multipartTtl?: number;
|
|
29
|
+
/**
|
|
30
|
+
* 崩溃残留临时文件的保留毫秒数,超时会在初始化时清理,默认为 1 小时。
|
|
31
|
+
*
|
|
32
|
+
* 该值应大于单次写入的最长耗时,避免误删仍在写入的临时文件。
|
|
33
|
+
*/
|
|
34
|
+
temporaryTtl?: number;
|
|
35
|
+
/**
|
|
36
|
+
* 是否对文件与目录执行 `fsync` 以保证断电持久性,默认为 `true`。
|
|
37
|
+
* 关闭可提升吞吐,但断电时最近的写入可能丢失。
|
|
38
|
+
*/
|
|
39
|
+
durable?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 生产级本地文件系统适配器。
|
|
43
|
+
*
|
|
44
|
+
* - 内容先写入内部临时目录并 `fsync`,再原子 `rename` / `link` 到目标位置,并同步父目录。
|
|
45
|
+
* - 元数据以内容文件的 inode 命名并在内容提交前落盘,内容与元数据始终成对可见;
|
|
46
|
+
* 并发写入同一键时,读取方看到的内容、ETag 与元数据总是来自同一次写入。
|
|
47
|
+
* - 读取先打开文件句柄,再基于该句柄读取状态与元数据,句柄打开后对象被删除或覆盖也不影响本次读取。
|
|
48
|
+
* - 拒绝路径穿越、符号链接逃逸、Windows 保留名与内部目录访问。
|
|
49
|
+
* - 初始化时回收崩溃残留的临时文件,新建分片会话时回收过期会话。
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const driver = new FileSystemDriver({
|
|
54
|
+
* root: '/data/oss',
|
|
55
|
+
* signing: { secret: config.ossSecret, baseUrl: 'https://api.example.com/files' },
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare class FileSystemDriver implements StorageDriver {
|
|
60
|
+
/**
|
|
61
|
+
* 适配器名称。
|
|
62
|
+
*/
|
|
63
|
+
readonly name = "filesystem";
|
|
64
|
+
/**
|
|
65
|
+
* 分片上传能力。
|
|
66
|
+
*/
|
|
67
|
+
readonly multipart: MultipartDriver;
|
|
68
|
+
private readonly root;
|
|
69
|
+
private readonly signer?;
|
|
70
|
+
private readonly directoryMode;
|
|
71
|
+
private readonly fileMode;
|
|
72
|
+
private readonly multipartTtl;
|
|
73
|
+
private readonly temporaryTtl;
|
|
74
|
+
private readonly durable;
|
|
75
|
+
private ready?;
|
|
76
|
+
/**
|
|
77
|
+
* 创建本地文件系统适配器。
|
|
78
|
+
*
|
|
79
|
+
* @param options 根目录、签名、权限与持久性配置。
|
|
80
|
+
* @throws {@link StorageError} 配置非法时抛出 `INVALID_OPTION`。
|
|
81
|
+
*/
|
|
82
|
+
constructor(options: FileSystemDriverOptions);
|
|
83
|
+
/**
|
|
84
|
+
* 原子写入对象:内容、ETag 与元数据作为一个整体提交。
|
|
85
|
+
*
|
|
86
|
+
* 提交顺序为:内容写入临时文件并同步 → 以临时文件 inode 写入元数据并同步 →
|
|
87
|
+
* 原子 `rename`(覆盖)或 `link`(禁止覆盖)到目标位置 → 同步父目录 → 回收被替换内容的元数据。
|
|
88
|
+
* 任一步骤失败或进程崩溃时,目标位置要么保持旧对象,要么已是完整的新对象。
|
|
89
|
+
*
|
|
90
|
+
* @param key 完整对象键。
|
|
91
|
+
* @param payload 上传内容。
|
|
92
|
+
* @param options 已归一化的写入选项。
|
|
93
|
+
* @returns 对象描述信息。
|
|
94
|
+
*/
|
|
95
|
+
put(key: string, payload: StoragePayload, options: DriverPutOptions): Promise<ObjectInfo>;
|
|
96
|
+
/**
|
|
97
|
+
* 以流方式读取对象。
|
|
98
|
+
*
|
|
99
|
+
* 先打开文件句柄,再从同一句柄读取状态与元数据,因此句柄打开后对象被删除或覆盖,
|
|
100
|
+
* 本次读取仍返回打开时刻的完整内容;返回的流结束或被取消时自动关闭句柄。
|
|
101
|
+
*
|
|
102
|
+
* @param key 完整对象键。
|
|
103
|
+
* @param options 读取范围与取消信号。
|
|
104
|
+
* @returns 对象描述信息与可读流。
|
|
105
|
+
*/
|
|
106
|
+
get(key: string, options: DriverGetOptions): Promise<ObjectStream>;
|
|
107
|
+
/**
|
|
108
|
+
* 读取对象描述信息。
|
|
109
|
+
*
|
|
110
|
+
* @param key 完整对象键。
|
|
111
|
+
* @returns 对象描述信息。
|
|
112
|
+
*/
|
|
113
|
+
head(key: string): Promise<ObjectInfo>;
|
|
114
|
+
/**
|
|
115
|
+
* 删除对象与元数据,并清理随之变空的父目录。
|
|
116
|
+
*
|
|
117
|
+
* @param key 完整对象键。
|
|
118
|
+
*/
|
|
119
|
+
delete(key: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* 按键名字典序分页列举对象,支持分隔符折叠目录。
|
|
122
|
+
*
|
|
123
|
+
* 实现按目录逐层有序遍历,在凑满一页后立即停止,不会一次性加载整个前缀;
|
|
124
|
+
* 设置分隔符时不会进入被折叠的子目录。每页只为当前页对象读取描述信息,
|
|
125
|
+
* 并发数受限,适合大多数单机场景。海量对象建议使用云端 OSS。
|
|
126
|
+
*
|
|
127
|
+
* @param options 前缀、分隔符、游标与页大小。
|
|
128
|
+
* @returns 当前页结果。
|
|
129
|
+
*/
|
|
130
|
+
list(options: DriverListOptions): Promise<ListObjectsResult>;
|
|
131
|
+
/**
|
|
132
|
+
* 在本地复制对象及其元数据;读取基于源文件句柄,复制过程中源对象被修改也不影响结果一致性。
|
|
133
|
+
*
|
|
134
|
+
* @param source 完整源对象键。
|
|
135
|
+
* @param destination 完整目标对象键。
|
|
136
|
+
* @param options 覆盖策略与取消信号。
|
|
137
|
+
* @returns 目标对象描述信息。
|
|
138
|
+
*/
|
|
139
|
+
copy(source: string, destination: string, options: DriverCopyOptions): Promise<ObjectInfo>;
|
|
140
|
+
/**
|
|
141
|
+
* 生成本地 HMAC 签名 URL。
|
|
142
|
+
*
|
|
143
|
+
* @param key 完整对象键。
|
|
144
|
+
* @param options 方法、有效期与头部约束。
|
|
145
|
+
* @returns 签名 URL。
|
|
146
|
+
*/
|
|
147
|
+
signUrl(key: string, options: DriverSignOptions): Promise<string>;
|
|
148
|
+
/**
|
|
149
|
+
* 校验本地签名 URL。
|
|
150
|
+
*
|
|
151
|
+
* @param request 请求信息。
|
|
152
|
+
* @returns 授权信息。
|
|
153
|
+
*/
|
|
154
|
+
verifyUrl(request: VerifyUrlRequest): VerifiedUrl;
|
|
155
|
+
/**
|
|
156
|
+
* 文件系统不持有长连接,关闭为空操作;读取流持有的句柄随流结束自动释放。
|
|
157
|
+
*/
|
|
158
|
+
close(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* 回收崩溃残留的临时文件与孤立元数据。
|
|
161
|
+
*
|
|
162
|
+
* 初始化时会自动清理临时文件;孤立元数据(内容已不存在或已被替换)的回收需要
|
|
163
|
+
* 遍历元数据目录,建议在低峰期由定时任务调用。
|
|
164
|
+
*
|
|
165
|
+
* @returns 删除的临时文件与元数据文件数量。
|
|
166
|
+
*/
|
|
167
|
+
cleanup(): Promise<{
|
|
168
|
+
temporary: number;
|
|
169
|
+
metadata: number;
|
|
170
|
+
}>;
|
|
171
|
+
private createMultipart;
|
|
172
|
+
private uploadPart;
|
|
173
|
+
private completeMultipart;
|
|
174
|
+
private abortMultipart;
|
|
175
|
+
private ensureRoot;
|
|
176
|
+
private objectPath;
|
|
177
|
+
/**
|
|
178
|
+
* 打开对象内容文件,拒绝符号链接、目录与特殊文件。
|
|
179
|
+
*
|
|
180
|
+
* 先以 `lstat` 排除符号链接,再打开并以 `fstat` 确认打开的正是该普通文件,
|
|
181
|
+
* 消除检查与打开之间被替换为符号链接的竞态窗口。
|
|
182
|
+
*/
|
|
183
|
+
private openObject;
|
|
184
|
+
private describe;
|
|
185
|
+
private headIfExists;
|
|
186
|
+
private writeTemporary;
|
|
187
|
+
/**
|
|
188
|
+
* 把临时文件原子提交到目标位置。
|
|
189
|
+
*
|
|
190
|
+
* @returns 被替换对象的 inode;目标此前不存在时返回 `undefined`。
|
|
191
|
+
*/
|
|
192
|
+
private commit;
|
|
193
|
+
private syncParent;
|
|
194
|
+
private metadataDirectory;
|
|
195
|
+
private writeMetadata;
|
|
196
|
+
private readMetadata;
|
|
197
|
+
/**
|
|
198
|
+
* 删除指定 inode 的元数据文件。
|
|
199
|
+
*
|
|
200
|
+
* 不在此处删除空的分组目录:并发写入可能正在向同一目录提交元数据,空目录由
|
|
201
|
+
* {@link FileSystemDriver.cleanup} 统一回收。
|
|
202
|
+
*/
|
|
203
|
+
private removeMetadata;
|
|
204
|
+
/**
|
|
205
|
+
* 判断元数据文件是否已失去对应的内容文件(对象被删除或已被新写入替换)。
|
|
206
|
+
*/
|
|
207
|
+
private isOrphan;
|
|
208
|
+
private writeFileAtomic;
|
|
209
|
+
private pruneDirectories;
|
|
210
|
+
/**
|
|
211
|
+
* 按全局字典序惰性产生列举条目。
|
|
212
|
+
*
|
|
213
|
+
* 目录以 `名称/` 参与排序,使逐层深度优先遍历的输出顺序与“扁平键排序”完全一致;
|
|
214
|
+
* 不与前缀或游标相交的目录会被整体跳过,设置分隔符时被折叠的目录不会被展开。
|
|
215
|
+
*/
|
|
216
|
+
private entries;
|
|
217
|
+
private uploadDirectory;
|
|
218
|
+
private readManifest;
|
|
219
|
+
private digestFile;
|
|
220
|
+
private requireSigner;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 计算键在前缀之后按分隔符折叠出的公共前缀。
|
|
224
|
+
*
|
|
225
|
+
* @param key 对象键或以 `/` 结尾的目录键。
|
|
226
|
+
* @param prefix 列举前缀。
|
|
227
|
+
* @param delimiter 分隔符;未设置时不折叠。
|
|
228
|
+
* @returns 公共前缀;未设置分隔符、键不以前缀开头(前缀落在目录内部)或前缀之后
|
|
229
|
+
* 不含分隔符时返回 `undefined`。
|
|
230
|
+
*/
|
|
231
|
+
export declare function foldedPrefix(key: string, prefix: string, delimiter: string | undefined): string | undefined;
|
|
232
|
+
/**
|
|
233
|
+
* 原子替换目标文件;目标被其他句柄短暂占用(Windows)时以退避重试,最长约 1 秒。
|
|
234
|
+
*
|
|
235
|
+
* POSIX 平台上 `rename` 从不因读取方占用而失败,因此只在首次失败时进入重试。
|
|
236
|
+
*
|
|
237
|
+
* @param from 源路径。
|
|
238
|
+
* @param to 目标路径。
|
|
239
|
+
* @param attempts 最大尝试次数。
|
|
240
|
+
*/
|
|
241
|
+
export declare function renameWithRetry(from: string, to: string, attempts?: number): Promise<void>;
|
|
242
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/filesystem/driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读取 Node.js 系统错误码。
|
|
3
|
+
*
|
|
4
|
+
* @param error 任意错误值。
|
|
5
|
+
* @returns 错误码;不存在时返回 `undefined`。
|
|
6
|
+
*/
|
|
7
|
+
export declare function errorCode(error: unknown): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* 对目录执行 `fsync`,确保其中的 `rename`、`link`、`unlink` 在断电后依然可见。
|
|
10
|
+
*
|
|
11
|
+
* Windows 与部分文件系统不允许打开或同步目录(返回 `EPERM`、`EISDIR`、`EINVAL`、
|
|
12
|
+
* `EACCES` 等),这些平台上目录项更新由文件系统自身保证,此时静默跳过。
|
|
13
|
+
*
|
|
14
|
+
* @param directory 目录绝对路径。
|
|
15
|
+
*/
|
|
16
|
+
export declare function syncDirectory(directory: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* 删除目录中修改时间早于阈值的条目,用于回收崩溃残留的临时文件与过期会话。
|
|
19
|
+
*
|
|
20
|
+
* 条目可能被并发操作删除,单个条目失败不会中断清理。
|
|
21
|
+
*
|
|
22
|
+
* @param directory 待清理目录。
|
|
23
|
+
* @param maxAge 保留的最长毫秒数。
|
|
24
|
+
* @param now 当前时间。
|
|
25
|
+
* @returns 实际删除的条目数。
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeStale(directory: string, maxAge: number, now?: number): Promise<number>;
|
|
28
|
+
//# sourceMappingURL=durability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durability.d.ts","sourceRoot":"","sources":["../../src/filesystem/durability.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUpE;AAID;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAatG"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地文件系统存储适配器入口。
|
|
3
|
+
*
|
|
4
|
+
* 通过 `@lhy-meta-server/meta-oss/filesystem` 引入,提供原子写入、路径安全、
|
|
5
|
+
* 分片上传与 HMAC 签名 URL 能力。
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { FileSystemDriver, type FileSystemDriverOptions } from './driver';
|
|
10
|
+
export { UrlSigner, type UrlSignerOptions } from './signer';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filesystem/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAA;AACzE,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{h as O,j as z,m}from"../index-7dpmhwry.js";import{createHash as s,randomUUID as S}from"node:crypto";import{createReadStream as o}from"node:fs";import{link as KB,lstat as b,mkdir as F,open as v,readdir as x,readFile as f,realpath as i,rename as c,rm as U,rmdir as n,stat as AB,unlink as UB}from"node:fs/promises";import{dirname as P,join as _,resolve as zB}from"node:path";import{Readable as qB}from"node:stream";import{open as GB,readdir as JB,rm as MB,stat as QB}from"node:fs/promises";import{join as VB}from"node:path";function A(B){return B?.code}async function u(B){let G;try{G=await GB(B,"r"),await G.sync()}catch(J){if(!XB.has(A(J)??""))throw J}finally{await G?.close()}}var XB=new Set(["EPERM","EISDIR","EINVAL","EACCES","EBADF","ENOTSUP"]);async function N(B,G,J=Date.now()){let M=0;for(let Q of await JB(B)){let V=VB(B,Q);try{if(J-(await QB(V)).mtimeMs<=G)continue;await MB(V,{recursive:!0,force:!0}),M+=1}catch{}}return M}import{createHash as ZB}from"node:crypto";function k(B){return ZB("sha256").update(B).digest("hex")}function j(B){return`${B}.json`}function l(B,G,J){let M;try{M=JSON.parse(B)}catch{return}if(typeof M?.contentType!=="string"||typeof M.etag!=="string"||M.key!==G||M.size!==J)return;return{key:G,size:J,contentType:M.contentType,etag:M.etag,metadata:M.metadata&&typeof M.metadata==="object"?M.metadata:{},...typeof M.cacheControl==="string"?{cacheControl:M.cacheControl}:{},...typeof M.contentDisposition==="string"?{contentDisposition:M.contentDisposition}:{}}}import{isAbsolute as $B,join as WB,relative as y,resolve as HB,sep as p}from"node:path";var w=".meta-oss",OB=/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i,YB=/[<>:"|?*]/;function C(B){let G=B.split("/");if(G[0]===w)throw D(B,"uses the reserved internal directory");for(let J of G){if(YB.test(J))throw D(B,"contains characters not allowed on Windows");if(OB.test(J))throw D(B,"uses a reserved device name");if(J.endsWith(".")||J.endsWith(" "))throw D(B,"segment ends with dot or space")}}function E(B,G){let J=HB(B,...G.split("/"));if(!I(B,J))throw D(G,"escapes the storage root");return J}function I(B,G){let J=y(B,G);return J!==""&&!J.startsWith(`..${p}`)&&J!==".."&&!$B(J)}function d(B,G){return y(B,G).split(p).join("/")}function L(B,...G){return WB(B,w,...G)}function D(B,G){return new O("INVALID_KEY",`Invalid object key: ${G}`,{key:B})}import{createHmac as LB,timingSafeEqual as _B}from"node:crypto";class T{secret;base;now;constructor(B){if(typeof B?.secret!=="string"||B.secret.length<32)throw new O("INVALID_OPTION","signing secret must be at least 32 characters");let G;try{G=new URL(B.baseUrl)}catch(J){throw new O("INVALID_OPTION","baseUrl must be an absolute URL",{cause:J})}if(!G.pathname.endsWith("/"))G.pathname+="/";this.secret=B.secret,this.base=G,this.now=B.now??Date.now}sign(B,G,J,M,Q){let V=Math.floor(this.now()/1000)+J,Z=new URL(B.split("/").map(encodeURIComponent).join("/"),this.base);if(Z.searchParams.set("method",G),Z.searchParams.set("expires",String(V)),M)Z.searchParams.set("contentType",M);if(Q)Z.searchParams.set("disposition",Q);return Z.searchParams.set("signature",this.digest(G,B,V,M,Q)),Z.href}verify(B){let G=typeof B.url==="string"?new URL(B.url):B.url;if(G.origin!==this.base.origin||!G.pathname.startsWith(this.base.pathname))throw q("URL does not match the signing base");let J=G.searchParams,M=J.get("method"),Q=J.get("signature"),V=J.get("expires")??"";if(M!=="GET"&&M!=="PUT"||!Q||!/^\d+$/.test(V))throw q("Malformed signed URL");let Z;try{Z=G.pathname.slice(this.base.pathname.length).split("/").map(decodeURIComponent).join("/")}catch(Y){throw q("Malformed object key",Y)}let H=Number(V),W=J.get("contentType")??void 0,$=J.get("disposition")??void 0,X=Buffer.from(this.digest(M,Z,H,W,$)),K=Buffer.from(Q);if(K.length!==X.length||!_B(K,X))throw q("Invalid signature");if(H*1000<this.now())throw q("Signed URL has expired");if((B.method??"GET").toUpperCase()!==M)throw q("HTTP method does not match");if(M==="PUT"&&W&&B.contentType!==W)throw q("Content-Type does not match");return{key:Z,method:M,expires:H,...W?{contentType:W}:{},...$?{responseContentDisposition:$}:{}}}digest(B,G,J,M,Q){return LB("sha256",this.secret).update([B,G,J,M??"",Q??""].join(`
|
|
2
|
+
`)).digest("base64url")}}function q(B,G){return new O("ACCESS_DENIED",B,{cause:G})}var a=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,R=new Set(["ENOENT","ENOTDIR"]);class e{name="filesystem";multipart;root;signer;directoryMode;fileMode;multipartTtl;temporaryTtl;durable;ready;constructor(B){if(typeof B?.root!=="string"||B.root.length===0)throw new O("INVALID_OPTION","root is required");this.root=zB(B.root),this.signer=B.signing?new T(B.signing):void 0,this.directoryMode=B.directoryMode??488,this.fileMode=B.fileMode??416,this.multipartTtl=B.multipartTtl??86400000,this.temporaryTtl=B.temporaryTtl??3600000,this.durable=B.durable??!0,this.multipart={createMultipart:(G,J)=>this.createMultipart(G,J),uploadPart:(G,J,M,Q)=>this.uploadPart(G,J,M,Q),completeMultipart:(G,J,M)=>this.completeMultipart(G,J,M),abortMultipart:(G)=>this.abortMultipart(G)}}async put(B,G,J){let M=await this.objectPath(B,!0),Q=await this.writeTemporary(G,J.signal),V;try{let Z=(await AB(Q.path,{bigint:!0})).ino;if(V=await this.writeMetadata(B,Z,{key:B,size:Q.size,contentType:J.contentType,metadata:J.metadata,etag:Q.etag,...J.cacheControl?{cacheControl:J.cacheControl}:{},...J.contentDisposition?{contentDisposition:J.contentDisposition}:{}}),J.signal.aborted)throw J.signal.reason;let H=await this.commit(Q.path,M,B,J.overwrite);if(V=void 0,H!==void 0&&H!==Z)await this.removeMetadata(B,H)}finally{if(await U(Q.path,{force:!0}),V)await U(V,{force:!0})}return this.head(B)}async get(B,G){let J=await this.openObject(B),M,Q;try{M=await this.describe(B,J);let W=G.range;if(W&&W.start>=M.size&&!(M.size===0&&W.start===0))throw new O("INVALID_OPTION","Requested range is not satisfiable",{key:B});Q=W&&M.size>0?Math.min(W.end??M.size-1,M.size-1):void 0}catch(W){throw await J.close(),W}let V=Q===void 0?void 0:G.range?.start,Z=J.createReadStream({start:V,end:Q,autoClose:!0,signal:G.signal}),H=qB.toWeb(Z);return{info:M,body:H,...Q===void 0?{}:{range:{start:V,end:Q}}}}async head(B){let G=await this.openObject(B);try{return await this.describe(B,G)}finally{await G.close()}}async delete(B){let G=await this.ensureRoot(),J=await this.objectPath(B,!1),M;try{let Q=await b(J,{bigint:!0});if(!Q.isFile())return;M=Q.ino,await UB(J)}catch(Q){if(R.has(A(Q)??""))return;throw z(Q,"delete",B)}await this.syncParent(J),await this.removeMetadata(B,M),await this.pruneDirectories(G,P(J))}async list(B){let G=await this.ensureRoot(),J=B.prefix.lastIndexOf("/"),M=J>=0?B.prefix.slice(0,J):"",Q=M?E(G,M):G,V=[],Z=!1;for await(let X of this.entries(G,Q,B)){if(X.prefix&&V.at(-1)?.name===X.name)continue;if(V.length===B.limit){Z=!0;break}V.push(X)}let H=V.filter((X)=>!X.prefix).map((X)=>X.name),W=[];for(let X=0;X<H.length;X+=32){let K=await Promise.all(H.slice(X,X+32).map((Y)=>this.headIfExists(Y)));for(let Y of K)if(Y)W.push(Y)}let $=V.at(-1);return{objects:W,prefixes:V.filter((X)=>X.prefix).map((X)=>X.name),...Z&&$?{cursor:$.name}:{}}}async copy(B,G,J){let{info:M,body:Q}=await this.get(B,{signal:J.signal});return this.put(G,m(Q),{signal:J.signal,overwrite:J.overwrite,contentType:M.contentType,metadata:M.metadata,cacheControl:M.cacheControl,contentDisposition:M.contentDisposition})}async signUrl(B,G){return await this.objectPath(B,!1),this.requireSigner().sign(B,G.method,G.expiresIn,G.contentType,G.responseContentDisposition)}verifyUrl(B){let G=this.requireSigner().verify(B);return C(G.key),G}async close(){}async cleanup(){let B=await this.ensureRoot(),G=await N(L(B,"tmp"),this.temporaryTtl),J=0,M=L(B,"meta");for(let Q of await x(M,{withFileTypes:!0})){if(!Q.isDirectory())continue;let V=_(M,Q.name);for(let Z of await x(V,{withFileTypes:!0})){if(!Z.isFile())continue;if(await this.isOrphan(_(V,Z.name),Z.name))await U(_(V,Z.name),{force:!0}),J+=1}try{await n(V)}catch{}}return{temporary:G,metadata:J}}async createMultipart(B,G){let J=await this.ensureRoot();await this.objectPath(B,!0),await N(L(J,"multipart"),this.multipartTtl);let M=S(),Q=L(J,"multipart",M);await F(Q,{recursive:!0,mode:this.directoryMode});let V={key:B,createdAt:Date.now(),options:{contentType:G.contentType,metadata:G.metadata,overwrite:G.overwrite,...G.cacheControl?{cacheControl:G.cacheControl}:{},...G.contentDisposition?{contentDisposition:G.contentDisposition}:{}}};return await this.writeFileAtomic(_(Q,"manifest.json"),JSON.stringify(V)),{key:B,uploadId:M}}async uploadPart(B,G,J,M){let Q=await this.uploadDirectory(B),V=await this.writeTemporary(J,M.signal);try{if(M.signal.aborted)throw M.signal.reason;await c(V.path,_(Q,`${G}.part`))}finally{await U(V.path,{force:!0})}return{partNumber:G,etag:V.etag}}async completeMultipart(B,G,J){let M=await this.uploadDirectory(B),Q=await this.readManifest(M,B),V=[];for(let $ of G){let X=_(M,`${$.partNumber}.part`),K;try{K=await this.digestFile(X)}catch(Y){throw new O("INVALID_OPTION",`Part ${$.partNumber} has not been uploaded`,{key:B.key,cause:Y})}if(K!==$.etag)throw new O("INVALID_OPTION",`Part ${$.partNumber} etag mismatch`,{key:B.key});V.push(X)}let{overwrite:Z,...H}=Q.options,W=await this.put(B.key,m(jB(V)),{...H,overwrite:Z,signal:J.signal});return await U(M,{recursive:!0,force:!0}),W}async abortMultipart(B){if(!a.test(B.uploadId))return;let G=await this.ensureRoot();await U(L(G,"multipart",B.uploadId),{recursive:!0,force:!0})}ensureRoot(){return this.ready??=(async()=>{await F(this.root,{recursive:!0,mode:this.directoryMode});let B=await i(this.root);for(let G of["tmp","meta","multipart"])await F(L(B,G),{recursive:!0,mode:this.directoryMode});return await N(L(B,"tmp"),this.temporaryTtl),B})().catch((B)=>{throw this.ready=void 0,z(B,"init")}),this.ready}async objectPath(B,G){C(B);let J=await this.ensureRoot(),M=E(J,B),Q=P(M);if(G)await F(Q,{recursive:!0,mode:this.directoryMode});let V;try{V=await i(Q)}catch(Z){if(R.has(A(Z)??""))return M;throw z(Z,"resolve",B)}if(V!==J&&!I(J,V))throw new O("INVALID_KEY","Invalid object key: escapes the storage root",{key:B});return _(V,M.slice(Q.length+1))}async openObject(B){let G=await this.objectPath(B,!1),J;try{let M=await b(G,{bigint:!0});if(!M.isFile())throw g(B);J=await v(G,"r");let Q=await J.stat({bigint:!0});if(!Q.isFile()||Q.ino!==M.ino||Q.dev!==M.dev)throw g(B);return J}catch(M){if(await J?.close(),R.has(A(M)??""))throw g(B,M);throw z(M,"open",B)}}async describe(B,G){let J=await G.stat({bigint:!0}),M=Number(J.size),Q=await this.readMetadata(B,J.ino,M);return{key:B,size:M,lastModified:J.mtime,contentType:Q?.contentType??"application/octet-stream",metadata:Q?.metadata??{},...Q?.etag?{etag:Q.etag}:{},...Q?.cacheControl?{cacheControl:Q.cacheControl}:{},...Q?.contentDisposition?{contentDisposition:Q.contentDisposition}:{}}}async headIfExists(B){try{return await this.head(B)}catch(G){if(G.code==="NOT_FOUND")return;throw G}}async writeTemporary(B,G){let J=await this.ensureRoot(),M=L(J,"tmp",`${S()}.tmp`),Q=await v(M,"wx",this.fileMode),V=s("md5"),Z=0,H=B.stream().getReader(),W=()=>{H.cancel(G.reason).then(r,r)};G.addEventListener("abort",W,{once:!0});try{if(G.aborted)throw G.reason;for(;;){let{done:$,value:X}=await H.read();if(G.aborted)throw G.reason;if($)break;V.update(X),Z+=X.byteLength,await Q.write(X)}if(this.durable)await Q.sync()}catch($){throw await Q.close(),await U(M,{force:!0}),$}finally{G.removeEventListener("abort",W),H.releaseLock()}return await Q.close(),{path:M,etag:V.digest("hex"),size:Z}}async commit(B,G,J,M){let Q;try{let V=await b(G,{bigint:!0});if(!V.isFile())throw new O("ALREADY_EXISTS","A directory or special file exists at this key",{key:J});Q=V.ino}catch(V){if(!R.has(A(V)??""))throw z(V,"put",J)}try{if(M)await RB(B,G);else await KB(B,G)}catch(V){if(A(V)==="EEXIST")throw new O("ALREADY_EXISTS","Object already exists",{key:J});if(BB.has(A(V)??""))throw new O("BACKEND","Object is locked by another reader or writer",{key:J,cause:V,retryable:!0});throw z(V,"put",J)}return await this.syncParent(G),M?Q:void 0}async syncParent(B){if(this.durable)await u(P(B))}async metadataDirectory(B){let G=await this.ensureRoot();return L(G,"meta",k(B))}async writeMetadata(B,G,J){let M=await this.metadataDirectory(B);await F(M,{recursive:!0,mode:this.directoryMode});let Q=_(M,j(G));return await this.writeFileAtomic(Q,JSON.stringify(J)),Q}async readMetadata(B,G,J){let M=_(await this.metadataDirectory(B),j(G));try{return l(await f(M,"utf8"),B,J)}catch(Q){if(R.has(A(Q)??""))return;throw z(Q,"head",B)}}async removeMetadata(B,G){let J=await this.metadataDirectory(B);await U(_(J,j(G)),{force:!0})}async isOrphan(B,G){let J;try{let M=JSON.parse(await f(B,"utf8"));if(typeof M?.key!=="string")return!0;J=M.key}catch{return!0}try{let M=await this.objectPath(J,!1),Q=await b(M,{bigint:!0});return!Q.isFile()||j(Q.ino)!==G}catch{return!0}}async writeFileAtomic(B,G){let J=await this.ensureRoot(),M=L(J,"tmp",`${S()}.tmp`),Q=await v(M,"wx",this.fileMode);try{if(await Q.writeFile(G),this.durable)await Q.sync()}finally{await Q.close()}try{await c(M,B)}finally{await U(M,{force:!0})}await this.syncParent(B)}async pruneDirectories(B,G){let J=G;while(J!==B&&I(B,J)){try{await n(J)}catch{return}J=P(J)}}async*entries(B,G,J){let M;try{M=await x(G,{withFileTypes:!0})}catch($){if(R.has(A($)??""))return;throw z($,"list")}let Q=G===B?"":`${d(B,G)}/`,V=M.filter(($)=>!(G===B&&$.name===w)).filter(($)=>$.isFile()||$.isDirectory()).map(($)=>({entry:$,key:Q+$.name+($.isDirectory()?"/":"")})).sort(($,X)=>$.key<X.key?-1:$.key>X.key?1:0),{prefix:Z,delimiter:H,cursor:W}=J;for(let{entry:$,key:X}of V){if($.isDirectory()){if(!X.startsWith(Z)&&!Z.startsWith(X))continue;if(W!==void 0&&W>=X&&!W.startsWith(X))continue;let Y=t(X,Z,H);if(Y===void 0)yield*this.entries(B,_(G,$.name),J);else if(W===void 0||Y>W)yield{name:Y,prefix:!0};continue}if(!X.startsWith(Z)||W!==void 0&&X<=W)continue;let K=t(X,Z,H);if(K===void 0)yield{name:X,prefix:!1};else if(W===void 0||K>W)yield{name:K,prefix:!0}}}async uploadDirectory(B){if(!a.test(B.uploadId))throw h(B);let G=await this.ensureRoot(),J=L(G,"multipart",B.uploadId);return await this.readManifest(J,B),J}async readManifest(B,G){let J;try{J=JSON.parse(await f(_(B,"manifest.json"),"utf8"))}catch(M){throw h(G,M)}if(J.key!==G.key)throw h(G);return J}async digestFile(B){let G=s("md5");for await(let J of o(B))G.update(J);return G.digest("hex")}requireSigner(){if(!this.signer)throw new O("UNSUPPORTED",'Signed URLs require the "signing" option on FileSystemDriver');return this.signer}}function t(B,G,J){if(!J||!B.startsWith(G))return;let M=B.indexOf(J,G.length);return M<0?void 0:B.slice(0,M+J.length)}var BB=new Set(["EPERM","EACCES","EBUSY"]);async function RB(B,G,J=8){for(let M=1;;M+=1)try{await c(B,G);return}catch(Q){if(M>=J||!BB.has(A(Q)??""))throw Q;await new Promise((V)=>setTimeout(V,Math.min(250,10*2**M)))}}async function*jB(B){for(let G of B)for await(let J of o(G))yield J}function r(){}function g(B,G){return new O("NOT_FOUND","Object not found",{key:B,cause:G})}function h(B,G){return new O("NOT_FOUND","Multipart upload not found",{key:B.key,cause:G})}export{T as UrlSigner,e as FileSystemDriver};
|
|
3
|
+
|
|
4
|
+
//# debugId=FD5C6DD2996003AE64756E2164756E21
|