@coralai/sps-cli 0.90.0 → 0.90.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/assembly/base.yml +4 -4
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +0 -2
- package/dist/console/index.js.map +1 -1
- package/dist/core/agents/ClaudeSdkBackend.d.ts +9 -0
- package/dist/core/agents/ClaudeSdkBackend.d.ts.map +1 -1
- package/dist/core/agents/ClaudeSdkBackend.js +5 -0
- package/dist/core/agents/ClaudeSdkBackend.js.map +1 -1
- package/dist/core/pathFence.d.ts +14 -14
- package/dist/core/pathFence.d.ts.map +1 -1
- package/dist/core/pathFence.js +11 -51
- package/dist/core/pathFence.js.map +1 -1
- package/dist/daemon/sessionDaemon.d.ts +16 -3
- package/dist/daemon/sessionDaemon.d.ts.map +1 -1
- package/dist/daemon/sessionDaemon.js +16 -7
- package/dist/daemon/sessionDaemon.js.map +1 -1
- package/dist/seams/agents-builtin/index.d.ts.map +1 -1
- package/dist/seams/agents-builtin/index.js +66 -51
- package/dist/seams/agents-builtin/index.js.map +1 -1
- package/dist/seams/memory/index.d.ts +8 -0
- package/dist/seams/memory/index.d.ts.map +1 -1
- package/dist/seams/memory/index.js +10 -0
- package/dist/seams/memory/index.js.map +1 -1
- package/dist/seams/memory/optional.d.ts +5 -0
- package/dist/seams/memory/optional.d.ts.map +1 -1
- package/dist/seams/memory/optional.js +7 -1
- package/dist/seams/memory/optional.js.map +1 -1
- package/dist/seams/workspace/index.d.ts +40 -0
- package/dist/seams/workspace/index.d.ts.map +1 -0
- package/dist/seams/workspace/index.js +46 -0
- package/dist/seams/workspace/index.js.map +1 -0
- package/docs/help/02-commands.md +2 -2
- package/docs/help/05-api.md +2 -12
- package/package.json +2 -2
- package/dist/console/routes/storage.d.ts +0 -21
- package/dist/console/routes/storage.d.ts.map +0 -1
- package/dist/console/routes/storage.js +0 -115
- package/dist/console/routes/storage.js.map +0 -1
- package/dist/daemon/storageSyncLoop.d.ts +0 -9
- package/dist/daemon/storageSyncLoop.d.ts.map +0 -1
- package/dist/daemon/storageSyncLoop.js +0 -70
- package/dist/daemon/storageSyncLoop.js.map +0 -1
- package/dist/providers/storage/FakeStorage.d.ts +0 -16
- package/dist/providers/storage/FakeStorage.d.ts.map +0 -1
- package/dist/providers/storage/FakeStorage.js +0 -37
- package/dist/providers/storage/FakeStorage.js.map +0 -1
- package/dist/providers/storage/MinioBackend.d.ts +0 -41
- package/dist/providers/storage/MinioBackend.d.ts.map +0 -1
- package/dist/providers/storage/MinioBackend.js +0 -85
- package/dist/providers/storage/MinioBackend.js.map +0 -1
- package/dist/providers/storage/StorageBackend.d.ts +0 -52
- package/dist/providers/storage/StorageBackend.d.ts.map +0 -1
- package/dist/providers/storage/StorageBackend.js +0 -29
- package/dist/providers/storage/StorageBackend.js.map +0 -1
- package/dist/providers/storage/keys.d.ts +0 -33
- package/dist/providers/storage/keys.d.ts.map +0 -1
- package/dist/providers/storage/keys.js +0 -52
- package/dist/providers/storage/keys.js.map +0 -1
- package/dist/providers/storage/storageConfig.d.ts +0 -46
- package/dist/providers/storage/storageConfig.d.ts.map +0 -1
- package/dist/providers/storage/storageConfig.js +0 -94
- package/dist/providers/storage/storageConfig.js.map +0 -1
- package/dist/providers/storage/syncProject.d.ts +0 -67
- package/dist/providers/storage/syncProject.d.ts.map +0 -1
- package/dist/providers/storage/syncProject.js +0 -136
- package/dist/providers/storage/syncProject.js.map +0 -1
- package/dist/providers/storage/syncRunner.d.ts +0 -59
- package/dist/providers/storage/syncRunner.d.ts.map +0 -1
- package/dist/providers/storage/syncRunner.js +0 -119
- package/dist/providers/storage/syncRunner.js.map +0 -1
- package/dist/providers/storage/syncable.d.ts +0 -47
- package/dist/providers/storage/syncable.d.ts.map +0 -1
- package/dist/providers/storage/syncable.js +0 -84
- package/dist/providers/storage/syncable.js.map +0 -1
- package/dist/seams/storage/index.d.ts +0 -53
- package/dist/seams/storage/index.d.ts.map +0 -1
- package/dist/seams/storage/index.js +0 -69
- package/dist/seams/storage/index.js.map +0 -1
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module daemon/storageSyncLoop
|
|
3
|
-
* @description 把工作区 → 桶的对账同步挂进 daemon 的常驻循环。
|
|
4
|
-
*
|
|
5
|
-
* @layer daemon
|
|
6
|
-
*
|
|
7
|
-
* 🔴 **为什么在 daemon 而不是 console**:console 是**共享事件循环**,同步要读整棵树、
|
|
8
|
-
* 算 md5、缩略图、发网络请求 —— 跑在请求路径里会阻塞 `/api/health` 探针与 SSE 心跳。
|
|
9
|
-
* > 判据:**同步在移交件里是对的 —— 它本来跑在 CLI 里;换了宿主之后才变错。**
|
|
10
|
-
*
|
|
11
|
-
* 🔴 **一轮没跑完不开下一轮。** 用 `setTimeout` 自排,不用 `setInterval`:
|
|
12
|
-
* `setInterval` 在一轮变慢时会叠罗汉 —— 两轮并发对同一个前缀列举 + 上传,
|
|
13
|
-
* 表现是"同步好像更忙了",实际是互相重复做功,而且**没有任何报错**。
|
|
14
|
-
*
|
|
15
|
-
* 🔴 **未配置 ⇒ 一行日志都不打。** 未配置是合法状态(sps 是通用工具,别人装它不需要对象存储);
|
|
16
|
-
* 每 N 分钟打一次"未配置"会把真正的告警淹掉。
|
|
17
|
-
*/
|
|
18
|
-
import { workspacesRoot } from '../core/workspacePaths.js';
|
|
19
|
-
import { activeStorageConfig } from '../providers/storage/storageConfig.js';
|
|
20
|
-
import { runSyncPass } from '../providers/storage/syncRunner.js';
|
|
21
|
-
/** 轮间隔。与 coral sidecar 同量级(它是分钟级);太密只会重复列举,收益为零。 */
|
|
22
|
-
const DEFAULT_INTERVAL_MS = 5 * 60_000;
|
|
23
|
-
/** 只同步近 N 天动过的项目。0 = 全部(首轮全量迁移时用)。 */
|
|
24
|
-
const DEFAULT_SINCE_DAYS = 30;
|
|
25
|
-
/** 单文件上限:显式给,不靠运气。 */
|
|
26
|
-
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
27
|
-
/**
|
|
28
|
-
* 起循环。**返回 handle 而不是 never** —— daemon 关停时要能停掉它,
|
|
29
|
-
* 否则进程退不干净(而"退不干净"的表现是重启时端口/socket 还被占着)。
|
|
30
|
-
*/
|
|
31
|
-
export function startStorageSyncLoop(log) {
|
|
32
|
-
const intervalMs = Number(process.env.SPS_STORAGE_SYNC_INTERVAL_MS) || DEFAULT_INTERVAL_MS;
|
|
33
|
-
const sinceDays = process.env.SPS_STORAGE_SYNC_SINCE_DAYS !== undefined
|
|
34
|
-
? Number(process.env.SPS_STORAGE_SYNC_SINCE_DAYS)
|
|
35
|
-
: DEFAULT_SINCE_DAYS;
|
|
36
|
-
const maxBytes = Number(process.env.SPS_STORAGE_SYNC_MAX_BYTES) || DEFAULT_MAX_BYTES;
|
|
37
|
-
let timer = null;
|
|
38
|
-
let stopped = false;
|
|
39
|
-
let announced = false;
|
|
40
|
-
const tick = async () => {
|
|
41
|
-
if (stopped)
|
|
42
|
-
return;
|
|
43
|
-
try {
|
|
44
|
-
if (activeStorageConfig() !== null && !announced) {
|
|
45
|
-
announced = true;
|
|
46
|
-
log(`[sps-sync] 常驻:根=${workspacesRoot()} 间隔=${intervalMs}ms 范围=近 ${sinceDays || '∞'} 天`);
|
|
47
|
-
}
|
|
48
|
-
await runSyncPass({ root: workspacesRoot(), sinceDays, maxBytes, log });
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
// 同步器**不能因为一轮失败就死掉**;但原因要留下。
|
|
52
|
-
log(`[sps-sync] 本轮异常:${e instanceof Error ? e.message : String(e)}`);
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
if (!stopped)
|
|
56
|
-
timer = setTimeout(() => void tick(), intervalMs);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
// 起步不立刻跑:daemon 启动那一刻正忙着恢复会话,别抢 IO。
|
|
60
|
-
timer = setTimeout(() => void tick(), 10_000);
|
|
61
|
-
return {
|
|
62
|
-
stop() {
|
|
63
|
-
stopped = true;
|
|
64
|
-
if (timer)
|
|
65
|
-
clearTimeout(timer);
|
|
66
|
-
timer = null;
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
//# sourceMappingURL=storageSyncLoop.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"storageSyncLoop.js","sourceRoot":"","sources":["../../src/daemon/storageSyncLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE,oDAAoD;AACpD,MAAM,mBAAmB,GAAG,CAAC,GAAG,MAAM,CAAC;AACvC,sCAAsC;AACtC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,sBAAsB;AACtB,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAM3C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAA2B;IAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,IAAI,mBAAmB,CAAC;IAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,KAAK,SAAS;QACrE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC;QACjD,CAAC,CAAC,kBAAkB,CAAC;IACvB,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,iBAAiB,CAAC;IAErF,IAAI,KAAK,GAAyC,IAAI,CAAC;IACvD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;QACrC,IAAI,OAAO;YAAE,OAAO;QACpB,IAAI,CAAC;YACH,IAAI,mBAAmB,EAAE,KAAK,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACjD,SAAS,GAAG,IAAI,CAAC;gBACjB,GAAG,CAAC,mBAAmB,cAAc,EAAE,OAAO,UAAU,WAAW,SAAS,IAAI,GAAG,IAAI,CAAC,CAAC;YAC3F,CAAC;YACD,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,6BAA6B;YAC7B,GAAG,CAAC,mBAAmB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvE,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO;gBAAE,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC;QAClE,CAAC;IACH,CAAC,CAAC;IAEF,oCAAoC;IACpC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IAE9C,OAAO;QACL,IAAI;YACF,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YAC/B,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type StorageBackend, type StorageObject } from './StorageBackend.js';
|
|
2
|
-
export declare class FakeStorage implements StorageBackend {
|
|
3
|
-
readonly objects: Map<string, {
|
|
4
|
-
bytes: Buffer;
|
|
5
|
-
contentType?: string;
|
|
6
|
-
etag: string;
|
|
7
|
-
}>;
|
|
8
|
-
putCalls: string[];
|
|
9
|
-
/** 下一次 put 到这个 key 时抛错(模拟上传失败)。 */
|
|
10
|
-
failNext: string | null;
|
|
11
|
-
put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
|
|
12
|
-
list(prefix: string): Promise<StorageObject[]>;
|
|
13
|
-
/** 没有这个 key ⇒ `null`(与真后端同语义:不抛)。 */
|
|
14
|
-
get(key: string): Promise<Buffer | null>;
|
|
15
|
-
}
|
|
16
|
-
//# sourceMappingURL=FakeStorage.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FakeStorage.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/FakeStorage.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAEvG,qBAAa,WAAY,YAAW,cAAc;IAChD,QAAQ,CAAC,OAAO;eAA4B,MAAM;sBAAgB,MAAM;cAAQ,MAAM;OAAM;IAC5F,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEzB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAMpD,qCAAqC;IAC/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAG/C"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/FakeStorage
|
|
3
|
-
* @description `StorageBackend` 的内存假实现。**它的存在本身就是"接口中性"的证据** ——
|
|
4
|
-
* 一个不认识 minio 的实现能满足整个接口,说明上层没有漏抽象。
|
|
5
|
-
*
|
|
6
|
-
* @layer providers
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ 放在 `.test.ts` 之外是因为 lint 禁止从测试文件导出(`noExportsInTest`),
|
|
9
|
-
* 而它被两个用例文件共用。**共用的测试替身要有自己的落点**,复制两份才是问题。
|
|
10
|
-
*/
|
|
11
|
-
import { createHash } from 'node:crypto';
|
|
12
|
-
import { StorageUnavailableError } from './StorageBackend.js';
|
|
13
|
-
export class FakeStorage {
|
|
14
|
-
objects = new Map();
|
|
15
|
-
putCalls = [];
|
|
16
|
-
/** 下一次 put 到这个 key 时抛错(模拟上传失败)。 */
|
|
17
|
-
failNext = null;
|
|
18
|
-
async put(key, bytes, contentType) {
|
|
19
|
-
if (this.failNext === key) {
|
|
20
|
-
this.failNext = null;
|
|
21
|
-
throw new StorageUnavailableError(`假的上传失败:${key}`);
|
|
22
|
-
}
|
|
23
|
-
this.putCalls.push(key);
|
|
24
|
-
// etag 用 md5:与单段上传的真实语义一致,否则用例会掩盖"分块上传 etag 不是 md5"那个坑
|
|
25
|
-
this.objects.set(key, { bytes, contentType, etag: createHash('md5').update(bytes).digest('hex') });
|
|
26
|
-
}
|
|
27
|
-
async list(prefix) {
|
|
28
|
-
return [...this.objects.entries()]
|
|
29
|
-
.filter(([k]) => k.startsWith(prefix))
|
|
30
|
-
.map(([key, v]) => ({ key, etag: v.etag, size: v.bytes.length }));
|
|
31
|
-
}
|
|
32
|
-
/** 没有这个 key ⇒ `null`(与真后端同语义:不抛)。 */
|
|
33
|
-
async get(key) {
|
|
34
|
-
return this.objects.get(key)?.bytes ?? null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
//# sourceMappingURL=FakeStorage.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FakeStorage.js","sourceRoot":"","sources":["../../../src/providers/storage/FakeStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAA2C,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEvG,MAAM,OAAO,WAAW;IACb,OAAO,GAAG,IAAI,GAAG,EAAiE,CAAC;IAC5F,QAAQ,GAAa,EAAE,CAAC;IACxB,mCAAmC;IACnC,QAAQ,GAAkB,IAAI,CAAC;IAE/B,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,WAAoB;QACxD,IAAI,IAAI,CAAC,QAAQ,KAAK,GAAG,EAAE,CAAC;YAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,MAAM,IAAI,uBAAuB,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,uDAAuD;QACvD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrG,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAc;QACvB,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;aAC/B,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;aACrC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,qCAAqC;IACrC,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IAC9C,CAAC;CACF"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/MinioBackend
|
|
3
|
-
* @description `StorageBackend` 的第一个实现:S3 兼容(MinIO)。
|
|
4
|
-
*
|
|
5
|
-
* @layer providers
|
|
6
|
-
*
|
|
7
|
-
* 🔴 **依赖走 `optionalDependencies` + 动态 import。**
|
|
8
|
-
* `@coralai/sps-cli` 是公共 npm 包,别人装它不需要对象存储。
|
|
9
|
-
* 没装 ⇒ 抛 `StorageUnavailableError` 并**指名说"未安装"**,
|
|
10
|
-
* **绝不静默降级**成"这轮没东西同步" —— 那会让"功能没装"伪装成"没有变化"。
|
|
11
|
-
*
|
|
12
|
-
* ⚠️ 这里是**唯一**允许出现 minio 类型的文件。上层只认 `StorageBackend`。
|
|
13
|
-
* 自检:换掉本文件,上层用例应当全绿(`storageBackend.test.ts` 就是拿假实现证明这一点的)。
|
|
14
|
-
*/
|
|
15
|
-
import type { StorageBackend, StorageObject } from './StorageBackend.js';
|
|
16
|
-
import type { StorageConfig } from './storageConfig.js';
|
|
17
|
-
/**
|
|
18
|
-
* `etag` 从 MinIO 回来时**带引号**(`"d41d8…"`),而本地算的 md5 不带。
|
|
19
|
-
* 不剥掉的话每一轮对账都判"变了" ⇒ **每轮重传全部文件**,而且完全不报错 ——
|
|
20
|
-
* 表现是"同步一直在工作",只是流量和耗时莫名其妙。
|
|
21
|
-
*/
|
|
22
|
-
export declare function normalizeEtag(raw: unknown): string;
|
|
23
|
-
export declare class MinioBackend implements StorageBackend {
|
|
24
|
-
private readonly cfg;
|
|
25
|
-
private client;
|
|
26
|
-
constructor(cfg: StorageConfig);
|
|
27
|
-
private ensure;
|
|
28
|
-
put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
|
|
29
|
-
list(prefix: string): Promise<StorageObject[]>;
|
|
30
|
-
/**
|
|
31
|
-
* 读一个对象。
|
|
32
|
-
*
|
|
33
|
-
* 🔴 **"没有这个 key" 与 "读不了" 必须分开**:
|
|
34
|
-
* 前者返回 `null`(调用方据此走兜底),后者抛 `StorageUnavailableError`。
|
|
35
|
-
* 混成一个的表现是"库是空的" —— 而真相可能是**权限不够**,
|
|
36
|
-
* 那会让人去补素材而不是去补策略。
|
|
37
|
-
* MinIO 的 NoSuchKey 走 code,不看消息文本(消息会随版本变)。
|
|
38
|
-
*/
|
|
39
|
-
get(key: string): Promise<Buffer | null>;
|
|
40
|
-
}
|
|
41
|
-
//# sourceMappingURL=MinioBackend.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"MinioBackend.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/MinioBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAexD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,qBAAa,YAAa,YAAW,cAAc;IAGrC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,OAAO,CAAC,MAAM,CAA0B;gBAEX,GAAG,EAAE,aAAa;YAEjC,MAAM;IAwBd,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAapE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAiBpD;;;;;;;;OAQG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAa/C"}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { StorageUnavailableError } from './StorageBackend.js';
|
|
2
|
-
/**
|
|
3
|
-
* `etag` 从 MinIO 回来时**带引号**(`"d41d8…"`),而本地算的 md5 不带。
|
|
4
|
-
* 不剥掉的话每一轮对账都判"变了" ⇒ **每轮重传全部文件**,而且完全不报错 ——
|
|
5
|
-
* 表现是"同步一直在工作",只是流量和耗时莫名其妙。
|
|
6
|
-
*/
|
|
7
|
-
export function normalizeEtag(raw) {
|
|
8
|
-
return String(raw ?? '').replace(/^"|"$/g, '');
|
|
9
|
-
}
|
|
10
|
-
export class MinioBackend {
|
|
11
|
-
cfg;
|
|
12
|
-
client = null;
|
|
13
|
-
constructor(cfg) {
|
|
14
|
-
this.cfg = cfg;
|
|
15
|
-
}
|
|
16
|
-
async ensure() {
|
|
17
|
-
if (this.client)
|
|
18
|
-
return this.client;
|
|
19
|
-
let mod;
|
|
20
|
-
try {
|
|
21
|
-
mod = (await import('minio'));
|
|
22
|
-
}
|
|
23
|
-
catch (cause) {
|
|
24
|
-
throw new StorageUnavailableError("对象存储后端未安装:缺可选依赖 'minio'。安装它(npm i minio)或关掉 ~/.coral/storage.json 的 enabled。", cause);
|
|
25
|
-
}
|
|
26
|
-
const u = new URL(this.cfg.endpoint);
|
|
27
|
-
this.client = new mod.Client({
|
|
28
|
-
endPoint: u.hostname,
|
|
29
|
-
port: u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80,
|
|
30
|
-
useSSL: u.protocol === 'https:',
|
|
31
|
-
accessKey: this.cfg.accessKey,
|
|
32
|
-
secretKey: this.cfg.secretKey,
|
|
33
|
-
region: this.cfg.region ?? 'us-east-1',
|
|
34
|
-
pathStyle: this.cfg.pathStyle ?? true,
|
|
35
|
-
});
|
|
36
|
-
return this.client;
|
|
37
|
-
}
|
|
38
|
-
async put(key, bytes, contentType) {
|
|
39
|
-
const c = await this.ensure();
|
|
40
|
-
// 显式给 size:不给的话客户端会走分块流式上传,而**分块上传的 etag 不是 md5**
|
|
41
|
-
// ⇒ 对账判据(etag == 本地 md5)当场失效,且失效方式是"永远判为不同"。
|
|
42
|
-
await c.putObject(this.cfg.bucket, key, bytes, bytes.length, contentType ? { 'Content-Type': contentType } : undefined);
|
|
43
|
-
}
|
|
44
|
-
async list(prefix) {
|
|
45
|
-
const c = await this.ensure();
|
|
46
|
-
const stream = c.listObjectsV2(this.cfg.bucket, prefix, true);
|
|
47
|
-
return await new Promise((resolve, reject) => {
|
|
48
|
-
const out = [];
|
|
49
|
-
stream.on('data', (o) => {
|
|
50
|
-
// 目录占位项没有 name/etag(MinIO 会回"前缀"条目);跳过它们,别当成对象。
|
|
51
|
-
if (!o.name)
|
|
52
|
-
return;
|
|
53
|
-
out.push({ key: o.name, etag: normalizeEtag(o.etag), size: o.size ?? 0 });
|
|
54
|
-
});
|
|
55
|
-
stream.on('end', () => resolve(out));
|
|
56
|
-
stream.on('error', (e) => reject(new StorageUnavailableError(`列举失败:${prefix}`, e)));
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* 读一个对象。
|
|
61
|
-
*
|
|
62
|
-
* 🔴 **"没有这个 key" 与 "读不了" 必须分开**:
|
|
63
|
-
* 前者返回 `null`(调用方据此走兜底),后者抛 `StorageUnavailableError`。
|
|
64
|
-
* 混成一个的表现是"库是空的" —— 而真相可能是**权限不够**,
|
|
65
|
-
* 那会让人去补素材而不是去补策略。
|
|
66
|
-
* MinIO 的 NoSuchKey 走 code,不看消息文本(消息会随版本变)。
|
|
67
|
-
*/
|
|
68
|
-
async get(key) {
|
|
69
|
-
const c = await this.ensure();
|
|
70
|
-
try {
|
|
71
|
-
const stream = await c.getObject(this.cfg.bucket, key);
|
|
72
|
-
const chunks = [];
|
|
73
|
-
for await (const ch of stream)
|
|
74
|
-
chunks.push(ch);
|
|
75
|
-
return Buffer.concat(chunks);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
const code = e.code;
|
|
79
|
-
if (code === 'NoSuchKey' || code === 'NotFound')
|
|
80
|
-
return null;
|
|
81
|
-
throw new StorageUnavailableError(`读取失败:${key}`, e);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
//# sourceMappingURL=MinioBackend.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"MinioBackend.js","sourceRoot":"","sources":["../../../src/providers/storage/MinioBackend.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/StorageBackend
|
|
3
|
-
* @description 对象存储的**中性接口**。换一个后端(MinIO / S3 / WebDAV / 别的 HTTP 端点),
|
|
4
|
-
* 上层几乎不用改 —— 与 `AgentBackend` / `RepoBackend` / `MediaProvider` 同一条自检判据。
|
|
5
|
-
*
|
|
6
|
-
* @layer providers
|
|
7
|
-
*
|
|
8
|
-
* 🔴 **接口里不出现任何一家存储的类型或词汇。** 这是"独立性"的全部来源:
|
|
9
|
-
* 独立性来自**接口中性**,不来自代码住在哪个包里。
|
|
10
|
-
* (§2.1:插件 ≠ 解耦。解耦是接口的性质,插件是分发的性质。)
|
|
11
|
-
*
|
|
12
|
-
* 🔴 **只有 put 与 list 两个动作,而且没有 delete。**
|
|
13
|
-
* · 没有 delete 是**有意的**:同步这条路的不变量是「**不删**」——
|
|
14
|
-
* 本地文件没了不等于用户要删桶里那份(工作区可能被重建、节点可能换了)。
|
|
15
|
-
* 接口上不给这个动作,比在实现里"记得别调"可靠。
|
|
16
|
-
* (同族:授权中心也**不该有 DeleteObject**,理由是 artifacts 未版本化 ⇒ 误删不可恢复。)
|
|
17
|
-
* · 没有 `stat` 是因为对账用 `list` 一次拿全前缀更省往返,而且**对账基准必须是桶本身**;
|
|
18
|
-
* 逐个 stat 会诱使实现方去缓存"上次看到的",那正是要避免的形态。
|
|
19
|
-
*/
|
|
20
|
-
/** 桶里一个对象的对账信息。**etag 是内容判据**(单段上传时等于 md5),不看时间。 */
|
|
21
|
-
export interface StorageObject {
|
|
22
|
-
key: string;
|
|
23
|
-
etag: string;
|
|
24
|
-
size: number;
|
|
25
|
-
}
|
|
26
|
-
export interface StorageBackend {
|
|
27
|
-
/**
|
|
28
|
-
* 写一个对象(覆盖同 key)。
|
|
29
|
-
*
|
|
30
|
-
* 🔴 `contentType` 不是可选的装饰:桶是**直连**的,没有一层服务器可以在返回时补救 ——
|
|
31
|
-
* 缺省的 `application/octet-stream` 会让预览图在浏览器里变成一次下载。
|
|
32
|
-
* 所以推的时候就得写对;拿不准时**由调用方决定**,后端不猜。
|
|
33
|
-
*/
|
|
34
|
-
put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
|
|
35
|
-
/** 列一个前缀下的全部对象(递归)。前缀为空即整桶 —— 调用方自己收窄。 */
|
|
36
|
-
list(prefix: string): Promise<StorageObject[]>;
|
|
37
|
-
/**
|
|
38
|
-
* 读一个对象。
|
|
39
|
-
*
|
|
40
|
-
* 🔴 **不存在返回 `null`,不抛** —— "有没有"是正常的问题
|
|
41
|
-
* (占位兜底就是先问"库里有没有")。而**连不上/没权限要抛**
|
|
42
|
-
* `StorageUnavailableError`:那两件事和"这个 key 没有"完全不同,
|
|
43
|
-
* 混在一起的表现是"库是空的",而真相是这个 key 读不了。
|
|
44
|
-
*/
|
|
45
|
-
get(key: string): Promise<Buffer | null>;
|
|
46
|
-
}
|
|
47
|
-
/** 后端不可用时抛这个,**不要静默降级**成"这轮没东西同步"。 */
|
|
48
|
-
export declare class StorageUnavailableError extends Error {
|
|
49
|
-
readonly cause?: unknown | undefined;
|
|
50
|
-
constructor(message: string, cause?: unknown | undefined);
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=StorageBackend.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StorageBackend.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/StorageBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,oDAAoD;AACpD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErE,0CAA0C;IAC1C,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAE/C;;;;;;;OAOG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1C;AAED,sCAAsC;AACtC,qBAAa,uBAAwB,SAAQ,KAAK;IACnB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;gBAAzC,OAAO,EAAE,MAAM,EAAW,KAAK,CAAC,EAAE,OAAO,YAAA;CAItD"}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/StorageBackend
|
|
3
|
-
* @description 对象存储的**中性接口**。换一个后端(MinIO / S3 / WebDAV / 别的 HTTP 端点),
|
|
4
|
-
* 上层几乎不用改 —— 与 `AgentBackend` / `RepoBackend` / `MediaProvider` 同一条自检判据。
|
|
5
|
-
*
|
|
6
|
-
* @layer providers
|
|
7
|
-
*
|
|
8
|
-
* 🔴 **接口里不出现任何一家存储的类型或词汇。** 这是"独立性"的全部来源:
|
|
9
|
-
* 独立性来自**接口中性**,不来自代码住在哪个包里。
|
|
10
|
-
* (§2.1:插件 ≠ 解耦。解耦是接口的性质,插件是分发的性质。)
|
|
11
|
-
*
|
|
12
|
-
* 🔴 **只有 put 与 list 两个动作,而且没有 delete。**
|
|
13
|
-
* · 没有 delete 是**有意的**:同步这条路的不变量是「**不删**」——
|
|
14
|
-
* 本地文件没了不等于用户要删桶里那份(工作区可能被重建、节点可能换了)。
|
|
15
|
-
* 接口上不给这个动作,比在实现里"记得别调"可靠。
|
|
16
|
-
* (同族:授权中心也**不该有 DeleteObject**,理由是 artifacts 未版本化 ⇒ 误删不可恢复。)
|
|
17
|
-
* · 没有 `stat` 是因为对账用 `list` 一次拿全前缀更省往返,而且**对账基准必须是桶本身**;
|
|
18
|
-
* 逐个 stat 会诱使实现方去缓存"上次看到的",那正是要避免的形态。
|
|
19
|
-
*/
|
|
20
|
-
/** 后端不可用时抛这个,**不要静默降级**成"这轮没东西同步"。 */
|
|
21
|
-
export class StorageUnavailableError extends Error {
|
|
22
|
-
cause;
|
|
23
|
-
constructor(message, cause) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.cause = cause;
|
|
26
|
-
this.name = 'StorageUnavailableError';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=StorageBackend.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StorageBackend.js","sourceRoot":"","sources":["../../../src/providers/storage/StorageBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiCH,sCAAsC;AACtC,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACV;IAAtC,YAAY,OAAe,EAAW,KAAe;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;QADqB,UAAK,GAAL,KAAK,CAAU;QAEnD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/keys
|
|
3
|
-
* @description 桶 key 的拼法。**与 coral `src/storage/keys.ts` 逐字对齐** —— 这是移交的对账面。
|
|
4
|
-
*
|
|
5
|
-
* @layer providers
|
|
6
|
-
*
|
|
7
|
-
* 🔴 **为什么要一个模块而不是各处拼字符串**(照抄 coral 头注里的理由,它是对的):
|
|
8
|
-
* 桶里**没有"路径不存在"这种错误**。写方把 key 拼错一个字符,PUT 照样 200,
|
|
9
|
-
* 只是那个对象从此没人读得到;读方拼错,拿到 404 却无法区分"没同步上来"和"key 写法不一致"。
|
|
10
|
-
* 文件系统会用 ENOENT 立刻告诉你,桶不会。
|
|
11
|
-
*
|
|
12
|
-
* 🔴 **移交期间这是两份实现。** 两份实现的安全做法不是"小心点",是**拿真桶内容对账**:
|
|
13
|
-
* 切换前跑一轮并行比对,逐 key 逐 etag 一致才算数(实施计划任务 5 的验收物)。
|
|
14
|
-
* > 判据:一条安全/正确性规则**做对过一次,不代表它在第二条路上也在**。
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ 这里只有**同步**这条路用得到的三个(assetKey / assetPrefix / thumbKey)。
|
|
17
|
-
* `published/` 与 `library/` 是别的路的写方,**不在本模块** —— 免得看起来像"sps 管着整个桶布局"。
|
|
18
|
-
*/
|
|
19
|
-
/** `./x` → `x`;去掉前导 `/`;`..` 一律抛(围栏在 `syncable`,这里是最后一道形状校验)。 */
|
|
20
|
-
export declare function normalizeRel(rel: string): string;
|
|
21
|
-
/** 项目在共享树下的相对路径(`t<租户>/<产品>/<项目>`),去掉尾部斜杠。 */
|
|
22
|
-
export declare function normalizeProject(projectPath: string): string;
|
|
23
|
-
export declare function assetKey(projectPath: string, rel: string): string;
|
|
24
|
-
/** 某项目全部资源的前缀(对账列举用)。 */
|
|
25
|
-
export declare function assetPrefix(projectPath: string): string;
|
|
26
|
-
/**
|
|
27
|
-
* 缩略图:与原图同一棵树下的 `.thumbs/`,**后缀直接追加而不是替换**。
|
|
28
|
-
*
|
|
29
|
-
* 追加(`hero.png.webp`)而不是替换(`hero.webp`)是有意的:替换会让
|
|
30
|
-
* `hero.png` 与 `hero.jpg` 撞成同一个缩略图 key,而撞了不报错 —— 后写的赢,预览图随机。
|
|
31
|
-
*/
|
|
32
|
-
export declare function thumbKey(projectPath: string, rel: string): string;
|
|
33
|
-
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,yBAAyB;AACzB,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMjE"}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/keys
|
|
3
|
-
* @description 桶 key 的拼法。**与 coral `src/storage/keys.ts` 逐字对齐** —— 这是移交的对账面。
|
|
4
|
-
*
|
|
5
|
-
* @layer providers
|
|
6
|
-
*
|
|
7
|
-
* 🔴 **为什么要一个模块而不是各处拼字符串**(照抄 coral 头注里的理由,它是对的):
|
|
8
|
-
* 桶里**没有"路径不存在"这种错误**。写方把 key 拼错一个字符,PUT 照样 200,
|
|
9
|
-
* 只是那个对象从此没人读得到;读方拼错,拿到 404 却无法区分"没同步上来"和"key 写法不一致"。
|
|
10
|
-
* 文件系统会用 ENOENT 立刻告诉你,桶不会。
|
|
11
|
-
*
|
|
12
|
-
* 🔴 **移交期间这是两份实现。** 两份实现的安全做法不是"小心点",是**拿真桶内容对账**:
|
|
13
|
-
* 切换前跑一轮并行比对,逐 key 逐 etag 一致才算数(实施计划任务 5 的验收物)。
|
|
14
|
-
* > 判据:一条安全/正确性规则**做对过一次,不代表它在第二条路上也在**。
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ 这里只有**同步**这条路用得到的三个(assetKey / assetPrefix / thumbKey)。
|
|
17
|
-
* `published/` 与 `library/` 是别的路的写方,**不在本模块** —— 免得看起来像"sps 管着整个桶布局"。
|
|
18
|
-
*/
|
|
19
|
-
/** `./x` → `x`;去掉前导 `/`;`..` 一律抛(围栏在 `syncable`,这里是最后一道形状校验)。 */
|
|
20
|
-
export function normalizeRel(rel) {
|
|
21
|
-
const clean = rel.replace(/^\.\//, '').replace(/^\/+/, '');
|
|
22
|
-
if (!clean)
|
|
23
|
-
throw new Error('相对路径为空');
|
|
24
|
-
if (clean.split('/').includes('..'))
|
|
25
|
-
throw new Error(`相对路径含 ..:${rel}`);
|
|
26
|
-
return clean;
|
|
27
|
-
}
|
|
28
|
-
/** 项目在共享树下的相对路径(`t<租户>/<产品>/<项目>`),去掉尾部斜杠。 */
|
|
29
|
-
export function normalizeProject(projectPath) {
|
|
30
|
-
return normalizeRel(projectPath).replace(/\/+$/, '');
|
|
31
|
-
}
|
|
32
|
-
export function assetKey(projectPath, rel) {
|
|
33
|
-
return `assets/${normalizeProject(projectPath)}/${normalizeRel(rel)}`;
|
|
34
|
-
}
|
|
35
|
-
/** 某项目全部资源的前缀(对账列举用)。 */
|
|
36
|
-
export function assetPrefix(projectPath) {
|
|
37
|
-
return `assets/${normalizeProject(projectPath)}/`;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* 缩略图:与原图同一棵树下的 `.thumbs/`,**后缀直接追加而不是替换**。
|
|
41
|
-
*
|
|
42
|
-
* 追加(`hero.png.webp`)而不是替换(`hero.webp`)是有意的:替换会让
|
|
43
|
-
* `hero.png` 与 `hero.jpg` 撞成同一个缩略图 key,而撞了不报错 —— 后写的赢,预览图随机。
|
|
44
|
-
*/
|
|
45
|
-
export function thumbKey(projectPath, rel) {
|
|
46
|
-
const clean = normalizeRel(rel);
|
|
47
|
-
const i = clean.lastIndexOf('/');
|
|
48
|
-
const dir = i < 0 ? '' : `${clean.slice(0, i)}/`;
|
|
49
|
-
const base = i < 0 ? clean : clean.slice(i + 1);
|
|
50
|
-
return `assets/${normalizeProject(projectPath)}/.thumbs/${dir}${base}.webp`;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=keys.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/providers/storage/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,iEAAiE;AACjE,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC;IACxE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,YAAY,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,WAAmB,EAAE,GAAW;IACvD,OAAO,UAAU,gBAAgB,CAAC,WAAW,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;AACxE,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,WAAW,CAAC,WAAmB;IAC7C,OAAO,UAAU,gBAAgB,CAAC,WAAW,CAAC,GAAG,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,WAAmB,EAAE,GAAW;IACvD,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;IACjD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,OAAO,UAAU,gBAAgB,CAAC,WAAW,CAAC,YAAY,GAAG,GAAG,IAAI,OAAO,CAAC;AAC9E,CAAC"}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export interface StorageConfig {
|
|
2
|
-
/** S3 兼容端点,如 `http://127.0.0.1:9000`。 */
|
|
3
|
-
endpoint: string;
|
|
4
|
-
/** 桶名。sps 只认这一个名字,不认识桶里的语义分区。 */
|
|
5
|
-
bucket: string;
|
|
6
|
-
accessKey: string;
|
|
7
|
-
secretKey: string;
|
|
8
|
-
/** S3 region;MinIO 通常用 `us-east-1`,给默认值只是为了签名能算出来。 */
|
|
9
|
-
region?: string;
|
|
10
|
-
/**
|
|
11
|
-
* 路径式寻址(`http://host/bucket/key`)。MinIO 默认需要它;
|
|
12
|
-
* 虚拟主机式(`http://bucket.host/key`)要求 DNS 配合,自建部署一般没有。
|
|
13
|
-
*/
|
|
14
|
-
pathStyle?: boolean;
|
|
15
|
-
/** 🔴 默认关:缺这个字段或为 false ⇒ 同步路径完全不存在。 */
|
|
16
|
-
enabled: boolean;
|
|
17
|
-
}
|
|
18
|
-
/** 对外可见的形状 —— **没有** secretKey,accessKey 也只留尾段用于"配的是哪一把"。 */
|
|
19
|
-
export interface RedactedStorageConfig {
|
|
20
|
-
endpoint: string;
|
|
21
|
-
bucket: string;
|
|
22
|
-
accessKeyTail: string;
|
|
23
|
-
region: string;
|
|
24
|
-
pathStyle: boolean;
|
|
25
|
-
enabled: boolean;
|
|
26
|
-
}
|
|
27
|
-
export declare function storageConfigFile(): string;
|
|
28
|
-
/**
|
|
29
|
-
* 读配置。文件不存在 → `null`(**不是**一个"默认配置")。
|
|
30
|
-
*
|
|
31
|
-
* 🔴 不给缺省对象:缺省成 `{enabled:false, …}` 会让"没配过"和"配了但关着"变得无法区分,
|
|
32
|
-
* 而这两者对调用方的意义不同(前者要引导去配,后者是有意关掉)。同族的坑今天在
|
|
33
|
-
* `template.json` 的 `publish` 上刚记过一次。
|
|
34
|
-
*/
|
|
35
|
-
export declare function readStorageConfig(): StorageConfig | null;
|
|
36
|
-
export declare function writeStorageConfig(cfg: StorageConfig): void;
|
|
37
|
-
/** 脱敏视图。**任何 HTTP 响应只能给这个**,不能给 `StorageConfig`。 */
|
|
38
|
-
export declare function redact(cfg: StorageConfig): RedactedStorageConfig;
|
|
39
|
-
/**
|
|
40
|
-
* 同步路径的**唯一入口判据**:配了、而且开着,才回配置;否则 `null`。
|
|
41
|
-
*
|
|
42
|
-
* 调用方拿到 `null` 必须**什么都不做**(不是"用默认桶"、也不是"报错") ——
|
|
43
|
-
* 未配置是一个合法状态:sps 是通用工具,别人装它不需要对象存储。
|
|
44
|
-
*/
|
|
45
|
-
export declare function activeStorageConfig(): StorageConfig | null;
|
|
46
|
-
//# sourceMappingURL=storageConfig.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"storageConfig.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/storageConfig.ts"],"names":[],"mappings":"AA+BA,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,GAAG,IAAI,CAuBxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAE3D;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,qBAAqB,CAUhE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,GAAG,IAAI,CAG1D"}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module providers/storage/storageConfig
|
|
3
|
-
* @description 对象存储接入的**节点级**配置(`~/.coral/storage.json`)。
|
|
4
|
-
*
|
|
5
|
-
* @layer providers
|
|
6
|
-
*
|
|
7
|
-
* 设计见 `docs/design/object-storage-and-workspace-sync.md` §2.1。四条钉死的规则里,本模块负责三条:
|
|
8
|
-
*
|
|
9
|
-
* 🔴 **① 配置里放"去哪儿",不放"同步什么"。**
|
|
10
|
-
* endpoint / bucket / 凭据 / 前缀 —— 全是"去哪儿"。
|
|
11
|
-
* "哪些相对路径算用户资产"是**产品知识**,来自 `template.json` 的声明,**不在这里**。
|
|
12
|
-
* 反面教材:coral sidecar 的 `scan.ts:38` 用「目录下含 assets/」当发现判据,
|
|
13
|
-
* 于是"只有 docs/、还没有 assets/"的设计师阶段项目**整批静默缺席**(实测 36 个)。
|
|
14
|
-
*
|
|
15
|
-
* 🔴 **③ 单节点级,不做多租户。** 与媒体 provider 同一条:**多租户归应用层,不做进引擎**。
|
|
16
|
-
* "这台节点用哪个桶" = 一份配置;"哪个租户进哪个桶"是平台的活。
|
|
17
|
-
* ⇒ 所以这里是**一个对象**而不是一张表。名字用 `storage.json` 而不是 `storage-providers.json`:
|
|
18
|
-
* **名字要跟着职责走** —— 复数名字装一个对象,会让下一个人以为可以配多份。
|
|
19
|
-
*
|
|
20
|
-
* 🔴 **④ 默认关。** 没有配置文件 ⇒ `enabled` 为 false ⇒ **整条同步路径不存在**,
|
|
21
|
-
* 而不是"用一个缺省的桶"。缺省被解释成"最宽松"是最危险的缺省。
|
|
22
|
-
*
|
|
23
|
-
* 凭据落盘走 `writeSecretFile`(tmp 0600 → rename → **chmod**,顺序不能反);
|
|
24
|
-
* 读侧永不回显 —— 对外一律走 `redact()`,与 `GET /:project/agent` 同一条规矩
|
|
25
|
-
* (那条的理由是:读者是消费方,不是密钥的所有者)。
|
|
26
|
-
*/
|
|
27
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
28
|
-
import { join } from 'node:path';
|
|
29
|
-
import { hardenSecretFile, writeSecretFile } from '../../infra/secretFile.js';
|
|
30
|
-
import { coralRoot } from '../../shared/runtimePaths.js';
|
|
31
|
-
export function storageConfigFile() {
|
|
32
|
-
return join(coralRoot(), 'storage.json');
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* 读配置。文件不存在 → `null`(**不是**一个"默认配置")。
|
|
36
|
-
*
|
|
37
|
-
* 🔴 不给缺省对象:缺省成 `{enabled:false, …}` 会让"没配过"和"配了但关着"变得无法区分,
|
|
38
|
-
* 而这两者对调用方的意义不同(前者要引导去配,后者是有意关掉)。同族的坑今天在
|
|
39
|
-
* `template.json` 的 `publish` 上刚记过一次。
|
|
40
|
-
*/
|
|
41
|
-
export function readStorageConfig() {
|
|
42
|
-
const p = storageConfigFile();
|
|
43
|
-
if (!existsSync(p))
|
|
44
|
-
return null;
|
|
45
|
-
// 读路径上顺手收权限:存量文件不需要迁移脚本。
|
|
46
|
-
hardenSecretFile(p);
|
|
47
|
-
try {
|
|
48
|
-
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
49
|
-
if (typeof raw.endpoint !== 'string' || typeof raw.bucket !== 'string')
|
|
50
|
-
return null;
|
|
51
|
-
if (typeof raw.accessKey !== 'string' || typeof raw.secretKey !== 'string')
|
|
52
|
-
return null;
|
|
53
|
-
return {
|
|
54
|
-
endpoint: raw.endpoint,
|
|
55
|
-
bucket: raw.bucket,
|
|
56
|
-
accessKey: raw.accessKey,
|
|
57
|
-
secretKey: raw.secretKey,
|
|
58
|
-
region: raw.region ?? 'us-east-1',
|
|
59
|
-
pathStyle: raw.pathStyle ?? true,
|
|
60
|
-
enabled: raw.enabled === true,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// 坏文件当成"没配" —— 但**不静默**:调用方拿到 null 会走"未配置"的响亮路径,
|
|
65
|
-
// 而不是拿着半个配置去连一个猜出来的端点。
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
export function writeStorageConfig(cfg) {
|
|
70
|
-
writeSecretFile(storageConfigFile(), `${JSON.stringify(cfg, null, 2)}\n`);
|
|
71
|
-
}
|
|
72
|
-
/** 脱敏视图。**任何 HTTP 响应只能给这个**,不能给 `StorageConfig`。 */
|
|
73
|
-
export function redact(cfg) {
|
|
74
|
-
return {
|
|
75
|
-
endpoint: cfg.endpoint,
|
|
76
|
-
bucket: cfg.bucket,
|
|
77
|
-
// 只留尾 4 位:够回答"配的是哪一把",不够拿去用。
|
|
78
|
-
accessKeyTail: cfg.accessKey.length > 4 ? cfg.accessKey.slice(-4) : '',
|
|
79
|
-
region: cfg.region ?? 'us-east-1',
|
|
80
|
-
pathStyle: cfg.pathStyle ?? true,
|
|
81
|
-
enabled: cfg.enabled,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* 同步路径的**唯一入口判据**:配了、而且开着,才回配置;否则 `null`。
|
|
86
|
-
*
|
|
87
|
-
* 调用方拿到 `null` 必须**什么都不做**(不是"用默认桶"、也不是"报错") ——
|
|
88
|
-
* 未配置是一个合法状态:sps 是通用工具,别人装它不需要对象存储。
|
|
89
|
-
*/
|
|
90
|
-
export function activeStorageConfig() {
|
|
91
|
-
const cfg = readStorageConfig();
|
|
92
|
-
return cfg?.enabled ? cfg : null;
|
|
93
|
-
}
|
|
94
|
-
//# sourceMappingURL=storageConfig.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"storageConfig.js","sourceRoot":"","sources":["../../../src/providers/storage/storageConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AA8BzD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,cAAc,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,CAAC,GAAG,iBAAiB,EAAE,CAAC;IAC9B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,yBAAyB;IACzB,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAA2B,CAAC;QAC3E,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACpF,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACxF,OAAO;YACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,WAAW;YACjC,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;YAChC,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,IAAI;SAC9B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;QACjD,uBAAuB;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAkB;IACnD,eAAe,CAAC,iBAAiB,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,MAAM,CAAC,GAAkB;IACvC,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,6BAA6B;QAC7B,aAAa,EAAE,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;QACtE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,WAAW;QACjC,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;QAChC,OAAO,EAAE,GAAG,CAAC,OAAO;KACrB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACnC,CAAC"}
|