@yefengr/pi-reach 0.0.4 → 0.0.6

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.
Files changed (53) hide show
  1. package/dist/attachments/reservation.fixture.d.ts +1 -0
  2. package/dist/attachments/reservation.fixture.js +25 -0
  3. package/dist/attachments/reservation.fixture.js.map +1 -0
  4. package/dist/attachments/reservations.d.ts +21 -0
  5. package/dist/attachments/reservations.js +189 -0
  6. package/dist/attachments/reservations.js.map +1 -0
  7. package/dist/attachments/safe-files.d.ts +17 -0
  8. package/dist/attachments/safe-files.js +108 -0
  9. package/dist/attachments/safe-files.js.map +1 -0
  10. package/dist/attachments/storage-path.d.ts +4 -0
  11. package/dist/attachments/storage-path.js +47 -0
  12. package/dist/attachments/storage-path.js.map +1 -0
  13. package/dist/attachments/store.d.ts +55 -0
  14. package/dist/attachments/store.js +536 -0
  15. package/dist/attachments/store.js.map +1 -0
  16. package/dist/attachments/types.d.ts +48 -0
  17. package/dist/attachments/types.js +20 -0
  18. package/dist/attachments/types.js.map +1 -0
  19. package/dist/index.js +30 -24
  20. package/dist/index.js.map +1 -1
  21. package/dist/runtime/attachment_binding.d.ts +17 -0
  22. package/dist/runtime/attachment_binding.js +70 -0
  23. package/dist/runtime/attachment_binding.js.map +1 -0
  24. package/dist/runtime/create_owner_binding.d.ts +27 -0
  25. package/dist/runtime/create_owner_binding.js +22 -0
  26. package/dist/runtime/create_owner_binding.js.map +1 -0
  27. package/dist/timeline/attachment_content.d.ts +21 -0
  28. package/dist/timeline/attachment_content.js +83 -0
  29. package/dist/timeline/attachment_content.js.map +1 -0
  30. package/dist/timeline/attachment_delivery.d.ts +65 -0
  31. package/dist/timeline/attachment_delivery.js +150 -0
  32. package/dist/timeline/attachment_delivery.js.map +1 -0
  33. package/dist/timeline/runtime.d.ts +4 -2
  34. package/dist/timeline/runtime.js +33 -34
  35. package/dist/timeline/runtime.js.map +1 -1
  36. package/dist/timeline/user_delivery.d.ts +7 -0
  37. package/dist/timeline/user_delivery.js +21 -12
  38. package/dist/timeline/user_delivery.js.map +1 -1
  39. package/dist/timeline/user_delivery_binding.d.ts +6 -1
  40. package/dist/timeline/user_delivery_binding.js +17 -1
  41. package/dist/timeline/user_delivery_binding.js.map +1 -1
  42. package/dist/timeline/v2_service.d.ts +6 -1
  43. package/dist/timeline/v2_service.js +16 -8
  44. package/dist/timeline/v2_service.js.map +1 -1
  45. package/dist/vendor/protocol/session/attachments.d.ts +78 -0
  46. package/dist/vendor/protocol/session/attachments.js +150 -0
  47. package/dist/vendor/protocol/session/frames.d.ts +500 -0
  48. package/dist/vendor/protocol/session/frames.js +37 -4
  49. package/dist/vendor/protocol/session/index.d.ts +1 -0
  50. package/dist/vendor/protocol/session/index.js +1 -0
  51. package/dist/vendor/protocol/session/schema.d.ts +2 -2
  52. package/dist/vendor/protocol/session/schema.js +2 -0
  53. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AttachmentStore, AttachmentStoreError } from "./store.js";
3
+ // 仅由隔离测试的 IPC 子进程调用;不使用默认附件目录。
4
+ const rootDir = process.argv[2];
5
+ const available = BigInt(process.argv[3]);
6
+ const allocationUnitBytes = Number(process.argv[4] ?? 1);
7
+ const contents = "x".repeat(Number(process.argv[5] ?? 4));
8
+ if (!rootDir || !process.send)
9
+ throw new Error("Isolated fixture requires root and IPC");
10
+ const store = new AttachmentStore({
11
+ rootDir, runtimeId: `fixture-${process.pid}`, minFreeBytes: 2,
12
+ testHooks: { availableBytes: () => available, allocationUnitBytes },
13
+ });
14
+ const scope = { ownerId: "fixture-owner", sessionId: "session", uploadScope: store.scopeFor("session") };
15
+ process.on("message", (message) => {
16
+ if (message !== "begin")
17
+ return;
18
+ void store.begin(scope, {
19
+ uploadId: "fixture-upload", fileName: "fixture.bin", mimeType: "application/octet-stream", byteLength: contents.length,
20
+ sha256: createHash("sha256").update(contents).digest("hex"),
21
+ }).then(() => process.send?.({ type: "result", accepted: true }), (error) => process.send?.({ type: "result", accepted: false,
22
+ code: error instanceof AttachmentStoreError ? error.code : "unexpected" }));
23
+ });
24
+ process.send({ type: "ready" });
25
+ //# sourceMappingURL=reservation.fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reservation.fixture.js","sourceRoot":"","sources":["../../src/attachments/reservation.fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnE,+BAA+B;AAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAChC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC1D,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;AACzF,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;IAChC,OAAO,EAAE,SAAS,EAAE,WAAW,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,CAAC;IAC7D,SAAS,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,mBAAmB,EAAE;CACpE,CAAC,CAAC;AACH,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;AACzG,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;IACzC,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO;IAChC,KAAK,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE;QACtB,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,0BAA0B,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM;QACtH,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KAC5D,CAAC,CAAC,IAAI,CACL,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EACxD,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK;QAClE,IAAI,EAAE,KAAK,YAAY,oBAAoB,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAC7E,CAAC;AACJ,CAAC,CAAC,CAAC;AACH,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type AttachmentStoreTestHooks } from "./types.js";
2
+ export declare class DiskReservation {
3
+ private readonly root;
4
+ private readonly byteLength;
5
+ private readonly floor;
6
+ private readonly hooks;
7
+ private readonly directory;
8
+ private readonly path;
9
+ private readonly record;
10
+ private published;
11
+ private allocationUnit;
12
+ constructor(root: string, runtimeId: string, uploadId: string, byteLength: number, floor: bigint, hooks?: AttachmentStoreTestHooks);
13
+ publish(): Promise<void>;
14
+ private replace;
15
+ private assertOwned;
16
+ private totalRemaining;
17
+ check(): Promise<void>;
18
+ private available;
19
+ reduce(remainingBytes: number): Promise<void>;
20
+ remove(): Promise<void>;
21
+ }
@@ -0,0 +1,189 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, opendir, rename, statfs, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { ATTACHMENT_MAX_FILE_BYTES } from "../vendor/protocol/session/index.js";
5
+ import { assertDirectories, assertRegularFile, createPrivateFile, ensurePrivateDirectory, readPrivateFile } from "./safe-files.js";
6
+ import { AttachmentStoreError, nodeErrorHasCode } from "./types.js";
7
+ const RECORD_MAX_BYTES = 4096;
8
+ const RECORD_MAX_COUNT = 4096;
9
+ const CHECK_ATTEMPTS = 3;
10
+ const CHECK_RETRY_MS = 5;
11
+ const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
12
+ const RECORD_NAME = new RegExp(`^${UUID}\\.json$`);
13
+ const PENDING_NAME = new RegExp(`^${UUID}\\.pending$`);
14
+ function roundAllocation(bytes, unit) {
15
+ const rounded = ((BigInt(bytes) + unit - 1n) / unit) * unit;
16
+ if (rounded > BigInt(Number.MAX_SAFE_INTEGER))
17
+ throw new AttachmentStoreError("io_error");
18
+ return Number(rounded);
19
+ }
20
+ function parseRecord(raw, unit) {
21
+ let data;
22
+ try {
23
+ data = JSON.parse(raw);
24
+ }
25
+ catch {
26
+ throw new AttachmentStoreError("io_error");
27
+ }
28
+ if (typeof data !== "object" || data === null)
29
+ throw new AttachmentStoreError("io_error");
30
+ const item = data;
31
+ if (Object.keys(item).sort().join(",") !== "pid,remainingBytes,runtimeId,uploadId" ||
32
+ !Number.isSafeInteger(item.pid) || (item.pid ?? 0) <= 0 ||
33
+ typeof item.runtimeId !== "string" || item.runtimeId.length < 1 || item.runtimeId.length > 256 ||
34
+ typeof item.uploadId !== "string" || item.uploadId.length < 1 || item.uploadId.length > 256 ||
35
+ !Number.isSafeInteger(item.remainingBytes) || (item.remainingBytes ?? -1) < 0 ||
36
+ (item.remainingBytes ?? Infinity) > roundAllocation(ATTACHMENT_MAX_FILE_BYTES, unit) ||
37
+ BigInt(item.remainingBytes ?? 0) % unit !== 0n) {
38
+ throw new AttachmentStoreError("io_error");
39
+ }
40
+ return item;
41
+ }
42
+ function provenDead(pid) {
43
+ try {
44
+ process.kill(pid, 0);
45
+ }
46
+ catch (error) {
47
+ return nodeErrorHasCode(error, "ESRCH");
48
+ }
49
+ // PID 复用和 EPERM 都保守占预算,不按年龄删除记录或原件。
50
+ return false;
51
+ }
52
+ export class DiskReservation {
53
+ root;
54
+ byteLength;
55
+ floor;
56
+ hooks;
57
+ directory;
58
+ path;
59
+ record;
60
+ published = false;
61
+ allocationUnit = 1n;
62
+ constructor(root, runtimeId, uploadId, byteLength, floor, hooks = {}) {
63
+ this.root = root;
64
+ this.byteLength = byteLength;
65
+ this.floor = floor;
66
+ this.hooks = hooks;
67
+ this.directory = join(root, ".reservations");
68
+ this.path = join(this.directory, `${randomUUID()}.json`);
69
+ this.record = { pid: process.pid, runtimeId, uploadId, remainingBytes: byteLength };
70
+ }
71
+ async publish() {
72
+ ensurePrivateDirectory(this.directory);
73
+ this.allocationUnit = this.hooks.availableBytes ? BigInt(this.hooks.allocationUnitBytes ?? 1) :
74
+ (await statfs(this.root, { bigint: true })).bsize;
75
+ if (this.allocationUnit <= 0n)
76
+ throw new AttachmentStoreError("io_error");
77
+ this.record.remainingBytes = roundAllocation(this.record.remainingBytes, this.allocationUnit);
78
+ await this.replace(this.record, true);
79
+ this.published = true;
80
+ try {
81
+ await this.check();
82
+ }
83
+ catch (error) {
84
+ await this.remove();
85
+ throw error;
86
+ }
87
+ }
88
+ async replace(record, initial) {
89
+ assertDirectories(this.directory);
90
+ const pending = join(this.directory, `${randomUUID()}.pending`);
91
+ const handle = await createPrivateFile(pending);
92
+ try {
93
+ await handle.writeFile(JSON.stringify(record));
94
+ await handle.sync();
95
+ await handle.close();
96
+ if (initial) {
97
+ // link 为独占发布;pending 从不计入 credits,也从不准许写原件。
98
+ await link(pending, this.path);
99
+ }
100
+ else {
101
+ await this.assertOwned();
102
+ await rename(pending, this.path);
103
+ }
104
+ }
105
+ finally {
106
+ await handle.close().catch(() => undefined);
107
+ await unlink(pending).catch((error) => {
108
+ if (!nodeErrorHasCode(error, "ENOENT"))
109
+ throw error;
110
+ });
111
+ }
112
+ }
113
+ async assertOwned() {
114
+ if (!this.published)
115
+ throw new AttachmentStoreError("invalid_upload");
116
+ assertDirectories(this.directory);
117
+ const current = parseRecord(await readPrivateFile(this.path, RECORD_MAX_BYTES), this.allocationUnit);
118
+ if (current.pid !== this.record.pid || current.runtimeId !== this.record.runtimeId ||
119
+ current.uploadId !== this.record.uploadId || current.remainingBytes !== this.record.remainingBytes) {
120
+ throw new AttachmentStoreError("invalid_upload");
121
+ }
122
+ }
123
+ async totalRemaining() {
124
+ let total = 0n;
125
+ let activeCount = 0;
126
+ // 流式枚举,不把历史死亡账本的总数当作活跃上传配额。
127
+ for await (const entry of await opendir(this.directory)) {
128
+ const name = entry.name;
129
+ const path = join(this.directory, name);
130
+ try {
131
+ assertRegularFile(path);
132
+ if (PENDING_NAME.test(name))
133
+ continue;
134
+ if (!RECORD_NAME.test(name))
135
+ throw new AttachmentStoreError("io_error");
136
+ const record = parseRecord(await readPrivateFile(path, RECORD_MAX_BYTES), this.allocationUnit);
137
+ if (provenDead(record.pid))
138
+ continue;
139
+ if (++activeCount > RECORD_MAX_COUNT)
140
+ throw new AttachmentStoreError("no_space", true);
141
+ total += BigInt(record.remainingBytes);
142
+ }
143
+ catch (error) {
144
+ // 删除只发生在该 reservation 已不再可能写入之后。
145
+ if (!nodeErrorHasCode(error, "ENOENT"))
146
+ throw error;
147
+ }
148
+ }
149
+ return total;
150
+ }
151
+ async check() {
152
+ for (let attempt = 0; attempt < CHECK_ATTEMPTS; attempt++) {
153
+ await this.assertOwned();
154
+ const remaining = await this.totalRemaining();
155
+ // 必须先扫描 credits,再读取空间。旧 credits 与新空间只会高估需求。
156
+ const available = this.hooks.availableBytes ? await this.hooks.availableBytes() : await this.available();
157
+ if (remaining + this.floor <= available)
158
+ return;
159
+ if (attempt + 1 < CHECK_ATTEMPTS)
160
+ await new Promise((resolve) => setTimeout(resolve, CHECK_RETRY_MS));
161
+ }
162
+ throw new AttachmentStoreError("no_space", true);
163
+ }
164
+ async available() {
165
+ const stats = await statfs(this.root, { bigint: true });
166
+ return stats.bavail * stats.bsize;
167
+ }
168
+ async reduce(remainingBytes) {
169
+ if (!Number.isSafeInteger(remainingBytes) || remainingBytes < 0 || remainingBytes > this.byteLength) {
170
+ throw new AttachmentStoreError("invalid_upload");
171
+ }
172
+ // 已落盘的尾块可承载后续字节,不重复预留同一个分配单元。
173
+ const credits = roundAllocation(this.byteLength, this.allocationUnit) -
174
+ roundAllocation(this.byteLength - remainingBytes, this.allocationUnit);
175
+ if (credits > this.record.remainingBytes)
176
+ throw new AttachmentStoreError("invalid_upload");
177
+ const next = { ...this.record, remainingBytes: credits };
178
+ await this.replace(next, false);
179
+ this.record.remainingBytes = credits;
180
+ }
181
+ async remove() {
182
+ if (!this.published)
183
+ return;
184
+ await this.assertOwned();
185
+ await unlink(this.path);
186
+ this.published = false;
187
+ }
188
+ }
189
+ //# sourceMappingURL=reservations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reservations.js","sourceRoot":"","sources":["../../src/attachments/reservations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type Stats } from "node:fs";
2
+ import { type FileHandle } from "node:fs/promises";
3
+ export declare function assertDirectories(directory: string): void;
4
+ export declare function ensurePrivateDirectory(directory: string): void;
5
+ export interface OwnedDirectory {
6
+ path: string;
7
+ inode?: number;
8
+ device?: number;
9
+ }
10
+ /** mkdir 与 identity 获取同步相邻;既有目录绝不成为本次上传的资源。 */
11
+ export declare function createOwnedDirectory(path: string, recordCreated: (directory: OwnedDirectory) => void): OwnedDirectory;
12
+ export declare function assertOwnedDirectory(directory: OwnedDirectory): void;
13
+ /** 只移除仍归本次上传所有的空目录,绝不递归删除或接管替换目录。 */
14
+ export declare function removeOwnedDirectory(directory: OwnedDirectory): void;
15
+ export declare function assertRegularFile(path: string): Stats;
16
+ export declare function createPrivateFile(path: string): Promise<FileHandle>;
17
+ export declare function readPrivateFile(path: string, maximumBytes: number): Promise<string>;
@@ -0,0 +1,108 @@
1
+ import { constants, chmodSync, lstatSync, mkdirSync, rmdirSync } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { dirname, parse, resolve, sep } from "node:path";
4
+ import { AttachmentStoreError, nodeErrorHasCode } from "./types.js";
5
+ export function assertDirectories(directory) {
6
+ const absolute = resolve(directory);
7
+ let current = parse(absolute).root;
8
+ for (const part of absolute.slice(current.length).split(sep).filter(Boolean)) {
9
+ current = resolve(current, part);
10
+ const stat = lstatSync(current);
11
+ if (stat.isSymbolicLink() || !stat.isDirectory())
12
+ throw new AttachmentStoreError("invalid_upload");
13
+ }
14
+ }
15
+ export function ensurePrivateDirectory(directory) {
16
+ const absolute = resolve(directory);
17
+ let current = parse(absolute).root;
18
+ for (const part of absolute.slice(current.length).split(sep).filter(Boolean)) {
19
+ current = resolve(current, part);
20
+ try {
21
+ mkdirSync(current, { mode: 0o700 });
22
+ }
23
+ catch (error) {
24
+ if (!nodeErrorHasCode(error, "EEXIST"))
25
+ throw error;
26
+ }
27
+ const stat = lstatSync(current);
28
+ if (stat.isSymbolicLink() || !stat.isDirectory())
29
+ throw new AttachmentStoreError("invalid_upload");
30
+ }
31
+ chmodSync(absolute, 0o700);
32
+ }
33
+ /** mkdir 与 identity 获取同步相邻;既有目录绝不成为本次上传的资源。 */
34
+ export function createOwnedDirectory(path, recordCreated) {
35
+ assertDirectories(dirname(path));
36
+ try {
37
+ mkdirSync(path, { mode: 0o700 });
38
+ }
39
+ catch (error) {
40
+ if (nodeErrorHasCode(error, "EEXIST"))
41
+ throw new AttachmentStoreError("invalid_upload");
42
+ throw error;
43
+ }
44
+ // mkdir 成功即转移资源,identity 获取失败也不得让有界账本漏掉已创建目录。
45
+ const directory = { path };
46
+ recordCreated(directory);
47
+ const stat = lstatSync(path);
48
+ if (!stat.isDirectory() || stat.isSymbolicLink())
49
+ throw new AttachmentStoreError("invalid_upload");
50
+ directory.inode = stat.ino;
51
+ directory.device = stat.dev;
52
+ return directory;
53
+ }
54
+ export function assertOwnedDirectory(directory) {
55
+ // 缺失创建时证据不能靠后续 stat 补齐,也不能因路径消失就提交成功清理。
56
+ if (directory.inode === undefined || directory.device === undefined)
57
+ throw new AttachmentStoreError("invalid_upload");
58
+ assertDirectories(dirname(directory.path));
59
+ const stat = lstatSync(directory.path);
60
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.ino !== directory.inode || stat.dev !== directory.device) {
61
+ throw new AttachmentStoreError("invalid_upload");
62
+ }
63
+ }
64
+ /** 只移除仍归本次上传所有的空目录,绝不递归删除或接管替换目录。 */
65
+ export function removeOwnedDirectory(directory) {
66
+ try {
67
+ assertOwnedDirectory(directory);
68
+ rmdirSync(directory.path);
69
+ }
70
+ catch (error) {
71
+ if (!nodeErrorHasCode(error, "ENOENT"))
72
+ throw error;
73
+ }
74
+ }
75
+ export function assertRegularFile(path) {
76
+ const stat = lstatSync(path);
77
+ if (!stat.isFile() || stat.isSymbolicLink())
78
+ throw new AttachmentStoreError("invalid_upload");
79
+ return stat;
80
+ }
81
+ export async function createPrivateFile(path) {
82
+ return open(path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
83
+ }
84
+ export async function readPrivateFile(path, maximumBytes) {
85
+ assertRegularFile(path);
86
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
87
+ try {
88
+ const stat = await handle.stat();
89
+ if (!stat.isFile() || stat.size > maximumBytes)
90
+ throw new AttachmentStoreError("io_error");
91
+ // 限长读取,不能在 stat 后使用无界 readFile。
92
+ const buffer = Buffer.alloc(maximumBytes + 1);
93
+ let length = 0;
94
+ while (length < buffer.length) {
95
+ const read = await handle.read(buffer, length, buffer.length - length, length);
96
+ if (read.bytesRead === 0)
97
+ break;
98
+ length += read.bytesRead;
99
+ }
100
+ if (length > maximumBytes)
101
+ throw new AttachmentStoreError("io_error");
102
+ return buffer.subarray(0, length).toString("utf8");
103
+ }
104
+ finally {
105
+ await handle.close();
106
+ }
107
+ }
108
+ //# sourceMappingURL=safe-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-files.js","sourceRoot":"","sources":["../../src/attachments/safe-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function localDateDirectory(date: Date): string;
2
+ export declare function safeAttachmentFileName(original: string): string;
3
+ /** attachmentId 只来自服务端 randomUUID,不接受客户端提供的目录组件。 */
4
+ export declare function attachmentStoragePath(root: string, attachmentId: string, originalName: string, date: Date): string;
@@ -0,0 +1,47 @@
1
+ import { join } from "node:path";
2
+ // 为不同文件系统的单组件限制留余量;按 UTF-8 字节计算而非 JS 字符数。
3
+ const MAX_FILE_NAME_BYTES = 240;
4
+ const FALLBACK_FILE_NAME = "attachment";
5
+ const WINDOWS_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³]|CONIN\$|CONOUT\$)(?:\.|$)/iu;
6
+ export function localDateDirectory(date) {
7
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
8
+ }
9
+ function truncateUtf8(value, maximumBytes) {
10
+ let bytes = 0;
11
+ let result = "";
12
+ for (const codepoint of value) {
13
+ const length = Buffer.byteLength(codepoint, "utf8");
14
+ if (bytes + length > maximumBytes)
15
+ break;
16
+ result += codepoint;
17
+ bytes += length;
18
+ }
19
+ return result;
20
+ }
21
+ function normalizeName(name) {
22
+ if (name === "" || name === "." || name === "..")
23
+ return FALLBACK_FILE_NAME;
24
+ const safe = name.replace(/[ .]+$/u, "_");
25
+ return WINDOWS_DEVICE_NAME.test(safe.trimStart()) ? `_${safe}` : safe;
26
+ }
27
+ export function safeAttachmentFileName(original) {
28
+ let name = normalizeName(original.replace(/[<>:"/\\|?*\p{Cc}\p{Cf}]/gu, "_"));
29
+ if (Buffer.byteLength(name, "utf8") <= MAX_FILE_NAME_BYTES)
30
+ return name;
31
+ const extensionStart = name.lastIndexOf(".");
32
+ if (extensionStart <= 0)
33
+ name = truncateUtf8(name, MAX_FILE_NAME_BYTES);
34
+ else {
35
+ // 至少给主名留一个完整 codepoint;异常超长扩展也必须受同一字节上限约束。
36
+ const extension = truncateUtf8(name.slice(extensionStart), MAX_FILE_NAME_BYTES - 4);
37
+ const stem = truncateUtf8(name.slice(0, extensionStart), MAX_FILE_NAME_BYTES - Buffer.byteLength(extension));
38
+ name = `${stem}${extension}`;
39
+ }
40
+ // 截断可能新产生设备名或尾部空格;重新归一化后仍须守住字节上限。
41
+ return truncateUtf8(normalizeName(name), MAX_FILE_NAME_BYTES).replace(/[ .]+$/u, "_");
42
+ }
43
+ /** attachmentId 只来自服务端 randomUUID,不接受客户端提供的目录组件。 */
44
+ export function attachmentStoragePath(root, attachmentId, originalName, date) {
45
+ return join(root, localDateDirectory(date), attachmentId, safeAttachmentFileName(originalName));
46
+ }
47
+ //# sourceMappingURL=storage-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-path.js","sourceRoot":"","sources":["../../src/attachments/storage-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AACxC,MAAM,mBAAmB,GAAG,yEAAyE,CAAC;AAEtG,MAAM,UAAU,kBAAkB,CAAC,IAAU;IAC3C,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AAC5H,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,YAAoB;IACvD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACpD,IAAI,KAAK,GAAG,MAAM,GAAG,YAAY;YAAE,MAAM;QACzC,MAAM,IAAI,SAAS,CAAC;QACpB,KAAK,IAAI,MAAM,CAAC;IAClB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,kBAAkB,CAAC;IAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC1C,OAAO,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,IAAI,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9E,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,mBAAmB;QAAE,OAAO,IAAI,CAAC;IAExE,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,cAAc,IAAI,CAAC;QAAE,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;SACnE,CAAC;QACJ,2CAA2C;QAC3C,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,mBAAmB,GAAG,CAAC,CAAC,CAAC;QACpF,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;QAC7G,IAAI,GAAG,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC;IAC/B,CAAC;IACD,kCAAkC;IAClC,OAAO,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,mBAAmB,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AACxF,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,YAAoB,EAAE,YAAoB,EAAE,IAAU;IACxG,OAAO,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC;AAClG,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { type AttachmentDescriptor } from "../vendor/protocol/session/index.js";
2
+ import { type AttachmentBeginInput, type AttachmentScope, type AttachmentStoreOptions, type AttachmentUploadState } from "./types.js";
3
+ export { AttachmentStoreError } from "./types.js";
4
+ export type { AttachmentBeginInput, AttachmentScope, AttachmentStoreOptions, AttachmentStoreTestHooks, AttachmentUploadState, } from "./types.js";
5
+ export declare class AttachmentStore {
6
+ private readonly options;
7
+ private readonly root;
8
+ private readonly floor;
9
+ private uploads;
10
+ private byAttachment;
11
+ private readonly resources;
12
+ private readonly metadataLimit;
13
+ private readonly ownerPending;
14
+ private readonly cleanups;
15
+ private sessionId?;
16
+ private lease?;
17
+ private disposed;
18
+ private disposing?;
19
+ constructor(options: AttachmentStoreOptions);
20
+ scopeFor(sessionId: string): string;
21
+ resetScope(sessionId: string): void;
22
+ private validScope;
23
+ private checkScope;
24
+ private key;
25
+ private newUpload;
26
+ private checkMetadataBudget;
27
+ /** 仅测试观察数量,不包含路径、元信息或原始错误。 */
28
+ debugCounts(): {
29
+ records: number;
30
+ attachments: number;
31
+ resources: number;
32
+ cleanups: number;
33
+ pending: number;
34
+ };
35
+ private state;
36
+ private enqueue;
37
+ private get;
38
+ private assertFile;
39
+ private checkedState;
40
+ begin(scope: AttachmentScope, input: AttachmentBeginInput): Promise<AttachmentUploadState>;
41
+ write(scope: AttachmentScope, uploadId: string, offset: number, bytes: Uint8Array): Promise<AttachmentUploadState>;
42
+ private compareChunk;
43
+ finish(scope: AttachmentScope, uploadId: string): Promise<AttachmentUploadState>;
44
+ status(scope: AttachmentScope, uploadId: string): Promise<AttachmentUploadState>;
45
+ cancel(scope: AttachmentScope, uploadId: string): Promise<AttachmentUploadState>;
46
+ discard(scope: AttachmentScope, attachmentId: string): Promise<"cancelled" | "retained">;
47
+ resolve(scope: AttachmentScope, attachmentIds: readonly string[]): ReadonlyArray<{
48
+ descriptor: AttachmentDescriptor;
49
+ path: string;
50
+ }>;
51
+ retain(scope: AttachmentScope, attachmentIds: readonly string[]): void;
52
+ private cleanup;
53
+ private scheduleCleanup;
54
+ dispose(): Promise<void>;
55
+ }