@yss123/cage-files 0.0.0-stage → 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shunshunyue and Cage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # @yss123/cage-files
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cage 的私有文件服务。管理上传会话、可恢复分片、SHA-256 校验、配额、私有下载和过期清理,内置本地磁盘实现,并提供自定义对象存储适配接口。适合业务附件;字段声明和业务事务内绑定仍由 kernel 负责。
4
+
5
+ 上传完成只得到临时文件 ID,调用业务命令成功后才正式绑定。文件字节、存储路径和公开 URL 不进入业务字段。
6
+
7
+ ## 安装与服务端配置
8
+
9
+ 需要 Node.js 24+、PostgreSQL 和持久存储,与其他 Cage 包保持同版本:
10
+
11
+ ```powershell
12
+ pnpm add @yss123/cage-files
13
+ ```
14
+
15
+ 标准 `@yss123/cage-http` 已装配本包。定制存储时在 API 入口配置,以下例子里的 `@app/domain` 来自脚手架应用:
16
+
17
+ ```ts
18
+ /** API 私有文件装配;目录持久保存,不能作为静态资源公开。 */
19
+ import { createServer } from '@yss123/cage-http';
20
+ import { LocalFileStorage } from '@yss123/cage-files';
21
+ import * as domain from '@app/domain';
22
+
23
+ const server = createServer({
24
+ domain,
25
+ files: {
26
+ storage: new LocalFileStorage('.cage-files'),
27
+ partBytes: 16 * 1024 * 1024,
28
+ maxFileBytes: 2 * 1024 ** 3,
29
+ retentionMs: 24 * 3600_000,
30
+ },
31
+ });
32
+ await server.listen();
33
+ process.once('SIGINT', () => void server.close());
34
+ process.once('SIGTERM', () => void server.close());
35
+ ```
36
+
37
+ 未传 `storage` 时会创建本地实现,目录由 `files.directory`、`CAGE_FILES_DIR` 或默认 `.cage-files` 决定。这个目录不属于可再生的 `.cage`;容器应使用持久卷,数据库元数据与文件字节应配套备份。
38
+
39
+ ## 业务接入的三个步骤
40
+
41
+ 1. 在聚合 `fields` 使用 kernel 的 `file/files`,并在目标用户命令 input 中声明同名 UUID 或 UUID 数组。字段只能位于聚合顶层。
42
+ 2. 运行 `pnpm gen`,审查迁移并执行 `pnpm migrate`,授予目标命令及所需读取/范围能力。
43
+ 3. 浏览器使用 `client.upload` 上传,成功后将返回 ID 交给目标业务命令;字段绑定与业务记录同事务提交。
44
+
45
+ 字段片段合并到已有聚合,不能单独作为完整 domain:
46
+
47
+ ```ts
48
+ import { files } from '@yss123/cage-kernel';
49
+
50
+ // 每个文件最多 20 MiB、最多 5 个,仅允许明确的 MIME 类型。
51
+ const attachments = files({
52
+ maxCount: 5,
53
+ maxBytes: 20 * 1024 * 1024,
54
+ accept: ['application/pdf', 'image/png'],
55
+ });
56
+ // 放入聚合 fields: { attachments };命令 input 需同名 z.array(z.string().uuid())。
57
+ ```
58
+
59
+ 下例为采购模板的页面操作,`selectedFile` 来自文件选择框。其他业务应替换目标命令、字段与生成契约:
60
+
61
+ ```ts
62
+ import { client, purchaseOrder } from '@app/contracts';
63
+
64
+ /** 上传失败会抛错;业务保存失败仍须展示结果,不能把上传成功当作订单已保存。 */
65
+ async function createWithAttachment(selectedFile: File) {
66
+ const task = client.upload(selectedFile, {
67
+ command: 'PurchaseOrder.create',
68
+ field: 'attachments',
69
+ });
70
+ try {
71
+ const uploaded = await task.start();
72
+ return await purchaseOrder.create({
73
+ amount: '100.00',
74
+ items: [{ sku: 'A-01', qty: 1 }],
75
+ attachments: [uploaded.id],
76
+ });
77
+ } finally {
78
+ task.dispose();
79
+ }
80
+ }
81
+ ```
82
+
83
+ 调用方捕获上传异常并检查返回值 `ok`。修改已有记录时,上传目标还须传 `targetId`。刷新后通过客户端查询本人会话,重新选择原文件,再传 `uploadId` 恢复;SDK 会核对已确认分片内容。
84
+
85
+ ## 公开入口与边界
86
+
87
+ | 入口 | 作用 |
88
+ | --- | --- |
89
+ | `FileService`、`FileServiceOptions` | 协调授权、会话、配额、上传、下载和清理;通常由 HTTP 层持有 |
90
+ | `LocalFileStorage` | 流式写入、校验、合并及范围读取的磁盘实现 |
91
+ | `FileStorage`、`StoredPart`、`ByteRange` | 自定义存储协议;适配 writePart/complete/read/remove/prune |
92
+ | `beginUploadSchema`、`UploadSession`、`FileInfo` | 服务端输入校验与公开上传/文件描述 |
93
+
94
+ 一个文件只能绑定一个对象的一个字段,不能覆盖原文件;替换需新 ID。下载依当前 Cookie 会话、对象范围和字段引用重新授权,不提供永久公开链接。分片/完成操作的预检查不保证最终命令必定通过。
95
+
96
+ API 启动后及每分钟执行有界清理;独立持有 `FileService` 时须自行定期调用 `cleanup()`,退出时 `close()`。本地多实例必须共享同一目录,不能只共享数据库。代理需允许分片请求体、关闭上传缓冲并配置合并超时。
97
+
98
+ 目前只内置本地存储;`FileStorage` 不代表已提供 S3/OSS SDK 或签名直传。内容头校验也不等于病毒扫描或完整格式验证。
99
+
100
+ 完整说明:[私有文件字段与断点续传](https://github.com/shunshunyue/cage/blob/main/docs/ai/files.md)。源码与反馈:[GitHub](https://github.com/shunshunyue/cage)。许可证:[MIT](https://github.com/shunshunyue/cage/blob/main/LICENSE)。
@@ -0,0 +1,7 @@
1
+ /** 文件包公开入口:协议、默认磁盘实现、上传会话服务分层导出。
2
+ * HTTP 层消费 FileService;扩展对象存储时实现 FileStorage;字段声明和事务内绑定仍由内核负责。
3
+ * 此处只重导出类型与实现,导入模块不会启动清理定时器或创建存储目录。
4
+ */
5
+ export * from './storage.js';
6
+ export * from './local.js';
7
+ export * from './service.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /** 文件包公开入口:协议、默认磁盘实现、上传会话服务分层导出。
2
+ * HTTP 层消费 FileService;扩展对象存储时实现 FileStorage;字段声明和事务内绑定仍由内核负责。
3
+ * 此处只重导出类型与实现,导入模块不会启动清理定时器或创建存储目录。
4
+ */
5
+ export * from './storage.js';
6
+ export * from './local.js';
7
+ export * from './service.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { ByteRange, FileStorage, StoredPart } from './storage.js';
3
+ /** 本地磁盘适配器:每个上传 UUID 一个私有目录,分片和最终文件再按执行 token 分离。
4
+ * 服务层只在短事务中登记结果,本类负责事务外写字节;临时文件校验并 fsync 后才 rename 发布。
5
+ * 存储目录必须跨服务重启保留,且不能作为静态资源目录;下载必须经过 FileService 授权。
6
+ */
7
+ export declare class LocalFileStorage implements FileStorage {
8
+ /** 由规范目录计算的稳定标识;避免修改存储位置后误处理旧会话。 */
9
+ readonly name: string;
10
+ /** 单片允许 64 KiB~64 MiB,最多一万片;尾片的实际大小由服务层计算。 */
11
+ readonly limits: {
12
+ minPartBytes: number;
13
+ maxPartBytes: number;
14
+ maxParts: number;
15
+ };
16
+ /** 规范化绝对路径,仅服务端内部使用。 */
17
+ readonly directory: string;
18
+ /** 只解析配置并建立身份,不创建目录;目录在首次写入时以私有权限创建。 */
19
+ constructor(directory?: string);
20
+ /** 验证服务端上传 ID 后定位目录,禁止调用方把任意路径拼接进存储根。 */
21
+ private dir;
22
+ /** 只接受本适配器生成的键格式,并再做绝对路径包含检查;非法键统一按输入错误拒绝。 */
23
+ private path;
24
+ /** 流式保存一片:边消费边计算长度/摘要,完整校验后持久化并原子改名。
25
+ * 同 token 的重试可替换其结果,不同 token 使用不同目标;任何失败都会清理本次临时文件。
26
+ * 返回存储键只代表字节完成,服务层仍须核对租约后才能把该片记录为已确认。
27
+ */
28
+ writePart(upload: string, token: string, source: Readable, o: {
29
+ bytes: number;
30
+ sha256: string;
31
+ signal: AbortSignal;
32
+ }): Promise<string>;
33
+ /** 顺序合并已确认分片,再次验证每片长度、摘要与总长度,防止磁盘内容在确认后发生变化。
34
+ * 异步生成器和 pipeline 保留背压,内存不随文件总大小增长;取消或校验失败不会发布最终键。
35
+ */
36
+ complete(upload: string, token: string, parts: readonly StoredPart[], o: {
37
+ bytes: number;
38
+ signal: AbortSignal;
39
+ }): Promise<string>;
40
+ /** 打开私有内容流,可按闭区间读取。文件不存在等 IO 错误直接上抛,不伪装为空文件。 */
41
+ read(key: string, range?: ByteRange): Promise<Readable>;
42
+ /** 幂等删除整个上传目录;只能用于服务层已认领的过期或解绑会话,不能代替业务解绑命令。 */
43
+ remove(upload: string): Promise<void>;
44
+ /** 保留唯一已提交的最终对象,回收分片和旧 token 产物;目录已消失视为清理完成。 */
45
+ prune(upload: string, keepKey: string): Promise<void>;
46
+ }
package/dist/local.js ADDED
@@ -0,0 +1,158 @@
1
+ // 私有目录不能挂载为静态资源。逐块写入/校验/合并,内存占用与文件总大小无关。
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { mkdir, open, readdir, rename, rm } from 'node:fs/promises';
4
+ import { resolve, join, sep } from 'node:path';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { Readable, Transform } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { cageError } from '@yss123/cage-kernel';
9
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10
+ /** 本地磁盘适配器:每个上传 UUID 一个私有目录,分片和最终文件再按执行 token 分离。
11
+ * 服务层只在短事务中登记结果,本类负责事务外写字节;临时文件校验并 fsync 后才 rename 发布。
12
+ * 存储目录必须跨服务重启保留,且不能作为静态资源目录;下载必须经过 FileService 授权。
13
+ */
14
+ export class LocalFileStorage {
15
+ /** 由规范目录计算的稳定标识;避免修改存储位置后误处理旧会话。 */
16
+ name;
17
+ /** 单片允许 64 KiB~64 MiB,最多一万片;尾片的实际大小由服务层计算。 */
18
+ limits = { minPartBytes: 64 * 1024, maxPartBytes: 64 * 1024 * 1024, maxParts: 10_000 };
19
+ /** 规范化绝对路径,仅服务端内部使用。 */
20
+ directory;
21
+ /** 只解析配置并建立身份,不创建目录;目录在首次写入时以私有权限创建。 */
22
+ constructor(directory = process.env.CAGE_FILES_DIR ?? '.cage-files') {
23
+ this.directory = resolve(directory);
24
+ // 配置指向另一目录时不能把旧文件误判成已删除;不向元数据暴露真实路径。
25
+ this.name = `local:v1:${createHash('sha256').update(process.platform === 'win32' ? this.directory.toLowerCase() : this.directory).digest('hex').slice(0, 16)}`;
26
+ }
27
+ /** 验证服务端上传 ID 后定位目录,禁止调用方把任意路径拼接进存储根。 */
28
+ dir(upload) {
29
+ if (!uuid.test(upload))
30
+ throw cageError('INVALID_INPUT', '非法上传 ID');
31
+ return join(this.directory, upload);
32
+ }
33
+ /** 只接受本适配器生成的键格式,并再做绝对路径包含检查;非法键统一按输入错误拒绝。 */
34
+ path(key) {
35
+ if (!/^[0-9a-f-]{36}\/(part|object)-[0-9a-f-]{36}$/.test(key))
36
+ throw cageError('INVALID_INPUT', '非法存储键');
37
+ const [id, name] = key.split('/');
38
+ const path = resolve(this.dir(id), name);
39
+ if (!path.startsWith(this.directory + sep))
40
+ throw cageError('INVALID_INPUT');
41
+ return path;
42
+ }
43
+ /** 流式保存一片:边消费边计算长度/摘要,完整校验后持久化并原子改名。
44
+ * 同 token 的重试可替换其结果,不同 token 使用不同目标;任何失败都会清理本次临时文件。
45
+ * 返回存储键只代表字节完成,服务层仍须核对租约后才能把该片记录为已确认。
46
+ */
47
+ async writePart(upload, token, source, o) {
48
+ const key = `${upload}/part-${token}`, destination = this.path(key);
49
+ await mkdir(this.dir(upload), { recursive: true, mode: 0o700 });
50
+ const temporary = `${destination}.${randomUUID()}.tmp`;
51
+ let bytes = 0;
52
+ const hash = createHash('sha256');
53
+ const verifier = new Transform({ transform(chunk, _encoding, callback) {
54
+ bytes += chunk.length;
55
+ if (bytes > o.bytes) {
56
+ callback(cageError('INVALID_INPUT', '分片超过声明长度'));
57
+ return;
58
+ }
59
+ hash.update(chunk);
60
+ callback(null, chunk);
61
+ } });
62
+ try {
63
+ await pipeline(source, verifier, createWriteStream(temporary, { flags: 'wx', mode: 0o600 }), { signal: o.signal });
64
+ if (bytes !== o.bytes || hash.digest('hex') !== o.sha256)
65
+ throw cageError('INVALID_INPUT', '分片长度或 SHA-256 不匹配');
66
+ // fsync 后才确认分片,避免成功响应仅表示写入了用户态缓冲。
67
+ const file = await open(temporary, 'r+');
68
+ try {
69
+ await file.sync();
70
+ }
71
+ finally {
72
+ await file.close();
73
+ }
74
+ o.signal.throwIfAborted();
75
+ await rename(temporary, destination);
76
+ return key;
77
+ }
78
+ finally {
79
+ await rm(temporary, { force: true });
80
+ }
81
+ }
82
+ /** 顺序合并已确认分片,再次验证每片长度、摘要与总长度,防止磁盘内容在确认后发生变化。
83
+ * 异步生成器和 pipeline 保留背压,内存不随文件总大小增长;取消或校验失败不会发布最终键。
84
+ */
85
+ async complete(upload, token, parts, o) {
86
+ const key = `${upload}/object-${token}`, destination = this.path(key);
87
+ const temporary = `${destination}.${randomUUID()}.tmp`;
88
+ const self = this;
89
+ async function* content() {
90
+ let total = 0;
91
+ for (const part of parts) {
92
+ o.signal.throwIfAborted();
93
+ let bytes = 0;
94
+ const hash = createHash('sha256');
95
+ for await (const chunk of createReadStream(self.path(part.key), { signal: o.signal })) {
96
+ bytes += chunk.length;
97
+ total += chunk.length;
98
+ if (bytes > part.bytes || total > o.bytes)
99
+ throw cageError('INVALID_INPUT', '分片内容发生变化');
100
+ hash.update(chunk);
101
+ yield chunk;
102
+ }
103
+ if (bytes !== part.bytes || hash.digest('hex') !== part.sha256)
104
+ throw cageError('INVALID_INPUT', '分片内容校验失败');
105
+ }
106
+ if (total !== o.bytes)
107
+ throw cageError('INVALID_INPUT', '文件长度不匹配');
108
+ }
109
+ try {
110
+ await pipeline(Readable.from(content()), createWriteStream(temporary, { flags: 'wx', mode: 0o600 }), { signal: o.signal });
111
+ const file = await open(temporary, 'r+');
112
+ try {
113
+ await file.sync();
114
+ }
115
+ finally {
116
+ await file.close();
117
+ }
118
+ o.signal.throwIfAborted();
119
+ await rename(temporary, destination);
120
+ return key;
121
+ }
122
+ finally {
123
+ await rm(temporary, { force: true });
124
+ }
125
+ }
126
+ /** 打开私有内容流,可按闭区间读取。文件不存在等 IO 错误直接上抛,不伪装为空文件。 */
127
+ async read(key, range) {
128
+ const file = await open(this.path(key), 'r');
129
+ return file.createReadStream(range ? { start: range.start, end: range.end } : {});
130
+ }
131
+ /** 幂等删除整个上传目录;只能用于服务层已认领的过期或解绑会话,不能代替业务解绑命令。 */
132
+ async remove(upload) {
133
+ const directory = this.dir(upload);
134
+ // dir() 校验 UUID,绝对路径必须仍在配置目录内,禁止删除根目录。
135
+ if (!directory.startsWith(this.directory + sep))
136
+ throw cageError('INVALID_INPUT');
137
+ await rm(directory, { recursive: true, force: true, maxRetries: 3 });
138
+ }
139
+ /** 保留唯一已提交的最终对象,回收分片和旧 token 产物;目录已消失视为清理完成。 */
140
+ async prune(upload, keepKey) {
141
+ const directory = this.dir(upload);
142
+ if (this.path(keepKey) !== join(directory, keepKey.split('/')[1]))
143
+ throw cageError('INVALID_INPUT');
144
+ let entries;
145
+ try {
146
+ entries = await readdir(directory);
147
+ }
148
+ catch (e) {
149
+ if (e.code === 'ENOENT')
150
+ return;
151
+ throw e;
152
+ }
153
+ for (const name of entries)
154
+ if (name !== keepKey.split('/')[1])
155
+ await rm(join(directory, name), { force: true });
156
+ }
157
+ }
158
+ //# sourceMappingURL=local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ import type { Readable } from 'node:stream';
2
+ import { z } from 'zod';
3
+ import { type Ctx, type Kernel } from '@yss123/cage-kernel';
4
+ import type { ByteRange, FileStorage } from './storage.js';
5
+ /** 创建上传的外部输入:命令/字段定位授权边界,targetId 仅用于修改已有对象。
6
+ * bytes 为正安全整数字节数;filename 仅作下载展示,禁止路径和控制字符;contentType 后续还会验内容。
7
+ */
8
+ export declare const beginUploadSchema: z.ZodObject<{
9
+ command: z.ZodString;
10
+ field: z.ZodString;
11
+ targetId: z.ZodOptional<z.ZodString>;
12
+ filename: z.ZodString;
13
+ bytes: z.ZodNumber;
14
+ contentType: z.ZodString;
15
+ }, z.core.$strip>;
16
+ /** 通过创建校验后的输入类型;不包含服务端生成的会话 ID、租约或存储路径。 */
17
+ export type BeginUpload = z.infer<typeof beginUploadSchema>;
18
+ /** 已完成分片的公开证明;number 从 1 开始,摘要用于断点续传时确认内容一致。 */
19
+ export interface UploadPart {
20
+ number: number;
21
+ bytes: number;
22
+ sha256: string;
23
+ }
24
+ /** 浏览器可见的上传进度。它不泄露存储键,也不能作为永久下载凭据。 */
25
+ export interface UploadSession {
26
+ /** 会话与业务入口;targetId 在新建对象场景为 null,绑定后仍保留原上传语境。 */
27
+ id: string;
28
+ aggregate: string;
29
+ field: string;
30
+ command: string;
31
+ targetId: string | null;
32
+ /** 展示文件名、精确总字节数和声明的 MIME;不代表客户端可指定物理存储位置。 */
33
+ filename: string;
34
+ bytes: number;
35
+ contentType: string;
36
+ /** ready 可交给命令绑定;bound 只能通过业务命令解绑;deleting/deleted 不再接收上传。 */
37
+ status: 'uploading' | 'completing' | 'ready' | 'bound' | 'deleting' | 'deleted';
38
+ /** 标准片大小、总片数、已确认片以及 ISO 到期时间;列表摘要的 parts 刻意为空。 */
39
+ partBytes: number;
40
+ partCount: number;
41
+ parts: UploadPart[];
42
+ expiresAt: string;
43
+ }
44
+ /** 经过当前权限检查后返回的文件描述,不包含可绕过权限的存储 URL。 */
45
+ export interface FileInfo {
46
+ id: string;
47
+ filename: string;
48
+ bytes: number;
49
+ contentType: string;
50
+ }
51
+ /** 单服务实例的存储策略。所有数值均须为正安全整数,并在构造时校验适配器容量限制。 */
52
+ export interface FileServiceOptions {
53
+ /** 可替换的私有存储;未提供时使用 LocalFileStorage。 */
54
+ storage?: FileStorage;
55
+ /** 默认 .cage-files;使用 storage 时由适配器决定目录。 */
56
+ directory?: string;
57
+ /** 标准分片字节数,默认 16 MiB;最后一片按剩余长度收尾。 */
58
+ partBytes?: number;
59
+ /** 每文件服务级字节上限,默认 2 GiB;还须满足业务字段自身限制。 */
60
+ maxFileBytes?: number;
61
+ /** 单用户未绑定会话累计字节上限,默认 8 GiB。 */
62
+ maxPendingBytesPerUser?: number;
63
+ /** 租户所有未删除会话累计声明字节上限,默认 100 GiB。 */
64
+ maxStoredBytesPerTenant?: number;
65
+ /** 单用户未绑定会话个数上限,默认 100;与字节配额共同限制占用。 */
66
+ maxPendingUploadsPerUser?: number;
67
+ /** 暂存保留时间,单位毫秒,默认一天;过期内容由后台清理回收。 */
68
+ retentionMs?: number;
69
+ /** 单次分片/合并 IO 总时限,默认 30 分钟。 */
70
+ ioTimeoutMs?: number;
71
+ }
72
+ /** 文件业务边界的协调器;实例由 API 生命周期管理,close 会取消并等待本实例在途 IO。
73
+ * 每次访问重新检查当前身份和策略,不把创建会话时的授权当作后续读写的永久许可。
74
+ */
75
+ export declare class FileService {
76
+ readonly kernel: Kernel;
77
+ /** 持久字节适配器;名称同时写入会话,防止跨存储空间误读或误清理。 */
78
+ readonly storage: FileStorage;
79
+ /** 已通过适配器限制校验的分片字节数,供 HTTP 层设置请求体限制。 */
80
+ readonly partBytes: number;
81
+ /** 服务级文件大小上限,单位字节。 */
82
+ readonly maxFileBytes: number;
83
+ private readonly options;
84
+ private readonly active;
85
+ private closing;
86
+ /** 合并默认策略并做启动期配置校验;不创建上传、不访问存储、不启动定时清理。 */
87
+ constructor(kernel: Kernel, options?: FileServiceOptions);
88
+ /** 上传/下载入口仅接受具名 user 身份;流程、任务及匿名上下文不能借此占用私有存储。 */
89
+ private identity;
90
+ /** 验证命令能力、文件字段及输入归属;已有对象还要通过当前 read 范围和起始状态检查。
91
+ * 这里只判断上传资格,最终业务命令仍会独立检查版本、硬约束、不变量和绑定合法性。
92
+ */
93
+ private policy;
94
+ /** 取得当前租户当前上传者的会话,并重新校验业务策略;不存在或存储不匹配统一隐藏为 NOT_FOUND。 */
95
+ private owned;
96
+ /** 在调用方事务中锁会话行,串行化修改、完成和取消;调用方应已用 owned/policy 完成授权。 */
97
+ private locked;
98
+ /** 可修改性检查与授权分离;过期、已绑定或进入删除阶段的会话不允许迟到写入重新激活。 */
99
+ private usable;
100
+ /** 将数据库 bigint/日期与内部状态投影成公开 DTO,只列出确实已有存储键的已确认分片。 */
101
+ private view;
102
+ /** 按片号返回内部元数据;仅供已完成所有权检查的调用使用,不能直接作为公开查询入口。 */
103
+ private parts;
104
+ /** 创建或重放同一次上传申请。租户咨询锁让多实例在同一短事务内核对并占用配额。
105
+ * 相同用户的同一幂等键只能对应相同规范请求和存储;超配额抛 RATE_LIMITED,不产生半成品会话。
106
+ * 此步骤只预留元数据与额度,客户端之后使用返回的 partBytes/partCount 上传字节。
107
+ */
108
+ begin(raw: unknown, ctx: Ctx, idempotencyKey: string): Promise<UploadSession>;
109
+ /** 断点恢复的权威进度查询;返回服务端已确认片清单,客户端不能凭本地发送进度认定上传成功。 */
110
+ get(id: string, ctx: Ctx): Promise<UploadSession>;
111
+ /** 查询同一业务入口下本人未过期的可恢复会话,最多一百条;摘要不携带完整分片表。 */
112
+ list(command: string, field: string, targetId: string | null, ctx: Ctx): Promise<UploadSession[]>;
113
+ /** 为事务外 IO 管理续租、总时限与停机取消。
114
+ * 续租丢失或数据库异常即中止信号;run 必须在提交元数据前再次检查信号和 token,避免迟到完成覆盖新执行者。
115
+ * active 持有 Promise 直到 finally,用于 close 等待真正停止,而非仅发出取消请求。
116
+ */
117
+ private lease;
118
+ /** 上传一片:先锁会话认领片号,再释放事务写存储,最后凭 token 确认结果。
119
+ * 相同片号和摘要的已完成请求可重放;内容变化或有效租约冲突会拒绝。尾片长度从总大小计算。
120
+ * IO 失败会销毁输入流并释放属于本 token 的租约,已被其他执行接管的记录保持不动。
121
+ */
122
+ putPart(id: string, number: number, sha256: string, source: Readable, ctx: Ctx): Promise<UploadPart>;
123
+ /** 最多读取首片前 8192 字节检查 MIME;文本仅接受合法 UTF-8 和允许的控制字符。
124
+ * 这是内容类型核对,不是病毒扫描,也不会验证整个 JSON/CSV 的业务格式。
125
+ */
126
+ private verifyType;
127
+ /** 确认片号连续且全部落盘后认领合并租约,验证类型并生成最终对象,再原子进入 ready。
128
+ * ready/bound 的重放不重复合并;失败只回退本次 token 仍持有的 completing 状态,允许后续安全恢复。
129
+ * 返回 ready 后仍需由调用方执行目标业务命令绑定附件,不能把上传成功当作业务保存成功。
130
+ */
131
+ complete(id: string, ctx: Ctx): Promise<UploadSession>;
132
+ /** 标记未绑定会话待删除并使执行 token 失效;实际字节回收交给 cleanup,故返回不表示磁盘已清空。 */
133
+ cancel(id: string, ctx: Ctx): Promise<{
134
+ ok: boolean;
135
+ }>;
136
+ /** 下载授权:已绑定文件必须仍存在于当前可读业务对象的对应字段;ready 暂存只允许上传者访问。
137
+ * 每次重新读取对象与范围,隐藏字段/撤权/解绑均可能使文件不可见,不依赖曾经拿到的文件 ID。
138
+ */
139
+ private accessible;
140
+ /** 返回授权后的展示元数据;不创建内容流,下载调用仍须再次检查并可靠审计。 */
141
+ info(id: string, ctx: Ctx): Promise<FileInfo>;
142
+ /** 在当前权限下读取完整文件或闭区间,并在交付流之前提交下载审计。
143
+ * 事务内锁会话再次核对绑定/到期状态,与解绑清理排序;审计失败即拒绝交付,不降级为未审计下载。
144
+ * 返回的 stream 由 HTTP 层消费;存储读取失败会直接上抛,内容不在数据库事务内传输。
145
+ */
146
+ download(id: string, ctx: Ctx, range?: ByteRange): Promise<{
147
+ info: {
148
+ id: string;
149
+ filename: any;
150
+ bytes: number;
151
+ contentType: any;
152
+ };
153
+ stream: Readable;
154
+ }>;
155
+ /** API 后台周期调用;数据库先认领,再在事务外删除。保留墓碑以回收迟到 IO 的残留文件。
156
+ * 每轮最多处理 limit 个过期会话及 limit 个已完成对象;有效合并/分片租约会推迟删除。
157
+ * 删除失败保留 deleting 供下轮重试;墓碑一天后重查,已完成对象则只回收其非最终分片。
158
+ */
159
+ cleanup(limit?: number): Promise<void>;
160
+ /** 阻止新 IO,向所有本实例在途操作发出取消并等待收尾;单个取消异常不妨碍等待其他操作。 */
161
+ close(): Promise<void>;
162
+ }
@@ -0,0 +1,443 @@
1
+ // 上传会话/分片保存在 PostgreSQL;IO 在事务外执行,以 token 和租约阻止迟到结果回写。
2
+ /** 本文件连接业务授权、上传元数据和私有存储,负责 begin → 分片 → complete → ready 的生命周期。
3
+ * ready 只表示字节已完成,真正绑定由业务命令在自己的事务中完成;服务不直接修改领域对象。
4
+ * 配额和状态在短数据库事务中决定,长时间 IO 通过续租与 token 栅栏保护,清理保留墓碑兜住迟到写入。
5
+ */
6
+ import { randomUUID, createHash } from 'node:crypto';
7
+ import { sql } from 'kysely';
8
+ import { z } from 'zod';
9
+ import { fileTypeFromBuffer } from 'file-type';
10
+ import { cageError, isFile } from '@yss123/cage-kernel';
11
+ import { LocalFileStorage } from './local.js';
12
+ /** 创建上传的外部输入:命令/字段定位授权边界,targetId 仅用于修改已有对象。
13
+ * bytes 为正安全整数字节数;filename 仅作下载展示,禁止路径和控制字符;contentType 后续还会验内容。
14
+ */
15
+ export const beginUploadSchema = z.object({
16
+ command: z.string().min(1).max(180), field: z.string().min(1).max(100), targetId: z.string().uuid().optional(),
17
+ filename: z.string().min(1).max(255).refine(name => !/[\x00-\x1f\x7f/\\]/.test(name), '文件名不能包含路径或控制字符'),
18
+ bytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), contentType: z.string().max(150),
19
+ });
20
+ const SHA = /^[0-9a-f]{64}$/;
21
+ /** IO 租约窗口为两分钟,活动操作每四分之一窗口续租;与完整 IO 总时限分别控制失联和超时。 */
22
+ const LEASE_MS = 120_000;
23
+ /** 将 Zod 的路径化问题统一转换为公开 INVALID_INPUT,避免把 Zod 异常细节直接暴露给 HTTP。 */
24
+ const parse = (schema, raw) => {
25
+ const result = schema.safeParse(raw);
26
+ if (!result.success)
27
+ throw cageError('INVALID_INPUT', result.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
28
+ return result.data;
29
+ };
30
+ /** 文件业务边界的协调器;实例由 API 生命周期管理,close 会取消并等待本实例在途 IO。
31
+ * 每次访问重新检查当前身份和策略,不把创建会话时的授权当作后续读写的永久许可。
32
+ */
33
+ export class FileService {
34
+ kernel;
35
+ /** 持久字节适配器;名称同时写入会话,防止跨存储空间误读或误清理。 */
36
+ storage;
37
+ /** 已通过适配器限制校验的分片字节数,供 HTTP 层设置请求体限制。 */
38
+ partBytes;
39
+ /** 服务级文件大小上限,单位字节。 */
40
+ maxFileBytes;
41
+ options;
42
+ active = new Map();
43
+ closing = false;
44
+ /** 合并默认策略并做启动期配置校验;不创建上传、不访问存储、不启动定时清理。 */
45
+ constructor(kernel, options = {}) {
46
+ this.kernel = kernel;
47
+ this.storage = options.storage ?? new LocalFileStorage(options.directory);
48
+ this.options = {
49
+ partBytes: 16 * 1024 * 1024, maxFileBytes: 2 * 1024 ** 3,
50
+ maxPendingBytesPerUser: 8 * 1024 ** 3, maxStoredBytesPerTenant: 100 * 1024 ** 3,
51
+ maxPendingUploadsPerUser: 100, retentionMs: 86400_000, ioTimeoutMs: 1800_000,
52
+ ...Object.fromEntries(Object.entries(options).filter(([k, v]) => k !== 'storage' && k !== 'directory' && v !== undefined)),
53
+ };
54
+ for (const [name, value] of Object.entries(this.options))
55
+ if (!Number.isSafeInteger(value) || value < 1)
56
+ throw new Error(`[CAGE-FILE-001] ${name} 必须为正安全整数`);
57
+ const limits = this.storage.limits;
58
+ if (this.options.partBytes < limits.minPartBytes || this.options.partBytes > limits.maxPartBytes || Math.ceil(this.options.maxFileBytes / this.options.partBytes) > limits.maxParts)
59
+ throw new Error('[CAGE-FILE-001] 分片配置超出存储适配器限制');
60
+ this.partBytes = this.options.partBytes;
61
+ this.maxFileBytes = this.options.maxFileBytes;
62
+ }
63
+ /** 上传/下载入口仅接受具名 user 身份;流程、任务及匿名上下文不能借此占用私有存储。 */
64
+ identity(ctx) {
65
+ if (!ctx.userId || ctx.actor?.kind !== 'user')
66
+ throw cageError('UNAUTHENTICATED');
67
+ }
68
+ /** 验证命令能力、文件字段及输入归属;已有对象还要通过当前 read 范围和起始状态检查。
69
+ * 这里只判断上传资格,最终业务命令仍会独立检查版本、硬约束、不变量和绑定合法性。
70
+ */
71
+ async policy(command, field, targetId, ctx) {
72
+ this.identity(ctx);
73
+ const registered = this.kernel.registry.command(command), definition = registered?.cmd;
74
+ if (!registered || !definition?.invokedBy.includes('user') || definition.public)
75
+ throw cageError('NOT_FOUND');
76
+ if (!ctx.capabilities.has(command))
77
+ throw cageError('FORBIDDEN', { capability: command });
78
+ const f = definition.agg.fields[field];
79
+ // 上传入口必须明确关联一个能接收该字段的用户命令,不能借只读/无关命令获得暂存空间。
80
+ if (!f || !isFile(f) || !(definition.input instanceof z.ZodObject) || !Object.hasOwn(definition.input.shape, field))
81
+ throw cageError('INVALID_INPUT', '目标命令未声明该文件输入字段');
82
+ if (definition.from === 'new') {
83
+ if (targetId)
84
+ throw cageError('INVALID_INPUT', '新建附件会话不能指定已有对象');
85
+ }
86
+ else {
87
+ if (!targetId)
88
+ throw cageError('INVALID_INPUT', '修改附件需要目标对象 ID');
89
+ const object = await this.kernel.read.get(definition.agg, targetId, ctx);
90
+ if (!definition.from.includes(object.status))
91
+ throw cageError('INVALID_TRANSITION');
92
+ }
93
+ return f;
94
+ }
95
+ /** 取得当前租户当前上传者的会话,并重新校验业务策略;不存在或存储不匹配统一隐藏为 NOT_FOUND。 */
96
+ async owned(id, ctx) {
97
+ this.identity(ctx);
98
+ parse(z.string().uuid(), id);
99
+ const row = await this.kernel.app.selectFrom('sys.file_upload').selectAll().where('id', '=', id).where('tenant_id', '=', ctx.tenantId).where('user_id', '=', ctx.userId).executeTakeFirst();
100
+ if (!row || row.storage !== this.storage.name)
101
+ throw cageError('NOT_FOUND');
102
+ await this.policy(row.command_name, row.field_name, row.target_id, ctx);
103
+ return row;
104
+ }
105
+ /** 在调用方事务中锁会话行,串行化修改、完成和取消;调用方应已用 owned/policy 完成授权。 */
106
+ async locked(tx, id, ctx) {
107
+ const row = await tx.selectFrom('sys.file_upload').selectAll().where('id', '=', id).where('tenant_id', '=', ctx.tenantId).where('user_id', '=', ctx.userId).forUpdate().executeTakeFirst();
108
+ if (!row || row.storage !== this.storage.name)
109
+ throw cageError('NOT_FOUND');
110
+ return row;
111
+ }
112
+ /** 可修改性检查与授权分离;过期、已绑定或进入删除阶段的会话不允许迟到写入重新激活。 */
113
+ usable(row) {
114
+ if (new Date(row.expires_at) <= new Date() || ['deleting', 'deleted', 'bound'].includes(row.status))
115
+ throw cageError('CONFLICT', '上传会话已过期或不可修改');
116
+ }
117
+ /** 将数据库 bigint/日期与内部状态投影成公开 DTO,只列出确实已有存储键的已确认分片。 */
118
+ view(row, parts) {
119
+ return { id: row.id, aggregate: row.aggregate_name, field: row.field_name, command: row.command_name, targetId: row.target_id,
120
+ filename: row.filename, bytes: Number(row.bytes), contentType: row.content_type, status: row.status,
121
+ partBytes: row.part_bytes, partCount: row.part_count, expiresAt: new Date(row.expires_at).toISOString(),
122
+ parts: parts.filter(p => p.storage_key).map(p => ({ number: p.number, bytes: p.bytes, sha256: p.sha256 })) };
123
+ }
124
+ /** 按片号返回内部元数据;仅供已完成所有权检查的调用使用,不能直接作为公开查询入口。 */
125
+ parts(id) { return this.kernel.app.selectFrom('sys.file_part').selectAll().where('upload_id', '=', id).orderBy('number').execute(); }
126
+ /** 创建或重放同一次上传申请。租户咨询锁让多实例在同一短事务内核对并占用配额。
127
+ * 相同用户的同一幂等键只能对应相同规范请求和存储;超配额抛 RATE_LIMITED,不产生半成品会话。
128
+ * 此步骤只预留元数据与额度,客户端之后使用返回的 partBytes/partCount 上传字节。
129
+ */
130
+ async begin(raw, ctx, idempotencyKey) {
131
+ if (this.closing)
132
+ throw cageError('CONFLICT', '文件服务正在停止');
133
+ const request = parse(beginUploadSchema, raw);
134
+ parse(z.string().min(1).max(200), idempotencyKey);
135
+ const field = await this.policy(request.command, request.field, request.targetId ?? null, ctx);
136
+ if (request.bytes > Math.min(field.filePolicy.maxBytes, this.maxFileBytes) || !field.filePolicy.accept.includes(request.contentType))
137
+ throw cageError('INVALID_INPUT', '文件大小或类型不符合字段限制');
138
+ const hash = createHash('sha256').update(JSON.stringify(request)).digest('hex');
139
+ const row = await this.kernel.app.transaction().execute(async (tx) => {
140
+ // 所有实例共用租户配额锁;只锁短元数据事务,不持有数据库事务传输字节。
141
+ await sql `select pg_advisory_xact_lock(hashtextextended(${`cage:files:${ctx.tenantId}`}, 0))`.execute(tx);
142
+ const existing = await tx.selectFrom('sys.file_upload').selectAll().where('tenant_id', '=', ctx.tenantId).where('user_id', '=', ctx.userId).where('idempotency_key', '=', idempotencyKey).executeTakeFirst();
143
+ if (existing) {
144
+ if (existing.request_hash !== hash || existing.storage !== this.storage.name)
145
+ throw cageError('CONFLICT', '幂等键已用于其他上传');
146
+ return existing;
147
+ }
148
+ const quota = await sql `select coalesce(sum(bytes),0) as total,
149
+ coalesce(sum(bytes) filter (where user_id = ${ctx.userId}::uuid and status <> 'bound'),0) as pending,
150
+ count(*) filter (where user_id = ${ctx.userId}::uuid and status <> 'bound') as count
151
+ from sys.file_upload where tenant_id = ${ctx.tenantId}::uuid and status <> 'deleted'`.execute(tx);
152
+ const usage = quota.rows[0];
153
+ if (Number(usage.total) + request.bytes > this.options.maxStoredBytesPerTenant || Number(usage.pending) + request.bytes > this.options.maxPendingBytesPerUser || Number(usage.count) >= this.options.maxPendingUploadsPerUser)
154
+ throw cageError('RATE_LIMITED', '文件存储或临时上传配额已满');
155
+ return tx.insertInto('sys.file_upload').values({ id: randomUUID(), tenant_id: ctx.tenantId, user_id: ctx.userId,
156
+ aggregate_name: this.kernel.registry.command(request.command).cmd.agg.name, field_name: request.field, command_name: request.command,
157
+ target_id: request.targetId ?? null, filename: request.filename, content_type: request.contentType, bytes: request.bytes,
158
+ part_bytes: this.partBytes, part_count: Math.ceil(request.bytes / this.partBytes), storage: this.storage.name, status: 'uploading',
159
+ expires_at: new Date(Date.now() + this.options.retentionMs), idempotency_key: idempotencyKey, request_hash: hash }).returningAll().executeTakeFirstOrThrow();
160
+ });
161
+ return this.view(row, await this.parts(row.id));
162
+ }
163
+ /** 断点恢复的权威进度查询;返回服务端已确认片清单,客户端不能凭本地发送进度认定上传成功。 */
164
+ async get(id, ctx) { return this.view(await this.owned(id, ctx), await this.parts(id)); }
165
+ /** 查询同一业务入口下本人未过期的可恢复会话,最多一百条;摘要不携带完整分片表。 */
166
+ async list(command, field, targetId, ctx) {
167
+ await this.policy(command, field, targetId, ctx);
168
+ let query = this.kernel.app.selectFrom('sys.file_upload').selectAll().where('tenant_id', '=', ctx.tenantId).where('user_id', '=', ctx.userId)
169
+ .where('command_name', '=', command).where('field_name', '=', field).where('storage', '=', this.storage.name).where('status', 'in', ['uploading', 'completing', 'ready']).where('expires_at', '>', new Date());
170
+ query = targetId ? query.where('target_id', '=', targetId) : query.where('target_id', 'is', null);
171
+ const rows = await query.orderBy('created_at', 'desc').limit(100).execute();
172
+ // 列表只给会话摘要;恢复时 get 取得服务端确认的分片清单。
173
+ return rows.map(row => this.view(row, []));
174
+ }
175
+ /** 为事务外 IO 管理续租、总时限与停机取消。
176
+ * 续租丢失或数据库异常即中止信号;run 必须在提交元数据前再次检查信号和 token,避免迟到完成覆盖新执行者。
177
+ * active 持有 Promise 直到 finally,用于 close 等待真正停止,而非仅发出取消请求。
178
+ */
179
+ lease(renew, run) {
180
+ if (this.closing)
181
+ return Promise.reject(cageError('CONFLICT', '文件服务正在停止'));
182
+ const controller = new AbortController();
183
+ let renewing = false;
184
+ const timer = setInterval(async () => {
185
+ if (renewing)
186
+ return;
187
+ renewing = true;
188
+ try {
189
+ if (!await renew())
190
+ controller.abort();
191
+ }
192
+ catch {
193
+ controller.abort();
194
+ }
195
+ finally {
196
+ renewing = false;
197
+ }
198
+ }, LEASE_MS / 4);
199
+ const timeout = setTimeout(() => controller.abort(), this.options.ioTimeoutMs);
200
+ const result = Promise.resolve().then(() => run(controller.signal)).finally(() => { clearInterval(timer); clearTimeout(timeout); this.active.delete(controller); });
201
+ this.active.set(controller, result);
202
+ return result;
203
+ }
204
+ /** 上传一片:先锁会话认领片号,再释放事务写存储,最后凭 token 确认结果。
205
+ * 相同片号和摘要的已完成请求可重放;内容变化或有效租约冲突会拒绝。尾片长度从总大小计算。
206
+ * IO 失败会销毁输入流并释放属于本 token 的租约,已被其他执行接管的记录保持不动。
207
+ */
208
+ async putPart(id, number, sha256, source, ctx) {
209
+ await this.owned(id, ctx);
210
+ if (!SHA.test(sha256) || !Number.isInteger(number) || number < 1)
211
+ throw cageError('INVALID_INPUT', '分片编号或 SHA-256 非法');
212
+ const token = randomUUID();
213
+ const claim = await this.kernel.app.transaction().execute(async (tx) => {
214
+ const row = await this.locked(tx, id, ctx);
215
+ this.usable(row);
216
+ if (row.status !== 'uploading' || number > row.part_count)
217
+ throw cageError('CONFLICT', '会话不能接收该分片');
218
+ const bytes = number === row.part_count ? Number(row.bytes) - (number - 1) * row.part_bytes : row.part_bytes;
219
+ const old = await tx.selectFrom('sys.file_part').selectAll().where('upload_id', '=', id).where('number', '=', number).executeTakeFirst();
220
+ if (old?.storage_key) {
221
+ if (old.sha256 !== sha256)
222
+ throw cageError('CONFLICT', '相同片号的内容不一致');
223
+ return { bytes, replay: true };
224
+ }
225
+ if (old?.lease_until && new Date(old.lease_until) > new Date())
226
+ throw cageError('CONFLICT', '分片正在上传');
227
+ await tx.insertInto('sys.file_part').values({ upload_id: id, number, bytes, sha256, token, lease_until: new Date(Date.now() + LEASE_MS) })
228
+ .onConflict(oc => oc.columns(['upload_id', 'number']).doUpdateSet({ bytes, sha256, token, storage_key: null, lease_until: new Date(Date.now() + LEASE_MS) })).execute();
229
+ return { bytes, replay: false };
230
+ });
231
+ if (claim.replay) {
232
+ source.resume();
233
+ return { number, bytes: claim.bytes, sha256 };
234
+ }
235
+ try {
236
+ return await this.lease(async () => {
237
+ const updated = await this.kernel.app.updateTable('sys.file_part').set({ lease_until: new Date(Date.now() + LEASE_MS) }).where('upload_id', '=', id).where('number', '=', number).where('token', '=', token).where('storage_key', 'is', null).executeTakeFirst();
238
+ return Number(updated.numUpdatedRows) === 1;
239
+ }, async (signal) => {
240
+ const key = await this.storage.writePart(id, token, source, { bytes: claim.bytes, sha256, signal });
241
+ signal.throwIfAborted();
242
+ await this.kernel.app.transaction().execute(async (tx) => {
243
+ const row = await this.locked(tx, id, ctx);
244
+ this.usable(row);
245
+ if (row.status !== 'uploading')
246
+ throw cageError('CONFLICT');
247
+ const updated = await tx.updateTable('sys.file_part').set({ storage_key: key, lease_until: null }).where('upload_id', '=', id).where('number', '=', number).where('token', '=', token).executeTakeFirst();
248
+ if (Number(updated.numUpdatedRows) !== 1)
249
+ throw cageError('CONFLICT', '分片执行租约已失效');
250
+ });
251
+ return { number, bytes: claim.bytes, sha256 };
252
+ });
253
+ }
254
+ catch (e) {
255
+ source.destroy();
256
+ await this.kernel.app.updateTable('sys.file_part').set({ lease_until: null }).where('upload_id', '=', id).where('number', '=', number).where('token', '=', token).where('storage_key', 'is', null).execute();
257
+ throw e;
258
+ }
259
+ }
260
+ /** 最多读取首片前 8192 字节检查 MIME;文本仅接受合法 UTF-8 和允许的控制字符。
261
+ * 这是内容类型核对,不是病毒扫描,也不会验证整个 JSON/CSV 的业务格式。
262
+ */
263
+ async verifyType(key, expected) {
264
+ const stream = await this.storage.read(key, { start: 0, end: 8191 });
265
+ const buffers = [];
266
+ try {
267
+ for await (const chunk of stream)
268
+ buffers.push(Buffer.from(chunk));
269
+ }
270
+ finally {
271
+ stream.destroy();
272
+ }
273
+ const prefix = Buffer.concat(buffers);
274
+ let detected;
275
+ try {
276
+ detected = (await fileTypeFromBuffer(prefix))?.mime;
277
+ }
278
+ catch { /* 短文本或截断头由下方保守判断。 */ }
279
+ if (!detected && ['text/plain', 'text/csv', 'application/json'].includes(expected)) {
280
+ try {
281
+ // 前缀末尾可能落在 UTF-8 多字节字符中间,stream 模式不误拒绝合法边界。
282
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(prefix, { stream: true });
283
+ if (!/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(text))
284
+ detected = expected;
285
+ }
286
+ catch { /* 非 UTF-8 内容不视为文本。 */ }
287
+ }
288
+ if ((detected ?? 'application/octet-stream') !== expected)
289
+ throw cageError('INVALID_INPUT', '文件内容与声明类型不一致');
290
+ }
291
+ /** 确认片号连续且全部落盘后认领合并租约,验证类型并生成最终对象,再原子进入 ready。
292
+ * ready/bound 的重放不重复合并;失败只回退本次 token 仍持有的 completing 状态,允许后续安全恢复。
293
+ * 返回 ready 后仍需由调用方执行目标业务命令绑定附件,不能把上传成功当作业务保存成功。
294
+ */
295
+ async complete(id, ctx) {
296
+ const owned = await this.owned(id, ctx);
297
+ // 已绑定后的重试不能重新合并或更改内容。
298
+ if (owned.status === 'bound')
299
+ return this.view(owned, []);
300
+ const token = randomUUID();
301
+ const claim = await this.kernel.app.transaction().execute(async (tx) => {
302
+ const row = await this.locked(tx, id, ctx);
303
+ this.usable(row);
304
+ if (row.status === 'ready')
305
+ return { row, parts: [], replay: true };
306
+ if (row.status === 'completing' && row.lease_until && new Date(row.lease_until) > new Date())
307
+ throw cageError('CONFLICT', '文件正在校验与合并');
308
+ const parts = await tx.selectFrom('sys.file_part').selectAll().where('upload_id', '=', id).orderBy('number').execute();
309
+ if (parts.length !== row.part_count || parts.some((p, i) => p.number !== i + 1 || !p.storage_key))
310
+ throw cageError('CONFLICT', '分片尚未全部完成');
311
+ await tx.updateTable('sys.file_upload').set({ status: 'completing', operation_token: token, lease_until: new Date(Date.now() + LEASE_MS) }).where('id', '=', id).execute();
312
+ return { row, parts, replay: false };
313
+ });
314
+ if (claim.replay)
315
+ return this.view(claim.row, await this.parts(id));
316
+ try {
317
+ return await this.lease(async () => {
318
+ const updated = await this.kernel.app.updateTable('sys.file_upload').set({ lease_until: new Date(Date.now() + LEASE_MS) }).where('id', '=', id).where('status', '=', 'completing').where('operation_token', '=', token).executeTakeFirst();
319
+ return Number(updated.numUpdatedRows) === 1;
320
+ }, async (signal) => {
321
+ const parts = claim.parts.map(p => ({ number: p.number, bytes: p.bytes, sha256: p.sha256, key: p.storage_key }));
322
+ await this.verifyType(parts[0].key, claim.row.content_type);
323
+ const key = await this.storage.complete(id, token, parts, { bytes: Number(claim.row.bytes), signal });
324
+ signal.throwIfAborted();
325
+ const row = await this.kernel.app.transaction().execute(async (tx) => {
326
+ const row = await this.locked(tx, id, ctx);
327
+ this.usable(row);
328
+ if (row.status !== 'completing' || row.operation_token !== token)
329
+ throw cageError('CONFLICT');
330
+ return tx.updateTable('sys.file_upload').set({ status: 'ready', storage_key: key, operation_token: null, lease_until: null })
331
+ .where('id', '=', id).returningAll().executeTakeFirstOrThrow();
332
+ });
333
+ return this.view(row, claim.parts);
334
+ });
335
+ }
336
+ catch (e) {
337
+ await this.kernel.app.updateTable('sys.file_upload').set({ status: 'uploading', operation_token: null, lease_until: null }).where('id', '=', id).where('status', '=', 'completing').where('operation_token', '=', token).execute();
338
+ throw e;
339
+ }
340
+ }
341
+ /** 标记未绑定会话待删除并使执行 token 失效;实际字节回收交给 cleanup,故返回不表示磁盘已清空。 */
342
+ async cancel(id, ctx) {
343
+ await this.owned(id, ctx);
344
+ await this.kernel.app.transaction().execute(async (tx) => {
345
+ const row = await this.locked(tx, id, ctx);
346
+ if (row.status === 'bound')
347
+ throw cageError('CONFLICT', '已绑定附件必须通过业务命令移除');
348
+ await tx.updateTable('sys.file_upload').set({ status: row.status === 'deleted' ? 'deleted' : 'deleting', expires_at: new Date(), operation_token: null }).where('id', '=', id).execute();
349
+ });
350
+ return { ok: true };
351
+ }
352
+ /** 下载授权:已绑定文件必须仍存在于当前可读业务对象的对应字段;ready 暂存只允许上传者访问。
353
+ * 每次重新读取对象与范围,隐藏字段/撤权/解绑均可能使文件不可见,不依赖曾经拿到的文件 ID。
354
+ */
355
+ async accessible(id, ctx) {
356
+ this.identity(ctx);
357
+ parse(z.string().uuid(), id);
358
+ const row = await this.kernel.app.selectFrom('sys.file_upload').selectAll().where('id', '=', id).where('tenant_id', '=', ctx.tenantId).executeTakeFirst();
359
+ if (!row || row.storage !== this.storage.name || !row.storage_key)
360
+ throw cageError('NOT_FOUND');
361
+ if (row.status === 'bound') {
362
+ const aggregate = this.kernel.registry.aggregate(row.aggregate_name), field = aggregate?.fields[row.field_name];
363
+ if (!aggregate || !field || !isFile(field))
364
+ throw cageError('NOT_FOUND');
365
+ const object = await this.kernel.read.get(aggregate, row.bound_id, ctx);
366
+ const value = object[row.field_name];
367
+ if (!(Array.isArray(value) ? value.includes(id) : value === id))
368
+ throw cageError('NOT_FOUND');
369
+ }
370
+ else {
371
+ if (row.status !== 'ready' || row.user_id !== ctx.userId || new Date(row.expires_at) <= new Date())
372
+ throw cageError('NOT_FOUND');
373
+ await this.policy(row.command_name, row.field_name, row.target_id, ctx);
374
+ }
375
+ return row;
376
+ }
377
+ /** 返回授权后的展示元数据;不创建内容流,下载调用仍须再次检查并可靠审计。 */
378
+ async info(id, ctx) {
379
+ const row = await this.accessible(id, ctx);
380
+ return { id: row.id, filename: row.filename, bytes: Number(row.bytes), contentType: row.content_type };
381
+ }
382
+ /** 在当前权限下读取完整文件或闭区间,并在交付流之前提交下载审计。
383
+ * 事务内锁会话再次核对绑定/到期状态,与解绑清理排序;审计失败即拒绝交付,不降级为未审计下载。
384
+ * 返回的 stream 由 HTTP 层消费;存储读取失败会直接上抛,内容不在数据库事务内传输。
385
+ */
386
+ async download(id, ctx, range) {
387
+ const row = await this.accessible(id, ctx), bytes = Number(row.bytes);
388
+ if (range && (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end < range.start || range.end >= bytes))
389
+ throw cageError('INVALID_INPUT', '下载范围非法');
390
+ await this.kernel.app.transaction().execute(async (tx) => {
391
+ // 与解绑/清理串行决定是否交付;权限使用当前请求上下文,下载从不携带持久 bearer URL。
392
+ const current = await tx.selectFrom('sys.file_upload').selectAll().where('id', '=', id).where('tenant_id', '=', ctx.tenantId).forUpdate().executeTakeFirstOrThrow();
393
+ if (current.status !== row.status || current.bound_id !== row.bound_id || (current.status !== 'bound' && new Date(current.expires_at) <= new Date()))
394
+ throw cageError('NOT_FOUND');
395
+ await this.kernel.writeAudit(tx, { tenantId: ctx.tenantId, actor: ctx.actor.id, actorKind: 'user', source: ctx.source,
396
+ kind: 'read', capability: `${row.aggregate_name}.read`, aggregate: row.aggregate_name, aggregateId: row.bound_id,
397
+ result: 'ok', diff: { fileId: id, field: row.field_name, operation: 'file.download', ...(range ? { range } : {}) }, requestId: ctx.requestId });
398
+ });
399
+ return { info: { id, filename: row.filename, bytes, contentType: row.content_type }, stream: await this.storage.read(row.storage_key, range) };
400
+ }
401
+ /** API 后台周期调用;数据库先认领,再在事务外删除。保留墓碑以回收迟到 IO 的残留文件。
402
+ * 每轮最多处理 limit 个过期会话及 limit 个已完成对象;有效合并/分片租约会推迟删除。
403
+ * 删除失败保留 deleting 供下轮重试;墓碑一天后重查,已完成对象则只回收其非最终分片。
404
+ */
405
+ async cleanup(limit = 100) {
406
+ const now = new Date();
407
+ const candidates = await this.kernel.app.selectFrom('sys.file_upload').select('id').where('storage', '=', this.storage.name)
408
+ .where('status', 'in', ['uploading', 'completing', 'ready', 'deleting', 'deleted']).where('expires_at', '<=', now).orderBy('expires_at').limit(limit).execute();
409
+ for (const candidate of candidates) {
410
+ const claimed = await this.kernel.app.transaction().execute(async (tx) => {
411
+ const row = await tx.selectFrom('sys.file_upload').selectAll().where('id', '=', candidate.id).forUpdate().executeTakeFirstOrThrow();
412
+ if (row.status === 'bound' || new Date(row.expires_at) > now || (row.lease_until && new Date(row.lease_until) > now))
413
+ return false;
414
+ const inFlight = await tx.selectFrom('sys.file_part').select('number').where('upload_id', '=', row.id).where('lease_until', '>', now).limit(1).executeTakeFirst();
415
+ if (inFlight)
416
+ return false;
417
+ await tx.updateTable('sys.file_upload').set({ status: 'deleting', operation_token: null }).where('id', '=', row.id).execute();
418
+ return true;
419
+ });
420
+ if (!claimed)
421
+ continue;
422
+ await this.storage.remove(candidate.id);
423
+ // 墓碑每天复查,避免大量旧墓碑占满每一轮待清理队列。
424
+ await this.kernel.app.updateTable('sys.file_upload').set({ status: 'deleted', storage_key: null, expires_at: new Date(Date.now() + 86400_000) }).where('id', '=', candidate.id).where('status', '=', 'deleting').execute();
425
+ }
426
+ const completed = await this.kernel.app.selectFrom('sys.file_upload').select(['id', 'storage_key']).where('storage', '=', this.storage.name).where('status', 'in', ['ready', 'bound']).where('storage_key', 'is not', null)
427
+ .where(eb => eb.or([eb('pruned_at', 'is', null), eb('pruned_at', '<', new Date(Date.now() - 86400_000))]))
428
+ .orderBy(sql `pruned_at asc nulls first`).limit(limit).execute();
429
+ for (const row of completed) {
430
+ await this.storage.prune(row.id, row.storage_key);
431
+ await this.kernel.app.updateTable('sys.file_upload').set({ pruned_at: new Date() }).where('id', '=', row.id).execute();
432
+ }
433
+ }
434
+ /** 阻止新 IO,向所有本实例在途操作发出取消并等待收尾;单个取消异常不妨碍等待其他操作。 */
435
+ async close() {
436
+ this.closing = true;
437
+ const work = [...this.active];
438
+ for (const [controller] of work)
439
+ controller.abort();
440
+ await Promise.allSettled(work.map(([, promise]) => promise));
441
+ }
442
+ }
443
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Readable } from 'node:stream';
2
+ /** 已由元数据层确认的分片。number 从 1 开始;bytes 是精确字节数;sha256 为小写十六进制摘要。
3
+ * key 是适配器返回的不透明位置,不向浏览器暴露,也不承担业务授权职责。
4
+ */
5
+ export interface StoredPart {
6
+ number: number;
7
+ key: string;
8
+ bytes: number;
9
+ sha256: string;
10
+ }
11
+ /** 下载的闭区间,两端都包含在返回内容中;合法性由 FileService 按文件总长度检查。 */
12
+ export interface ByteRange {
13
+ start: number;
14
+ end: number;
15
+ }
16
+ /** 私有字节存储协议,由 FileService 在数据库事务外调用。
17
+ * 适配器负责内容完整性、流式 IO 和取消;会话所有权、配额、租约与业务绑定由上层处理。
18
+ * 写入返回的 key 必须在进程重启后仍可读取;失败不得让其他执行 token 的内容失效。
19
+ */
20
+ export interface FileStorage {
21
+ /** 持久标识;重启后必须指向同一存储空间。 */
22
+ readonly name: string;
23
+ /** 分片最小/最大字节数及每文件最大分片数;服务启动时据此拒绝不兼容配置。 */
24
+ readonly limits: {
25
+ minPartBytes: number;
26
+ maxPartBytes: number;
27
+ maxParts: number;
28
+ };
29
+ /** 所有操作都必须可重试;token 隔离过期执行者,不能覆盖其他 token 的产物。 */
30
+ writePart(upload: string, token: string, source: Readable, options: {
31
+ bytes: number;
32
+ sha256: string;
33
+ signal: AbortSignal;
34
+ }): Promise<string>;
35
+ /** 按调用方给出的顺序合并已确认分片;bytes 是最终总长度,signal 失效后不得报告成功。 */
36
+ complete(upload: string, token: string, parts: readonly StoredPart[], options: {
37
+ bytes: number;
38
+ signal: AbortSignal;
39
+ }): Promise<string>;
40
+ /** 返回可读流而非完整 Buffer;调用方负责消费或销毁流,适配器不得公开持久访问 URL。 */
41
+ read(key: string, range?: ByteRange): Promise<Readable>;
42
+ /** 删除该上传下的分片及最终内容;没有内容也应成功。 */
43
+ remove(upload: string): Promise<void>;
44
+ /** 完成后的分片回收,不删除最终内容;失败时后续周期重试。 */
45
+ prune(upload: string, keepKey: string): Promise<void>;
46
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@yss123/cage-files",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.1",
4
+ "description": "Cage 私有文件与可恢复分片上传:本地存储及对象存储适配接口",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "kysely": "^0.28.5",
19
+ "zod": "^4.1.5",
20
+ "file-type": "^21.0.0",
21
+ "@yss123/cage-kernel": "0.3.1"
22
+ },
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/shunshunyue/cage.git",
27
+ "directory": "packages/files"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "engines": {
34
+ "node": ">=24"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit"
39
+ }
6
40
  }