oasis_test_v2 2.2.118 → 2.2.120
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/index.js +1039 -353
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -756,7 +756,7 @@ function isDispatchFenced(error3) {
|
|
|
756
756
|
function isCompanyMembersLockLost(error3) {
|
|
757
757
|
return typeof error3 === "object" && error3 !== null && error3.companyMembersLockLost === true;
|
|
758
758
|
}
|
|
759
|
-
var SeqConflictError, BlobNotFoundError, BlobRangeNotSatisfiableError, BlobBackendError, BlobTooLargeError, BlobIntegrityError, AssistantBindConflictError, TraceSeqConflictError, DispatchUndecidableError, DispatchFencedError, CompanyMembersLockLostError;
|
|
759
|
+
var SeqConflictError, BlobNotFoundError, BlobRangeNotSatisfiableError, BlobBackendError, BlobTooLargeError, BlobSweepAbortedError, BlobIntegrityError, AssistantBindConflictError, TraceSeqConflictError, DispatchUndecidableError, DispatchFencedError, CompanyMembersLockLostError;
|
|
760
760
|
var init_errors = __esm({
|
|
761
761
|
"../contract/src/errors.ts"() {
|
|
762
762
|
"use strict";
|
|
@@ -802,6 +802,15 @@ var init_errors = __esm({
|
|
|
802
802
|
}
|
|
803
803
|
name = "BlobTooLargeError";
|
|
804
804
|
};
|
|
805
|
+
BlobSweepAbortedError = class extends Error {
|
|
806
|
+
constructor(candidateRows, totalRows, maxDeleteRatio) {
|
|
807
|
+
super(`blob sweep aborted: ${candidateRows}/${totalRows} rows would be deleted, above maxDeleteRatio ${maxDeleteRatio}; nothing deleted`);
|
|
808
|
+
this.candidateRows = candidateRows;
|
|
809
|
+
this.totalRows = totalRows;
|
|
810
|
+
this.maxDeleteRatio = maxDeleteRatio;
|
|
811
|
+
}
|
|
812
|
+
name = "BlobSweepAbortedError";
|
|
813
|
+
};
|
|
805
814
|
BlobIntegrityError = class extends Error {
|
|
806
815
|
constructor(expectedHash, actualHash) {
|
|
807
816
|
super(`blob hash mismatch: expected ${expectedHash}, got ${actualHash}`);
|
|
@@ -869,6 +878,24 @@ var init_errors = __esm({
|
|
|
869
878
|
});
|
|
870
879
|
|
|
871
880
|
// ../contract/src/storage.ts
|
|
881
|
+
function planBlobSweep(entries, reachable, opts = {}, nowMs = Date.now()) {
|
|
882
|
+
const cutoff = nowMs - Math.max(0, opts.graceMs ?? 0);
|
|
883
|
+
const candidates = entries.filter((e5) => !reachable.has(e5.hash) && (opts.graceMs ? e5.putAtMs < cutoff : true));
|
|
884
|
+
const report = {
|
|
885
|
+
dryRun: opts.dryRun === true,
|
|
886
|
+
totalRows: entries.length,
|
|
887
|
+
candidateRows: candidates.length,
|
|
888
|
+
candidateBytes: candidates.reduce((n3, e5) => n3 + e5.size, 0),
|
|
889
|
+
deletedRows: 0,
|
|
890
|
+
deletedBytes: 0,
|
|
891
|
+
orphansFound: 0,
|
|
892
|
+
orphansDeleted: 0
|
|
893
|
+
};
|
|
894
|
+
if (opts.maxDeleteRatio !== void 0 && entries.length > 0 && candidates.length / entries.length > opts.maxDeleteRatio) {
|
|
895
|
+
throw new BlobSweepAbortedError(candidates.length, entries.length, opts.maxDeleteRatio);
|
|
896
|
+
}
|
|
897
|
+
return { report, toDelete: opts.dryRun ? [] : candidates };
|
|
898
|
+
}
|
|
872
899
|
async function putBlobStream(store, source, opts = {}) {
|
|
873
900
|
if (store.putStream) return store.putStream(source, opts);
|
|
874
901
|
const parts = [];
|
|
@@ -2819,6 +2846,52 @@ var init_file_limits2 = __esm({
|
|
|
2819
2846
|
}
|
|
2820
2847
|
});
|
|
2821
2848
|
|
|
2849
|
+
// ../server/src/raw-upload.ts
|
|
2850
|
+
function isRawUploadRequest(method, pathname) {
|
|
2851
|
+
return RAW_UPLOAD_ROUTES.some((r5) => r5.method === method && r5.path.test(pathname));
|
|
2852
|
+
}
|
|
2853
|
+
function rawUploadName(req) {
|
|
2854
|
+
const raw = req.headers["x-oasis-filename"];
|
|
2855
|
+
const value2 = Array.isArray(raw) ? raw[0] : raw;
|
|
2856
|
+
if (!value2) return void 0;
|
|
2857
|
+
let name;
|
|
2858
|
+
try {
|
|
2859
|
+
name = decodeURIComponent(value2);
|
|
2860
|
+
} catch {
|
|
2861
|
+
return void 0;
|
|
2862
|
+
}
|
|
2863
|
+
return name.trim() || void 0;
|
|
2864
|
+
}
|
|
2865
|
+
function rawUploadContentType(req) {
|
|
2866
|
+
const value2 = req.headers["content-type"]?.trim();
|
|
2867
|
+
return value2 && value2 !== "application/octet-stream" ? value2 : void 0;
|
|
2868
|
+
}
|
|
2869
|
+
async function putRawUpload(store, req, opts) {
|
|
2870
|
+
const code2 = opts.tooLargeCode ?? "FILE_TOO_LARGE";
|
|
2871
|
+
const tooLarge = () => new ApiError(413, code2, `\u6587\u4EF6\u8D85\u8FC7 ${opts.maxBytes / 1024 / 1024} MiB \u4E0A\u9650`);
|
|
2872
|
+
const declared = req.headers["content-length"];
|
|
2873
|
+
if (declared !== void 0 && Number(declared) > opts.maxBytes) throw tooLarge();
|
|
2874
|
+
try {
|
|
2875
|
+
return await putBlobStream(store, req.iterator({ destroyOnReturn: false }), { maxBytes: opts.maxBytes });
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
if (err instanceof BlobTooLargeError) throw tooLarge();
|
|
2878
|
+
throw err;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
var RAW_UPLOAD_ROUTES;
|
|
2882
|
+
var init_raw_upload = __esm({
|
|
2883
|
+
"../server/src/raw-upload.ts"() {
|
|
2884
|
+
"use strict";
|
|
2885
|
+
init_src();
|
|
2886
|
+
init_router();
|
|
2887
|
+
RAW_UPLOAD_ROUTES = [
|
|
2888
|
+
{ method: "PUT", path: /^\/api\/chat\/attachments\/raw$/ },
|
|
2889
|
+
{ method: "POST", path: /^\/api\/workorders\/[^/]+\/files\/raw$/ },
|
|
2890
|
+
{ method: "POST", path: /^\/api\/projects\/[^/]+\/files\/raw$/ }
|
|
2891
|
+
];
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2822
2895
|
// ../server/src/chat-system-message.ts
|
|
2823
2896
|
async function appendChatMessageOnce(store, m4) {
|
|
2824
2897
|
const opts = { source: "system" };
|
|
@@ -138745,7 +138818,7 @@ ${block}
|
|
|
138745
138818
|
}
|
|
138746
138819
|
return out;
|
|
138747
138820
|
}
|
|
138748
|
-
var
|
|
138821
|
+
var DEFAULT_BATCH2 = {
|
|
138749
138822
|
delayMs: 600,
|
|
138750
138823
|
longThresholdChars: 1e3,
|
|
138751
138824
|
longDelayMs: 2e3,
|
|
@@ -138764,11 +138837,11 @@ ${block}
|
|
|
138764
138837
|
var _a2, _b, _c5, _d, _e5, _f, _g;
|
|
138765
138838
|
const t = (_b = (_a2 = cfg === null || cfg === void 0 ? void 0 : cfg.batch) === null || _a2 === void 0 ? void 0 : _a2.text) !== null && _b !== void 0 ? _b : {};
|
|
138766
138839
|
return {
|
|
138767
|
-
delayMs: (_c5 = t.delayMs) !== null && _c5 !== void 0 ? _c5 :
|
|
138768
|
-
longThresholdChars: (_d = t.longThresholdChars) !== null && _d !== void 0 ? _d :
|
|
138769
|
-
longDelayMs: (_e5 = t.longDelayMs) !== null && _e5 !== void 0 ? _e5 :
|
|
138770
|
-
maxMessages: (_f = t.maxMessages) !== null && _f !== void 0 ? _f :
|
|
138771
|
-
maxChars: (_g = t.maxChars) !== null && _g !== void 0 ? _g :
|
|
138840
|
+
delayMs: (_c5 = t.delayMs) !== null && _c5 !== void 0 ? _c5 : DEFAULT_BATCH2.delayMs,
|
|
138841
|
+
longThresholdChars: (_d = t.longThresholdChars) !== null && _d !== void 0 ? _d : DEFAULT_BATCH2.longThresholdChars,
|
|
138842
|
+
longDelayMs: (_e5 = t.longDelayMs) !== null && _e5 !== void 0 ? _e5 : DEFAULT_BATCH2.longDelayMs,
|
|
138843
|
+
maxMessages: (_f = t.maxMessages) !== null && _f !== void 0 ? _f : DEFAULT_BATCH2.maxMessages,
|
|
138844
|
+
maxChars: (_g = t.maxChars) !== null && _g !== void 0 ? _g : DEFAULT_BATCH2.maxChars
|
|
138772
138845
|
};
|
|
138773
138846
|
}
|
|
138774
138847
|
var SeenCache = class {
|
|
@@ -153854,15 +153927,34 @@ async function reachableBlobs(refs, blobs) {
|
|
|
153854
153927
|
}
|
|
153855
153928
|
return reachable;
|
|
153856
153929
|
}
|
|
153857
|
-
|
|
153930
|
+
function projectFileRefs(projects) {
|
|
153931
|
+
return async () => {
|
|
153932
|
+
const out = /* @__PURE__ */ new Set();
|
|
153933
|
+
for (const p4 of await projects.listProjects()) {
|
|
153934
|
+
for (const f6 of await projects.listProjectFiles(p4.id)) if (f6.blobId) out.add(f6.blobId);
|
|
153935
|
+
for (const loc of await projects.listContentLocations(p4.id)) if (loc.blob?.blobId) out.add(loc.blob.blobId);
|
|
153936
|
+
}
|
|
153937
|
+
return out;
|
|
153938
|
+
};
|
|
153939
|
+
}
|
|
153940
|
+
async function gc(refs, blobs, opts = {}) {
|
|
153858
153941
|
const reachable = await reachableBlobs(refs, blobs);
|
|
153859
|
-
await
|
|
153860
|
-
|
|
153942
|
+
for (const extra of opts.extraRefs ?? []) for (const h5 of await extra()) reachable.add(h5);
|
|
153943
|
+
const report = await blobs.sweep(reachable, {
|
|
153944
|
+
dryRun: opts.dryRun === true,
|
|
153945
|
+
graceMs: opts.graceMs ?? GC_DEFAULT_GRACE_MS,
|
|
153946
|
+
maxDeleteRatio: opts.maxDeleteRatio ?? GC_DEFAULT_MAX_DELETE_RATIO,
|
|
153947
|
+
...opts.orphans ? { orphans: true } : {}
|
|
153948
|
+
});
|
|
153949
|
+
return { reachableCount: reachable.size, report: report ?? null };
|
|
153861
153950
|
}
|
|
153951
|
+
var GC_DEFAULT_GRACE_MS, GC_DEFAULT_MAX_DELETE_RATIO;
|
|
153862
153952
|
var init_gc = __esm({
|
|
153863
153953
|
"../core/src/gc.ts"() {
|
|
153864
153954
|
"use strict";
|
|
153865
153955
|
init_manifest_store();
|
|
153956
|
+
GC_DEFAULT_GRACE_MS = 24 * 60 * 60 * 1e3;
|
|
153957
|
+
GC_DEFAULT_MAX_DELETE_RATIO = 0.2;
|
|
153866
153958
|
}
|
|
153867
153959
|
});
|
|
153868
153960
|
|
|
@@ -213973,11 +214065,30 @@ ${acceptanceCriteria.map((c5) => `- ${c5}`).join("\n")}`] : []
|
|
|
213973
214065
|
return { message: "tool_call \u5DF2\u7559\u75D5" };
|
|
213974
214066
|
}
|
|
213975
214067
|
case "gc": {
|
|
214068
|
+
const dryRun = args["dryRun"] === true;
|
|
214069
|
+
const orphans = args["orphans"] === true;
|
|
214070
|
+
const ratio = (key2, lo, hi) => {
|
|
214071
|
+
const v2 = args[key2];
|
|
214072
|
+
if (v2 === void 0 || v2 === null) return void 0;
|
|
214073
|
+
if (typeof v2 !== "number" || !Number.isFinite(v2) || v2 < lo || v2 > hi) throw new Error(`\u53C2\u6570 ${key2} \u5FC5\u987B\u662F ${lo}\u2013${hi} \u4E4B\u95F4\u7684\u6570`);
|
|
214074
|
+
return v2;
|
|
214075
|
+
};
|
|
214076
|
+
const graceHours = ratio("graceHours", 0, 24 * 365);
|
|
214077
|
+
const maxDeleteRatio = ratio("maxDeleteRatio", 0, 1);
|
|
214078
|
+
if (!ctx.projectState && !dryRun) throw new Error("gc\uFF1A\u62FF\u4E0D\u5230\u672C\u516C\u53F8\u9879\u76EE\u5E93\uFF0C\u9879\u76EE\u6587\u4EF6\u65E0\u6CD5\u8FDB\u53EF\u8FBE\u96C6\uFF1B\u53EA\u5141\u8BB8 --dry-run");
|
|
213976
214079
|
const result = await gc(async () => {
|
|
213977
214080
|
if (!engineStore) return [];
|
|
213978
214081
|
return engineStore.transaction((tx) => tx.listAllArtifactRefs());
|
|
213979
|
-
}, blobs
|
|
213980
|
-
|
|
214082
|
+
}, blobs, {
|
|
214083
|
+
...ctx.projectState ? { extraRefs: [projectFileRefs(ctx.projectState)] } : {},
|
|
214084
|
+
dryRun,
|
|
214085
|
+
orphans,
|
|
214086
|
+
...graceHours !== void 0 ? { graceMs: graceHours * 36e5 } : {},
|
|
214087
|
+
...maxDeleteRatio !== void 0 ? { maxDeleteRatio } : {}
|
|
214088
|
+
});
|
|
214089
|
+
const r5 = result.report;
|
|
214090
|
+
const summary = !r5 ? "\uFF08\u5B58\u50A8\u672A\u56DE\u62A5\u6E05\u626B\u6570\uFF09" : dryRun ? `dry-run\uFF1A\u5171 ${r5.totalRows} \u884C\uFF0C\u4F1A\u5220 ${r5.candidateRows} \u884C / ${r5.candidateBytes} \u5B57\u8282${orphans ? `\uFF0C\u5B64\u513F\u5BF9\u8C61 ${r5.orphansFound} \u4E2A` : ""}\uFF1B\u672A\u5220\u9664\u4EFB\u4F55\u5185\u5BB9` : `\u5171 ${r5.totalRows} \u884C\uFF0C\u5220\u9664 ${r5.deletedRows} \u884C / ${r5.deletedBytes} \u5B57\u8282${orphans ? `\uFF0C\u5B64\u513F\u5BF9\u8C61 ${r5.orphansDeleted} \u4E2A` : ""}`;
|
|
214091
|
+
return { message: `GC \u5B8C\u6210\uFF1A\u4E8B\u5B9E\u67DC\u53EF\u8FBE blob ${result.reachableCount} \u4E2A\uFF1B${summary}`, data: result };
|
|
213981
214092
|
}
|
|
213982
214093
|
default:
|
|
213983
214094
|
throw new Error(`\u672A\u77E5\u547D\u4EE4: ${command6}`);
|
|
@@ -215099,13 +215210,15 @@ async function startOasisServer(opts) {
|
|
|
215099
215210
|
void (async () => {
|
|
215100
215211
|
try {
|
|
215101
215212
|
res.setHeader("access-control-allow-origin", "*");
|
|
215102
|
-
res.setHeader("access-control-allow-methods", "GET,POST,PATCH,DELETE,OPTIONS");
|
|
215103
|
-
res.setHeader("access-control-allow-headers", "authorization,content-type");
|
|
215213
|
+
res.setHeader("access-control-allow-methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
215214
|
+
res.setHeader("access-control-allow-headers", "authorization,content-type,x-oasis-company,x-oasis-filename");
|
|
215104
215215
|
if (req.method === "OPTIONS") {
|
|
215105
215216
|
res.writeHead(204).end();
|
|
215106
215217
|
return;
|
|
215107
215218
|
}
|
|
215108
215219
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
215220
|
+
const rawUpload = isRawUploadRequest(req.method, url.pathname);
|
|
215221
|
+
if (rawUpload) res.setHeader("connection", "close");
|
|
215109
215222
|
if (url.pathname === "/healthz" && url.searchParams.get("deep") === "1") {
|
|
215110
215223
|
const db = opts.deepHealth ? await opts.deepHealth() : { ok: true, db: "none" };
|
|
215111
215224
|
res.writeHead(db.ok ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ ok: db.ok, ...getBuildInfo(), db }));
|
|
@@ -215283,13 +215396,13 @@ async function startOasisServer(opts) {
|
|
|
215283
215396
|
const requestMaxBytes = fileUploadRequestMaxBytes(fileLimits);
|
|
215284
215397
|
const chunks = [];
|
|
215285
215398
|
let requestBytes = 0;
|
|
215286
|
-
const declared = Number(req.headers["content-length"] ?? 0);
|
|
215399
|
+
const declared = rawUpload ? 0 : Number(req.headers["content-length"] ?? 0);
|
|
215287
215400
|
if (declared > requestMaxBytes) {
|
|
215288
215401
|
req.resume();
|
|
215289
215402
|
res.writeHead(413, { "content-type": "application/json" }).end(JSON.stringify({ error: { code: "UPLOAD_TOO_LARGE", message: "\u4E0A\u4F20\u8BF7\u6C42\u8D85\u8FC7\u901A\u9053\u5BB9\u91CF" } }));
|
|
215290
215403
|
return;
|
|
215291
215404
|
}
|
|
215292
|
-
for await (const chunk of req) {
|
|
215405
|
+
if (!rawUpload) for await (const chunk of req) {
|
|
215293
215406
|
requestBytes += chunk.length;
|
|
215294
215407
|
if (requestBytes > requestMaxBytes) {
|
|
215295
215408
|
res.writeHead(413, { "content-type": "application/json" }).end(JSON.stringify({ error: { code: "UPLOAD_TOO_LARGE", message: "\u4E0A\u4F20\u8BF7\u6C42\u8D85\u8FC7\u901A\u9053\u5BB9\u91CF" } }));
|
|
@@ -215536,7 +215649,7 @@ async function startOasisServer(opts) {
|
|
|
215536
215649
|
}
|
|
215537
215650
|
const chatAnchorMessageId = await anchorMessageIdFor(chatOrigin, chatStoreNowOrNull);
|
|
215538
215651
|
const spawnArgs = body2.args;
|
|
215539
|
-
const needsProject = body2.command === "spawn" && (spawnArgs?.["createdVia"] === "seed" || spawnArgs?.["project_id"] !== void 0 || spawnArgs?.["projectId"] !== void 0);
|
|
215652
|
+
const needsProject = body2.command === "gc" || body2.command === "spawn" && (spawnArgs?.["createdVia"] === "seed" || spawnArgs?.["project_id"] !== void 0 || spawnArgs?.["projectId"] !== void 0);
|
|
215540
215653
|
const assertCmdPlanMembers = (plan) => assertPlanMembers(plan).catch((err) => {
|
|
215541
215654
|
if (err instanceof NotCompanyMemberError) throw new ApiError(err.status, err.code, err.message);
|
|
215542
215655
|
throw err;
|
|
@@ -216784,6 +216897,21 @@ async function startOasisServer(opts) {
|
|
|
216784
216897
|
}
|
|
216785
216898
|
return;
|
|
216786
216899
|
}
|
|
216900
|
+
if (url.pathname === "/api/chat/attachments/raw" && req.method === "PUT" && opts.assets) {
|
|
216901
|
+
const uploadKind = url.searchParams.get("purpose");
|
|
216902
|
+
const maxBytes = uploadKind === "knowledge" ? fileLimits.knowledgeBytes : uploadKind === "cli" ? fileLimits.cliBytes : fileLimits.attachmentBytes;
|
|
216903
|
+
try {
|
|
216904
|
+
const out = await putRawUpload(opts.assets, req, { maxBytes, tooLargeCode: "ATTACHMENT_TOO_LARGE" });
|
|
216905
|
+
res.writeHead(201, { "content-type": "application/json" }).end(JSON.stringify({ blobRef: out.hash }));
|
|
216906
|
+
} catch (err) {
|
|
216907
|
+
const e5 = err instanceof ApiError ? err : null;
|
|
216908
|
+
if (!e5) console.error(`[chat/attachments/raw] ${err instanceof Error ? err.message : String(err)}`);
|
|
216909
|
+
res.writeHead(e5?.status ?? 500, { "content-type": "application/json" }).end(JSON.stringify({
|
|
216910
|
+
error: e5 ? { code: e5.code, message: e5.status === 413 ? `\u9644\u4EF6\u8D85\u8FC7 ${maxBytes / 1024 / 1024}MB \u4E0A\u9650` : e5.message } : { code: "ATTACHMENT_UPLOAD_FAILED", message: "\u9644\u4EF6\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5" }
|
|
216911
|
+
}));
|
|
216912
|
+
}
|
|
216913
|
+
return;
|
|
216914
|
+
}
|
|
216787
216915
|
if (url.pathname === "/api/chat/attachments" && req.method === "POST" && opts.uploadChatAttachment) {
|
|
216788
216916
|
try {
|
|
216789
216917
|
const body2 = JSON.parse(rawBody);
|
|
@@ -219402,6 +219530,7 @@ var init_server3 = __esm({
|
|
|
219402
219530
|
"use strict";
|
|
219403
219531
|
init_src();
|
|
219404
219532
|
init_file_limits2();
|
|
219533
|
+
init_raw_upload();
|
|
219405
219534
|
http = __toESM(require("node:http"), 1);
|
|
219406
219535
|
import_node_crypto44 = require("node:crypto");
|
|
219407
219536
|
import_node_stream3 = require("node:stream");
|
|
@@ -223172,7 +223301,7 @@ var init_blob_object_backend = __esm({
|
|
|
223172
223301
|
for await (const file of walk(base)) {
|
|
223173
223302
|
const st = await fs25.promises.stat(file).catch(() => null);
|
|
223174
223303
|
if (!st) continue;
|
|
223175
|
-
yield { key: path24.relative(this.root, file).split(path24.sep).join("/"), size: st.size, mtimeMs: st.mtimeMs };
|
|
223304
|
+
yield { key: path24.relative(this.root, file).split(path24.sep).join("/"), size: st.size, mtimeMs: Math.max(st.mtimeMs, st.ctimeMs) };
|
|
223176
223305
|
}
|
|
223177
223306
|
}
|
|
223178
223307
|
};
|
|
@@ -225338,15 +225467,29 @@ var init_service4 = __esm({
|
|
|
225338
225467
|
}
|
|
225339
225468
|
/** 上传一个项目普通文件:内容入 blob 库,记一条元信息旁存(非 oplog 事实)。 */
|
|
225340
225469
|
async addProjectFile(input) {
|
|
225341
|
-
|
|
225470
|
+
const blobs = this.projectFileBlobs();
|
|
225342
225471
|
assertFileBytes(input.bytes);
|
|
225343
|
-
const blobId = await
|
|
225472
|
+
const blobId = await blobs.put(input.bytes);
|
|
225473
|
+
const { bytes: bytes2, ...meta } = input;
|
|
225474
|
+
return this.recordProjectFile({ ...meta, blobId, size: bytes2.byteLength });
|
|
225475
|
+
}
|
|
225476
|
+
/**
|
|
225477
|
+
* 项目文件的字节柜。原始字节上传(`POST /api/projects/:id/files/raw`,ADR 0638 S4)由路由把请求体
|
|
225478
|
+
* 流式写进这里,再调 {@link recordProjectFile} 记行。
|
|
225479
|
+
*/
|
|
225480
|
+
projectFileBlobs() {
|
|
225481
|
+
if (!this.blobs) throw new Error("\u9879\u76EE\u6587\u4EF6\u4E0A\u4F20\u9700\u8981 blob \u5E93\uFF08domain \u672A\u6CE8\u5165 blobs\uFF09");
|
|
225482
|
+
return this.blobs;
|
|
225483
|
+
}
|
|
225484
|
+
/** 字节已进柜之后的那一半:记一条项目文件元信息。 */
|
|
225485
|
+
async recordProjectFile(input) {
|
|
225486
|
+
const { blobId, size } = input;
|
|
225344
225487
|
const file = {
|
|
225345
225488
|
projectId: input.projectId,
|
|
225346
225489
|
fileId: `pf_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
|
225347
225490
|
name: input.name,
|
|
225348
225491
|
...input.path !== void 0 ? { path: input.path } : {},
|
|
225349
|
-
size
|
|
225492
|
+
size,
|
|
225350
225493
|
blobId,
|
|
225351
225494
|
...input.contentType !== void 0 ? { contentType: input.contentType } : {},
|
|
225352
225495
|
...input.uploadedBy !== void 0 ? { uploadedBy: input.uploadedBy } : {},
|
|
@@ -226919,13 +227062,25 @@ var init_dev_store = __esm({
|
|
|
226919
227062
|
async put(bytes2) {
|
|
226920
227063
|
const hash3 = (0, import_node_crypto51.createHash)("sha256").update(bytes2).digest("hex");
|
|
226921
227064
|
const existing = fs26.statSync(this.fileOf(hash3), { throwIfNoEntry: false });
|
|
226922
|
-
if (existing?.isFile() && existing.size === bytes2.byteLength)
|
|
227065
|
+
if (existing?.isFile() && existing.size === bytes2.byteLength) {
|
|
227066
|
+
this.touch(hash3);
|
|
227067
|
+
return hash3;
|
|
227068
|
+
}
|
|
226923
227069
|
return (await this.putStream(singleChunk(bytes2))).hash;
|
|
226924
227070
|
}
|
|
227071
|
+
/** 重复 put 也刷新 mtime:GC 宽限期按它算(同 PG 的 `last_put_at`)。 */
|
|
227072
|
+
touch(hash3) {
|
|
227073
|
+
const now = /* @__PURE__ */ new Date();
|
|
227074
|
+
try {
|
|
227075
|
+
fs26.utimesSync(this.fileOf(hash3), now, now);
|
|
227076
|
+
} catch {
|
|
227077
|
+
}
|
|
227078
|
+
}
|
|
226925
227079
|
/** 流式写(ADR 0638 §3.2):边收边算 hash,超 `maxBytes` / hash 不符删残件抛错。 */
|
|
226926
227080
|
async putStream(source, opts) {
|
|
226927
227081
|
const staged = await stageBlob(this.tmpDir, source, opts);
|
|
226928
227082
|
await commitStagedFile(staged, this.fileOf(staged.hash), 420);
|
|
227083
|
+
this.touch(staged.hash);
|
|
226929
227084
|
const contentType = sniffContentType(staged.head);
|
|
226930
227085
|
return { hash: staged.hash, size: staged.size, ...contentType ? { contentType } : {} };
|
|
226931
227086
|
}
|
|
@@ -226981,10 +227136,20 @@ var init_dev_store = __esm({
|
|
|
226981
227136
|
async has(hash3) {
|
|
226982
227137
|
return BLOB_NAME.test(hash3) && fs26.statSync(this.fileOf(hash3), { throwIfNoEntry: false })?.isFile() === true;
|
|
226983
227138
|
}
|
|
226984
|
-
async sweep(reachable) {
|
|
227139
|
+
async sweep(reachable, opts = {}) {
|
|
227140
|
+
const entries = [];
|
|
226985
227141
|
for (const name of fs26.readdirSync(this.dir)) {
|
|
226986
|
-
if (BLOB_NAME.test(name)
|
|
227142
|
+
if (!BLOB_NAME.test(name)) continue;
|
|
227143
|
+
const st = fs26.statSync(this.fileOf(name), { throwIfNoEntry: false });
|
|
227144
|
+
if (st?.isFile()) entries.push({ hash: name, size: st.size, putAtMs: st.mtimeMs });
|
|
227145
|
+
}
|
|
227146
|
+
const { report, toDelete } = planBlobSweep(entries, reachable, opts);
|
|
227147
|
+
for (const e5 of toDelete) {
|
|
227148
|
+
fs26.rmSync(this.fileOf(e5.hash), { force: true });
|
|
227149
|
+
report.deletedRows++;
|
|
227150
|
+
report.deletedBytes += e5.size;
|
|
226987
227151
|
}
|
|
227152
|
+
return report;
|
|
226988
227153
|
}
|
|
226989
227154
|
};
|
|
226990
227155
|
MUTATORS = [
|
|
@@ -229104,6 +229269,18 @@ async function copySessionOutputs(deps) {
|
|
|
229104
229269
|
result.skipped.push({ path: file.path, code: "BUDGET" });
|
|
229105
229270
|
continue;
|
|
229106
229271
|
}
|
|
229272
|
+
const release2 = file.size > OUTPUT_COPY_SERIAL_BYTES && deps.acquireLargeFileSlot ? await deps.acquireLargeFileSlot() : void 0;
|
|
229273
|
+
try {
|
|
229274
|
+
await copyOne(file);
|
|
229275
|
+
} finally {
|
|
229276
|
+
release2?.();
|
|
229277
|
+
}
|
|
229278
|
+
}
|
|
229279
|
+
async function copyOne(file) {
|
|
229280
|
+
if (Date.now() >= deadline) {
|
|
229281
|
+
result.skipped.push({ path: file.path, code: "BUDGET" });
|
|
229282
|
+
return;
|
|
229283
|
+
}
|
|
229107
229284
|
let read3;
|
|
229108
229285
|
try {
|
|
229109
229286
|
read3 = await deps.bridge.read(
|
|
@@ -229113,20 +229290,20 @@ async function copySessionOutputs(deps) {
|
|
|
229113
229290
|
);
|
|
229114
229291
|
} catch {
|
|
229115
229292
|
result.skipped.push({ path: file.path, code: "READ_FAILED" });
|
|
229116
|
-
|
|
229293
|
+
return;
|
|
229117
229294
|
}
|
|
229118
229295
|
if (!read3.ok) {
|
|
229119
229296
|
result.skipped.push({ path: file.path, code: read3.code === "TOO_LARGE" ? "TOO_LARGE" : "READ_FAILED" });
|
|
229120
|
-
|
|
229297
|
+
return;
|
|
229121
229298
|
}
|
|
229122
229299
|
const bytes2 = Buffer.from(read3.contentBase64, "base64");
|
|
229123
229300
|
if (bytes2.length > limits.maxFileBytes) {
|
|
229124
229301
|
result.skipped.push({ path: file.path, code: "TOO_LARGE" });
|
|
229125
|
-
|
|
229302
|
+
return;
|
|
229126
229303
|
}
|
|
229127
229304
|
if (bytes2.length !== file.size || read3.mtime && read3.mtime !== file.mtime) {
|
|
229128
229305
|
result.skipped.push({ path: file.path, code: "FILE_CHANGED" });
|
|
229129
|
-
|
|
229306
|
+
return;
|
|
229130
229307
|
}
|
|
229131
229308
|
const saved = existing?.[file.path];
|
|
229132
229309
|
if (saved && savedContentHash(saved) === (0, import_node_crypto53.createHash)("sha256").update(bytes2).digest("hex")) {
|
|
@@ -229134,14 +229311,14 @@ async function copySessionOutputs(deps) {
|
|
|
229134
229311
|
delete aligned.editedAt;
|
|
229135
229312
|
realigned.push(aligned);
|
|
229136
229313
|
result.unchanged.push(file.path);
|
|
229137
|
-
|
|
229314
|
+
return;
|
|
229138
229315
|
}
|
|
229139
229316
|
let blobRef;
|
|
229140
229317
|
try {
|
|
229141
|
-
blobRef = await deps.putBlob(
|
|
229318
|
+
blobRef = await deps.putBlob(bytes2);
|
|
229142
229319
|
} catch {
|
|
229143
229320
|
result.skipped.push({ path: file.path, code: "STORE_FAILED" });
|
|
229144
|
-
|
|
229321
|
+
return;
|
|
229145
229322
|
}
|
|
229146
229323
|
result.copied.push({
|
|
229147
229324
|
path: file.path,
|
|
@@ -229167,6 +229344,8 @@ async function copySessionOutputs(deps) {
|
|
|
229167
229344
|
}
|
|
229168
229345
|
function createOutputCopyScheduler(deps) {
|
|
229169
229346
|
const inFlight = /* @__PURE__ */ new Map();
|
|
229347
|
+
const largeFiles = createKeyedMutex();
|
|
229348
|
+
const acquireLargeFileSlot = () => largeFiles.acquire("large-output-copy");
|
|
229170
229349
|
return {
|
|
229171
229350
|
schedule(args) {
|
|
229172
229351
|
const { store, chatSessionId, companyId } = args;
|
|
@@ -229190,6 +229369,7 @@ function createOutputCopyScheduler(deps) {
|
|
|
229190
229369
|
readCopies,
|
|
229191
229370
|
mergeCopies,
|
|
229192
229371
|
putBlob: deps.putBlob,
|
|
229372
|
+
acquireLargeFileSlot,
|
|
229193
229373
|
...deps.limits ? { limits: deps.limits } : {}
|
|
229194
229374
|
});
|
|
229195
229375
|
const maxFileMiB = Math.round((deps.limits ?? OUTPUT_COPY_LIMITS).maxFileBytes / 1024 / 1024);
|
|
@@ -229226,17 +229406,19 @@ function createOutputCopyScheduler(deps) {
|
|
|
229226
229406
|
}
|
|
229227
229407
|
};
|
|
229228
229408
|
}
|
|
229229
|
-
var import_node_crypto53, OUTPUT_COPY_LIMITS;
|
|
229409
|
+
var import_node_crypto53, OUTPUT_COPY_LIMITS, OUTPUT_COPY_SERIAL_BYTES;
|
|
229230
229410
|
var init_outputs_copy = __esm({
|
|
229231
229411
|
"../server/src/domains/chat-sessions/outputs-copy.ts"() {
|
|
229232
229412
|
"use strict";
|
|
229233
229413
|
import_node_crypto53 = require("node:crypto");
|
|
229234
229414
|
init_src();
|
|
229415
|
+
init_keyed_mutex();
|
|
229235
229416
|
OUTPUT_COPY_LIMITS = {
|
|
229236
229417
|
maxFileBytes: FILE_CONTENT_MAX_BYTES,
|
|
229237
229418
|
maxFiles: 50,
|
|
229238
229419
|
maxTotalBytes: 2 * FILE_CONTENT_MAX_BYTES
|
|
229239
229420
|
};
|
|
229421
|
+
OUTPUT_COPY_SERIAL_BYTES = 20 * 1024 * 1024;
|
|
229240
229422
|
}
|
|
229241
229423
|
});
|
|
229242
229424
|
|
|
@@ -239360,6 +239542,24 @@ function projectsDomain(opts) {
|
|
|
239360
239542
|
});
|
|
239361
239543
|
return { status: 201, body: file };
|
|
239362
239544
|
});
|
|
239545
|
+
router.post("/api/projects/:id/files/raw", async (req) => {
|
|
239546
|
+
const name = rawUploadName(req.raw);
|
|
239547
|
+
if (!name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 X-Oasis-Filename");
|
|
239548
|
+
const path50 = req.query.get("path") ?? void 0;
|
|
239549
|
+
const contentType = rawUploadContentType(req.raw);
|
|
239550
|
+
const service = await svc(req);
|
|
239551
|
+
const put3 = await putRawUpload(service.projectFileBlobs(), req.raw, { maxBytes: currentFileLimits().attachmentBytes });
|
|
239552
|
+
const file = await service.recordProjectFile({
|
|
239553
|
+
projectId: req.params.id,
|
|
239554
|
+
name,
|
|
239555
|
+
...path50 !== void 0 ? { path: path50 } : {},
|
|
239556
|
+
blobId: put3.hash,
|
|
239557
|
+
size: put3.size,
|
|
239558
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
239559
|
+
uploadedBy: req.auth.actor
|
|
239560
|
+
});
|
|
239561
|
+
return { status: 201, body: file };
|
|
239562
|
+
});
|
|
239363
239563
|
router.put("/api/projects/:id/files/:fileId", async (req) => {
|
|
239364
239564
|
const b6 = req.body ?? {};
|
|
239365
239565
|
const base642 = b6.content_base64 ?? b6.contentBase64;
|
|
@@ -240122,6 +240322,8 @@ var init_routes5 = __esm({
|
|
|
240122
240322
|
"../server/src/domains/projects/routes.ts"() {
|
|
240123
240323
|
"use strict";
|
|
240124
240324
|
init_router();
|
|
240325
|
+
init_file_limits2();
|
|
240326
|
+
init_raw_upload();
|
|
240125
240327
|
init_src();
|
|
240126
240328
|
init_chat_files2();
|
|
240127
240329
|
init_chat_sessions();
|
|
@@ -246047,33 +246249,26 @@ function collabDomain(opts) {
|
|
|
246047
246249
|
}));
|
|
246048
246250
|
return { status: 200, body: { total: items.length, items } };
|
|
246049
246251
|
});
|
|
246050
|
-
|
|
246252
|
+
const resolveUploadTarget = async (req) => {
|
|
246051
246253
|
const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
|
|
246052
246254
|
const ws = req.params.id;
|
|
246053
246255
|
const exists = [...kernel.model.artifacts.values()].some((a5) => a5.workspace === ws);
|
|
246054
246256
|
if (!exists) {
|
|
246055
|
-
return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
|
|
246257
|
+
return { denied: { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } } };
|
|
246056
246258
|
}
|
|
246057
246259
|
if (!opts.assets || !artifacts?.addWorkOrderFile) {
|
|
246058
246260
|
return {
|
|
246059
|
-
|
|
246060
|
-
|
|
246261
|
+
denied: {
|
|
246262
|
+
status: 501,
|
|
246263
|
+
body: { error: { code: "unavailable", message: "\u672C\u90E8\u7F72\u672A\u63A5\u5165\u4EFB\u52A1\u6587\u4EF6\u4E0A\u4F20\uFF08\u7F3A\u8D44\u4EA7\u67DC\u6216 addWorkOrderFile\uFF09" } }
|
|
246264
|
+
}
|
|
246061
246265
|
};
|
|
246062
246266
|
}
|
|
246063
|
-
|
|
246064
|
-
|
|
246065
|
-
|
|
246066
|
-
}
|
|
246067
|
-
const
|
|
246068
|
-
const contentType = typeof body2.contentType === "string" && body2.contentType.trim() ? body2.contentType.trim() : void 0;
|
|
246069
|
-
let bytes2;
|
|
246070
|
-
try {
|
|
246071
|
-
bytes2 = new Uint8Array(Buffer.from(body2.contentBase64, "base64"));
|
|
246072
|
-
} catch (err) {
|
|
246073
|
-
return { status: 400, body: { error: { code: "bad_request", message: `contentBase64 \u89E3\u7801\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` } } };
|
|
246074
|
-
}
|
|
246075
|
-
assertFileBytes(bytes2);
|
|
246076
|
-
const blobId = await opts.assets.put(bytes2);
|
|
246267
|
+
return { target: { ws, registry: registry2, assets: opts.assets, addWorkOrderFile: artifacts.addWorkOrderFile.bind(artifacts) } };
|
|
246268
|
+
};
|
|
246269
|
+
const linkUploadedFile = async (req, target, input) => {
|
|
246270
|
+
const { ws, registry: registry2, addWorkOrderFile } = target;
|
|
246271
|
+
const { blobId, name, size, contentType } = input;
|
|
246077
246272
|
const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
246078
246273
|
const fileId = workOrderFileId(ws, blobId, name);
|
|
246079
246274
|
const file = {
|
|
@@ -246081,14 +246276,14 @@ function collabDomain(opts) {
|
|
|
246081
246276
|
fileId,
|
|
246082
246277
|
name,
|
|
246083
246278
|
blobId,
|
|
246084
|
-
size
|
|
246279
|
+
size,
|
|
246085
246280
|
...contentType ? { contentType } : {},
|
|
246086
246281
|
linkedAt,
|
|
246087
246282
|
// 上传者:项目页「创建人」列与预览副标据此署名(建单带入的那条路记的是建单发起人)。
|
|
246088
246283
|
uploadedBy: req.auth.actor
|
|
246089
246284
|
};
|
|
246090
246285
|
try {
|
|
246091
|
-
await
|
|
246286
|
+
await addWorkOrderFile(file);
|
|
246092
246287
|
} catch (err) {
|
|
246093
246288
|
return {
|
|
246094
246289
|
status: 500,
|
|
@@ -246101,7 +246296,7 @@ function collabDomain(opts) {
|
|
|
246101
246296
|
name,
|
|
246102
246297
|
kind: "file",
|
|
246103
246298
|
content_type: contentType ?? null,
|
|
246104
|
-
size
|
|
246299
|
+
size,
|
|
246105
246300
|
updated_at: linkedAt,
|
|
246106
246301
|
blob_ref: blobId,
|
|
246107
246302
|
source_session_id: null,
|
|
@@ -246111,6 +246306,35 @@ function collabDomain(opts) {
|
|
|
246111
246306
|
edit_target: workOrderFileEditTarget(ws, file)
|
|
246112
246307
|
};
|
|
246113
246308
|
return { status: 201, body: { item } };
|
|
246309
|
+
};
|
|
246310
|
+
router.post("/api/workorders/:id/files", async (req) => {
|
|
246311
|
+
const resolved = await resolveUploadTarget(req);
|
|
246312
|
+
if ("denied" in resolved) return resolved.denied;
|
|
246313
|
+
const { target } = resolved;
|
|
246314
|
+
const body2 = req.body ?? {};
|
|
246315
|
+
if (typeof body2.contentBase64 !== "string" || !body2.contentBase64) {
|
|
246316
|
+
return { status: 400, body: { error: { code: "bad_request", message: "\u7F3A\u5C11 contentBase64" } } };
|
|
246317
|
+
}
|
|
246318
|
+
const name = typeof body2.name === "string" && body2.name.trim() ? body2.name.trim() : "untitled";
|
|
246319
|
+
const contentType = typeof body2.contentType === "string" && body2.contentType.trim() ? body2.contentType.trim() : void 0;
|
|
246320
|
+
let bytes2;
|
|
246321
|
+
try {
|
|
246322
|
+
bytes2 = new Uint8Array(Buffer.from(body2.contentBase64, "base64"));
|
|
246323
|
+
} catch (err) {
|
|
246324
|
+
return { status: 400, body: { error: { code: "bad_request", message: `contentBase64 \u89E3\u7801\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` } } };
|
|
246325
|
+
}
|
|
246326
|
+
assertFileBytes(bytes2);
|
|
246327
|
+
const blobId = await target.assets.put(bytes2);
|
|
246328
|
+
return linkUploadedFile(req, target, { blobId, name, size: bytes2.byteLength, ...contentType ? { contentType } : {} });
|
|
246329
|
+
});
|
|
246330
|
+
router.post("/api/workorders/:id/files/raw", async (req) => {
|
|
246331
|
+
const resolved = await resolveUploadTarget(req);
|
|
246332
|
+
if ("denied" in resolved) return resolved.denied;
|
|
246333
|
+
const { target } = resolved;
|
|
246334
|
+
const name = rawUploadName(req.raw) ?? "untitled";
|
|
246335
|
+
const contentType = rawUploadContentType(req.raw);
|
|
246336
|
+
const put3 = await putRawUpload(target.assets, req.raw, { maxBytes: currentFileLimits().attachmentBytes });
|
|
246337
|
+
return linkUploadedFile(req, target, { blobId: put3.hash, name, size: put3.size, ...contentType ? { contentType } : {} });
|
|
246114
246338
|
});
|
|
246115
246339
|
router.put("/api/workorders/:id/files/:fileId", async (req) => {
|
|
246116
246340
|
const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
|
|
@@ -246690,6 +246914,7 @@ var init_collab = __esm({
|
|
|
246690
246914
|
"../server/src/domains/collab/index.ts"() {
|
|
246691
246915
|
"use strict";
|
|
246692
246916
|
init_file_limits2();
|
|
246917
|
+
init_raw_upload();
|
|
246693
246918
|
init_src8();
|
|
246694
246919
|
init_src();
|
|
246695
246920
|
init_chat_workorder_files();
|
|
@@ -263871,6 +264096,44 @@ function readBytesSegments(pool, table, hash3, r5) {
|
|
|
263871
264096
|
cancelled = true;
|
|
263872
264097
|
} };
|
|
263873
264098
|
}
|
|
264099
|
+
async function sweepBlobRows(pool, table, reachable, opts = {}) {
|
|
264100
|
+
const keep = [...reachable];
|
|
264101
|
+
const graceMs = Math.max(0, Math.floor(opts.graceMs ?? 0));
|
|
264102
|
+
const head = await pool.query(
|
|
264103
|
+
`SELECT now() - ($1::bigint * interval '1 millisecond') AS cutoff, (SELECT count(*)::int FROM ${table}) AS total`,
|
|
264104
|
+
[graceMs]
|
|
264105
|
+
);
|
|
264106
|
+
const cutoff = head.rows[0].cutoff;
|
|
264107
|
+
const totalRows = Number(head.rows[0].total);
|
|
264108
|
+
const cand = await pool.query(
|
|
264109
|
+
`SELECT count(*)::int AS n, sum(size)::text AS bytes FROM ${table}
|
|
264110
|
+
WHERE NOT (hash = ANY($1::text[])) AND last_put_at < $2`,
|
|
264111
|
+
[keep, cutoff]
|
|
264112
|
+
);
|
|
264113
|
+
const candidateRows = Number(cand.rows[0]?.n ?? 0);
|
|
264114
|
+
const report = {
|
|
264115
|
+
dryRun: opts.dryRun === true,
|
|
264116
|
+
totalRows,
|
|
264117
|
+
candidateRows,
|
|
264118
|
+
candidateBytes: Number(cand.rows[0]?.bytes ?? 0),
|
|
264119
|
+
deletedRows: 0,
|
|
264120
|
+
deletedBytes: 0,
|
|
264121
|
+
orphansFound: 0,
|
|
264122
|
+
orphansDeleted: 0
|
|
264123
|
+
};
|
|
264124
|
+
if (opts.maxDeleteRatio !== void 0 && totalRows > 0 && candidateRows / totalRows > opts.maxDeleteRatio) {
|
|
264125
|
+
throw new BlobSweepAbortedError(candidateRows, totalRows, opts.maxDeleteRatio);
|
|
264126
|
+
}
|
|
264127
|
+
if (opts.dryRun || candidateRows === 0) return { report, deleted: [], cutoff };
|
|
264128
|
+
const res = await pool.query(
|
|
264129
|
+
`DELETE FROM ${table} WHERE NOT (hash = ANY($1::text[])) AND last_put_at < $2 RETURNING hash, location, size`,
|
|
264130
|
+
[keep, cutoff]
|
|
264131
|
+
);
|
|
264132
|
+
const deleted = res.rows.map((r5) => ({ hash: r5.hash, location: r5.location, size: Number(r5.size) }));
|
|
264133
|
+
report.deletedRows = deleted.length;
|
|
264134
|
+
report.deletedBytes = deleted.reduce((n3, r5) => n3 + r5.size, 0);
|
|
264135
|
+
return { report, deleted, cutoff };
|
|
264136
|
+
}
|
|
263874
264137
|
var import_node_crypto80, ident5, isUniqueViolation, PostgresOplogStore, PG_READ_CHUNK, PostgresBlobStore;
|
|
263875
264138
|
var init_postgres = __esm({
|
|
263876
264139
|
"../storage/src/postgres.ts"() {
|
|
@@ -264093,10 +264356,9 @@ var init_postgres = __esm({
|
|
|
264093
264356
|
const res = await this.pool.query(`SELECT 1 FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
264094
264357
|
return res.rowCount === 1;
|
|
264095
264358
|
}
|
|
264096
|
-
|
|
264097
|
-
|
|
264098
|
-
|
|
264099
|
-
]);
|
|
264359
|
+
/** 字节都在 bytea,删行即删字节;没有对象后端,`orphans` 不适用。 */
|
|
264360
|
+
async sweep(reachable, opts = {}) {
|
|
264361
|
+
return (await sweepBlobRows(this.pool, this.t, reachable, opts)).report;
|
|
264100
264362
|
}
|
|
264101
264363
|
};
|
|
264102
264364
|
}
|
|
@@ -278963,7 +279225,7 @@ var init_cutover_aware_dispatches = __esm({
|
|
|
278963
279225
|
});
|
|
278964
279226
|
|
|
278965
279227
|
// ../storage/src/cutover-aware-blobs.ts
|
|
278966
|
-
var CutoverAwareBlobStore;
|
|
279228
|
+
var CutoverAwareBlobStore, SharedBlobSweepRefusedError;
|
|
278967
279229
|
var init_cutover_aware_blobs = __esm({
|
|
278968
279230
|
"../storage/src/cutover-aware-blobs.ts"() {
|
|
278969
279231
|
"use strict";
|
|
@@ -279026,11 +279288,20 @@ var init_cutover_aware_blobs = __esm({
|
|
|
279026
279288
|
* ★ 清扫也走**写闸**,而且冻结期必须真的拒——不是"跳过一次"。
|
|
279027
279289
|
*
|
|
279028
279290
|
* 冻结窗口正是 operator 在做最后一次逐行复制的时候:此刻放行 sweep,
|
|
279029
|
-
* 会按一份**还没补完**的可达集去删字节,删掉的是正在搬的那批。
|
|
279291
|
+
* 会按一份**还没补完**的可达集去删字节,删掉的是正在搬的那批。dry-run 也走写闸:冻结期报出来的数没有意义。
|
|
279030
279292
|
*/
|
|
279031
|
-
async sweep(reachable) {
|
|
279032
|
-
|
|
279293
|
+
async sweep(reachable, opts) {
|
|
279294
|
+
const target = await this.pickWritable();
|
|
279295
|
+
if (target === this.o.legacy && !opts?.dryRun) throw new SharedBlobSweepRefusedError(this.o.companyId);
|
|
279296
|
+
return target.sweep(reachable, opts);
|
|
279297
|
+
}
|
|
279298
|
+
};
|
|
279299
|
+
SharedBlobSweepRefusedError = class extends Error {
|
|
279300
|
+
constructor(companyId) {
|
|
279301
|
+
super(`\u516C\u53F8 ${companyId} \u7684 blob \u8FD8\u5728\u5171\u4EAB\u4E8B\u5B9E\u67DC\uFF08\u672A\u5207\u5230\u516C\u53F8 schema\uFF09\uFF0C\u6309\u5355\u516C\u53F8\u53EF\u8FBE\u96C6\u6E05\u626B\u4F1A\u5220\u5230\u522B\u5BB6\u5B57\u8282\uFF1A\u53EA\u5141\u8BB8 dry-run`);
|
|
279302
|
+
this.companyId = companyId;
|
|
279033
279303
|
}
|
|
279304
|
+
name = "SharedBlobSweepRefusedError";
|
|
279034
279305
|
};
|
|
279035
279306
|
}
|
|
279036
279307
|
});
|
|
@@ -279637,6 +279908,357 @@ var init_cutover_aware_trace = __esm({
|
|
|
279637
279908
|
}
|
|
279638
279909
|
});
|
|
279639
279910
|
|
|
279911
|
+
// ../storage/src/externalized-blobs.ts
|
|
279912
|
+
function factObjectKey(namespace, hash3) {
|
|
279913
|
+
return `${factObjectPrefix(namespace)}${hash3.slice(0, 2)}/${hash3}`;
|
|
279914
|
+
}
|
|
279915
|
+
function factObjectPrefix(namespace) {
|
|
279916
|
+
return `facts/${namespace}/`;
|
|
279917
|
+
}
|
|
279918
|
+
async function drainToBuffer(body2) {
|
|
279919
|
+
const parts = [];
|
|
279920
|
+
for await (const chunk of body2) parts.push(chunk);
|
|
279921
|
+
return new Uint8Array(Buffer.concat(parts));
|
|
279922
|
+
}
|
|
279923
|
+
var import_node_crypto87, fs36, BLOB_NAME2, MIGRATION_SELECTORS, ExternalizedBlobStore, ObjectBlobStore;
|
|
279924
|
+
var init_externalized_blobs = __esm({
|
|
279925
|
+
"../storage/src/externalized-blobs.ts"() {
|
|
279926
|
+
"use strict";
|
|
279927
|
+
import_node_crypto87 = require("node:crypto");
|
|
279928
|
+
fs36 = __toESM(require("node:fs"), 1);
|
|
279929
|
+
init_src();
|
|
279930
|
+
init_blob_object_backend();
|
|
279931
|
+
init_postgres();
|
|
279932
|
+
BLOB_NAME2 = /^[0-9a-f]{64}$/;
|
|
279933
|
+
MIGRATION_SELECTORS = {
|
|
279934
|
+
pg: `location = 'pg'`,
|
|
279935
|
+
ext: `location = 'ext'`,
|
|
279936
|
+
"ext-with-bytes": `location = 'ext' AND bytes IS NOT NULL`,
|
|
279937
|
+
internalize: `location = 'ext' OR bytes IS NULL`
|
|
279938
|
+
};
|
|
279939
|
+
ExternalizedBlobStore = class _ExternalizedBlobStore {
|
|
279940
|
+
constructor(pool, t, namespace, backend, opts) {
|
|
279941
|
+
this.pool = pool;
|
|
279942
|
+
this.t = t;
|
|
279943
|
+
this.namespace = namespace;
|
|
279944
|
+
this.backend = backend;
|
|
279945
|
+
this.opts = opts;
|
|
279946
|
+
}
|
|
279947
|
+
/** 命名空间 = schema 名(ADR 0638 D3)。 */
|
|
279948
|
+
static async open(pool, schema, backend, opts = {}) {
|
|
279949
|
+
const t = await ensureBlobsTable(pool, schema);
|
|
279950
|
+
return new _ExternalizedBlobStore(pool, t, schema, backend, opts);
|
|
279951
|
+
}
|
|
279952
|
+
key(hash3) {
|
|
279953
|
+
return factObjectKey(this.namespace, hash3);
|
|
279954
|
+
}
|
|
279955
|
+
async put(bytes2) {
|
|
279956
|
+
return (await this.putStream(singleChunk(bytes2))).hash;
|
|
279957
|
+
}
|
|
279958
|
+
async putStream(source, opts = {}) {
|
|
279959
|
+
const staged = await stageBlob(this.backend.stagingDir, source, opts);
|
|
279960
|
+
const contentType = sniffContentType(staged.head);
|
|
279961
|
+
const result = { hash: staged.hash, size: staged.size, ...contentType ? { contentType } : {} };
|
|
279962
|
+
try {
|
|
279963
|
+
await this.place(staged, contentType);
|
|
279964
|
+
} catch (err) {
|
|
279965
|
+
await discardStaged(staged);
|
|
279966
|
+
throw err;
|
|
279967
|
+
}
|
|
279968
|
+
return result;
|
|
279969
|
+
}
|
|
279970
|
+
async place(staged, contentType) {
|
|
279971
|
+
const { hash: hash3, size } = staged;
|
|
279972
|
+
const touched = await this.pool.query(
|
|
279973
|
+
`UPDATE ${this.t} SET last_put_at = now() WHERE hash = $1 RETURNING location`,
|
|
279974
|
+
[hash3]
|
|
279975
|
+
);
|
|
279976
|
+
const row = touched.rows[0];
|
|
279977
|
+
if (row?.location === "pg") {
|
|
279978
|
+
await discardStaged(staged);
|
|
279979
|
+
return;
|
|
279980
|
+
}
|
|
279981
|
+
if (row) {
|
|
279982
|
+
if (await this.backend.head(this.key(hash3)) === null) {
|
|
279983
|
+
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
279984
|
+
} else {
|
|
279985
|
+
await discardStaged(staged);
|
|
279986
|
+
}
|
|
279987
|
+
return;
|
|
279988
|
+
}
|
|
279989
|
+
const bytea = this.opts.dualWrite ? await fs36.promises.readFile(staged.file) : null;
|
|
279990
|
+
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
279991
|
+
await this.pool.query(
|
|
279992
|
+
`INSERT INTO ${this.t} AS b (hash, bytes, size, content_type, location) VALUES ($1, $2, $3, $4, 'ext')
|
|
279993
|
+
ON CONFLICT (hash) DO UPDATE SET last_put_at = now()`,
|
|
279994
|
+
[hash3, bytea, size, contentType ?? null]
|
|
279995
|
+
);
|
|
279996
|
+
}
|
|
279997
|
+
/** §3.2 第 1 条:只读元数据行,不访问对象后端。 */
|
|
279998
|
+
async meta(hash3) {
|
|
279999
|
+
const res = await this.pool.query(`SELECT size, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
280000
|
+
const row = res.rows[0];
|
|
280001
|
+
if (!row) return null;
|
|
280002
|
+
return { size: Number(row.size), ...row.content_type ? { contentType: row.content_type } : {} };
|
|
280003
|
+
}
|
|
280004
|
+
async has(hash3) {
|
|
280005
|
+
const res = await this.pool.query(`SELECT 1 FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
280006
|
+
return res.rowCount === 1;
|
|
280007
|
+
}
|
|
280008
|
+
async get(hash3) {
|
|
280009
|
+
const res = await this.pool.query(
|
|
280010
|
+
`SELECT location, size, CASE WHEN location = 'pg' THEN bytes END AS bytes FROM ${this.t} WHERE hash = $1`,
|
|
280011
|
+
[hash3]
|
|
280012
|
+
);
|
|
280013
|
+
const row = res.rows[0];
|
|
280014
|
+
if (!row) throw new BlobNotFoundError(hash3);
|
|
280015
|
+
if (row.location === "pg") {
|
|
280016
|
+
if (row.bytes === null) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
280017
|
+
return new Uint8Array(row.bytes);
|
|
280018
|
+
}
|
|
280019
|
+
const size = Number(row.size);
|
|
280020
|
+
const handle = await this.openExt(hash3, { start: 0, end: size - 1 });
|
|
280021
|
+
return drainToBuffer(handle.body);
|
|
280022
|
+
}
|
|
280023
|
+
async openRead(hash3, range2) {
|
|
280024
|
+
const res = await this.pool.query(
|
|
280025
|
+
`SELECT location, size, content_type, (location = 'pg' AND bytes IS NULL) AS purged FROM ${this.t} WHERE hash = $1`,
|
|
280026
|
+
[hash3]
|
|
280027
|
+
);
|
|
280028
|
+
const row = res.rows[0];
|
|
280029
|
+
if (!row) throw new BlobNotFoundError(hash3);
|
|
280030
|
+
if (row.purged) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
280031
|
+
const meta = { size: Number(row.size), ...row.content_type ? { contentType: row.content_type } : {} };
|
|
280032
|
+
const r5 = resolveBlobRange(hash3, meta.size, range2);
|
|
280033
|
+
const stream = row.location === "pg" ? readBytesSegments(this.pool, this.t, hash3, r5) : await this.openExt(hash3, r5);
|
|
280034
|
+
return { meta, range: r5, ...stream };
|
|
280035
|
+
}
|
|
280036
|
+
/** §3.2 第 2 条:行在、对象不在 → BlobBackendError;后端其它故障同样包成它(都是 5xx,不是 404)。 */
|
|
280037
|
+
async openExt(hash3, r5) {
|
|
280038
|
+
const key2 = this.key(hash3);
|
|
280039
|
+
try {
|
|
280040
|
+
return await this.backend.openRead(key2, r5);
|
|
280041
|
+
} catch (err) {
|
|
280042
|
+
if (err instanceof ObjectMissingError) throw new BlobBackendError(hash3, key2);
|
|
280043
|
+
throw new BlobBackendError(hash3, key2, err);
|
|
280044
|
+
}
|
|
280045
|
+
}
|
|
280046
|
+
/**
|
|
280047
|
+
* GC 执行(ADR 0638 §3.5):按行清扫(宽限期、删除比例上限、dry-run 见 {@link sweepBlobRows}),
|
|
280048
|
+
* **先删行、再删对象**——崩在中间只留无行对象,读者恒见「没有」,由孤儿扫描清理。
|
|
280049
|
+
* 只动本 schema 的行与本命名空间前缀 `facts/<schema>/` 下的对象:别家同一份字节在别的前缀里,
|
|
280050
|
+
* s3 模式同桶的资产柜在 `assets/` 前缀,都删不到(D3)。
|
|
280051
|
+
*/
|
|
280052
|
+
async sweep(reachable, opts = {}) {
|
|
280053
|
+
const { report, deleted } = await sweepBlobRows(this.pool, this.t, reachable, opts);
|
|
280054
|
+
const ext = deleted.filter((r5) => r5.location === "ext" && BLOB_NAME2.test(r5.hash)).map((r5) => r5.hash);
|
|
280055
|
+
const back = ext.length > 0 ? await this.existingRows(ext) : /* @__PURE__ */ new Set();
|
|
280056
|
+
for (const hash3 of ext) if (!back.has(hash3)) await this.backend.delete(this.key(hash3));
|
|
280057
|
+
if (opts.orphans) {
|
|
280058
|
+
const o3 = await this.sweepOrphans(Math.max(0, opts.graceMs ?? 0), opts.dryRun === true);
|
|
280059
|
+
report.orphansFound = o3.found;
|
|
280060
|
+
report.orphansDeleted = o3.deleted;
|
|
280061
|
+
}
|
|
280062
|
+
return report;
|
|
280063
|
+
}
|
|
280064
|
+
async existingRows(hashes) {
|
|
280065
|
+
const res = await this.pool.query(`SELECT hash FROM ${this.t} WHERE hash = ANY($1::text[])`, [hashes]);
|
|
280066
|
+
return new Set(res.rows.map((r5) => r5.hash));
|
|
280067
|
+
}
|
|
280068
|
+
/**
|
|
280069
|
+
* 孤儿对象扫描(§3.5 第 3 条,`gc --orphans`):列出 `facts/<schema>/` 下全部对象(fs 递归、s3 ListObjectsV2 分页),
|
|
280070
|
+
* 删除**无对应行且早于宽限期**的。宽限期挡住「对象已放、行还没写」的在途写入与租户迁移复制(先放对象、再写行)。
|
|
280071
|
+
* 只认 `facts/<schema>/<hh>/<hash>` 这一种形状,别的文件名一律不碰。
|
|
280072
|
+
*/
|
|
280073
|
+
async sweepOrphans(graceMs, dryRun) {
|
|
280074
|
+
const prefix = factObjectPrefix(this.namespace);
|
|
280075
|
+
const cutoffMs = Date.now() - graceMs;
|
|
280076
|
+
let found2 = 0;
|
|
280077
|
+
let deleted = 0;
|
|
280078
|
+
let page = [];
|
|
280079
|
+
const flush2 = async () => {
|
|
280080
|
+
if (page.length === 0) return;
|
|
280081
|
+
const rows = await this.existingRows(page.map((o3) => o3.hash));
|
|
280082
|
+
for (const o3 of page) {
|
|
280083
|
+
if (rows.has(o3.hash)) continue;
|
|
280084
|
+
found2++;
|
|
280085
|
+
if (!dryRun) {
|
|
280086
|
+
await this.backend.delete(o3.key);
|
|
280087
|
+
deleted++;
|
|
280088
|
+
}
|
|
280089
|
+
}
|
|
280090
|
+
page = [];
|
|
280091
|
+
};
|
|
280092
|
+
for await (const o3 of this.backend.list(prefix)) {
|
|
280093
|
+
const rest = o3.key.slice(prefix.length).split("/");
|
|
280094
|
+
const hash3 = rest[1] ?? "";
|
|
280095
|
+
if (rest.length !== 2 || !BLOB_NAME2.test(hash3) || rest[0] !== hash3.slice(0, 2)) continue;
|
|
280096
|
+
if (o3.mtimeMs >= cutoffMs) continue;
|
|
280097
|
+
page.push({ key: o3.key, hash: hash3 });
|
|
280098
|
+
if (page.length >= 500) await flush2();
|
|
280099
|
+
}
|
|
280100
|
+
await flush2();
|
|
280101
|
+
return { found: found2, deleted };
|
|
280102
|
+
}
|
|
280103
|
+
/** §3.4:仅 s3 + `OASIS_BLOB_S3_DIRECT=redirect` 时给 URL;`pg` 行、不存在的 hash、其它后端一律 null。 */
|
|
280104
|
+
async presignGet(hash3, opts) {
|
|
280105
|
+
if (!this.backend.presignGet) return null;
|
|
280106
|
+
const res = await this.pool.query(`SELECT location, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
280107
|
+
const row = res.rows[0];
|
|
280108
|
+
if (!row || row.location !== "ext") return null;
|
|
280109
|
+
const contentType = opts.contentType ?? row.content_type ?? void 0;
|
|
280110
|
+
return this.backend.presignGet(this.key(hash3), { ...opts, ...contentType ? { contentType } : {} });
|
|
280111
|
+
}
|
|
280112
|
+
/* ── 迁移命令(`oasis blobs externalize / internalize`,ADR 0638 §6.2 / §6.3)的单行原语 ── */
|
|
280113
|
+
/**
|
|
280114
|
+
* 把一行 `location='pg'` 搬到对象后端:读 bytea → 原子写 → 读回校验 sha256 → `UPDATE … SET location='ext'`。
|
|
280115
|
+
* **保留 bytea**(清空由 `purgePgBytes` 在观察期后做)。可重复执行:已是 `ext` 的行跳过。
|
|
280116
|
+
* 返回本行字节数;跳过返回 0。
|
|
280117
|
+
*/
|
|
280118
|
+
async externalizeOne(hash3) {
|
|
280119
|
+
const res = await this.pool.query(`SELECT location, bytes, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
280120
|
+
const row = res.rows[0];
|
|
280121
|
+
if (!row || row.location !== "pg") return 0;
|
|
280122
|
+
if (row.bytes === null) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
280123
|
+
const bytes2 = new Uint8Array(row.bytes);
|
|
280124
|
+
const staged = await stageBlob(this.backend.stagingDir, singleChunk(bytes2), { expectedHash: hash3 });
|
|
280125
|
+
const contentType = row.content_type ?? sniffContentType(staged.head);
|
|
280126
|
+
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
280127
|
+
const back = await drainToBuffer((await this.backend.openRead(this.key(hash3), { start: 0, end: bytes2.byteLength - 1 })).body);
|
|
280128
|
+
if (!Buffer.from(back).equals(Buffer.from(bytes2))) throw new BlobBackendError(hash3, this.key(hash3), new Error("read-back mismatch"));
|
|
280129
|
+
await this.pool.query(`UPDATE ${this.t} SET location = 'ext' WHERE hash = $1 AND location = 'pg'`, [hash3]);
|
|
280130
|
+
return bytes2.byteLength;
|
|
280131
|
+
}
|
|
280132
|
+
/** 回滚原语:对象写回 bytea、`location='pg'`(§6.3)。已是 `pg` 且 bytea 在的行跳过。 */
|
|
280133
|
+
async internalizeOne(hash3) {
|
|
280134
|
+
const res = await this.pool.query(`SELECT location, size, bytes IS NULL AS empty FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
280135
|
+
const row = res.rows[0];
|
|
280136
|
+
if (!row || row.location === "pg" && !row.empty) return 0;
|
|
280137
|
+
const size = Number(row.size);
|
|
280138
|
+
const bytes2 = await drainToBuffer((await this.openExt(hash3, { start: 0, end: size - 1 })).body);
|
|
280139
|
+
const actual = (0, import_node_crypto87.createHash)("sha256").update(bytes2).digest("hex");
|
|
280140
|
+
if (actual !== hash3) throw new BlobBackendError(hash3, this.key(hash3), new Error(`sha256 mismatch: ${actual}`));
|
|
280141
|
+
await this.pool.query(`UPDATE ${this.t} SET bytes = $2, location = 'pg' WHERE hash = $1`, [hash3, Buffer.from(bytes2)]);
|
|
280142
|
+
return bytes2.byteLength;
|
|
280143
|
+
}
|
|
280144
|
+
/** 列出待处理的 hash(分批、按 hash 升序,便于中断续跑)。 */
|
|
280145
|
+
async listHashes(where, after, limit) {
|
|
280146
|
+
const res = await this.pool.query(
|
|
280147
|
+
`SELECT hash FROM ${this.t} WHERE (${MIGRATION_SELECTORS[where]}) AND hash > $1 ORDER BY hash LIMIT $2`,
|
|
280148
|
+
[after, limit]
|
|
280149
|
+
);
|
|
280150
|
+
return res.rows.map((r5) => r5.hash);
|
|
280151
|
+
}
|
|
280152
|
+
/** 待处理的行数与字节数(`oasis blobs … --dry-run` 与进度分母)。 */
|
|
280153
|
+
async pendingStats(where) {
|
|
280154
|
+
const res = await this.pool.query(
|
|
280155
|
+
`SELECT count(*)::bigint AS rows, coalesce(sum(size), 0)::bigint AS bytes FROM ${this.t} WHERE ${MIGRATION_SELECTORS[where]}`
|
|
280156
|
+
);
|
|
280157
|
+
const r5 = res.rows[0] ?? {};
|
|
280158
|
+
return { rows: Number(r5.rows ?? 0), bytes: Number(r5.bytes ?? 0) };
|
|
280159
|
+
}
|
|
280160
|
+
/**
|
|
280161
|
+
* 清空给定行的 bytea(§6.2 第 6 步 `--purge-pg-bytes` 的一批):**先确认对象在后端、大小与行一致**,
|
|
280162
|
+
* 只清这些——bytea 是清掉前最后一份副本,对象丢了(误删、恢复不全)时宁可留着。
|
|
280163
|
+
* 返回清掉的行数、这些行的字节数与未通过核对的 hash。
|
|
280164
|
+
*/
|
|
280165
|
+
async purgePgBytesVerified(hashes) {
|
|
280166
|
+
if (hashes.length === 0) return { purged: 0, bytes: 0, unverified: [] };
|
|
280167
|
+
const res = await this.pool.query(
|
|
280168
|
+
`SELECT hash, size FROM ${this.t} WHERE hash = ANY($1::text[]) AND location = 'ext' AND bytes IS NOT NULL`,
|
|
280169
|
+
[hashes]
|
|
280170
|
+
);
|
|
280171
|
+
const ok3 = [];
|
|
280172
|
+
const unverified = [];
|
|
280173
|
+
for (const row of res.rows) {
|
|
280174
|
+
const head = await this.backend.head(this.key(row.hash));
|
|
280175
|
+
if (head && head.size === Number(row.size)) ok3.push(row.hash);
|
|
280176
|
+
else unverified.push(row.hash);
|
|
280177
|
+
}
|
|
280178
|
+
if (ok3.length === 0) return { purged: 0, bytes: 0, unverified };
|
|
280179
|
+
const upd = await this.pool.query(
|
|
280180
|
+
`UPDATE ${this.t} SET bytes = NULL WHERE hash = ANY($1::text[]) AND location = 'ext' AND bytes IS NOT NULL RETURNING size`,
|
|
280181
|
+
[ok3]
|
|
280182
|
+
);
|
|
280183
|
+
return { purged: upd.rowCount ?? 0, bytes: upd.rows.reduce((n3, r5) => n3 + Number(r5.size), 0), unverified };
|
|
280184
|
+
}
|
|
280185
|
+
/** 观察期后清空已外部化行的 bytea(§6.2 第 6 步),一批一条语句。返回本批清掉的行数。 */
|
|
280186
|
+
async purgePgBytes(limit) {
|
|
280187
|
+
const res = await this.pool.query(
|
|
280188
|
+
`UPDATE ${this.t} SET bytes = NULL WHERE hash IN (
|
|
280189
|
+
SELECT hash FROM ${this.t} WHERE location = 'ext' AND bytes IS NOT NULL ORDER BY hash LIMIT $1)`,
|
|
280190
|
+
[limit]
|
|
280191
|
+
);
|
|
280192
|
+
return res.rowCount ?? 0;
|
|
280193
|
+
}
|
|
280194
|
+
/** 各 location 的行数(迁移进度 / 观察期核对 `count(*) WHERE location='pg'` 为 0)。 */
|
|
280195
|
+
async locationCounts() {
|
|
280196
|
+
const res = await this.pool.query(`SELECT
|
|
280197
|
+
count(*) FILTER (WHERE location = 'pg')::int AS pg,
|
|
280198
|
+
count(*) FILTER (WHERE location = 'ext')::int AS ext,
|
|
280199
|
+
count(*) FILTER (WHERE location = 'ext' AND bytes IS NOT NULL)::int AS ext_with_bytes
|
|
280200
|
+
FROM ${this.t}`);
|
|
280201
|
+
const r5 = res.rows[0] ?? {};
|
|
280202
|
+
return { pg: Number(r5.pg ?? 0), ext: Number(r5.ext ?? 0), extWithBytes: Number(r5.ext_with_bytes ?? 0) };
|
|
280203
|
+
}
|
|
280204
|
+
};
|
|
280205
|
+
ObjectBlobStore = class {
|
|
280206
|
+
constructor(backend, prefix) {
|
|
280207
|
+
this.backend = backend;
|
|
280208
|
+
this.prefix = prefix;
|
|
280209
|
+
}
|
|
280210
|
+
key(hash3) {
|
|
280211
|
+
return `${this.prefix.replace(/\/+$/, "")}/${hash3.slice(0, 2)}/${hash3}`;
|
|
280212
|
+
}
|
|
280213
|
+
async put(bytes2) {
|
|
280214
|
+
return (await this.putStream(singleChunk(bytes2))).hash;
|
|
280215
|
+
}
|
|
280216
|
+
async putStream(source, opts = {}) {
|
|
280217
|
+
const staged = await stageBlob(this.backend.stagingDir, source, opts);
|
|
280218
|
+
const contentType = sniffContentType(staged.head);
|
|
280219
|
+
await this.backend.commit(staged, this.key(staged.hash), contentType ? { contentType } : {});
|
|
280220
|
+
return { hash: staged.hash, size: staged.size, ...contentType ? { contentType } : {} };
|
|
280221
|
+
}
|
|
280222
|
+
async meta(hash3) {
|
|
280223
|
+
if (!BLOB_NAME2.test(hash3)) return null;
|
|
280224
|
+
const head = await this.backend.head(this.key(hash3));
|
|
280225
|
+
if (!head) return null;
|
|
280226
|
+
const contentType = head.contentType && head.contentType !== "application/octet-stream" ? head.contentType : void 0;
|
|
280227
|
+
return { size: head.size, ...contentType ? { contentType } : {} };
|
|
280228
|
+
}
|
|
280229
|
+
async has(hash3) {
|
|
280230
|
+
return await this.meta(hash3) !== null;
|
|
280231
|
+
}
|
|
280232
|
+
async get(hash3) {
|
|
280233
|
+
const meta = await this.meta(hash3);
|
|
280234
|
+
if (!meta) throw new BlobNotFoundError(hash3);
|
|
280235
|
+
return drainToBuffer((await this.backend.openRead(this.key(hash3), { start: 0, end: meta.size - 1 })).body);
|
|
280236
|
+
}
|
|
280237
|
+
async openRead(hash3, range2) {
|
|
280238
|
+
const meta = await this.meta(hash3);
|
|
280239
|
+
if (!meta) throw new BlobNotFoundError(hash3);
|
|
280240
|
+
const r5 = resolveBlobRange(hash3, meta.size, range2);
|
|
280241
|
+
try {
|
|
280242
|
+
return { meta, range: r5, ...await this.backend.openRead(this.key(hash3), r5) };
|
|
280243
|
+
} catch (err) {
|
|
280244
|
+
if (err instanceof ObjectMissingError) throw new BlobNotFoundError(hash3);
|
|
280245
|
+
throw err;
|
|
280246
|
+
}
|
|
280247
|
+
}
|
|
280248
|
+
async sweep(reachable) {
|
|
280249
|
+
for await (const o3 of this.backend.list(`${this.prefix.replace(/\/+$/, "")}/`)) {
|
|
280250
|
+
const hash3 = o3.key.slice(o3.key.lastIndexOf("/") + 1);
|
|
280251
|
+
if (BLOB_NAME2.test(hash3) && !reachable.has(hash3)) await this.backend.delete(o3.key);
|
|
280252
|
+
}
|
|
280253
|
+
}
|
|
280254
|
+
async presignGet(hash3, opts) {
|
|
280255
|
+
if (!this.backend.presignGet || !await this.has(hash3)) return null;
|
|
280256
|
+
return this.backend.presignGet(this.key(hash3), opts);
|
|
280257
|
+
}
|
|
280258
|
+
};
|
|
280259
|
+
}
|
|
280260
|
+
});
|
|
280261
|
+
|
|
279640
280262
|
// ../storage/src/tenancy-attribution.ts
|
|
279641
280263
|
function realCompanyId(v2) {
|
|
279642
280264
|
return typeof v2 === "string" && v2.trim().length > 0 ? v2 : null;
|
|
@@ -280820,7 +281442,7 @@ function sourceSetConservation(input) {
|
|
|
280820
281442
|
return input.sourceFinal === input.copied + input.approvedRuntimeConversion + input.approvedArchive + input.approvedDiscard;
|
|
280821
281443
|
}
|
|
280822
281444
|
function payloadChecksum(value2) {
|
|
280823
|
-
return (0,
|
|
281445
|
+
return (0, import_node_crypto88.createHash)("sha256").update(JSON.stringify(value2)).digest("hex");
|
|
280824
281446
|
}
|
|
280825
281447
|
async function copyWorkorderDraftsIdempotent(pool, companyId, companySchema, manifestId, opts) {
|
|
280826
281448
|
const cs = quoteIdent2(companySchema);
|
|
@@ -281002,6 +281624,23 @@ async function copyBlobsIdempotent(pool, companyId, companySchema, manifestId, o
|
|
|
281002
281624
|
const at2 = body2.lastIndexOf("@");
|
|
281003
281625
|
mine.add(at2 >= 0 ? body2.slice(0, at2) : body2);
|
|
281004
281626
|
}
|
|
281627
|
+
if (opts.takeLegacyRows && mine.size > 0) {
|
|
281628
|
+
const legacyNs = opts.legacySchema ?? "public";
|
|
281629
|
+
const hasLocation = (await pool.query(
|
|
281630
|
+
`SELECT 1 FROM information_schema.columns WHERE table_schema=$1 AND table_name='blobs' AND column_name='location'`,
|
|
281631
|
+
[legacyNs]
|
|
281632
|
+
)).rowCount === 1;
|
|
281633
|
+
const ext = hasLocation ? (await pool.query(
|
|
281634
|
+
`SELECT hash FROM ${ls}.blobs WHERE hash = ANY($1::text[]) AND location = 'ext' ORDER BY hash`,
|
|
281635
|
+
[[...mine]]
|
|
281636
|
+
)).rows.map((r5) => r5.hash) : [];
|
|
281637
|
+
if (ext.length > 0 && !opts.objectBackend) {
|
|
281638
|
+
throw new Error(`blob \u590D\u5236\uFF1A${legacyNs}.blobs \u6709 ${ext.length} \u884C\u5B57\u8282\u5728\u5BF9\u8C61\u540E\u7AEF\uFF08location='ext'\uFF09\uFF0C\u9700\u8981\u4F20\u5165 objectBackend \u5148\u590D\u5236\u5BF9\u8C61`);
|
|
281639
|
+
}
|
|
281640
|
+
for (const hash3 of ext) {
|
|
281641
|
+
await opts.objectBackend.copy(factObjectKey(legacyNs, hash3), factObjectKey(companySchema, hash3));
|
|
281642
|
+
}
|
|
281643
|
+
}
|
|
281005
281644
|
let copied = 0;
|
|
281006
281645
|
const client = await pool.connect();
|
|
281007
281646
|
try {
|
|
@@ -281212,13 +281851,14 @@ async function copyTraceFamilyIdempotent(pool, companyId, companySchema, manifes
|
|
|
281212
281851
|
sequences
|
|
281213
281852
|
};
|
|
281214
281853
|
}
|
|
281215
|
-
var
|
|
281854
|
+
var import_node_crypto88, MissingInstallerActorsError, TypeCatalogNotCoveredError, ORG_REGISTRY_TABLES, TRACE_FAMILY;
|
|
281216
281855
|
var init_tenant_migration_operator = __esm({
|
|
281217
281856
|
"../storage/src/tenant-migration-operator.ts"() {
|
|
281218
281857
|
"use strict";
|
|
281219
|
-
|
|
281858
|
+
import_node_crypto88 = require("node:crypto");
|
|
281220
281859
|
init_esm();
|
|
281221
281860
|
init_pg_ident();
|
|
281861
|
+
init_externalized_blobs();
|
|
281222
281862
|
init_tenancy_attribution();
|
|
281223
281863
|
init_tenant_cutover();
|
|
281224
281864
|
MissingInstallerActorsError = class extends Error {
|
|
@@ -281452,8 +282092,8 @@ var init_tenant_new_company_provision = __esm({
|
|
|
281452
282092
|
// ../storage/src/knowledge-snapshot-crypto.ts
|
|
281453
282093
|
function sealSnapshot(snapshot, key2) {
|
|
281454
282094
|
if (snapshot.content.startsWith(PREFIX4)) return structuredClone(snapshot);
|
|
281455
|
-
const iv = (0,
|
|
281456
|
-
const cipher = (0,
|
|
282095
|
+
const iv = (0, import_node_crypto89.randomBytes)(12);
|
|
282096
|
+
const cipher = (0, import_node_crypto89.createCipheriv)("aes-256-gcm", bytes(key2), iv);
|
|
281457
282097
|
cipher.setAAD(aad2(snapshot));
|
|
281458
282098
|
const encrypted = Buffer.concat([cipher.update(snapshot.content, "utf8"), cipher.final()]);
|
|
281459
282099
|
return { ...structuredClone(snapshot), content: `${PREFIX4}${iv.toString("base64url")}.${cipher.getAuthTag().toString("base64url")}.${encrypted.toString("base64url")}` };
|
|
@@ -281463,7 +282103,7 @@ function openSnapshot(snapshot, key2) {
|
|
|
281463
282103
|
const packed = snapshot.content.slice(PREFIX4.length).split(".");
|
|
281464
282104
|
if (packed.length !== 3 || packed.some((part) => !part)) throw new Error(`knowledge snapshot ${snapshot.ref} ciphertext is malformed`);
|
|
281465
282105
|
const [iv, tag, data] = packed;
|
|
281466
|
-
const decipher = (0,
|
|
282106
|
+
const decipher = (0, import_node_crypto89.createDecipheriv)("aes-256-gcm", bytes(key2), Buffer.from(iv, "base64url"));
|
|
281467
282107
|
decipher.setAAD(aad2(snapshot));
|
|
281468
282108
|
decipher.setAuthTag(Buffer.from(tag, "base64url"));
|
|
281469
282109
|
return {
|
|
@@ -281471,11 +282111,11 @@ function openSnapshot(snapshot, key2) {
|
|
|
281471
282111
|
content: Buffer.concat([decipher.update(Buffer.from(data, "base64url")), decipher.final()]).toString("utf8")
|
|
281472
282112
|
};
|
|
281473
282113
|
}
|
|
281474
|
-
var
|
|
282114
|
+
var import_node_crypto89, PREFIX4, bytes, aad2;
|
|
281475
282115
|
var init_knowledge_snapshot_crypto = __esm({
|
|
281476
282116
|
"../storage/src/knowledge-snapshot-crypto.ts"() {
|
|
281477
282117
|
"use strict";
|
|
281478
|
-
|
|
282118
|
+
import_node_crypto89 = require("node:crypto");
|
|
281479
282119
|
PREFIX4 = "oasis-knowledge:v1:";
|
|
281480
282120
|
bytes = (key2) => {
|
|
281481
282121
|
const value2 = Buffer.from(key2);
|
|
@@ -282373,7 +283013,7 @@ function reportedCost(v2) {
|
|
|
282373
283013
|
}
|
|
282374
283014
|
function newUsageBackfillManifestId(now = /* @__PURE__ */ new Date()) {
|
|
282375
283015
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
282376
|
-
return `ulb-${stamp}-${(0,
|
|
283016
|
+
return `ulb-${stamp}-${(0, import_node_crypto90.randomBytes)(3).toString("hex")}`;
|
|
282377
283017
|
}
|
|
282378
283018
|
async function backfillUsageLedger(ledger, opts) {
|
|
282379
283019
|
if (!opts.dryRun && !opts.manifestId) throw new Error("execute \u9700\u8981 manifestId");
|
|
@@ -282922,11 +283562,11 @@ async function audit(c5, s3, manifestId, runId, field, before, after, createdAt)
|
|
|
282922
283562
|
[manifestId, runId, field, before == null ? null : JSON.stringify(before), after == null ? null : JSON.stringify(after), createdAt]
|
|
282923
283563
|
);
|
|
282924
283564
|
}
|
|
282925
|
-
var
|
|
283565
|
+
var import_node_crypto90, BUCKETS, ENDED_SQL, START_EVIDENCE_SQL, CHAT_RUN_SQL, ALLOWED_MODEL_EVIDENCE_TIERS, emptyStep, MODEL_BACKFILL_SOURCE, fourOf, sameUsage, ChatRunLookup;
|
|
282926
283566
|
var init_usage_ledger_backfill = __esm({
|
|
282927
283567
|
"../storage/src/usage-ledger-backfill.ts"() {
|
|
282928
283568
|
"use strict";
|
|
282929
|
-
|
|
283569
|
+
import_node_crypto90 = require("node:crypto");
|
|
282930
283570
|
init_src();
|
|
282931
283571
|
init_pg_ident();
|
|
282932
283572
|
init_postgres_trace();
|
|
@@ -283023,278 +283663,6 @@ var init_usage_ledger_backfill = __esm({
|
|
|
283023
283663
|
}
|
|
283024
283664
|
});
|
|
283025
283665
|
|
|
283026
|
-
// ../storage/src/externalized-blobs.ts
|
|
283027
|
-
function factObjectKey(namespace, hash3) {
|
|
283028
|
-
return `${factObjectPrefix(namespace)}${hash3.slice(0, 2)}/${hash3}`;
|
|
283029
|
-
}
|
|
283030
|
-
function factObjectPrefix(namespace) {
|
|
283031
|
-
return `facts/${namespace}/`;
|
|
283032
|
-
}
|
|
283033
|
-
async function drainToBuffer(body2) {
|
|
283034
|
-
const parts = [];
|
|
283035
|
-
for await (const chunk of body2) parts.push(chunk);
|
|
283036
|
-
return new Uint8Array(Buffer.concat(parts));
|
|
283037
|
-
}
|
|
283038
|
-
var import_node_crypto90, fs36, BLOB_NAME2, ExternalizedBlobStore, ObjectBlobStore;
|
|
283039
|
-
var init_externalized_blobs = __esm({
|
|
283040
|
-
"../storage/src/externalized-blobs.ts"() {
|
|
283041
|
-
"use strict";
|
|
283042
|
-
import_node_crypto90 = require("node:crypto");
|
|
283043
|
-
fs36 = __toESM(require("node:fs"), 1);
|
|
283044
|
-
init_src();
|
|
283045
|
-
init_blob_object_backend();
|
|
283046
|
-
init_postgres();
|
|
283047
|
-
BLOB_NAME2 = /^[0-9a-f]{64}$/;
|
|
283048
|
-
ExternalizedBlobStore = class _ExternalizedBlobStore {
|
|
283049
|
-
constructor(pool, t, namespace, backend, opts) {
|
|
283050
|
-
this.pool = pool;
|
|
283051
|
-
this.t = t;
|
|
283052
|
-
this.namespace = namespace;
|
|
283053
|
-
this.backend = backend;
|
|
283054
|
-
this.opts = opts;
|
|
283055
|
-
}
|
|
283056
|
-
/** 命名空间 = schema 名(ADR 0638 D3)。 */
|
|
283057
|
-
static async open(pool, schema, backend, opts = {}) {
|
|
283058
|
-
const t = await ensureBlobsTable(pool, schema);
|
|
283059
|
-
return new _ExternalizedBlobStore(pool, t, schema, backend, opts);
|
|
283060
|
-
}
|
|
283061
|
-
key(hash3) {
|
|
283062
|
-
return factObjectKey(this.namespace, hash3);
|
|
283063
|
-
}
|
|
283064
|
-
async put(bytes2) {
|
|
283065
|
-
return (await this.putStream(singleChunk(bytes2))).hash;
|
|
283066
|
-
}
|
|
283067
|
-
async putStream(source, opts = {}) {
|
|
283068
|
-
const staged = await stageBlob(this.backend.stagingDir, source, opts);
|
|
283069
|
-
const contentType = sniffContentType(staged.head);
|
|
283070
|
-
const result = { hash: staged.hash, size: staged.size, ...contentType ? { contentType } : {} };
|
|
283071
|
-
try {
|
|
283072
|
-
await this.place(staged, contentType);
|
|
283073
|
-
} catch (err) {
|
|
283074
|
-
await discardStaged(staged);
|
|
283075
|
-
throw err;
|
|
283076
|
-
}
|
|
283077
|
-
return result;
|
|
283078
|
-
}
|
|
283079
|
-
async place(staged, contentType) {
|
|
283080
|
-
const { hash: hash3, size } = staged;
|
|
283081
|
-
const existing = await this.pool.query(`SELECT location FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283082
|
-
const row = existing.rows[0];
|
|
283083
|
-
if (row?.location === "pg") {
|
|
283084
|
-
await discardStaged(staged);
|
|
283085
|
-
await this.touch(hash3);
|
|
283086
|
-
return;
|
|
283087
|
-
}
|
|
283088
|
-
if (row) {
|
|
283089
|
-
if (await this.backend.head(this.key(hash3)) === null) {
|
|
283090
|
-
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
283091
|
-
} else {
|
|
283092
|
-
await discardStaged(staged);
|
|
283093
|
-
}
|
|
283094
|
-
await this.touch(hash3);
|
|
283095
|
-
return;
|
|
283096
|
-
}
|
|
283097
|
-
const bytea = this.opts.dualWrite ? await fs36.promises.readFile(staged.file) : null;
|
|
283098
|
-
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
283099
|
-
await this.pool.query(
|
|
283100
|
-
`INSERT INTO ${this.t} AS b (hash, bytes, size, content_type, location) VALUES ($1, $2, $3, $4, 'ext')
|
|
283101
|
-
ON CONFLICT (hash) DO UPDATE SET last_put_at = now()`,
|
|
283102
|
-
[hash3, bytea, size, contentType ?? null]
|
|
283103
|
-
);
|
|
283104
|
-
}
|
|
283105
|
-
async touch(hash3) {
|
|
283106
|
-
await this.pool.query(`UPDATE ${this.t} SET last_put_at = now() WHERE hash = $1`, [hash3]);
|
|
283107
|
-
}
|
|
283108
|
-
/** §3.2 第 1 条:只读元数据行,不访问对象后端。 */
|
|
283109
|
-
async meta(hash3) {
|
|
283110
|
-
const res = await this.pool.query(`SELECT size, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283111
|
-
const row = res.rows[0];
|
|
283112
|
-
if (!row) return null;
|
|
283113
|
-
return { size: Number(row.size), ...row.content_type ? { contentType: row.content_type } : {} };
|
|
283114
|
-
}
|
|
283115
|
-
async has(hash3) {
|
|
283116
|
-
const res = await this.pool.query(`SELECT 1 FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283117
|
-
return res.rowCount === 1;
|
|
283118
|
-
}
|
|
283119
|
-
async get(hash3) {
|
|
283120
|
-
const res = await this.pool.query(
|
|
283121
|
-
`SELECT location, size, CASE WHEN location = 'pg' THEN bytes END AS bytes FROM ${this.t} WHERE hash = $1`,
|
|
283122
|
-
[hash3]
|
|
283123
|
-
);
|
|
283124
|
-
const row = res.rows[0];
|
|
283125
|
-
if (!row) throw new BlobNotFoundError(hash3);
|
|
283126
|
-
if (row.location === "pg") {
|
|
283127
|
-
if (row.bytes === null) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
283128
|
-
return new Uint8Array(row.bytes);
|
|
283129
|
-
}
|
|
283130
|
-
const size = Number(row.size);
|
|
283131
|
-
const handle = await this.openExt(hash3, { start: 0, end: size - 1 });
|
|
283132
|
-
return drainToBuffer(handle.body);
|
|
283133
|
-
}
|
|
283134
|
-
async openRead(hash3, range2) {
|
|
283135
|
-
const res = await this.pool.query(
|
|
283136
|
-
`SELECT location, size, content_type, (location = 'pg' AND bytes IS NULL) AS purged FROM ${this.t} WHERE hash = $1`,
|
|
283137
|
-
[hash3]
|
|
283138
|
-
);
|
|
283139
|
-
const row = res.rows[0];
|
|
283140
|
-
if (!row) throw new BlobNotFoundError(hash3);
|
|
283141
|
-
if (row.purged) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
283142
|
-
const meta = { size: Number(row.size), ...row.content_type ? { contentType: row.content_type } : {} };
|
|
283143
|
-
const r5 = resolveBlobRange(hash3, meta.size, range2);
|
|
283144
|
-
const stream = row.location === "pg" ? readBytesSegments(this.pool, this.t, hash3, r5) : await this.openExt(hash3, r5);
|
|
283145
|
-
return { meta, range: r5, ...stream };
|
|
283146
|
-
}
|
|
283147
|
-
/** §3.2 第 2 条:行在、对象不在 → BlobBackendError;后端其它故障同样包成它(都是 5xx,不是 404)。 */
|
|
283148
|
-
async openExt(hash3, r5) {
|
|
283149
|
-
const key2 = this.key(hash3);
|
|
283150
|
-
try {
|
|
283151
|
-
return await this.backend.openRead(key2, r5);
|
|
283152
|
-
} catch (err) {
|
|
283153
|
-
if (err instanceof ObjectMissingError) throw new BlobBackendError(hash3, key2);
|
|
283154
|
-
throw new BlobBackendError(hash3, key2, err);
|
|
283155
|
-
}
|
|
283156
|
-
}
|
|
283157
|
-
/**
|
|
283158
|
-
* GC 执行:删除可达集之外的行,**先删行、再删对象**(崩在中间只留无行对象,读者恒见「没有」)。
|
|
283159
|
-
* 只动本 schema 的行与本命名空间前缀下的对象——别家同一份字节在别的前缀里,删不到(ADR 0638 D3)。
|
|
283160
|
-
* 宽限期、dry-run、删除比例上限、孤儿扫描归 S5(§3.5)。
|
|
283161
|
-
*/
|
|
283162
|
-
async sweep(reachable) {
|
|
283163
|
-
const res = await this.pool.query(
|
|
283164
|
-
`DELETE FROM ${this.t} WHERE NOT (hash = ANY($1::text[])) RETURNING hash, location`,
|
|
283165
|
-
[[...reachable]]
|
|
283166
|
-
);
|
|
283167
|
-
for (const row of res.rows) {
|
|
283168
|
-
if (row.location === "ext" && BLOB_NAME2.test(row.hash)) await this.backend.delete(this.key(row.hash));
|
|
283169
|
-
}
|
|
283170
|
-
}
|
|
283171
|
-
/** §3.4:仅 s3 + `OASIS_BLOB_S3_DIRECT=redirect` 时给 URL;`pg` 行、不存在的 hash、其它后端一律 null。 */
|
|
283172
|
-
async presignGet(hash3, opts) {
|
|
283173
|
-
if (!this.backend.presignGet) return null;
|
|
283174
|
-
const res = await this.pool.query(`SELECT location, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283175
|
-
const row = res.rows[0];
|
|
283176
|
-
if (!row || row.location !== "ext") return null;
|
|
283177
|
-
const contentType = opts.contentType ?? row.content_type ?? void 0;
|
|
283178
|
-
return this.backend.presignGet(this.key(hash3), { ...opts, ...contentType ? { contentType } : {} });
|
|
283179
|
-
}
|
|
283180
|
-
/* ── 迁移命令(`oasis blobs externalize / internalize`,ADR 0638 §6.2 / §6.3)的单行原语 ── */
|
|
283181
|
-
/**
|
|
283182
|
-
* 把一行 `location='pg'` 搬到对象后端:读 bytea → 原子写 → 读回校验 sha256 → `UPDATE … SET location='ext'`。
|
|
283183
|
-
* **保留 bytea**(清空由 `purgePgBytes` 在观察期后做)。可重复执行:已是 `ext` 的行跳过。
|
|
283184
|
-
* 返回本行字节数;跳过返回 0。
|
|
283185
|
-
*/
|
|
283186
|
-
async externalizeOne(hash3) {
|
|
283187
|
-
const res = await this.pool.query(`SELECT location, bytes, content_type FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283188
|
-
const row = res.rows[0];
|
|
283189
|
-
if (!row || row.location !== "pg") return 0;
|
|
283190
|
-
if (row.bytes === null) throw new BlobBackendError(hash3, `${this.t}.bytes`);
|
|
283191
|
-
const bytes2 = new Uint8Array(row.bytes);
|
|
283192
|
-
const staged = await stageBlob(this.backend.stagingDir, singleChunk(bytes2), { expectedHash: hash3 });
|
|
283193
|
-
const contentType = row.content_type ?? sniffContentType(staged.head);
|
|
283194
|
-
await this.backend.commit(staged, this.key(hash3), contentType ? { contentType } : {});
|
|
283195
|
-
const back = await drainToBuffer((await this.backend.openRead(this.key(hash3), { start: 0, end: bytes2.byteLength - 1 })).body);
|
|
283196
|
-
if (!Buffer.from(back).equals(Buffer.from(bytes2))) throw new BlobBackendError(hash3, this.key(hash3), new Error("read-back mismatch"));
|
|
283197
|
-
await this.pool.query(`UPDATE ${this.t} SET location = 'ext' WHERE hash = $1 AND location = 'pg'`, [hash3]);
|
|
283198
|
-
return bytes2.byteLength;
|
|
283199
|
-
}
|
|
283200
|
-
/** 回滚原语:对象写回 bytea、`location='pg'`(§6.3)。已是 `pg` 且 bytea 在的行跳过。 */
|
|
283201
|
-
async internalizeOne(hash3) {
|
|
283202
|
-
const res = await this.pool.query(`SELECT location, size, bytes IS NULL AS empty FROM ${this.t} WHERE hash = $1`, [hash3]);
|
|
283203
|
-
const row = res.rows[0];
|
|
283204
|
-
if (!row || row.location === "pg" && !row.empty) return 0;
|
|
283205
|
-
const size = Number(row.size);
|
|
283206
|
-
const bytes2 = await drainToBuffer((await this.openExt(hash3, { start: 0, end: size - 1 })).body);
|
|
283207
|
-
const actual = (0, import_node_crypto90.createHash)("sha256").update(bytes2).digest("hex");
|
|
283208
|
-
if (actual !== hash3) throw new BlobBackendError(hash3, this.key(hash3), new Error(`sha256 mismatch: ${actual}`));
|
|
283209
|
-
await this.pool.query(`UPDATE ${this.t} SET bytes = $2, location = 'pg' WHERE hash = $1`, [hash3, Buffer.from(bytes2)]);
|
|
283210
|
-
return bytes2.byteLength;
|
|
283211
|
-
}
|
|
283212
|
-
/** 列出待处理的 hash(分批、按 hash 升序,便于中断续跑)。 */
|
|
283213
|
-
async listHashes(where, after, limit) {
|
|
283214
|
-
const cond = where === "pg" ? `location = 'pg'` : where === "ext" ? `location = 'ext'` : `location = 'ext' AND bytes IS NOT NULL`;
|
|
283215
|
-
const res = await this.pool.query(
|
|
283216
|
-
`SELECT hash FROM ${this.t} WHERE ${cond} AND hash > $1 ORDER BY hash LIMIT $2`,
|
|
283217
|
-
[after, limit]
|
|
283218
|
-
);
|
|
283219
|
-
return res.rows.map((r5) => r5.hash);
|
|
283220
|
-
}
|
|
283221
|
-
/** 观察期后清空已外部化行的 bytea(§6.2 第 6 步),一批一条语句。返回本批清掉的行数。 */
|
|
283222
|
-
async purgePgBytes(limit) {
|
|
283223
|
-
const res = await this.pool.query(
|
|
283224
|
-
`UPDATE ${this.t} SET bytes = NULL WHERE hash IN (
|
|
283225
|
-
SELECT hash FROM ${this.t} WHERE location = 'ext' AND bytes IS NOT NULL ORDER BY hash LIMIT $1)`,
|
|
283226
|
-
[limit]
|
|
283227
|
-
);
|
|
283228
|
-
return res.rowCount ?? 0;
|
|
283229
|
-
}
|
|
283230
|
-
/** 各 location 的行数(迁移进度 / 观察期核对 `count(*) WHERE location='pg'` 为 0)。 */
|
|
283231
|
-
async locationCounts() {
|
|
283232
|
-
const res = await this.pool.query(`SELECT
|
|
283233
|
-
count(*) FILTER (WHERE location = 'pg')::int AS pg,
|
|
283234
|
-
count(*) FILTER (WHERE location = 'ext')::int AS ext,
|
|
283235
|
-
count(*) FILTER (WHERE location = 'ext' AND bytes IS NOT NULL)::int AS ext_with_bytes
|
|
283236
|
-
FROM ${this.t}`);
|
|
283237
|
-
const r5 = res.rows[0] ?? {};
|
|
283238
|
-
return { pg: Number(r5.pg ?? 0), ext: Number(r5.ext ?? 0), extWithBytes: Number(r5.ext_with_bytes ?? 0) };
|
|
283239
|
-
}
|
|
283240
|
-
};
|
|
283241
|
-
ObjectBlobStore = class {
|
|
283242
|
-
constructor(backend, prefix) {
|
|
283243
|
-
this.backend = backend;
|
|
283244
|
-
this.prefix = prefix;
|
|
283245
|
-
}
|
|
283246
|
-
key(hash3) {
|
|
283247
|
-
return `${this.prefix.replace(/\/+$/, "")}/${hash3.slice(0, 2)}/${hash3}`;
|
|
283248
|
-
}
|
|
283249
|
-
async put(bytes2) {
|
|
283250
|
-
return (await this.putStream(singleChunk(bytes2))).hash;
|
|
283251
|
-
}
|
|
283252
|
-
async putStream(source, opts = {}) {
|
|
283253
|
-
const staged = await stageBlob(this.backend.stagingDir, source, opts);
|
|
283254
|
-
const contentType = sniffContentType(staged.head);
|
|
283255
|
-
await this.backend.commit(staged, this.key(staged.hash), contentType ? { contentType } : {});
|
|
283256
|
-
return { hash: staged.hash, size: staged.size, ...contentType ? { contentType } : {} };
|
|
283257
|
-
}
|
|
283258
|
-
async meta(hash3) {
|
|
283259
|
-
if (!BLOB_NAME2.test(hash3)) return null;
|
|
283260
|
-
const head = await this.backend.head(this.key(hash3));
|
|
283261
|
-
if (!head) return null;
|
|
283262
|
-
const contentType = head.contentType && head.contentType !== "application/octet-stream" ? head.contentType : void 0;
|
|
283263
|
-
return { size: head.size, ...contentType ? { contentType } : {} };
|
|
283264
|
-
}
|
|
283265
|
-
async has(hash3) {
|
|
283266
|
-
return await this.meta(hash3) !== null;
|
|
283267
|
-
}
|
|
283268
|
-
async get(hash3) {
|
|
283269
|
-
const meta = await this.meta(hash3);
|
|
283270
|
-
if (!meta) throw new BlobNotFoundError(hash3);
|
|
283271
|
-
return drainToBuffer((await this.backend.openRead(this.key(hash3), { start: 0, end: meta.size - 1 })).body);
|
|
283272
|
-
}
|
|
283273
|
-
async openRead(hash3, range2) {
|
|
283274
|
-
const meta = await this.meta(hash3);
|
|
283275
|
-
if (!meta) throw new BlobNotFoundError(hash3);
|
|
283276
|
-
const r5 = resolveBlobRange(hash3, meta.size, range2);
|
|
283277
|
-
try {
|
|
283278
|
-
return { meta, range: r5, ...await this.backend.openRead(this.key(hash3), r5) };
|
|
283279
|
-
} catch (err) {
|
|
283280
|
-
if (err instanceof ObjectMissingError) throw new BlobNotFoundError(hash3);
|
|
283281
|
-
throw err;
|
|
283282
|
-
}
|
|
283283
|
-
}
|
|
283284
|
-
async sweep(reachable) {
|
|
283285
|
-
for await (const o3 of this.backend.list(`${this.prefix.replace(/\/+$/, "")}/`)) {
|
|
283286
|
-
const hash3 = o3.key.slice(o3.key.lastIndexOf("/") + 1);
|
|
283287
|
-
if (BLOB_NAME2.test(hash3) && !reachable.has(hash3)) await this.backend.delete(o3.key);
|
|
283288
|
-
}
|
|
283289
|
-
}
|
|
283290
|
-
async presignGet(hash3, opts) {
|
|
283291
|
-
if (!this.backend.presignGet || !await this.has(hash3)) return null;
|
|
283292
|
-
return this.backend.presignGet(this.key(hash3), opts);
|
|
283293
|
-
}
|
|
283294
|
-
};
|
|
283295
|
-
}
|
|
283296
|
-
});
|
|
283297
|
-
|
|
283298
283666
|
// ../../node_modules/.pnpm/@aws-sdk+checksums@3.1001.1/node_modules/@aws-sdk/checksums/dist-es/submodules/flexible-checksums/constants.js
|
|
283299
283667
|
var RequestChecksumCalculation, DEFAULT_REQUEST_CHECKSUM_CALCULATION, ResponseChecksumValidation, DEFAULT_RESPONSE_CHECKSUM_VALIDATION, ChecksumAlgorithm, ChecksumLocation, DEFAULT_CHECKSUM_ALGORITHM;
|
|
283300
283668
|
var init_constants = __esm({
|
|
@@ -319850,7 +320218,7 @@ var s3_object_backend_exports = {};
|
|
|
319850
320218
|
__export(s3_object_backend_exports, {
|
|
319851
320219
|
S3ObjectBackend: () => S3ObjectBackend
|
|
319852
320220
|
});
|
|
319853
|
-
var fs37, import_client_s3, import_lib_storage, import_s3_request_presigner, MULTIPART_THRESHOLD, isNotFound, S3ObjectBackend;
|
|
320221
|
+
var fs37, import_client_s3, import_lib_storage, import_s3_request_presigner, MULTIPART_THRESHOLD, MULTIPART_PART_SIZE, MULTIPART_QUEUE_SIZE, isNotFound, S3ObjectBackend;
|
|
319854
320222
|
var init_s3_object_backend = __esm({
|
|
319855
320223
|
"../storage/src/s3-object-backend.ts"() {
|
|
319856
320224
|
"use strict";
|
|
@@ -319861,6 +320229,8 @@ var init_s3_object_backend = __esm({
|
|
|
319861
320229
|
init_blob_object_backend();
|
|
319862
320230
|
init_blob_backend_config();
|
|
319863
320231
|
MULTIPART_THRESHOLD = 16 * 1024 * 1024;
|
|
320232
|
+
MULTIPART_PART_SIZE = 8 * 1024 * 1024;
|
|
320233
|
+
MULTIPART_QUEUE_SIZE = 2;
|
|
319864
320234
|
isNotFound = (err) => {
|
|
319865
320235
|
const e5 = err;
|
|
319866
320236
|
return e5?.name === "NotFound" || e5?.name === "NoSuchKey" || e5?.$metadata?.httpStatusCode === 404;
|
|
@@ -319959,8 +320329,8 @@ var init_s3_object_backend = __esm({
|
|
|
319959
320329
|
await new import_lib_storage.Upload({
|
|
319960
320330
|
client: this.client,
|
|
319961
320331
|
params: { Bucket: this.bucket, Key, Body: fs37.createReadStream(staged.file), ContentType, ChecksumAlgorithm: "SHA256" },
|
|
319962
|
-
partSize:
|
|
319963
|
-
queueSize:
|
|
320332
|
+
partSize: MULTIPART_PART_SIZE,
|
|
320333
|
+
queueSize: MULTIPART_QUEUE_SIZE,
|
|
319964
320334
|
leavePartsOnError: false
|
|
319965
320335
|
}).done();
|
|
319966
320336
|
}
|
|
@@ -320146,6 +320516,99 @@ var init_blob_backend_config = __esm({
|
|
|
320146
320516
|
}
|
|
320147
320517
|
});
|
|
320148
320518
|
|
|
320519
|
+
// ../storage/src/blob-migration.ts
|
|
320520
|
+
async function migrateBlobs(store, op2, opts) {
|
|
320521
|
+
const selector = SELECTOR[op2];
|
|
320522
|
+
const pending = await store.pendingStats(selector);
|
|
320523
|
+
const report = {
|
|
320524
|
+
op: op2,
|
|
320525
|
+
dryRun: opts.dryRun === true,
|
|
320526
|
+
interrupted: false,
|
|
320527
|
+
batches: 0,
|
|
320528
|
+
candidateRows: pending.rows,
|
|
320529
|
+
candidateBytes: pending.bytes,
|
|
320530
|
+
rows: 0,
|
|
320531
|
+
bytes: 0,
|
|
320532
|
+
failed: 0,
|
|
320533
|
+
failures: [],
|
|
320534
|
+
counts: { pg: 0, ext: 0, extWithBytes: 0 }
|
|
320535
|
+
};
|
|
320536
|
+
const fail = (hash3, err) => {
|
|
320537
|
+
report.failed++;
|
|
320538
|
+
if (report.failures.length < MAX_FAILURES_KEPT) {
|
|
320539
|
+
const cause = err instanceof Error && err.cause instanceof Error ? `\uFF1A${err.cause.message}` : "";
|
|
320540
|
+
report.failures.push({ hash: hash3, error: `${err instanceof Error ? err.message : String(err)}${cause}` });
|
|
320541
|
+
}
|
|
320542
|
+
};
|
|
320543
|
+
const stop = () => opts.shouldStop?.() === true;
|
|
320544
|
+
const sleep7 = opts.sleep ?? defaultSleep2;
|
|
320545
|
+
const batch = Math.max(1, Math.floor(opts.batch));
|
|
320546
|
+
if (!report.dryRun) {
|
|
320547
|
+
let after = "";
|
|
320548
|
+
for (; ; ) {
|
|
320549
|
+
if (stop()) {
|
|
320550
|
+
report.interrupted = true;
|
|
320551
|
+
break;
|
|
320552
|
+
}
|
|
320553
|
+
const hashes = await store.listHashes(selector, after, batch);
|
|
320554
|
+
if (hashes.length === 0) break;
|
|
320555
|
+
if (op2 === "purge-pg-bytes") {
|
|
320556
|
+
const r5 = await store.purgePgBytesVerified(hashes);
|
|
320557
|
+
report.rows += r5.purged;
|
|
320558
|
+
report.bytes += r5.bytes;
|
|
320559
|
+
for (const h5 of r5.unverified) fail(h5, new Error("\u5BF9\u8C61\u4E0D\u5728\u540E\u7AEF\u6216\u5927\u5C0F\u4E0E\u884C\u4E0D\u7B26\uFF0Cbytea \u4FDD\u7559"));
|
|
320560
|
+
after = hashes[hashes.length - 1];
|
|
320561
|
+
} else {
|
|
320562
|
+
for (const hash3 of hashes) {
|
|
320563
|
+
if (stop()) {
|
|
320564
|
+
report.interrupted = true;
|
|
320565
|
+
break;
|
|
320566
|
+
}
|
|
320567
|
+
try {
|
|
320568
|
+
const n3 = op2 === "externalize" ? await store.externalizeOne(hash3) : await store.internalizeOne(hash3);
|
|
320569
|
+
report.rows++;
|
|
320570
|
+
report.bytes += n3;
|
|
320571
|
+
} catch (err) {
|
|
320572
|
+
fail(hash3, err);
|
|
320573
|
+
}
|
|
320574
|
+
after = hash3;
|
|
320575
|
+
}
|
|
320576
|
+
}
|
|
320577
|
+
report.batches++;
|
|
320578
|
+
opts.onProgress?.(progressOf(report));
|
|
320579
|
+
if (report.interrupted || hashes.length < batch) break;
|
|
320580
|
+
if (opts.pauseMs > 0) await sleep7(opts.pauseMs);
|
|
320581
|
+
}
|
|
320582
|
+
}
|
|
320583
|
+
report.counts = await store.locationCounts();
|
|
320584
|
+
return report;
|
|
320585
|
+
}
|
|
320586
|
+
function progressOf(r5) {
|
|
320587
|
+
return { batches: r5.batches, candidateRows: r5.candidateRows, candidateBytes: r5.candidateBytes, rows: r5.rows, bytes: r5.bytes, failed: r5.failed };
|
|
320588
|
+
}
|
|
320589
|
+
async function listBlobSchemas(pool) {
|
|
320590
|
+
const res = await pool.query(
|
|
320591
|
+
`SELECT table_schema AS s FROM information_schema.tables
|
|
320592
|
+
WHERE table_name = 'blobs' AND table_type = 'BASE TABLE'
|
|
320593
|
+
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
320594
|
+
ORDER BY table_schema`
|
|
320595
|
+
);
|
|
320596
|
+
return res.rows.map((r5) => r5.s);
|
|
320597
|
+
}
|
|
320598
|
+
var SELECTOR, MAX_FAILURES_KEPT, defaultSleep2;
|
|
320599
|
+
var init_blob_migration = __esm({
|
|
320600
|
+
"../storage/src/blob-migration.ts"() {
|
|
320601
|
+
"use strict";
|
|
320602
|
+
SELECTOR = {
|
|
320603
|
+
externalize: "pg",
|
|
320604
|
+
"purge-pg-bytes": "ext-with-bytes",
|
|
320605
|
+
internalize: "internalize"
|
|
320606
|
+
};
|
|
320607
|
+
MAX_FAILURES_KEPT = 50;
|
|
320608
|
+
defaultSleep2 = (ms) => new Promise((r5) => setTimeout(r5, ms));
|
|
320609
|
+
}
|
|
320610
|
+
});
|
|
320611
|
+
|
|
320149
320612
|
// ../storage/src/index.ts
|
|
320150
320613
|
var src_exports = {};
|
|
320151
320614
|
__export(src_exports, {
|
|
@@ -320211,6 +320674,7 @@ __export(src_exports, {
|
|
|
320211
320674
|
S3_DEFAULT_TIMEOUTS: () => S3_DEFAULT_TIMEOUTS,
|
|
320212
320675
|
STALE_STAGING_MS: () => STALE_STAGING_MS,
|
|
320213
320676
|
SchemaMappingConflictError: () => SchemaMappingConflictError,
|
|
320677
|
+
SharedBlobSweepRefusedError: () => SharedBlobSweepRefusedError,
|
|
320214
320678
|
SkillCatalogRetiredError: () => SkillCatalogRetiredError,
|
|
320215
320679
|
TENANCY_MOVE_PLANS: () => TENANCY_MOVE_PLANS,
|
|
320216
320680
|
TENANCY_TABLES: () => TENANCY_TABLES,
|
|
@@ -320277,10 +320741,12 @@ __export(src_exports, {
|
|
|
320277
320741
|
inventoryZeroTenantTables: () => inventoryZeroTenantTables,
|
|
320278
320742
|
isUniqueViolation: () => isUniqueViolation2,
|
|
320279
320743
|
legacyTurnId: () => legacyTurnId,
|
|
320744
|
+
listBlobSchemas: () => listBlobSchemas,
|
|
320280
320745
|
listChatItemsSchemas: () => listChatItemsSchemas,
|
|
320281
320746
|
listTenancyCompanies: () => listTenancyCompanies,
|
|
320282
320747
|
loadKnownCompanies: () => loadKnownCompanies,
|
|
320283
320748
|
maxGate: () => maxGate,
|
|
320749
|
+
migrateBlobs: () => migrateBlobs,
|
|
320284
320750
|
missingTypeNames: () => missingTypeNames,
|
|
320285
320751
|
moveDefaultCompanySchema: () => moveDefaultCompanySchema,
|
|
320286
320752
|
moveTenancyRows: () => moveTenancyRows,
|
|
@@ -320311,6 +320777,7 @@ __export(src_exports, {
|
|
|
320311
320777
|
singleChunk: () => singleChunk,
|
|
320312
320778
|
sourceSetConservation: () => sourceSetConservation,
|
|
320313
320779
|
stageBlob: () => stageBlob,
|
|
320780
|
+
sweepBlobRows: () => sweepBlobRows,
|
|
320314
320781
|
synthesizedItemId: () => synthesizedItemId,
|
|
320315
320782
|
undoUsageLedgerBackfill: () => undoUsageLedgerBackfill,
|
|
320316
320783
|
usageBucket: () => usageBucket,
|
|
@@ -320370,6 +320837,7 @@ var init_src10 = __esm({
|
|
|
320370
320837
|
init_blob_object_backend();
|
|
320371
320838
|
init_externalized_blobs();
|
|
320372
320839
|
init_blob_backend_config();
|
|
320840
|
+
init_blob_migration();
|
|
320373
320841
|
}
|
|
320374
320842
|
});
|
|
320375
320843
|
|
|
@@ -322881,6 +323349,146 @@ var init_usage_ledger_backfill_command = __esm({
|
|
|
322881
323349
|
}
|
|
322882
323350
|
});
|
|
322883
323351
|
|
|
323352
|
+
// ../cli/src/blobs-command.ts
|
|
323353
|
+
var blobs_command_exports = {};
|
|
323354
|
+
__export(blobs_command_exports, {
|
|
323355
|
+
BLOBS_USAGE: () => BLOBS_USAGE,
|
|
323356
|
+
BlobsArgError: () => BlobsArgError,
|
|
323357
|
+
parseBlobsArgs: () => parseBlobsArgs,
|
|
323358
|
+
runBlobs: () => runBlobs
|
|
323359
|
+
});
|
|
323360
|
+
function parseBlobsArgs(positional, flags) {
|
|
323361
|
+
const on = (k6) => flags.has(k6) && flags.get(k6) !== "false";
|
|
323362
|
+
const sub = positional[0];
|
|
323363
|
+
if (sub !== "externalize" && sub !== "internalize") throw new BlobsArgError("\u5B50\u547D\u4EE4\u5FC5\u987B\u662F externalize \u6216 internalize");
|
|
323364
|
+
if (on("purge-pg-bytes") && sub !== "externalize") throw new BlobsArgError("--purge-pg-bytes \u53EA\u914D externalize");
|
|
323365
|
+
const op2 = on("purge-pg-bytes") ? "purge-pg-bytes" : sub;
|
|
323366
|
+
const schema = flags.get("schema");
|
|
323367
|
+
const all2 = on("all-schemas");
|
|
323368
|
+
if (schema !== void 0 && all2) throw new BlobsArgError("--schema \u4E0E --all-schemas \u53EA\u80FD\u9009\u4E00\u4E2A");
|
|
323369
|
+
if (schema === void 0 && !all2) throw new BlobsArgError("\u5FC5\u987B\u6307\u5B9A --schema <schema> \u6216 --all-schemas");
|
|
323370
|
+
if (schema !== void 0 && (schema === "" || schema === "true")) throw new BlobsArgError("--schema \u9700\u8981 schema \u540D\uFF08\u5982 company_oasis-prod\u3001public\uFF09");
|
|
323371
|
+
const int2 = (k6, dflt, min2) => {
|
|
323372
|
+
const raw = flags.get(k6);
|
|
323373
|
+
if (raw === void 0) return dflt;
|
|
323374
|
+
const n3 = Number(raw);
|
|
323375
|
+
if (!Number.isInteger(n3) || n3 < min2) throw new BlobsArgError(`--${k6} \u9700\u8981\u4E0D\u5C0F\u4E8E ${min2} \u7684\u6574\u6570`);
|
|
323376
|
+
return n3;
|
|
323377
|
+
};
|
|
323378
|
+
return {
|
|
323379
|
+
op: op2,
|
|
323380
|
+
scope: schema !== void 0 ? { schema } : { all: true },
|
|
323381
|
+
batch: int2("batch", DEFAULT_BATCH[op2], 1),
|
|
323382
|
+
pauseMs: int2("pause-ms", DEFAULT_PAUSE_MS, 0),
|
|
323383
|
+
dryRun: on("dry-run")
|
|
323384
|
+
};
|
|
323385
|
+
}
|
|
323386
|
+
async function runBlobs(positional, flags, io) {
|
|
323387
|
+
let args;
|
|
323388
|
+
try {
|
|
323389
|
+
args = parseBlobsArgs(positional, flags);
|
|
323390
|
+
} catch (err) {
|
|
323391
|
+
if (!(err instanceof BlobsArgError)) throw err;
|
|
323392
|
+
io.err(`${err.message}
|
|
323393
|
+
${BLOBS_USAGE}`);
|
|
323394
|
+
return 2;
|
|
323395
|
+
}
|
|
323396
|
+
const tag = `[blobs ${args.op}]`;
|
|
323397
|
+
const pgDsn = io.env["OASIS_PG_DSN"] ?? io.env["DATABASE_URL"];
|
|
323398
|
+
if (!pgDsn) {
|
|
323399
|
+
io.err(`${tag} \u9700\u8981 OASIS_PG_DSN\uFF08\u6216 DATABASE_URL\uFF09\u2014\u2014\u8BF7\u4EE5 serve \u7684\u73AF\u5883\u6267\u884C\u672C\u547D\u4EE4\u3002`);
|
|
323400
|
+
return 2;
|
|
323401
|
+
}
|
|
323402
|
+
const storage = await Promise.resolve().then(() => (init_src10(), src_exports));
|
|
323403
|
+
let config2;
|
|
323404
|
+
try {
|
|
323405
|
+
config2 = storage.readBlobBackendConfig(io.env, { hasPg: true, dataDir: io.dir });
|
|
323406
|
+
} catch (err) {
|
|
323407
|
+
io.err(`${tag} \u5BF9\u8C61\u540E\u7AEF\u914D\u7F6E\u6709\u8BEF\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
323408
|
+
return 2;
|
|
323409
|
+
}
|
|
323410
|
+
if (config2.kind === "pg") {
|
|
323411
|
+
io.err(`${tag} \u5F53\u524D OASIS_BLOB_BACKEND=pg\uFF08\u6216\u672A\u8BBE\uFF09\uFF1A\u672C\u547D\u4EE4\u8981\u4E0E serve \u540C\u4E00\u4E2A\u5BF9\u8C61\u540E\u7AEF\uFF08fs / s3\uFF09\u3002` + (args.op === "internalize" ? "\u56DE\u6EDA\u987B\u5728\u5207\u56DE pg \u4E4B\u524D\u3001\u4ECD\u662F fs / s3 \u914D\u7F6E\u65F6\u6267\u884C\uFF08ADR 0638 \xA76.3\uFF09\u3002" : ""));
|
|
323412
|
+
return 2;
|
|
323413
|
+
}
|
|
323414
|
+
if (args.op === "purge-pg-bytes" && config2.dualWrite && !args.dryRun) {
|
|
323415
|
+
io.err(`${tag} OASIS_BLOB_PG_DUAL_WRITE=on\uFF1A\u89C2\u5BDF\u671F\u8FD8\u6CA1\u7ED3\u675F\u3002\u5148\u5173\u53CC\u5199\u5E76\u91CD\u542F serve\uFF0C\u518D\u6E05 bytea\uFF08ADR 0638 \xA76.2 \u7B2C 6 \u6B65\uFF09\u3002`);
|
|
323416
|
+
return 2;
|
|
323417
|
+
}
|
|
323418
|
+
if (args.op === "internalize" && !config2.dualWrite) {
|
|
323419
|
+
io.out(`${tag} \u63D0\u793A\uFF1AOASIS_BLOB_PG_DUAL_WRITE \u672A\u5F00\u3002\u56DE\u6EDA\u671F\u95F4 serve \u65B0\u5199\u5165\u7684\u6587\u4EF6\u53EA\u5728\u5BF9\u8C61\u540E\u7AEF\uFF0C\u5207\u56DE pg \u540E\u4F1A\u8BFB\u4E0D\u5230\uFF1B\u5EFA\u8BAE\u5148\u5F00\u53CC\u5199\u5E76\u91CD\u542F serve\uFF0C\u518D\u8DD1\u672C\u547D\u4EE4\uFF08\u89C1 docs/oasis-dev-storage.md \xA72.5.4\uFF09\u3002`);
|
|
323420
|
+
}
|
|
323421
|
+
const pool = storage.createPgPool(pgDsn);
|
|
323422
|
+
try {
|
|
323423
|
+
const existing = await storage.listBlobSchemas(pool);
|
|
323424
|
+
let schemas;
|
|
323425
|
+
if ("schema" in args.scope) {
|
|
323426
|
+
if (!existing.includes(args.scope.schema)) {
|
|
323427
|
+
io.err(`${tag} schema ${args.scope.schema} \u6CA1\u6709 blobs \u8868\u3002\u6709 blobs \u8868\u7684 schema\uFF1A${existing.join(" ") || "\uFF08\u65E0\uFF09"}`);
|
|
323428
|
+
return 2;
|
|
323429
|
+
}
|
|
323430
|
+
schemas = [args.scope.schema];
|
|
323431
|
+
} else {
|
|
323432
|
+
schemas = existing;
|
|
323433
|
+
}
|
|
323434
|
+
const backend = await storage.createObjectBackend(config2);
|
|
323435
|
+
if (!backend) throw new Error("\u5BF9\u8C61\u540E\u7AEF\u88C5\u914D\u5931\u8D25");
|
|
323436
|
+
io.out(`${tag} \u540E\u7AEF ${storage.describeBlobBackend(config2)}\uFF1B${schemas.length} \u4E2A schema\uFF1A${schemas.join(" ")}\uFF1B\u6BCF\u6279 ${args.batch} \u884C\u3001\u6279\u95F4 ${args.pauseMs}ms${args.dryRun ? "\uFF1Bdry-run\uFF08\u4E00\u884C\u4E0D\u52A8\uFF09" : ""}`);
|
|
323437
|
+
const total = { rows: 0, bytes: 0, failed: 0 };
|
|
323438
|
+
for (const schema of schemas) {
|
|
323439
|
+
const store = await storage.ExternalizedBlobStore.open(pool, schema, backend, { dualWrite: config2.dualWrite });
|
|
323440
|
+
const r5 = await storage.migrateBlobs(store, args.op, {
|
|
323441
|
+
batch: args.batch,
|
|
323442
|
+
pauseMs: args.pauseMs,
|
|
323443
|
+
dryRun: args.dryRun,
|
|
323444
|
+
...io.shouldStop ? { shouldStop: io.shouldStop } : {},
|
|
323445
|
+
...io.sleep ? { sleep: io.sleep } : {},
|
|
323446
|
+
onProgress: (p4) => io.out(`${tag} ${schema} \u7B2C ${p4.batches} \u6279\uFF1A\u5DF2\u5904\u7406 ${p4.rows}/${p4.candidateRows} \u884C\u3001${fmtBytes(p4.bytes)}/${fmtBytes(p4.candidateBytes)}\uFF0C\u5931\u8D25 ${p4.failed}`)
|
|
323447
|
+
});
|
|
323448
|
+
const c5 = r5.counts;
|
|
323449
|
+
const tail = `\uFF08location\uFF1Apg ${c5.pg}\u3001ext ${c5.ext}\uFF0C\u5176\u4E2D bytea \u672A\u6E05 ${c5.extWithBytes}\uFF09`;
|
|
323450
|
+
if (r5.dryRun) {
|
|
323451
|
+
io.out(`${tag} ${schema} dry-run\uFF1A\u5F85\u5904\u7406 ${r5.candidateRows} \u884C\u3001${fmtBytes(r5.candidateBytes)}${tail}`);
|
|
323452
|
+
} else {
|
|
323453
|
+
io.out(`${tag} ${schema} ${r5.interrupted ? "\u5DF2\u4E2D\u65AD" : "\u5B8C\u6210"}\uFF1A\u5904\u7406 ${r5.rows} \u884C\u3001${fmtBytes(r5.bytes)}\uFF0C\u5931\u8D25 ${r5.failed}${tail}`);
|
|
323454
|
+
for (const f6 of r5.failures) io.out(` \u2717 ${f6.hash}\uFF1A${f6.error}`);
|
|
323455
|
+
if (r5.failed > r5.failures.length) io.out(` \u2026\u53E6\u6709 ${r5.failed - r5.failures.length} \u6761\u5931\u8D25\u672A\u5217\u51FA`);
|
|
323456
|
+
}
|
|
323457
|
+
total.rows += r5.rows;
|
|
323458
|
+
total.bytes += r5.bytes;
|
|
323459
|
+
total.failed += r5.failed;
|
|
323460
|
+
if (r5.interrupted) {
|
|
323461
|
+
io.err(`${tag} \u5DF2\u4E2D\u65AD\uFF1A\u5DF2\u5904\u7406\u7684\u884C\u4E0D\u4F1A\u91CD\u505A\uFF0C\u91CD\u8DD1\u540C\u4E00\u6761\u547D\u4EE4\u4ECE\u5269\u4E0B\u7684\u63A5\u7740\u505A\u3002`);
|
|
323462
|
+
return 130;
|
|
323463
|
+
}
|
|
323464
|
+
}
|
|
323465
|
+
if (!args.dryRun) {
|
|
323466
|
+
io.out(`${tag} \u5408\u8BA1\uFF1A\u5904\u7406 ${total.rows} \u884C\u3001${fmtBytes(total.bytes)}\uFF0C\u5931\u8D25 ${total.failed}\u3002` + (total.failed > 0 ? "\u5931\u8D25\u7684\u884C\u4FDD\u6301\u539F\u72B6\uFF0C\u6392\u67E5\u540E\u91CD\u8DD1\u540C\u4E00\u547D\u4EE4\u53EA\u91CD\u8BD5\u5B83\u4EEC\u3002" : ""));
|
|
323467
|
+
}
|
|
323468
|
+
return total.failed > 0 ? 1 : 0;
|
|
323469
|
+
} finally {
|
|
323470
|
+
await pool.end().catch(() => void 0);
|
|
323471
|
+
}
|
|
323472
|
+
}
|
|
323473
|
+
var BLOBS_USAGE, BlobsArgError, DEFAULT_BATCH, DEFAULT_PAUSE_MS, MB, fmtBytes;
|
|
323474
|
+
var init_blobs_command = __esm({
|
|
323475
|
+
"../cli/src/blobs-command.ts"() {
|
|
323476
|
+
"use strict";
|
|
323477
|
+
BLOBS_USAGE = [
|
|
323478
|
+
"\u7528\u6CD5\uFF1A",
|
|
323479
|
+
" oasis blobs externalize (--schema <s> | --all-schemas) [--batch 50] [--pause-ms 200] [--dry-run]",
|
|
323480
|
+
" oasis blobs externalize --purge-pg-bytes (--schema <s> | --all-schemas) [--batch 500] [--pause-ms 200] [--dry-run]",
|
|
323481
|
+
" oasis blobs internalize (--schema <s> | --all-schemas) [--batch 50] [--pause-ms 200] [--dry-run]"
|
|
323482
|
+
].join("\n");
|
|
323483
|
+
BlobsArgError = class extends Error {
|
|
323484
|
+
};
|
|
323485
|
+
DEFAULT_BATCH = { externalize: 50, "purge-pg-bytes": 500, internalize: 50 };
|
|
323486
|
+
DEFAULT_PAUSE_MS = 200;
|
|
323487
|
+
MB = 1024 * 1024;
|
|
323488
|
+
fmtBytes = (n3) => n3 >= MB ? `${(n3 / MB).toFixed(1)} MiB` : `${n3} B`;
|
|
323489
|
+
}
|
|
323490
|
+
});
|
|
323491
|
+
|
|
322884
323492
|
// src/index.ts
|
|
322885
323493
|
var fs55 = __toESM(require("node:fs"));
|
|
322886
323494
|
init_src();
|
|
@@ -336858,6 +337466,46 @@ var COMMAND_DECLS = {
|
|
|
336858
337466
|
"oasis usage-ledger-backfill --all --verify"
|
|
336859
337467
|
]
|
|
336860
337468
|
},
|
|
337469
|
+
blobs: {
|
|
337470
|
+
group: "\u57FA\u7840\u8BBE\u65BD",
|
|
337471
|
+
usage: "oasis blobs <externalize|internalize> (--schema <s> | --all-schemas) ...",
|
|
337472
|
+
description: "\u4E8B\u5B9E\u67DC bytea \u8FC1\u51FA / \u6E05\u7A7A / \u56DE\u6EDA\uFF08ADR 0638 \xA76.2\u3001\xA76.3\uFF09\u3002\u4EE5 serve \u7684\u73AF\u5883\u5728\u90E8\u7F72\u673A\u4E0A\u6267\u884C\uFF0C\u5BF9\u8C61\u540E\u7AEF\u4E0E serve \u540C\u4E00\u5957\u88C5\u914D\u3002",
|
|
337473
|
+
subCommands: ["blobs externalize", "blobs internalize"]
|
|
337474
|
+
},
|
|
337475
|
+
"blobs externalize": {
|
|
337476
|
+
group: "\u57FA\u7840\u8BBE\u65BD",
|
|
337477
|
+
usage: "oasis blobs externalize [--purge-pg-bytes] (--schema <s> | --all-schemas) [--batch 50] [--pause-ms 200] [--dry-run]",
|
|
337478
|
+
description: "\u628A location='pg' \u7684\u884C\u9010\u4E2A\u5199\u8FDB\u5BF9\u8C61\u540E\u7AEF\uFF08\u8BFB\u56DE\u6821\u9A8C sha256 \u540E\u6539 location='ext'\uFF0C\u4FDD\u7559 bytea\uFF09\uFF1B\u5E26 --purge-pg-bytes \u65F6\u6539\u4E3A\u5206\u6279\u6E05\u7A7A\u5DF2\u5916\u90E8\u5316\u884C\u7684 bytea\uFF08\u5148\u6838\u5BF9\u5BF9\u8C61\u5728\u4E14\u5927\u5C0F\u4E00\u81F4\uFF09\u3002\u5206\u6279\u3001\u6279\u95F4\u9650\u901F\u3001\u6BCF\u6279\u8F93\u51FA\u8FDB\u5EA6\uFF1BCtrl-C \u5904\u7406\u5B8C\u5F53\u524D\u4E00\u884C\u518D\u505C\uFF0C\u91CD\u8DD1\u540C\u4E00\u547D\u4EE4\u7EED\u8DD1\u3002\u9000\u51FA\u7801\uFF1A0 \u5B8C\u6210\u30011 \u6709\u884C\u5931\u8D25\u30012 \u53C2\u6570/\u73AF\u5883\u9519\u8BEF\u3001130 \u88AB\u4E2D\u65AD\u3002",
|
|
337479
|
+
flags: [
|
|
337480
|
+
{ name: "schema", desc: "\u53EA\u5904\u7406\u8FD9\u4E2A schema\uFF08\u5982 company_oasis-prod\u3001public\uFF09\uFF1B\u987B\u5DF2\u6709 blobs \u8868" },
|
|
337481
|
+
{ name: "all-schemas", desc: "\u6240\u6709\u6709 blobs \u8868\u7684 schema\uFF0C\u542B\u5171\u4EAB schema public", boolean: true },
|
|
337482
|
+
{ name: "purge-pg-bytes", desc: "\u6E05\u7A7A\u5DF2\u5916\u90E8\u5316\u884C\u7684 bytea\uFF08\u89C2\u5BDF\u671F\u7ED3\u675F\u3001\u5173\u53CC\u5199\u5E76\u91CD\u542F serve \u4E4B\u540E\uFF1B\u53CC\u5199\u5F00\u7740\u65F6\u62D2\u7EDD\uFF09", boolean: true },
|
|
337483
|
+
{ name: "batch", desc: "\u6BCF\u6279\u884C\u6570\uFF08\u7F3A\u7701 50\uFF1B--purge-pg-bytes \u7F3A\u7701 500\uFF09" },
|
|
337484
|
+
{ name: "pause-ms", desc: "\u6279\u95F4\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u7F3A\u7701 200\uFF09" },
|
|
337485
|
+
{ name: "dry-run", desc: "\u53EA\u62A5\u6BCF\u4E2A schema \u5F85\u5904\u7406\u7684\u884C\u6570\u548C\u5B57\u8282\u6570\uFF0C\u4E00\u884C\u4E0D\u52A8", boolean: true }
|
|
337486
|
+
],
|
|
337487
|
+
examples: [
|
|
337488
|
+
"oasis blobs externalize --all-schemas --dry-run",
|
|
337489
|
+
"oasis blobs externalize --schema company_oasis-prod --batch 50",
|
|
337490
|
+
"oasis blobs externalize --purge-pg-bytes --all-schemas"
|
|
337491
|
+
]
|
|
337492
|
+
},
|
|
337493
|
+
"blobs internalize": {
|
|
337494
|
+
group: "\u57FA\u7840\u8BBE\u65BD",
|
|
337495
|
+
usage: "oasis blobs internalize (--schema <s> | --all-schemas) [--batch 50] [--pause-ms 200] [--dry-run]",
|
|
337496
|
+
description: "\u56DE\u6EDA\uFF1A\u628A\u5BF9\u8C61\u5199\u56DE bytea\uFF08\u6821\u9A8C sha256\uFF09\u5E76\u6539 location='pg'\u3002\u987B\u5728\u5207\u56DE OASIS_BLOB_BACKEND=pg \u4E4B\u524D\u3001\u4ECD\u662F fs / s3 \u914D\u7F6E\u65F6\u6267\u884C\uFF1B\u5EFA\u8BAE\u5148\u5F00\u53CC\u5199\u5E76\u91CD\u542F serve\u3002\u5206\u6279\u3001\u9650\u901F\u3001\u8FDB\u5EA6\u3001\u53EF\u4E2D\u65AD\u7EED\u8DD1\uFF0C\u9000\u51FA\u7801\u540C externalize\u3002",
|
|
337497
|
+
flags: [
|
|
337498
|
+
{ name: "schema", desc: "\u53EA\u5904\u7406\u8FD9\u4E2A schema\uFF1B\u987B\u5DF2\u6709 blobs \u8868" },
|
|
337499
|
+
{ name: "all-schemas", desc: "\u6240\u6709\u6709 blobs \u8868\u7684 schema\uFF0C\u542B\u5171\u4EAB schema public", boolean: true },
|
|
337500
|
+
{ name: "batch", desc: "\u6BCF\u6279\u884C\u6570\uFF08\u7F3A\u7701 50\uFF09" },
|
|
337501
|
+
{ name: "pause-ms", desc: "\u6279\u95F4\u505C\u987F\u6BEB\u79D2\u6570\uFF08\u7F3A\u7701 200\uFF09" },
|
|
337502
|
+
{ name: "dry-run", desc: "\u53EA\u62A5\u5F85\u5199\u56DE\u7684\u884C\u6570\u548C\u5B57\u8282\u6570\uFF0C\u4E00\u884C\u4E0D\u52A8", boolean: true }
|
|
337503
|
+
],
|
|
337504
|
+
examples: [
|
|
337505
|
+
"oasis blobs internalize --all-schemas --dry-run",
|
|
337506
|
+
"oasis blobs internalize --all-schemas"
|
|
337507
|
+
]
|
|
337508
|
+
},
|
|
336861
337509
|
kb: {
|
|
336862
337510
|
group: "\u77E5\u8BC6\u5E93",
|
|
336863
337511
|
usage: "oasis kb <ask|ingest|status|maintain|lint> ...",
|
|
@@ -337630,9 +338278,15 @@ var COMMAND_DECLS = {
|
|
|
337630
338278
|
// —— 运维 ——
|
|
337631
338279
|
gc: {
|
|
337632
338280
|
group: "\u8FD0\u7EF4",
|
|
337633
|
-
usage: "oasis gc",
|
|
337634
|
-
description: "\u5B64\u513F blob \u56DE\u6536\uFF08mark-and-sweep\uFF1B\u64CD\u4F5C\u8005\u547D\u4EE4\
|
|
337635
|
-
|
|
338281
|
+
usage: "oasis gc [--dry-run] [--grace-hours 24] [--max-delete-ratio 0.2] [--orphans]",
|
|
338282
|
+
description: "\u4E8B\u5B9E\u67DC\u5B64\u513F blob \u56DE\u6536\uFF08mark-and-sweep\uFF1B\u64CD\u4F5C\u8005\u547D\u4EE4\uFF0C\u624B\u52A8\u89E6\u53D1\uFF0CADR 0638 \xA73.5\uFF09\u3002\u53EF\u8FBE\u96C6 = \u4EA4\u4ED8\u7269 + \u9879\u76EE\u6587\u4EF6\uFF1B\u6700\u540E\u4E00\u6B21\u5199\u5165\u5728\u5BBD\u9650\u671F\u5185\u7684\u4E0D\u5220\uFF1B\u5F85\u5220\u8D85\u8FC7\u6BD4\u4F8B\u4E0A\u9650\u5219\u4E2D\u6B62\u3001\u4E00\u6761\u4E0D\u5220\u3002\u5148\u8DD1 --dry-run \u770B\u62A5\u544A\u518D\u771F\u5220\u3002",
|
|
338283
|
+
flags: [
|
|
338284
|
+
{ name: "dry-run", desc: "\u53EA\u62A5\u4F1A\u5220\u591A\u5C11\u884C / \u5B57\u8282 / \u5B64\u513F\u5BF9\u8C61\uFF0C\u4E00\u6761\u4E0D\u5220", boolean: true },
|
|
338285
|
+
{ name: "grace-hours", desc: "\u5BBD\u9650\u671F\uFF08\u5C0F\u65F6\uFF0C\u7F3A\u7701 24\uFF09\uFF1A\u8FD9\u6BB5\u65F6\u95F4\u5185\u5199\u5165\u8FC7\u7684 blob \u4E0D\u5220" },
|
|
338286
|
+
{ name: "max-delete-ratio", desc: "\u5F85\u5220\u884C\u6570 / \u603B\u884C\u6570\u4E0A\u9650\uFF080\u20131\uFF0C\u7F3A\u7701 0.2\uFF09\uFF1B\u8D85\u8FC7\u5373\u4E2D\u6B62" },
|
|
338287
|
+
{ name: "orphans", desc: "\u540C\u65F6\u6E05\u626B\u5BF9\u8C61\u5B58\u50A8\u91CC\u65E0\u5143\u6570\u636E\u884C\u7684\u5B64\u513F\u5BF9\u8C61\uFF08fs / s3 \u540E\u7AEF\uFF09", boolean: true }
|
|
338288
|
+
],
|
|
338289
|
+
examples: ["oasis gc --dry-run", "oasis gc --dry-run --orphans", "oasis gc --orphans"]
|
|
337636
338290
|
},
|
|
337637
338291
|
broadcasts: {
|
|
337638
338292
|
group: "\u8FD0\u7EF4",
|
|
@@ -339277,7 +339931,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback2) {
|
|
|
339277
339931
|
}
|
|
339278
339932
|
}
|
|
339279
339933
|
}
|
|
339280
|
-
var CLI_VERSION = "2.2.
|
|
339934
|
+
var CLI_VERSION = "2.2.120" ? "2.2.120" : "dev";
|
|
339281
339935
|
function apiHeaders(token2, company) {
|
|
339282
339936
|
const headers = {
|
|
339283
339937
|
authorization: `Bearer ${token2}`,
|
|
@@ -339639,6 +340293,31 @@ async function runCli(argv, println = console.log, progressln = console.error) {
|
|
|
339639
340293
|
});
|
|
339640
340294
|
return;
|
|
339641
340295
|
}
|
|
340296
|
+
if (command6 === "blobs") {
|
|
340297
|
+
const { runBlobs: runBlobs2 } = await Promise.resolve().then(() => (init_blobs_command(), blobs_command_exports));
|
|
340298
|
+
let stopRequested = false;
|
|
340299
|
+
const onSignal = () => {
|
|
340300
|
+
if (stopRequested) process.exit(130);
|
|
340301
|
+
stopRequested = true;
|
|
340302
|
+
process.stderr.write("[blobs] \u6536\u5230\u4E2D\u65AD\uFF1A\u5904\u7406\u5B8C\u5F53\u524D\u8FD9\u4E00\u884C\u540E\u505C\u6B62\uFF08\u518D\u6309\u4E00\u6B21\u7ACB\u5373\u9000\u51FA\uFF09\n");
|
|
340303
|
+
};
|
|
340304
|
+
process.on("SIGINT", onSignal);
|
|
340305
|
+
process.on("SIGTERM", onSignal);
|
|
340306
|
+
try {
|
|
340307
|
+
process.exitCode = await runBlobs2(positional, flags, {
|
|
340308
|
+
out: (line2) => println(line2),
|
|
340309
|
+
err: (line2) => process.stderr.write(`${line2}
|
|
340310
|
+
`),
|
|
340311
|
+
env: process.env,
|
|
340312
|
+
dir,
|
|
340313
|
+
shouldStop: () => stopRequested
|
|
340314
|
+
});
|
|
340315
|
+
} finally {
|
|
340316
|
+
process.off("SIGINT", onSignal);
|
|
340317
|
+
process.off("SIGTERM", onSignal);
|
|
340318
|
+
}
|
|
340319
|
+
return;
|
|
340320
|
+
}
|
|
339642
340321
|
if (command6 === "node-token") {
|
|
339643
340322
|
const store = createNodeTokenStore(path42.join(dir, "node-tokens.json"));
|
|
339644
340323
|
const sub = positional[0];
|
|
@@ -342301,7 +342980,14 @@ ${res.warning}`);
|
|
|
342301
342980
|
break;
|
|
342302
342981
|
}
|
|
342303
342982
|
case "gc": {
|
|
342304
|
-
const
|
|
342983
|
+
const graceHours = flags.get("grace-hours");
|
|
342984
|
+
const maxDeleteRatio = flags.get("max-delete-ratio");
|
|
342985
|
+
const { message } = await api.cmd("gc", {
|
|
342986
|
+
...flags.get("dry-run") === "true" ? { dryRun: true } : {},
|
|
342987
|
+
...flags.get("orphans") === "true" ? { orphans: true } : {},
|
|
342988
|
+
...graceHours !== void 0 ? { graceHours: Number(graceHours) } : {},
|
|
342989
|
+
...maxDeleteRatio !== void 0 ? { maxDeleteRatio: Number(maxDeleteRatio) } : {}
|
|
342990
|
+
});
|
|
342305
342991
|
println(message);
|
|
342306
342992
|
break;
|
|
342307
342993
|
}
|
|
@@ -343448,7 +344134,7 @@ function shimScript() {
|
|
|
343448
344134
|
}
|
|
343449
344135
|
|
|
343450
344136
|
// src/index.ts
|
|
343451
|
-
var PKG_VERSION = true ? "2.2.
|
|
344137
|
+
var PKG_VERSION = true ? "2.2.120" : "dev";
|
|
343452
344138
|
var LOCAL_BIN = localBin();
|
|
343453
344139
|
var NPM_PREFIX = npmPrefix();
|
|
343454
344140
|
var INSTANCE = DEFAULT_INSTANCE;
|