swarm-deploy 0.1.0 → 0.3.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +780 -50
  3. package/RELEASING.md +2 -2
  4. package/SECURITY.md +2 -3
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +182 -25
  7. package/dist/cli.js.map +1 -1
  8. package/dist/client.d.ts +32 -27
  9. package/dist/client.js +103 -54
  10. package/dist/client.js.map +1 -1
  11. package/dist/errors.d.ts +6 -0
  12. package/dist/errors.js +6 -0
  13. package/dist/errors.js.map +1 -1
  14. package/dist/files.d.ts +13 -28
  15. package/dist/files.js +13 -78
  16. package/dist/files.js.map +1 -1
  17. package/dist/hooks-module.d.ts +9 -0
  18. package/dist/hooks-module.js +118 -0
  19. package/dist/hooks-module.js.map +1 -0
  20. package/dist/hooks.d.ts +69 -0
  21. package/dist/hooks.js +88 -0
  22. package/dist/hooks.js.map +1 -0
  23. package/dist/index.d.ts +8 -4
  24. package/dist/index.js +3 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/release.d.ts +26 -0
  27. package/dist/release.js +419 -0
  28. package/dist/release.js.map +1 -0
  29. package/dist/server.d.ts +97 -0
  30. package/dist/server.js +527 -53
  31. package/dist/server.js.map +1 -1
  32. package/dist/storage/commit-journal.d.ts +39 -2
  33. package/dist/storage/commit-journal.js +140 -2
  34. package/dist/storage/commit-journal.js.map +1 -1
  35. package/dist/storage/commit-store.d.ts +69 -8
  36. package/dist/storage/commit-store.js +374 -22
  37. package/dist/storage/commit-store.js.map +1 -1
  38. package/dist/storage/layout.js +7 -1
  39. package/dist/storage/layout.js.map +1 -1
  40. package/dist/storage/link-store.d.ts +88 -0
  41. package/dist/storage/link-store.js +448 -0
  42. package/dist/storage/link-store.js.map +1 -0
  43. package/dist/storage/recovery.d.ts +2 -1
  44. package/dist/storage/recovery.js +8 -1
  45. package/dist/storage/recovery.js.map +1 -1
  46. package/dist/storage/retention.d.ts +41 -21
  47. package/dist/storage/retention.js +162 -8
  48. package/dist/storage/retention.js.map +1 -1
  49. package/dist/storage/tar-session-store.d.ts +36 -5
  50. package/dist/storage/tar-session-store.js +283 -55
  51. package/dist/storage/tar-session-store.js.map +1 -1
  52. package/dist/storage/tree-fs.d.ts +44 -0
  53. package/dist/storage/tree-fs.js +580 -0
  54. package/dist/storage/tree-fs.js.map +1 -0
  55. package/dist/storage/tree-staging.d.ts +9 -0
  56. package/dist/storage/tree-staging.js +64 -0
  57. package/dist/storage/tree-staging.js.map +1 -0
  58. package/dist/storage/types.d.ts +15 -0
  59. package/dist/symlinks.d.ts +44 -0
  60. package/dist/symlinks.js +153 -0
  61. package/dist/symlinks.js.map +1 -0
  62. package/dist/tar-protocol/controls.d.ts +52 -0
  63. package/dist/tar-protocol/controls.js +160 -2
  64. package/dist/tar-protocol/controls.js.map +1 -1
  65. package/dist/tar-protocol/direct-wire.d.ts +8 -3
  66. package/dist/tar-protocol/direct-wire.js +14 -3
  67. package/dist/tar-protocol/direct-wire.js.map +1 -1
  68. package/dist/tar-protocol/manifest.d.ts +13 -1
  69. package/dist/tar-protocol/manifest.js +26 -1
  70. package/dist/tar-protocol/manifest.js.map +1 -1
  71. package/dist/tar-protocol/tree-extract.d.ts +37 -0
  72. package/dist/tar-protocol/tree-extract.js +240 -0
  73. package/dist/tar-protocol/tree-extract.js.map +1 -0
  74. package/dist/tar-protocol/tree-manifest.d.ts +45 -0
  75. package/dist/tar-protocol/tree-manifest.js +277 -0
  76. package/dist/tar-protocol/tree-manifest.js.map +1 -0
  77. package/dist/tar-protocol/tree-path.d.ts +15 -0
  78. package/dist/tar-protocol/tree-path.js +68 -0
  79. package/dist/tar-protocol/tree-path.js.map +1 -0
  80. package/dist/tar-protocol/tree.d.ts +68 -0
  81. package/dist/tar-protocol/tree.js +269 -0
  82. package/dist/tar-protocol/tree.js.map +1 -0
  83. package/dist/tar-protocol/ustar.d.ts +14 -0
  84. package/dist/tar-protocol/ustar.js +81 -1
  85. package/dist/tar-protocol/ustar.js.map +1 -1
  86. package/dist/types.d.ts +3 -0
  87. package/package.json +3 -1
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TarSessionStore = void 0;
6
+ exports.TarSessionStore = exports.TREE_SESSION_VERSION = void 0;
7
7
  const b4a_1 = __importDefault(require("b4a"));
8
8
  const error_code_js_1 = require("../error-code.js");
9
9
  const _fs_1 = __importDefault(require("#fs"));
@@ -12,12 +12,19 @@ const sodium_native_1 = __importDefault(require("sodium-native"));
12
12
  const errors_js_1 = require("../errors.js");
13
13
  const controls_js_1 = require("../tar-protocol/controls.js");
14
14
  const extract_js_1 = require("../tar-protocol/extract.js");
15
+ const tree_extract_js_1 = require("../tar-protocol/tree-extract.js");
15
16
  const manifest_js_1 = require("../tar-protocol/manifest.js");
17
+ const tree_manifest_js_1 = require("../tar-protocol/tree-manifest.js");
16
18
  const atomic_file_js_1 = require("./atomic-file.js");
17
19
  const commit_journal_js_1 = require("./commit-journal.js");
18
20
  const layout_js_1 = require("./layout.js");
21
+ const tree_staging_js_1 = require("./tree-staging.js");
22
+ const tree_fs_js_1 = require("./tree-fs.js");
19
23
  const validation_js_1 = require("../validation.js");
20
- const VERSION = 2;
24
+ const VERSION = 3;
25
+ const LEGACY_VERSION = 2;
26
+ /** Directory sessions are persisted at a version no file-only server understands. */
27
+ exports.TREE_SESSION_VERSION = 4;
21
28
  const RECEIVING = 'receiving';
22
29
  const VERIFIED = 'verified';
23
30
  const DELETING = 'deleting';
@@ -43,12 +50,22 @@ function key(value, name) {
43
50
  throw problem(`Invalid ${name}`);
44
51
  }
45
52
  function sameMetadata(session, metadata, owner) {
46
- return (sodium_native_1.default.sodium_memcmp(session.ownerKey, owner) &&
47
- session.name === metadata.name &&
53
+ if (!sodium_native_1.default.sodium_memcmp(session.ownerKey, owner) ||
54
+ session.name !== metadata.name ||
55
+ session.sourceParent !== metadata.sourceParent ||
56
+ session.tarSize !== metadata.tarSize ||
57
+ !sodium_native_1.default.sodium_memcmp(session.tarDigest, b4a_1.default.from(metadata.tarSha256, 'hex'))) {
58
+ return false;
59
+ }
60
+ if ((0, controls_js_1.isTreeMetadata)(metadata)) {
61
+ return (session.kind === 'directory' &&
62
+ session.entryCount === metadata.entryCount &&
63
+ session.size === metadata.payloadBytes &&
64
+ sodium_native_1.default.sodium_memcmp(session.digest, b4a_1.default.from(metadata.treeSha256, 'hex')));
65
+ }
66
+ return (session.kind === 'file' &&
48
67
  session.size === metadata.fileSize &&
49
- session.tarSize === metadata.tarSize &&
50
- sodium_native_1.default.sodium_memcmp(session.digest, b4a_1.default.from(metadata.fileSha256, 'hex')) &&
51
- sodium_native_1.default.sodium_memcmp(session.tarDigest, b4a_1.default.from(metadata.tarSha256, 'hex')));
68
+ sodium_native_1.default.sodium_memcmp(session.digest, b4a_1.default.from(metadata.fileSha256, 'hex')));
52
69
  }
53
70
  async function writeAll(handle, value, position) {
54
71
  let offset = 0;
@@ -140,6 +157,15 @@ class TarSessionStore {
140
157
  filePath(id) {
141
158
  return _path_1.default.join(this.layout.staging, `${id}.part`);
142
159
  }
160
+ treeStagingPath(id) {
161
+ return _path_1.default.join(this.layout.staging, `${id}.tree`);
162
+ }
163
+ /** Removes a staged tree through the contained primitives; never `storage.rm`. */
164
+ removeStagedTree(session) {
165
+ return (0, tree_fs_js_1.removeTree)(this.treeStagingPath(session.id), this.layout.staging, this.storage, {
166
+ layout: this.layout
167
+ });
168
+ }
143
169
  journalPath(id) {
144
170
  return _path_1.default.join(this.layout.journals, `${id}.json`);
145
171
  }
@@ -151,12 +177,76 @@ class TarSessionStore {
151
177
  throw problem('Session store is not available');
152
178
  }
153
179
  }
154
- metadata(metadata) {
155
- return (0, controls_js_1.decodeMetadataRecord)((0, controls_js_1.encodeMetadataRecord)(metadata));
180
+ metadata(input) {
181
+ return (0, controls_js_1.decodeAnyMetadataRecord)((0, controls_js_1.encodeAnyMetadataRecord)(input));
182
+ }
183
+ fileMetadata(input) {
184
+ const metadata = this.metadata(input);
185
+ if ((0, controls_js_1.isTreeMetadata)(metadata))
186
+ throw problem('Invalid file session metadata');
187
+ return metadata;
188
+ }
189
+ treeFromDisk(id, record) {
190
+ const persisted = record;
191
+ if (persisted.kind !== 'directory' || persisted.transferId !== id) {
192
+ throw problem('Invalid tree session');
193
+ }
194
+ const ownerKey = bytes(persisted.ownerKey, 'owner key');
195
+ uint(persisted.entryCount, 'tree entry count');
196
+ uint(persisted.payloadBytes, 'tree payload size');
197
+ uint(persisted.tarSize, 'TAR size');
198
+ uint(persisted.createdAt, 'creation time');
199
+ uint(persisted.updatedAt, 'update time');
200
+ if (!persisted.partialTar ||
201
+ persisted.partialTar.path !== `${id}.tar.part` ||
202
+ ![RECEIVING, VERIFIED, DELETING].includes(persisted.state)) {
203
+ throw problem('Invalid tree session metadata');
204
+ }
205
+ uint(persisted.partialTar.size, 'partial TAR size');
206
+ if (persisted.partialTar.size > persisted.tarSize)
207
+ throw problem('Oversized partial TAR');
208
+ const metadata = (0, controls_js_1.decodeTreeMetadataRecord)((0, controls_js_1.encodeTreeMetadataRecord)({
209
+ v: 1,
210
+ kind: 'directory',
211
+ name: persisted.name,
212
+ ...(persisted.sourceParent === undefined ? {} : { sourceParent: persisted.sourceParent }),
213
+ entryCount: persisted.entryCount,
214
+ payloadBytes: persisted.payloadBytes,
215
+ treeSha256: hex(bytes(persisted.treeSha256, 'tree digest')),
216
+ tarSize: persisted.tarSize,
217
+ tarSha256: hex(bytes(persisted.tarSha256, 'TAR digest')),
218
+ transferId: id,
219
+ reset: false
220
+ }));
221
+ (0, tree_manifest_js_1.assertTreeMetadataTransferId)(ownerKey, metadata);
222
+ return {
223
+ id,
224
+ transferId: b4a_1.default.from(id, 'hex'),
225
+ ownerKey,
226
+ kind: 'directory',
227
+ name: metadata.name,
228
+ sourceParent: metadata.sourceParent,
229
+ size: metadata.payloadBytes,
230
+ digest: b4a_1.default.from(metadata.treeSha256, 'hex'),
231
+ entryCount: metadata.entryCount,
232
+ tarSize: metadata.tarSize,
233
+ tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
234
+ tarPath: this.tarPath(id),
235
+ treePath: this.treeStagingPath(id),
236
+ partialTarSize: persisted.partialTar.size,
237
+ createdAt: persisted.createdAt,
238
+ updatedAt: persisted.updatedAt,
239
+ state: persisted.state
240
+ };
156
241
  }
157
242
  fromDisk(id, record) {
243
+ if (record.version === exports.TREE_SESSION_VERSION)
244
+ return this.treeFromDisk(id, record);
158
245
  const persisted = record;
159
- if (persisted.version !== VERSION || persisted.transferId !== id) {
246
+ if ((persisted.version !== LEGACY_VERSION && persisted.version !== VERSION) ||
247
+ persisted.transferId !== id ||
248
+ (persisted.version === LEGACY_VERSION &&
249
+ Object.prototype.hasOwnProperty.call(record, 'sourceParent'))) {
160
250
  throw problem('Invalid TAR session');
161
251
  }
162
252
  const ownerKey = bytes(persisted.ownerKey, 'owner key');
@@ -174,9 +264,10 @@ class TarSessionStore {
174
264
  if (persisted.partialTar.size > persisted.tarSize) {
175
265
  throw problem('Oversized partial TAR');
176
266
  }
177
- const metadata = this.metadata({
267
+ const metadata = this.fileMetadata({
178
268
  v: 1,
179
269
  name: persisted.name,
270
+ ...(persisted.sourceParent === undefined ? {} : { sourceParent: persisted.sourceParent }),
180
271
  fileSize: persisted.fileSize,
181
272
  fileSha256: hex(bytes(persisted.fileSha256, 'file digest')),
182
273
  tarSize: persisted.tarSize,
@@ -189,7 +280,9 @@ class TarSessionStore {
189
280
  id,
190
281
  transferId,
191
282
  ownerKey,
283
+ kind: 'file',
192
284
  name: metadata.name,
285
+ sourceParent: metadata.sourceParent,
193
286
  size: metadata.fileSize,
194
287
  digest: b4a_1.default.from(metadata.fileSha256, 'hex'),
195
288
  tarSize: metadata.tarSize,
@@ -201,12 +294,38 @@ class TarSessionStore {
201
294
  state: persisted.state
202
295
  };
203
296
  }
297
+ /**
298
+ * Version 3 only adds `sourceParent`, so a session without one is written as
299
+ * version 2. An older server can then still read, and resume, every session
300
+ * whose record it fully understands; only parent-bearing ones are lost to a
301
+ * downgrade.
302
+ */
204
303
  serialize(session) {
304
+ if (session.kind === 'directory') {
305
+ return {
306
+ version: exports.TREE_SESSION_VERSION,
307
+ kind: 'directory',
308
+ transferId: session.id,
309
+ ownerKey: hex(session.ownerKey),
310
+ name: session.name,
311
+ sourceParent: session.sourceParent,
312
+ entryCount: session.entryCount,
313
+ payloadBytes: session.size,
314
+ treeSha256: hex(session.digest),
315
+ tarSize: session.tarSize,
316
+ tarSha256: hex(session.tarDigest),
317
+ partialTar: { path: `${session.id}.tar.part`, size: session.partialTarSize },
318
+ createdAt: session.createdAt,
319
+ updatedAt: session.updatedAt,
320
+ state: session.state
321
+ };
322
+ }
205
323
  return {
206
- version: VERSION,
324
+ version: session.sourceParent === undefined ? LEGACY_VERSION : VERSION,
207
325
  transferId: session.id,
208
326
  ownerKey: hex(session.ownerKey),
209
327
  name: session.name,
328
+ sourceParent: session.sourceParent,
210
329
  fileSize: session.size,
211
330
  fileSha256: hex(session.digest),
212
331
  tarSize: session.tarSize,
@@ -237,6 +356,23 @@ class TarSessionStore {
237
356
  });
238
357
  await (0, layout_js_1.withSafeDirectoryIdentity)(directory, this.storage, () => syncDirectory(directory, this.storage));
239
358
  }
359
+ /** A verified directory session must still own its staging tree. */
360
+ async assertStagedTree(session) {
361
+ const state = await (0, tree_fs_js_1.inspectTreePath)(this.treeStagingPath(session.id), this.layout.staging, this.storage, { layout: this.layout });
362
+ if (state !== 'DIRECTORY')
363
+ throw problem('Session is not verified');
364
+ }
365
+ /** Verified directory sessions mid-commit may only have a durable journal tree. */
366
+ async assertVerifiedDirectorySession(session) {
367
+ const treeState = await (0, tree_fs_js_1.inspectTreePath)(this.treeStagingPath(session.id), this.layout.staging, this.storage, { layout: this.layout });
368
+ if (treeState === 'DIRECTORY') {
369
+ await this.assertStagedTree(session);
370
+ return;
371
+ }
372
+ const journal = await (0, commit_journal_js_1.readCommitJournal)(session.id, this.layout, this.storage);
373
+ if (!journal || !(0, commit_journal_js_1.isDirectoryJournal)(journal))
374
+ throw problem('Session is not verified');
375
+ }
240
376
  async purgeCorruptSession(id) {
241
377
  await this.remove(this.tarPath(id), this.layout.staging);
242
378
  await this.remove(this.sessionPath(id), this.layout.sessions);
@@ -361,7 +497,10 @@ class TarSessionStore {
361
497
  }
362
498
  if (session.state === DELETING) {
363
499
  await this.remove(session.tarPath, this.layout.staging);
364
- await this.remove(this.filePath(id), this.layout.staging);
500
+ if (session.kind === 'directory')
501
+ await this.removeStagedTree(session);
502
+ else
503
+ await this.remove(this.filePath(id), this.layout.staging);
365
504
  await this.remove(this.sessionPath(id), this.layout.sessions);
366
505
  continue;
367
506
  }
@@ -372,6 +511,24 @@ class TarSessionStore {
372
511
  await this.writeSession(session);
373
512
  }
374
513
  }
514
+ else if (session.kind === 'directory') {
515
+ const treeState = await (0, tree_fs_js_1.inspectTreePath)(this.treeStagingPath(id), this.layout.staging, this.storage, { layout: this.layout });
516
+ if (treeState !== 'DIRECTORY') {
517
+ const journal = await (0, commit_journal_js_1.readCommitJournal)(id, this.layout, this.storage);
518
+ if (!journal || !(0, commit_journal_js_1.isDirectoryJournal)(journal)) {
519
+ await this.purgeCorruptSession(id);
520
+ if (treeState !== 'MISSING') {
521
+ await (0, tree_fs_js_1.removeTree)(this.treeStagingPath(id), this.layout.staging, this.storage, {
522
+ layout: this.layout
523
+ }).catch(() => { });
524
+ }
525
+ continue;
526
+ }
527
+ }
528
+ else {
529
+ await this.assertVerifiedDirectorySession(session);
530
+ }
531
+ }
375
532
  else {
376
533
  await (0, layout_js_1.assertSafeFile)(this.filePath(id), this.storage);
377
534
  }
@@ -384,12 +541,24 @@ class TarSessionStore {
384
541
  const expected = new Set();
385
542
  for (const session of this.sessions.values()) {
386
543
  expected.add(`${session.id}.tar.part`);
387
- if (session.state === VERIFIED)
388
- expected.add(`${session.id}.part`);
544
+ if (session.state === VERIFIED) {
545
+ expected.add(session.kind === 'directory' ? `${session.id}.tree` : `${session.id}.part`);
546
+ }
389
547
  }
390
548
  for (const name of await this.storage.readdir(this.layout.staging)) {
391
549
  if (expected.has(name))
392
550
  continue;
551
+ const tree = /^([0-9a-f]{64})\.tree$/.exec(name);
552
+ if (tree) {
553
+ // A journal-owned tree belongs to a commit in flight; anything else is residue.
554
+ if (await (0, commit_journal_js_1.readCommitJournal)(tree[1], this.layout, this.storage))
555
+ continue;
556
+ await (0, tree_fs_js_1.removeTree)(_path_1.default.join(this.layout.staging, name), this.layout.staging, this.storage, {
557
+ layout: this.layout
558
+ });
559
+ this.purgedSessions++;
560
+ continue;
561
+ }
393
562
  const staged = /^([0-9a-f]{64})\.(?:part|tar\.part)$/.exec(name);
394
563
  if (staged) {
395
564
  const journal = await (0, commit_journal_js_1.readCommitJournal)(staged[1], this.layout, this.storage);
@@ -408,7 +577,10 @@ class TarSessionStore {
408
577
  this.assertReady();
409
578
  key(ownerKey, 'owner key');
410
579
  const metadata = this.metadata(input);
411
- (0, manifest_js_1.assertMetadataTransferId)(ownerKey, metadata);
580
+ if ((0, controls_js_1.isTreeMetadata)(metadata))
581
+ (0, tree_manifest_js_1.assertTreeMetadataTransferId)(ownerKey, metadata);
582
+ else
583
+ (0, manifest_js_1.assertMetadataTransferId)(ownerKey, metadata);
412
584
  const id = metadata.transferId;
413
585
  if (this.resumeTtl !== undefined)
414
586
  await this.expireUnlocked(this.resumeTtl);
@@ -446,27 +618,49 @@ class TarSessionStore {
446
618
  throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.FILE_BUSY, 'Destination has an active session');
447
619
  }
448
620
  }
449
- const reservation = metadata.tarSize + metadata.fileSize;
621
+ const reservation = metadata.tarSize + ((0, controls_js_1.isTreeMetadata)(metadata) ? metadata.payloadBytes : metadata.fileSize);
450
622
  if (this.reservedBytes > this.maxStagingBytes - reservation) {
451
623
  throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.STAGING_LIMIT, 'Staging capacity exceeded');
452
624
  }
453
625
  await this.diskReserve(reservation);
454
626
  const now = this.clock.now();
455
- session = {
456
- id,
457
- transferId: b4a_1.default.from(id, 'hex'),
458
- ownerKey: b4a_1.default.from(ownerKey),
459
- name: metadata.name,
460
- size: metadata.fileSize,
461
- digest: b4a_1.default.from(metadata.fileSha256, 'hex'),
462
- tarSize: metadata.tarSize,
463
- tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
464
- tarPath: this.tarPath(id),
465
- partialTarSize: 0,
466
- createdAt: now,
467
- updatedAt: now,
468
- state: RECEIVING
469
- };
627
+ session = (0, controls_js_1.isTreeMetadata)(metadata)
628
+ ? {
629
+ id,
630
+ transferId: b4a_1.default.from(id, 'hex'),
631
+ ownerKey: b4a_1.default.from(ownerKey),
632
+ kind: 'directory',
633
+ name: metadata.name,
634
+ sourceParent: metadata.sourceParent,
635
+ size: metadata.payloadBytes,
636
+ digest: b4a_1.default.from(metadata.treeSha256, 'hex'),
637
+ entryCount: metadata.entryCount,
638
+ tarSize: metadata.tarSize,
639
+ tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
640
+ tarPath: this.tarPath(id),
641
+ treePath: this.treeStagingPath(id),
642
+ partialTarSize: 0,
643
+ createdAt: now,
644
+ updatedAt: now,
645
+ state: RECEIVING
646
+ }
647
+ : {
648
+ id,
649
+ transferId: b4a_1.default.from(id, 'hex'),
650
+ ownerKey: b4a_1.default.from(ownerKey),
651
+ kind: 'file',
652
+ name: metadata.name,
653
+ sourceParent: metadata.sourceParent,
654
+ size: metadata.fileSize,
655
+ digest: b4a_1.default.from(metadata.fileSha256, 'hex'),
656
+ tarSize: metadata.tarSize,
657
+ tarDigest: b4a_1.default.from(metadata.tarSha256, 'hex'),
658
+ tarPath: this.tarPath(id),
659
+ partialTarSize: 0,
660
+ createdAt: now,
661
+ updatedAt: now,
662
+ state: RECEIVING
663
+ };
470
664
  await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, async () => {
471
665
  const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'create', this.storage);
472
666
  await handle.sync();
@@ -621,26 +815,31 @@ class TarSessionStore {
621
815
  let file = null;
622
816
  let fileOffset = 0;
623
817
  try {
624
- file = await (0, layout_js_1.openSafeRegularFile)(this.filePath(session.id), 'create', this.storage);
625
- const source = this.readTar(session);
626
- await (0, extract_js_1.validateAndExtractTar)(source, metadata, {
627
- writeTar: () => { },
628
- writeFile: async (chunk) => {
629
- await writeAll(file, chunk, fileOffset);
630
- fileOffset += chunk.byteLength;
631
- },
632
- complete: async () => {
633
- await file.sync();
634
- },
635
- abort: async () => {
636
- await file?.close().catch(() => { });
637
- file = null;
638
- await this.remove(this.filePath(session.id), this.layout.staging).catch(() => { });
639
- }
640
- });
641
- await file.close();
642
- file = null;
643
- await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, () => syncDirectory(this.layout.staging, this.storage));
818
+ if (session.kind === 'directory') {
819
+ await this.extractTree(session, metadata);
820
+ }
821
+ else {
822
+ file = await (0, layout_js_1.openSafeRegularFile)(this.filePath(session.id), 'create', this.storage);
823
+ const source = this.readTar(session);
824
+ await (0, extract_js_1.validateAndExtractTar)(source, metadata, {
825
+ writeTar: () => { },
826
+ writeFile: async (chunk) => {
827
+ await writeAll(file, chunk, fileOffset);
828
+ fileOffset += chunk.byteLength;
829
+ },
830
+ complete: async () => {
831
+ await file.sync();
832
+ },
833
+ abort: async () => {
834
+ await file?.close().catch(() => { });
835
+ file = null;
836
+ await this.remove(this.filePath(session.id), this.layout.staging).catch(() => { });
837
+ }
838
+ });
839
+ await file.close();
840
+ file = null;
841
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, () => syncDirectory(this.layout.staging, this.storage));
842
+ }
644
843
  await this.run(async () => {
645
844
  if (this.sessions.get(session.id) !== session ||
646
845
  session.state !== RECEIVING ||
@@ -668,7 +867,10 @@ class TarSessionStore {
668
867
  await file.close().catch(() => { });
669
868
  if (session.state !== VERIFIED) {
670
869
  try {
671
- await this.remove(this.filePath(session.id), this.layout.staging);
870
+ if (session.kind === 'directory')
871
+ await this.removeStagedTree(session);
872
+ else
873
+ await this.remove(this.filePath(session.id), this.layout.staging);
672
874
  }
673
875
  catch (cleanupCause) {
674
876
  await this.quarantineVerificationFailure(session, error, cleanupCause);
@@ -689,6 +891,26 @@ class TarSessionStore {
689
891
  task.then(settled, settled);
690
892
  return task;
691
893
  }
894
+ /**
895
+ * Extracts the complete directory archive into its staging tree, then proves
896
+ * the tree from the bytes on disk alone: the recomputed canonical digest,
897
+ * entry count, and payload size must equal the offered values.
898
+ */
899
+ async extractTree(session, metadata) {
900
+ const treePath = this.treeStagingPath(session.id);
901
+ const options = { layout: this.layout };
902
+ // Residue of an interrupted attempt never survives into a new extraction.
903
+ await (0, tree_fs_js_1.removeTree)(treePath, this.layout.staging, this.storage, options);
904
+ const target = (0, tree_staging_js_1.createTreeStagingTarget)(treePath, this.layout.staging, this.storage, this.layout);
905
+ await (0, tree_extract_js_1.validateAndExtractTreeTar)(this.readTar(session), metadata, target);
906
+ const staged = await (0, tree_fs_js_1.digestTree)(treePath, this.storage);
907
+ if (staged.entryCount !== metadata.entryCount ||
908
+ staged.payloadBytes !== metadata.payloadBytes ||
909
+ !sodium_native_1.default.sodium_memcmp(staged.treeSha256, b4a_1.default.from(metadata.treeSha256, 'hex'))) {
910
+ throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.CHECKSUM_MISMATCH, 'Staged tree digest mismatch');
911
+ }
912
+ await (0, layout_js_1.withSafeDirectoryIdentity)(this.layout.staging, this.storage, () => syncDirectory(this.layout.staging, this.storage));
913
+ }
692
914
  async *readTar(session) {
693
915
  const handle = await (0, layout_js_1.openSafeRegularFile)(session.tarPath, 'read', this.storage);
694
916
  try {
@@ -709,7 +931,10 @@ class TarSessionStore {
709
931
  const session = this.sessions.get(hex(transferId));
710
932
  if (!session || session.state !== VERIFIED)
711
933
  throw problem('Session is not verified');
712
- await (0, layout_js_1.assertSafeFile)(this.filePath(session.id), this.storage);
934
+ if (session.kind === 'directory')
935
+ await this.assertVerifiedDirectorySession(session);
936
+ else
937
+ await (0, layout_js_1.assertSafeFile)(this.filePath(session.id), this.storage);
713
938
  return session;
714
939
  });
715
940
  }
@@ -728,7 +953,10 @@ class TarSessionStore {
728
953
  try {
729
954
  await this.writeSession(session);
730
955
  await this.remove(session.tarPath, this.layout.staging);
731
- await this.remove(this.filePath(session.id), this.layout.staging);
956
+ if (session.kind === 'directory')
957
+ await this.removeStagedTree(session);
958
+ else
959
+ await this.remove(this.filePath(session.id), this.layout.staging);
732
960
  await this.remove(this.sessionPath(session.id), this.layout.sessions);
733
961
  }
734
962
  finally {