amalgm 0.1.239 → 0.1.241
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/lib/shared-realtime-tunnel.js +18 -4
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -14,21 +14,36 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { AMALGM_DIR } = require('../config');
|
|
16
16
|
const { appendStateEvent } = require('../state/events');
|
|
17
|
-
const
|
|
17
|
+
const durable = require('./durable');
|
|
18
18
|
const transition = require('./transition');
|
|
19
|
+
const {
|
|
20
|
+
CHUNK_BYTES,
|
|
21
|
+
describeBytes,
|
|
22
|
+
manifestChecksum,
|
|
23
|
+
sha256,
|
|
24
|
+
} = require('./content');
|
|
19
25
|
|
|
20
|
-
const CONTRACT = 'git-bundle-chunks@
|
|
21
|
-
const PAYLOAD_CONTRACT = 'opaque-chunks@
|
|
22
|
-
const CHUNK_BYTES = 512 * 1024;
|
|
26
|
+
const CONTRACT = 'git-bundle-chunks@2';
|
|
27
|
+
const PAYLOAD_CONTRACT = 'opaque-chunks@2';
|
|
23
28
|
const ROOT = path.join(AMALGM_DIR, 'state', 'repo-objects');
|
|
24
29
|
const MANIFESTS = path.join(ROOT, 'manifests');
|
|
25
30
|
const PAYLOADS = path.join(ROOT, 'payloads');
|
|
26
31
|
const CHUNKS = path.join(ROOT, 'chunks');
|
|
27
32
|
const REQUESTS = path.join(ROOT, 'requests');
|
|
28
33
|
const preparing = new Map();
|
|
34
|
+
const UPLOAD_RETRY_BASE_MS = 1_000;
|
|
35
|
+
const UPLOAD_RETRY_MAX_MS = 60_000;
|
|
36
|
+
|
|
37
|
+
function isProjectId(value) {
|
|
38
|
+
return require('./tree/identity').isUuid(String(value || ''));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function locatorChecksum(chunks) {
|
|
42
|
+
return manifestChecksum(chunks);
|
|
43
|
+
}
|
|
29
44
|
|
|
30
|
-
function
|
|
31
|
-
return
|
|
45
|
+
function locatorBytes(chunks) {
|
|
46
|
+
return chunks.reduce((total, chunk) => total + chunk.bytes, 0);
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
function ensureDirs() {
|
|
@@ -56,8 +71,7 @@ function readJson(file) {
|
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
function writeJson(file, value) {
|
|
59
|
-
|
|
60
|
-
fs.writeFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
74
|
+
durable.writeJson(file, value);
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
function publicLocator(manifest) {
|
|
@@ -83,14 +97,18 @@ function publicPayloadLocator(manifest) {
|
|
|
83
97
|
};
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
function describeChunks(bytes) {
|
|
101
|
+
return describeBytes(bytes).map((chunk) => ({ ...chunk, uploaded: false }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeChunks(bytes, chunks = describeChunks(bytes)) {
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const chunk of chunks) {
|
|
107
|
+
const part = bytes.subarray(offset, offset + chunk.bytes);
|
|
108
|
+
const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
109
|
+
const held = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
|
|
110
|
+
if (!held || sha256(held) !== chunk.sha256) durable.writeFile(chunkFile, part);
|
|
111
|
+
offset += chunk.bytes;
|
|
94
112
|
}
|
|
95
113
|
return chunks;
|
|
96
114
|
}
|
|
@@ -113,8 +131,8 @@ async function prepareBundle(worktreeRoot, repoKey, resourceId, oid, file) {
|
|
|
113
131
|
repoKey,
|
|
114
132
|
oid,
|
|
115
133
|
ref,
|
|
116
|
-
checksum:
|
|
117
|
-
bytes:
|
|
134
|
+
checksum: locatorChecksum(chunks),
|
|
135
|
+
bytes: locatorBytes(chunks),
|
|
118
136
|
chunks,
|
|
119
137
|
createdAt: new Date().toISOString(),
|
|
120
138
|
});
|
|
@@ -128,23 +146,29 @@ function publishPayload(key, bytesInput, repoKey) {
|
|
|
128
146
|
ensureDirs();
|
|
129
147
|
const bytes = Buffer.isBuffer(bytesInput) ? bytesInput : Buffer.from(bytesInput);
|
|
130
148
|
const project = require('./wall').projectForRepo(repoKey);
|
|
131
|
-
const checksum = sha256(bytes);
|
|
132
149
|
if (!project?.resourceId) {
|
|
133
|
-
return { kind: 'pending', contract: PAYLOAD_CONTRACT
|
|
150
|
+
return { kind: 'pending', contract: PAYLOAD_CONTRACT };
|
|
134
151
|
}
|
|
152
|
+
const chunks = describeChunks(bytes);
|
|
153
|
+
const checksum = locatorChecksum(chunks);
|
|
135
154
|
// Payload content is its identity. A later checkpoint that still contains
|
|
136
155
|
// the same large file must reference the already-uploaded bytes instead of
|
|
137
156
|
// creating another upload merely because the surrounding checkpoint moved.
|
|
138
157
|
const file = payloadFile(project.resourceId, checksum);
|
|
139
158
|
let manifest = readJson(file);
|
|
140
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
!manifest
|
|
161
|
+
|| manifest.contract !== PAYLOAD_CONTRACT
|
|
162
|
+
|| manifest.checksum !== checksum
|
|
163
|
+
) {
|
|
164
|
+
writeChunks(bytes, chunks);
|
|
141
165
|
manifest = {
|
|
142
166
|
contract: PAYLOAD_CONTRACT,
|
|
143
167
|
resourceId: project.resourceId,
|
|
144
168
|
repoKey,
|
|
145
169
|
checksum,
|
|
146
|
-
bytes:
|
|
147
|
-
chunks
|
|
170
|
+
bytes: locatorBytes(chunks),
|
|
171
|
+
chunks,
|
|
148
172
|
createdAt: new Date().toISOString(),
|
|
149
173
|
};
|
|
150
174
|
writeJson(file, manifest);
|
|
@@ -182,18 +206,38 @@ async function bundleSourceFor(worktreeRoot, repoKey, oid) {
|
|
|
182
206
|
|
|
183
207
|
async function objectSourceFor(worktreeRoot, repoKey, oid) {
|
|
184
208
|
if (!oid || !repoKey) return null;
|
|
185
|
-
if (await originContains(worktreeRoot, oid)) return { kind: 'remote' };
|
|
186
209
|
return bundleSourceFor(worktreeRoot, repoKey, oid);
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
function claimUpload() {
|
|
190
213
|
ensureDirs();
|
|
214
|
+
const now = Date.now();
|
|
191
215
|
for (const directory of [MANIFESTS, PAYLOADS]) {
|
|
192
216
|
for (const name of fs.readdirSync(directory).sort()) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
217
|
+
const file = path.join(directory, name);
|
|
218
|
+
const manifest = readJson(file);
|
|
219
|
+
const chunk = manifest?.chunks?.find((entry) => (
|
|
220
|
+
!entry.uploaded
|
|
221
|
+
&& !entry.uploadTerminal
|
|
222
|
+
&& (
|
|
223
|
+
!entry.uploadRetryAfter
|
|
224
|
+
|| !Number.isFinite(Date.parse(entry.uploadRetryAfter))
|
|
225
|
+
|| Date.parse(entry.uploadRetryAfter) <= now
|
|
226
|
+
)
|
|
227
|
+
));
|
|
195
228
|
if (!chunk) continue;
|
|
196
|
-
const
|
|
229
|
+
const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
230
|
+
const bytes = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
|
|
231
|
+
if (
|
|
232
|
+
!bytes
|
|
233
|
+
|| bytes.length !== chunk.bytes
|
|
234
|
+
|| sha256(bytes) !== chunk.sha256
|
|
235
|
+
) {
|
|
236
|
+
fs.rmSync(chunkFile, { force: true });
|
|
237
|
+
fs.rmSync(file, { force: true });
|
|
238
|
+
wake('delete', chunk.sha256);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
197
241
|
return {
|
|
198
242
|
resourceId: manifest.resourceId,
|
|
199
243
|
sha256: chunk.sha256,
|
|
@@ -219,6 +263,10 @@ function acknowledgeUpload(resourceId, digest) {
|
|
|
219
263
|
for (const chunk of manifest.chunks) {
|
|
220
264
|
if (chunk.sha256 === digest && !chunk.uploaded) {
|
|
221
265
|
chunk.uploaded = true;
|
|
266
|
+
delete chunk.uploadAttempts;
|
|
267
|
+
delete chunk.uploadLastError;
|
|
268
|
+
delete chunk.uploadRetryAfter;
|
|
269
|
+
delete chunk.uploadTerminal;
|
|
222
270
|
touched = true;
|
|
223
271
|
changed = true;
|
|
224
272
|
}
|
|
@@ -239,21 +287,73 @@ function acknowledgeUpload(resourceId, digest) {
|
|
|
239
287
|
return { changed, completed };
|
|
240
288
|
}
|
|
241
289
|
|
|
290
|
+
function failUpload(resourceId, digest, failure = {}) {
|
|
291
|
+
if (!isProjectId(resourceId) || !/^[0-9a-f]{64}$/.test(String(digest || ''))) {
|
|
292
|
+
return { changed: false, terminal: false };
|
|
293
|
+
}
|
|
294
|
+
ensureDirs();
|
|
295
|
+
const failedAt = new Date().toISOString();
|
|
296
|
+
const retryable = failure?.retryable !== false;
|
|
297
|
+
let changed = false;
|
|
298
|
+
let terminal = false;
|
|
299
|
+
let retryAfter = null;
|
|
300
|
+
let attempts = 0;
|
|
301
|
+
for (const directory of [MANIFESTS, PAYLOADS]) {
|
|
302
|
+
for (const name of fs.readdirSync(directory)) {
|
|
303
|
+
const file = path.join(directory, name);
|
|
304
|
+
const manifest = readJson(file);
|
|
305
|
+
if (manifest?.resourceId !== resourceId || !manifest?.chunks) continue;
|
|
306
|
+
const chunk = manifest.chunks.find((entry) => (
|
|
307
|
+
entry.sha256 === digest && !entry.uploaded
|
|
308
|
+
));
|
|
309
|
+
if (!chunk) continue;
|
|
310
|
+
attempts = Math.max(attempts, Number(chunk.uploadAttempts || 0) + 1);
|
|
311
|
+
chunk.uploadAttempts = attempts;
|
|
312
|
+
chunk.uploadLastError = {
|
|
313
|
+
code: typeof failure?.code === 'string' ? failure.code : 'repo_object_upload_failed',
|
|
314
|
+
message: typeof failure?.message === 'string' ? failure.message : 'repo object upload failed',
|
|
315
|
+
retryable,
|
|
316
|
+
at: failedAt,
|
|
317
|
+
};
|
|
318
|
+
if (retryable) {
|
|
319
|
+
const delayMs = Math.min(
|
|
320
|
+
UPLOAD_RETRY_MAX_MS,
|
|
321
|
+
UPLOAD_RETRY_BASE_MS * (2 ** Math.min(attempts - 1, 16)),
|
|
322
|
+
);
|
|
323
|
+
retryAfter = new Date(Date.now() + delayMs).toISOString();
|
|
324
|
+
chunk.uploadRetryAfter = retryAfter;
|
|
325
|
+
delete chunk.uploadTerminal;
|
|
326
|
+
} else {
|
|
327
|
+
chunk.uploadTerminal = { ...chunk.uploadLastError };
|
|
328
|
+
delete chunk.uploadRetryAfter;
|
|
329
|
+
terminal = true;
|
|
330
|
+
}
|
|
331
|
+
writeJson(file, manifest);
|
|
332
|
+
changed = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (changed) wake('update', digest);
|
|
336
|
+
return { changed, terminal, retryAfter, attempts };
|
|
337
|
+
}
|
|
338
|
+
|
|
242
339
|
function validatePayloadLocator(locator) {
|
|
243
340
|
if (
|
|
244
341
|
locator?.kind !== 'chunks'
|
|
245
342
|
|| locator?.contract !== PAYLOAD_CONTRACT
|
|
246
|
-
||
|
|
343
|
+
|| !isProjectId(locator.resourceId)
|
|
247
344
|
|| !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
|
|
248
345
|
|| !Number.isSafeInteger(locator.bytes)
|
|
249
|
-
|| locator.bytes <
|
|
346
|
+
|| locator.bytes < 1
|
|
250
347
|
|| !Array.isArray(locator.chunks)
|
|
348
|
+
|| locator.chunks.length < 1
|
|
251
349
|
|| locator.chunks.some((chunk) => (
|
|
252
350
|
!/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
|
|
253
351
|
|| !Number.isSafeInteger(chunk?.bytes)
|
|
254
|
-
|| chunk.bytes <
|
|
352
|
+
|| chunk.bytes < 1
|
|
255
353
|
|| chunk.bytes > CHUNK_BYTES
|
|
256
354
|
))
|
|
355
|
+
|| locatorBytes(locator.chunks) !== locator.bytes
|
|
356
|
+
|| locatorChecksum(locator.chunks) !== locator.checksum
|
|
257
357
|
) throw new Error('invalid checkpoint payload locator');
|
|
258
358
|
}
|
|
259
359
|
|
|
@@ -263,7 +363,11 @@ function readPayload(locator) {
|
|
|
263
363
|
let waiting = false;
|
|
264
364
|
for (const chunk of locator.chunks) {
|
|
265
365
|
const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
266
|
-
if (fs.existsSync(file)
|
|
366
|
+
if (fs.existsSync(file)) {
|
|
367
|
+
const held = fs.readFileSync(file);
|
|
368
|
+
if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
|
|
369
|
+
fs.rmSync(file, { force: true });
|
|
370
|
+
}
|
|
267
371
|
writeJson(requestFile(locator.resourceId, chunk.sha256), {
|
|
268
372
|
resourceId: locator.resourceId,
|
|
269
373
|
sha256: chunk.sha256,
|
|
@@ -282,7 +386,7 @@ function readPayload(locator) {
|
|
|
282
386
|
return bytes;
|
|
283
387
|
});
|
|
284
388
|
const bytes = Buffer.concat(parts);
|
|
285
|
-
if (bytes.length !== locator.bytes
|
|
389
|
+
if (bytes.length !== locator.bytes) {
|
|
286
390
|
throw new Error('checkpoint payload verification failed');
|
|
287
391
|
}
|
|
288
392
|
writeJson(payloadFile(locator.resourceId, locator.checksum), {
|
|
@@ -303,21 +407,29 @@ function requestFile(resourceId, digest) {
|
|
|
303
407
|
|
|
304
408
|
function claimDownload() {
|
|
305
409
|
ensureDirs();
|
|
306
|
-
const name
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
410
|
+
for (const name of fs.readdirSync(REQUESTS).filter((entry) => entry.endsWith('.json')).sort()) {
|
|
411
|
+
const file = path.join(REQUESTS, name);
|
|
412
|
+
const request = readJson(file);
|
|
413
|
+
if (
|
|
414
|
+
request
|
|
415
|
+
&& Object.keys(request).length === 2
|
|
416
|
+
&& isProjectId(request.resourceId)
|
|
417
|
+
&& /^[0-9a-f]{64}$/.test(String(request.sha256 || ''))
|
|
418
|
+
) return request;
|
|
419
|
+
fs.rmSync(file, { force: true });
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
310
422
|
}
|
|
311
423
|
|
|
312
424
|
function receiveDownload(resourceId, digest, dataBase64) {
|
|
313
|
-
if (
|
|
425
|
+
if (!isProjectId(resourceId)) {
|
|
314
426
|
throw new Error('invalid repo object project');
|
|
315
427
|
}
|
|
316
428
|
if (!/^[0-9a-f]{64}$/.test(String(digest || ''))) throw new Error('invalid repo object checksum');
|
|
317
429
|
ensureDirs();
|
|
318
430
|
const bytes = Buffer.from(String(dataBase64 || ''), 'base64');
|
|
319
431
|
if (sha256(bytes) !== digest) throw new Error('repo object checksum mismatch');
|
|
320
|
-
|
|
432
|
+
durable.writeFile(path.join(CHUNKS, `${digest}.bin`), bytes);
|
|
321
433
|
fs.rmSync(requestFile(resourceId, digest), { force: true });
|
|
322
434
|
wake('update', digest);
|
|
323
435
|
require('./reducer').reduceNow().catch(() => {});
|
|
@@ -326,17 +438,24 @@ function receiveDownload(resourceId, digest, dataBase64) {
|
|
|
326
438
|
|
|
327
439
|
function validateLocator(locator) {
|
|
328
440
|
if (
|
|
329
|
-
locator?.
|
|
330
|
-
||
|
|
331
|
-
||
|
|
441
|
+
locator?.kind !== 'bundle'
|
|
442
|
+
|| locator?.contract !== CONTRACT
|
|
443
|
+
|| !isProjectId(locator.resourceId)
|
|
444
|
+
|| !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(locator.oid || ''))
|
|
332
445
|
|| !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
|
|
446
|
+
|| locator.ref !== `refs/amalgm/exports/${locator.oid}`
|
|
447
|
+
|| !Number.isSafeInteger(locator.bytes)
|
|
448
|
+
|| locator.bytes < 1
|
|
333
449
|
|| !Array.isArray(locator.chunks)
|
|
450
|
+
|| locator.chunks.length < 1
|
|
334
451
|
|| locator.chunks.some((chunk) => (
|
|
335
452
|
!/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
|
|
336
453
|
|| !Number.isSafeInteger(chunk?.bytes)
|
|
337
|
-
|| chunk.bytes <
|
|
454
|
+
|| chunk.bytes < 1
|
|
338
455
|
|| chunk.bytes > CHUNK_BYTES
|
|
339
456
|
))
|
|
457
|
+
|| locatorBytes(locator.chunks) !== locator.bytes
|
|
458
|
+
|| locatorChecksum(locator.chunks) !== locator.checksum
|
|
340
459
|
) throw new Error('invalid git bundle locator');
|
|
341
460
|
}
|
|
342
461
|
|
|
@@ -346,7 +465,11 @@ function prepareBundleImport(locator) {
|
|
|
346
465
|
let waiting = false;
|
|
347
466
|
for (const chunk of locator.chunks) {
|
|
348
467
|
const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
349
|
-
if (fs.existsSync(file)
|
|
468
|
+
if (fs.existsSync(file)) {
|
|
469
|
+
const held = fs.readFileSync(file);
|
|
470
|
+
if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
|
|
471
|
+
fs.rmSync(file, { force: true });
|
|
472
|
+
}
|
|
350
473
|
writeJson(requestFile(locator.resourceId, chunk.sha256), {
|
|
351
474
|
resourceId: locator.resourceId,
|
|
352
475
|
sha256: chunk.sha256,
|
|
@@ -359,19 +482,24 @@ function prepareBundleImport(locator) {
|
|
|
359
482
|
}
|
|
360
483
|
|
|
361
484
|
const bundleFile = path.join(ROOT, `${locator.checksum}.bundle`);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
485
|
+
const parts = locator.chunks.map((chunk) => (
|
|
486
|
+
fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`))
|
|
487
|
+
));
|
|
488
|
+
const bundleMatches = fs.existsSync(bundleFile) && (() => {
|
|
489
|
+
const held = fs.readFileSync(bundleFile);
|
|
490
|
+
let offset = 0;
|
|
491
|
+
for (let index = 0; index < locator.chunks.length; index += 1) {
|
|
492
|
+
const chunk = locator.chunks[index];
|
|
493
|
+
const part = held.subarray(offset, offset + chunk.bytes);
|
|
494
|
+
if (part.length !== chunk.bytes || sha256(part) !== chunk.sha256) return false;
|
|
495
|
+
offset += chunk.bytes;
|
|
373
496
|
}
|
|
374
|
-
|
|
497
|
+
return offset === held.length && held.length === locator.bytes;
|
|
498
|
+
})();
|
|
499
|
+
if (!bundleMatches) {
|
|
500
|
+
const bundle = Buffer.concat(parts);
|
|
501
|
+
if (bundle.length !== locator.bytes) throw new Error('git bundle verification failed');
|
|
502
|
+
durable.writeFile(bundleFile, bundle);
|
|
375
503
|
}
|
|
376
504
|
return { state: 'ready', file: bundleFile };
|
|
377
505
|
}
|
|
@@ -387,10 +515,12 @@ module.exports = {
|
|
|
387
515
|
CHUNK_BYTES,
|
|
388
516
|
CONTRACT,
|
|
389
517
|
PAYLOAD_CONTRACT,
|
|
518
|
+
_test: { describeChunks, locatorBytes, locatorChecksum },
|
|
390
519
|
acknowledgeUpload,
|
|
391
520
|
bundleSourceFor,
|
|
392
521
|
claimDownload,
|
|
393
522
|
claimUpload,
|
|
523
|
+
failUpload,
|
|
394
524
|
importFromLocator,
|
|
395
525
|
objectSourceFor,
|
|
396
526
|
prepareBundleImport,
|