@coralai/sps-cli 0.65.87 → 0.65.91
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/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +2 -0
- package/dist/console/index.js.map +1 -1
- package/dist/console/lib/artPaths.d.ts +5 -5
- package/dist/console/lib/artPaths.d.ts.map +1 -1
- package/dist/console/lib/artPaths.js +8 -26
- package/dist/console/lib/artPaths.js.map +1 -1
- package/dist/console/routes/storage.d.ts +21 -0
- package/dist/console/routes/storage.d.ts.map +1 -0
- package/dist/console/routes/storage.js +115 -0
- package/dist/console/routes/storage.js.map +1 -0
- package/dist/core/pathFence.d.ts +20 -0
- package/dist/core/pathFence.d.ts.map +1 -0
- package/dist/core/pathFence.js +60 -0
- package/dist/core/pathFence.js.map +1 -0
- package/dist/daemon/sessionDaemon.d.ts +1 -0
- package/dist/daemon/sessionDaemon.d.ts.map +1 -1
- package/dist/daemon/sessionDaemon.js +6 -0
- package/dist/daemon/sessionDaemon.js.map +1 -1
- package/dist/daemon/storageSyncLoop.d.ts +9 -0
- package/dist/daemon/storageSyncLoop.d.ts.map +1 -0
- package/dist/daemon/storageSyncLoop.js +70 -0
- package/dist/daemon/storageSyncLoop.js.map +1 -0
- package/dist/providers/storage/FakeStorage.d.ts +14 -0
- package/dist/providers/storage/FakeStorage.d.ts.map +1 -0
- package/dist/providers/storage/FakeStorage.js +33 -0
- package/dist/providers/storage/FakeStorage.js.map +1 -0
- package/dist/providers/storage/MinioBackend.d.ts +31 -0
- package/dist/providers/storage/MinioBackend.d.ts.map +1 -0
- package/dist/providers/storage/MinioBackend.js +60 -0
- package/dist/providers/storage/MinioBackend.js.map +1 -0
- package/dist/providers/storage/StorageBackend.d.ts +43 -0
- package/dist/providers/storage/StorageBackend.d.ts.map +1 -0
- package/dist/providers/storage/StorageBackend.js +29 -0
- package/dist/providers/storage/StorageBackend.js.map +1 -0
- package/dist/providers/storage/keys.d.ts +33 -0
- package/dist/providers/storage/keys.d.ts.map +1 -0
- package/dist/providers/storage/keys.js +52 -0
- package/dist/providers/storage/keys.js.map +1 -0
- package/dist/providers/storage/scanWorkspace.d.ts +56 -0
- package/dist/providers/storage/scanWorkspace.d.ts.map +1 -0
- package/dist/providers/storage/scanWorkspace.js +205 -0
- package/dist/providers/storage/scanWorkspace.js.map +1 -0
- package/dist/providers/storage/storageConfig.d.ts +46 -0
- package/dist/providers/storage/storageConfig.d.ts.map +1 -0
- package/dist/providers/storage/storageConfig.js +94 -0
- package/dist/providers/storage/storageConfig.js.map +1 -0
- package/dist/providers/storage/syncProject.d.ts +67 -0
- package/dist/providers/storage/syncProject.d.ts.map +1 -0
- package/dist/providers/storage/syncProject.js +136 -0
- package/dist/providers/storage/syncProject.js.map +1 -0
- package/dist/providers/storage/syncRunner.d.ts +59 -0
- package/dist/providers/storage/syncRunner.d.ts.map +1 -0
- package/dist/providers/storage/syncRunner.js +119 -0
- package/dist/providers/storage/syncRunner.js.map +1 -0
- package/dist/providers/storage/syncScope.d.ts +25 -0
- package/dist/providers/storage/syncScope.d.ts.map +1 -0
- package/dist/providers/storage/syncScope.js +68 -0
- package/dist/providers/storage/syncScope.js.map +1 -0
- package/dist/providers/storage/syncable.d.ts +47 -0
- package/dist/providers/storage/syncable.d.ts.map +1 -0
- package/dist/providers/storage/syncable.js +84 -0
- package/dist/providers/storage/syncable.js.map +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module providers/storage/scanWorkspace
|
|
3
|
+
* @description 按声明枚举工作区里要同步的文件 + 发现项目 + 新鲜度。
|
|
4
|
+
*
|
|
5
|
+
* @layer providers
|
|
6
|
+
*
|
|
7
|
+
* 🔴 **发现判据只看布局,不猜产品语义。** coral sidecar 用「目录下含 `assets/`」当发现判据,
|
|
8
|
+
* 于是"只有 `docs/`、还没有 `assets/`"的**设计师阶段项目整批静默缺席**(实测 163 个里 36 个)。
|
|
9
|
+
* 这里的判据是**三段布局**(`t<租户>/<产品>/<项目>`)—— 它是 sps 自己的工作区约定,
|
|
10
|
+
* 不是对产品内容的猜测。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **新鲜度按「同步范围」算,不是按某个固定目录算。** coral 那侧改对了发现判据、
|
|
13
|
+
* 却坏在紧挨着的下一行:`projectLastModified(p.abs, 'assets')` 对只有 `docs/` 的项目返回 `0`,
|
|
14
|
+
* `0 >= cutoff` 恒假 ⇒ 36 个又被当成"30 天没动"跳过。
|
|
15
|
+
* > 判据一:**范围一旦放宽,紧跟其后的每一道过滤都要跟着放宽** ——
|
|
16
|
+
* > 漏掉的那道会把范围**原样缩回去**,而且报的是**另一个原因**("太旧"),查的人会去看时间。
|
|
17
|
+
* > 判据二:**别让一个返回值同时表示两件事** —— 那个 `0` 同时是"从没改过"和"目录不存在"。
|
|
18
|
+
* ⇒ 所以这里 `scopeLastModified` 回 `number | null`:**null = 范围内一个路径都不存在**,
|
|
19
|
+
* 与"存在但很旧"分开,由调用方分别计数、分别报。
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
/** 发现共享根下的全部项目(三段布局)。不做任何内容判断。 */
|
|
25
|
+
export async function discoverProjects(root) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let tenants;
|
|
28
|
+
try {
|
|
29
|
+
tenants = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
for (const t of tenants) {
|
|
35
|
+
let products;
|
|
36
|
+
try {
|
|
37
|
+
products = (await readdir(join(root, t), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const p of products) {
|
|
43
|
+
let projects;
|
|
44
|
+
try {
|
|
45
|
+
projects = (await readdir(join(root, t, p), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const g of projects)
|
|
51
|
+
out.push({ rel: `${t}/${p}/${g}`, abs: join(root, t, p, g), product: p });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 声明范围内最近一次改动的时间。
|
|
58
|
+
*
|
|
59
|
+
* @returns 毫秒时间戳;**`null` = 范围内一个路径都不存在**(与"存在但很旧"是两件事)
|
|
60
|
+
*/
|
|
61
|
+
export async function scopeLastModified(projectAbs, include) {
|
|
62
|
+
let newest = null;
|
|
63
|
+
const bump = (ms) => {
|
|
64
|
+
if (newest === null || ms > newest)
|
|
65
|
+
newest = ms;
|
|
66
|
+
};
|
|
67
|
+
for (const entry of include) {
|
|
68
|
+
const abs = join(projectAbs, entry.replace(/\/+$/, ''));
|
|
69
|
+
let st;
|
|
70
|
+
try {
|
|
71
|
+
st = await stat(abs);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue; // 这一项不存在 —— 不是"很旧",继续看下一项
|
|
75
|
+
}
|
|
76
|
+
if (!st.isDirectory()) {
|
|
77
|
+
bump(st.mtimeMs);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
await walkMtime(abs, bump);
|
|
81
|
+
}
|
|
82
|
+
return newest;
|
|
83
|
+
}
|
|
84
|
+
async function walkMtime(dir, bump) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
// 🔴 只看普通文件与目录:软链**不跟随**(跟随会把树外文件的 mtime 当成本项目的动静)。
|
|
94
|
+
if (e.isSymbolicLink())
|
|
95
|
+
continue;
|
|
96
|
+
const p = join(dir, e.name);
|
|
97
|
+
if (e.isDirectory()) {
|
|
98
|
+
await walkMtime(p, bump);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
bump((await stat(p)).mtimeMs);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* 读不到就不算动静 */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 按声明枚举文件并算 md5。
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ `maxBytes`:整份读进内存算 md5(与 coral 同做法 —— 上传时本来也要这份字节)。
|
|
113
|
+
* 上限**显式**给,不靠运气:树里有 172MB 的项目,一个失控的大文件不该把进程带走。
|
|
114
|
+
* 超限的算**失败**(计入 errors),不是静默跳过 —— 静默跳过会让"这个资源永远不在桶里"没人知道。
|
|
115
|
+
*/
|
|
116
|
+
export async function scanByScope(projectAbs, include, maxBytes) {
|
|
117
|
+
const files = [];
|
|
118
|
+
const unreadable = [];
|
|
119
|
+
const missing = [];
|
|
120
|
+
const skippedDot = [];
|
|
121
|
+
const oversized = [];
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
/** ENOENT 不是"读不到",是"没有"。分开归类,否则告警恒响。 */
|
|
124
|
+
const isMissing = (e) => typeof e === 'object' && e !== null && e.code === 'ENOENT';
|
|
125
|
+
const addFile = async (abs, rel) => {
|
|
126
|
+
if (seen.has(rel))
|
|
127
|
+
return; // 声明里目录与文件重叠时不重复推
|
|
128
|
+
seen.add(rel);
|
|
129
|
+
let st;
|
|
130
|
+
try {
|
|
131
|
+
st = await stat(abs);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
(isMissing(e) ? missing : unreadable).push(rel);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (st.size > maxBytes) {
|
|
138
|
+
oversized.push(rel);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let bytes;
|
|
142
|
+
try {
|
|
143
|
+
bytes = await readFile(abs);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// 0600 读不到走 unreadable(必须被报出来);文件刚被删走 ENOENT ⇒ missing
|
|
147
|
+
(isMissing(e) ? missing : unreadable).push(rel);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
files.push({ rel, abs, size: st.size, md5: createHash('md5').update(bytes).digest('hex') });
|
|
151
|
+
};
|
|
152
|
+
const walk = async (absDir, relDir) => {
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(absDir, { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
(isMissing(e) ? missing : unreadable).push(relDir);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const e of entries) {
|
|
162
|
+
// 软链不跟随:枚举阶段就排除,别指望后面的围栏兜底
|
|
163
|
+
// (围栏仍然会跑 —— 两道都在,但**枚举不该把它交给围栏去拒**)。
|
|
164
|
+
if (e.isSymbolicLink())
|
|
165
|
+
continue;
|
|
166
|
+
// 点开头条目:枚举阶段就不产出。它们是中间产物(`.sprites.pre-orient.png`),
|
|
167
|
+
// 交给下限会触发**整趟拒绝** ⇒ 整个项目永久停同步,而且理由看起来像安全事件。
|
|
168
|
+
if (e.name.startsWith('.')) {
|
|
169
|
+
skippedDot.push(relDir ? `${relDir}/${e.name}` : e.name);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const abs = join(absDir, e.name);
|
|
173
|
+
const rel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
174
|
+
if (e.isDirectory())
|
|
175
|
+
await walk(abs, rel);
|
|
176
|
+
else if (e.isFile())
|
|
177
|
+
await addFile(abs, rel);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
for (const entry of include) {
|
|
181
|
+
const isDir = entry.endsWith('/');
|
|
182
|
+
const clean = entry.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
183
|
+
if (!clean)
|
|
184
|
+
continue;
|
|
185
|
+
const abs = join(projectAbs, clean);
|
|
186
|
+
if (isDir) {
|
|
187
|
+
await walk(abs, clean);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// 不带尾斜杠的声明可能仍是目录(声明方少写了斜杠)—— 按实际类型走,别按写法猜
|
|
191
|
+
let st = null;
|
|
192
|
+
try {
|
|
193
|
+
st = await stat(abs);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
continue; // 声明了但项目里没有:合法(比如设计师还没写 PRD),不是错误
|
|
197
|
+
}
|
|
198
|
+
if (st.isDirectory())
|
|
199
|
+
await walk(abs, clean);
|
|
200
|
+
else
|
|
201
|
+
await addFile(abs, clean);
|
|
202
|
+
}
|
|
203
|
+
return { files, unreadable, missing, skippedDot, oversized };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=scanWorkspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanWorkspace.js","sourceRoot":"","sources":["../../../src/providers/storage/scanWorkspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module providers/storage/syncProject
|
|
3
|
+
* @description 一个项目的一轮对账同步:枚举 → 过下限 → 与桶对账 → 上传(缩略图先、原图后)。
|
|
4
|
+
*
|
|
5
|
+
* @layer providers
|
|
6
|
+
*
|
|
7
|
+
* 九条不变量见 `docs/design/storage-sync-implementation.md` §〇。落在代码里的要害是这几条:
|
|
8
|
+
*
|
|
9
|
+
* 🔴 **对账基准是桶本身,不是本地状态文件。** 本地清单一旦与桶脱节(桶被清过 / 上轮崩在一半 /
|
|
10
|
+
* 换了机器)会**永久跳过**某些文件且不报错;以桶的实际内容为准,一轮巡检就自愈。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **不删。** 本地文件没了 ≠ 用户要删桶里那份(工作区可能被重建、节点可能换了)。
|
|
13
|
+
* `StorageBackend` 接口上**根本没有 delete** —— 比"记得别调"可靠。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **幂等判据的覆盖面必须等于产物面。** 这一轮产出两样(原图 + 缩略图),判据就得同时问两样:
|
|
16
|
+
* 只问原图 ⇒ 一张生成失败过的缩略图**永远不会被补上**,表现是"同步很成功、预览是空的"。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **缩略图先写、原图后写**,且缩略图**上传**失败时本轮不写原图 ——
|
|
19
|
+
* 原图 etag 与本地相等是这条流水线的**提交点**,顺序反了会永久停留在旧缩略图且不报错。
|
|
20
|
+
* ⚠️ 区分**生成**失败(确定性:图坏了,照常传原图,否则坏文件把自己永远挡在桶外)
|
|
21
|
+
* 与**上传**失败(会留下陈旧缩略图 ⇒ 必须回退)。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **枚举完再判下限,再推第一个字节。** 下限若写在上传循环里,
|
|
24
|
+
* 排在拒绝项前面的文件**已经进桶了** —— 「整体失败」是关于**副作用**的断言,不是返回码。
|
|
25
|
+
*
|
|
26
|
+
* 🔴🔴 **但被拒的文件是"跳过并报出来",不是"整趟不推"**(coral 2026-08-19 指出,已改):
|
|
27
|
+
* 同一条下限用在**发布**路上,"整趟失败"是对的 —— 半个包比拒绝更难查;
|
|
28
|
+
* 用在**周期性**的同步路上就变成**那个项目永久停止同步**,而且理由看起来像安全事件。
|
|
29
|
+
* > 判据:**同一条规则,在"一次性动作"和"周期性动作"上后果完全不同。**
|
|
30
|
+
* > 周期性的路上,任何"整趟失败"都要能自己恢复,否则它等于一个**不报警的开关**。
|
|
31
|
+
* 而下限要防的事(凭据进桶)本来就是**逐文件**的 —— 跳过那一个就够了,
|
|
32
|
+
* 赔上整个项目既不更安全,又制造了一个静默停摆。
|
|
33
|
+
*/
|
|
34
|
+
import type { StorageBackend } from './StorageBackend.js';
|
|
35
|
+
import { type ProjectDir } from './scanWorkspace.js';
|
|
36
|
+
import { type SyncRejection } from './syncable.js';
|
|
37
|
+
export declare const contentTypeOf: (rel: string) => string | undefined;
|
|
38
|
+
export declare const thumbnailable: (rel: string) => boolean;
|
|
39
|
+
export interface SyncStats {
|
|
40
|
+
project: string;
|
|
41
|
+
scanned: number;
|
|
42
|
+
uploaded: number;
|
|
43
|
+
/** 与 uploaded **分开计** —— 合在一起就看不出预览到底有没有产出。 */
|
|
44
|
+
thumbs: number;
|
|
45
|
+
skipped: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
unreadable: number;
|
|
48
|
+
/** 声明里有、盘上没有 —— **不是错误**(设计师还没写 PRD 也走这里)。与 unreadable 分开。 */
|
|
49
|
+
missing: number;
|
|
50
|
+
/** 枚举阶段跳过的点开头条目(中间产物)。不触发整趟拒绝。 */
|
|
51
|
+
skippedDot: number;
|
|
52
|
+
oversized: number;
|
|
53
|
+
/** 被安全下限拒的项。**整趟不推**(见模块头)。 */
|
|
54
|
+
rejected: SyncRejection[];
|
|
55
|
+
/** 用了窄兜底(该产品没有声明)。**必须被报出来**。 */
|
|
56
|
+
usedDefaultScope: boolean;
|
|
57
|
+
errors: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface SyncDeps {
|
|
60
|
+
storage: StorageBackend;
|
|
61
|
+
/** 缩略图生成。注入是为了让用例不必依赖 sharp 的原生二进制。 */
|
|
62
|
+
makeThumb: (input: Buffer) => Promise<Buffer>;
|
|
63
|
+
/** 单文件上限(字节)。显式给,不靠运气。 */
|
|
64
|
+
maxBytes: number;
|
|
65
|
+
}
|
|
66
|
+
export declare function syncProject(deps: SyncDeps, project: ProjectDir, include: string[], usedDefaultScope: boolean): Promise<SyncStats>;
|
|
67
|
+
//# sourceMappingURL=syncProject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncProject.d.ts","sourceRoot":"","sources":["../../../src/providers/storage/syncProject.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAS9D,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAA6C,CAAC;AAInG,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,OAAiD,CAAC;AAE9F,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,iCAAiC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,cAAc,CAAC;IACxB,uCAAuC;IACvC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,UAAU,EACnB,OAAO,EAAE,MAAM,EAAE,EACjB,gBAAgB,EAAE,OAAO,GACxB,OAAO,CAAC,SAAS,CAAC,CAmFpB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module providers/storage/syncProject
|
|
3
|
+
* @description 一个项目的一轮对账同步:枚举 → 过下限 → 与桶对账 → 上传(缩略图先、原图后)。
|
|
4
|
+
*
|
|
5
|
+
* @layer providers
|
|
6
|
+
*
|
|
7
|
+
* 九条不变量见 `docs/design/storage-sync-implementation.md` §〇。落在代码里的要害是这几条:
|
|
8
|
+
*
|
|
9
|
+
* 🔴 **对账基准是桶本身,不是本地状态文件。** 本地清单一旦与桶脱节(桶被清过 / 上轮崩在一半 /
|
|
10
|
+
* 换了机器)会**永久跳过**某些文件且不报错;以桶的实际内容为准,一轮巡检就自愈。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **不删。** 本地文件没了 ≠ 用户要删桶里那份(工作区可能被重建、节点可能换了)。
|
|
13
|
+
* `StorageBackend` 接口上**根本没有 delete** —— 比"记得别调"可靠。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **幂等判据的覆盖面必须等于产物面。** 这一轮产出两样(原图 + 缩略图),判据就得同时问两样:
|
|
16
|
+
* 只问原图 ⇒ 一张生成失败过的缩略图**永远不会被补上**,表现是"同步很成功、预览是空的"。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **缩略图先写、原图后写**,且缩略图**上传**失败时本轮不写原图 ——
|
|
19
|
+
* 原图 etag 与本地相等是这条流水线的**提交点**,顺序反了会永久停留在旧缩略图且不报错。
|
|
20
|
+
* ⚠️ 区分**生成**失败(确定性:图坏了,照常传原图,否则坏文件把自己永远挡在桶外)
|
|
21
|
+
* 与**上传**失败(会留下陈旧缩略图 ⇒ 必须回退)。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **枚举完再判下限,再推第一个字节。** 下限若写在上传循环里,
|
|
24
|
+
* 排在拒绝项前面的文件**已经进桶了** —— 「整体失败」是关于**副作用**的断言,不是返回码。
|
|
25
|
+
*
|
|
26
|
+
* 🔴🔴 **但被拒的文件是"跳过并报出来",不是"整趟不推"**(coral 2026-08-19 指出,已改):
|
|
27
|
+
* 同一条下限用在**发布**路上,"整趟失败"是对的 —— 半个包比拒绝更难查;
|
|
28
|
+
* 用在**周期性**的同步路上就变成**那个项目永久停止同步**,而且理由看起来像安全事件。
|
|
29
|
+
* > 判据:**同一条规则,在"一次性动作"和"周期性动作"上后果完全不同。**
|
|
30
|
+
* > 周期性的路上,任何"整趟失败"都要能自己恢复,否则它等于一个**不报警的开关**。
|
|
31
|
+
* 而下限要防的事(凭据进桶)本来就是**逐文件**的 —— 跳过那一个就够了,
|
|
32
|
+
* 赔上整个项目既不更安全,又制造了一个静默停摆。
|
|
33
|
+
*/
|
|
34
|
+
import { readFile } from 'node:fs/promises';
|
|
35
|
+
import { extname } from 'node:path';
|
|
36
|
+
import { assetKey, assetPrefix, thumbKey } from './keys.js';
|
|
37
|
+
import { scanByScope } from './scanWorkspace.js';
|
|
38
|
+
import { screenAll } from './syncable.js';
|
|
39
|
+
/** 桶是直连的 —— **没有一层服务器能在返回时补救**,缺省的 octet-stream 会让预览图变成一次下载。 */
|
|
40
|
+
const MIME = {
|
|
41
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp',
|
|
42
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.json': 'application/json', '.txt': 'text/plain',
|
|
43
|
+
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4',
|
|
44
|
+
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.md': 'text/markdown',
|
|
45
|
+
};
|
|
46
|
+
export const contentTypeOf = (rel) => MIME[extname(rel).toLowerCase()];
|
|
47
|
+
/** 能缩略的栅格图。**不认识的类型一律否**,别去猜;SVG 不缩(本来就小,且避开服务端光栅化任意 SVG)。 */
|
|
48
|
+
const RASTER = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.avif']);
|
|
49
|
+
export const thumbnailable = (rel) => RASTER.has(extname(rel).toLowerCase());
|
|
50
|
+
export async function syncProject(deps, project, include, usedDefaultScope) {
|
|
51
|
+
const stats = {
|
|
52
|
+
project: project.rel, scanned: 0, uploaded: 0, thumbs: 0, skipped: 0, failed: 0,
|
|
53
|
+
unreadable: 0, missing: 0, skippedDot: 0, oversized: 0, rejected: [], usedDefaultScope, errors: [],
|
|
54
|
+
};
|
|
55
|
+
const scan = await scanByScope(project.abs, include, deps.maxBytes);
|
|
56
|
+
stats.scanned = scan.files.length;
|
|
57
|
+
stats.unreadable = scan.unreadable.length;
|
|
58
|
+
stats.missing = scan.missing.length;
|
|
59
|
+
stats.skippedDot = scan.skippedDot.length;
|
|
60
|
+
stats.oversized = scan.oversized.length;
|
|
61
|
+
for (const rel of scan.unreadable.slice(0, 3))
|
|
62
|
+
stats.errors.push(`权限读不到(已跳过) ${rel}`);
|
|
63
|
+
for (const rel of scan.oversized.slice(0, 3))
|
|
64
|
+
stats.errors.push(`超过单文件上限(未同步) ${rel}`);
|
|
65
|
+
// 🔴 下限在**推第一个字节之前**整趟判完(不是边推边判)。
|
|
66
|
+
// 但结果是**把被拒的那些从名单里摘掉**,不是整趟不推 —— 理由见模块头。
|
|
67
|
+
stats.rejected = await screenAll(scan.files.map((f) => ({ rel: f.rel, abs: f.abs })), project.abs);
|
|
68
|
+
const blocked = new Set(stats.rejected.map((r) => r.rel));
|
|
69
|
+
// 拒绝项要把名字报出去:清单会过期,而**漏掉的必须能被看见**。
|
|
70
|
+
for (const r of stats.rejected.slice(0, 5))
|
|
71
|
+
stats.errors.push(`安全下限拒绝 ${r.code}: ${r.rel}`);
|
|
72
|
+
const allowed = scan.files.filter((f) => !blocked.has(f.rel));
|
|
73
|
+
let remote;
|
|
74
|
+
try {
|
|
75
|
+
remote = new Map((await deps.storage.list(assetPrefix(project.rel))).map((o) => [o.key, o.etag]));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
stats.failed++;
|
|
79
|
+
stats.errors.push(`列举桶失败:${e instanceof Error ? e.message : String(e)}`);
|
|
80
|
+
return stats;
|
|
81
|
+
}
|
|
82
|
+
for (const f of allowed) {
|
|
83
|
+
const key = assetKey(project.rel, f.rel);
|
|
84
|
+
const changed = remote.get(key) !== f.md5; // ETag == md5,**不看 mtime**
|
|
85
|
+
const tKey = thumbnailable(f.rel) ? thumbKey(project.rel, f.rel) : null;
|
|
86
|
+
const thumbMissing = tKey !== null && !remote.has(tKey);
|
|
87
|
+
if (!changed && !thumbMissing) {
|
|
88
|
+
stats.skipped++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const fail = (e, what) => {
|
|
92
|
+
stats.failed++;
|
|
93
|
+
if (stats.errors.length < 8)
|
|
94
|
+
stats.errors.push(`${what} ${f.rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
95
|
+
};
|
|
96
|
+
let bytes;
|
|
97
|
+
try {
|
|
98
|
+
bytes = await readFile(f.abs);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
fail(e, '读文件');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let thumbUploadFailed = false;
|
|
105
|
+
if (tKey !== null && (changed || thumbMissing)) {
|
|
106
|
+
let thumb = null;
|
|
107
|
+
try {
|
|
108
|
+
thumb = await deps.makeThumb(bytes);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
fail(e, '生成缩略图'); // 生成失败是确定性的 ⇒ 照常传原图
|
|
112
|
+
}
|
|
113
|
+
if (thumb) {
|
|
114
|
+
try {
|
|
115
|
+
await deps.storage.put(tKey, thumb, 'image/webp');
|
|
116
|
+
stats.thumbs++;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
fail(e, '上传缩略图');
|
|
120
|
+
thumbUploadFailed = true; // ⇒ 本轮不写原图,免得下一轮判"没变"而永远停在旧缩略图
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!changed || thumbUploadFailed)
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
await deps.storage.put(key, bytes, contentTypeOf(f.rel));
|
|
128
|
+
stats.uploaded++;
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
fail(e, '上传'); // 单文件失败不中断整轮 —— 下轮对账会再试,但原因要留下
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return stats;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=syncProject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncProject.js","sourceRoot":"","sources":["../../../src/providers/storage/syncProject.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module providers/storage/syncRunner
|
|
3
|
+
* @description 对账循环:发现项目 → 新鲜度过滤 → 逐项目同步 → 汇总。
|
|
4
|
+
*
|
|
5
|
+
* @layer providers
|
|
6
|
+
*
|
|
7
|
+
* 🔴 **它是对账循环,不是事件驱动**(照抄 coral 的判断,它是对的):
|
|
8
|
+
* 文件事件会漏(worker 在别的用户下写 / 容器里写 / 进程崩在一半),
|
|
9
|
+
* 而漏掉的表现是"桶里少了一个文件",**没有任何报错**。对账每轮都会自愈。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **跑在 daemon,不在 console 的请求路径。**
|
|
12
|
+
* sidecar 是独立进程,而 console 是**共享事件循环** —— 同一段代码换了宿主就会开始
|
|
13
|
+
* 阻塞 `/api/health` 探针与 SSE 心跳。
|
|
14
|
+
* > 判据:**同步在移交件里是对的 —— 它本来跑在 CLI 里;换了宿主之后才变错。**
|
|
15
|
+
*
|
|
16
|
+
* 🔴 **每一类"没做"都要有自己的计数**,别合并成一个"跳过":
|
|
17
|
+
* 超期跳过 / 范围内一个路径都不存在 / 权限读不到 / 超大 / 被下限拒 —— 五种原因,五个数。
|
|
18
|
+
* 合并的代价今天实测过:coral 那个 `0` 同时表示"从没改过"和"目录不存在",吃掉了 36 个项目。
|
|
19
|
+
*/
|
|
20
|
+
import type { StorageBackend } from './StorageBackend.js';
|
|
21
|
+
import { type SyncStats } from './syncProject.js';
|
|
22
|
+
export interface SyncPassOptions {
|
|
23
|
+
root: string;
|
|
24
|
+
/** 只同步近 N 天动过的项目;0 = 全部。 */
|
|
25
|
+
sinceDays: number;
|
|
26
|
+
/** 单文件上限。 */
|
|
27
|
+
maxBytes: number;
|
|
28
|
+
/** 只跑一个项目(相对共享根),排障用。 */
|
|
29
|
+
onlyProject?: string;
|
|
30
|
+
log?: (line: string) => void;
|
|
31
|
+
/** 注入点:用例用假后端 / 假缩略图,不碰 minio 与 sharp 的原生依赖。 */
|
|
32
|
+
storage?: StorageBackend;
|
|
33
|
+
makeThumb?: (input: Buffer) => Promise<Buffer>;
|
|
34
|
+
now?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface PassSummary {
|
|
37
|
+
projects: number;
|
|
38
|
+
scanned: number;
|
|
39
|
+
uploaded: number;
|
|
40
|
+
thumbs: number;
|
|
41
|
+
skipped: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
unreadable: number;
|
|
44
|
+
missing: number;
|
|
45
|
+
skippedDot: number;
|
|
46
|
+
oversized: number;
|
|
47
|
+
rejected: number;
|
|
48
|
+
/** 因超过 sinceDays 没动而跳过。 */
|
|
49
|
+
skippedOld: number;
|
|
50
|
+
/** 🔴 范围内**一个路径都不存在** —— 与"很旧"分开数(它多半意味着声明写错了)。 */
|
|
51
|
+
skippedEmptyScope: number;
|
|
52
|
+
/** 用了窄兜底(该产品没有声明)的项目数。 */
|
|
53
|
+
defaultScope: number;
|
|
54
|
+
perProject: SyncStats[];
|
|
55
|
+
}
|
|
56
|
+
export declare function runSyncPass(opts: SyncPassOptions): Promise<PassSummary | null>;
|
|
57
|
+
/** 汇总行。每一类"没做"都占一格 —— 一个悄悄缩小了范围的同步器,和一个坏掉的同步器,日志上一模一样。 */
|
|
58
|
+
export declare function formatSummary(sum: PassSummary, started: number, opts: SyncPassOptions): string;
|
|
59
|
+
//# sourceMappingURL=syncRunner.d.ts.map
|