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.
- package/CHANGELOG.md +60 -0
- package/README.md +780 -50
- package/RELEASING.md +2 -2
- package/SECURITY.md +2 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +32 -27
- package/dist/client.js +103 -54
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/files.d.ts +13 -28
- package/dist/files.js +13 -78
- package/dist/files.js.map +1 -1
- package/dist/hooks-module.d.ts +9 -0
- package/dist/hooks-module.js +118 -0
- package/dist/hooks-module.js.map +1 -0
- package/dist/hooks.d.ts +69 -0
- package/dist/hooks.js +88 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/release.d.ts +26 -0
- package/dist/release.js +419 -0
- package/dist/release.js.map +1 -0
- package/dist/server.d.ts +97 -0
- package/dist/server.js +527 -53
- package/dist/server.js.map +1 -1
- package/dist/storage/commit-journal.d.ts +39 -2
- package/dist/storage/commit-journal.js +140 -2
- package/dist/storage/commit-journal.js.map +1 -1
- package/dist/storage/commit-store.d.ts +69 -8
- package/dist/storage/commit-store.js +374 -22
- package/dist/storage/commit-store.js.map +1 -1
- package/dist/storage/layout.js +7 -1
- package/dist/storage/layout.js.map +1 -1
- package/dist/storage/link-store.d.ts +88 -0
- package/dist/storage/link-store.js +448 -0
- package/dist/storage/link-store.js.map +1 -0
- package/dist/storage/recovery.d.ts +2 -1
- package/dist/storage/recovery.js +8 -1
- package/dist/storage/recovery.js.map +1 -1
- package/dist/storage/retention.d.ts +41 -21
- package/dist/storage/retention.js +162 -8
- package/dist/storage/retention.js.map +1 -1
- package/dist/storage/tar-session-store.d.ts +36 -5
- package/dist/storage/tar-session-store.js +283 -55
- package/dist/storage/tar-session-store.js.map +1 -1
- package/dist/storage/tree-fs.d.ts +44 -0
- package/dist/storage/tree-fs.js +580 -0
- package/dist/storage/tree-fs.js.map +1 -0
- package/dist/storage/tree-staging.d.ts +9 -0
- package/dist/storage/tree-staging.js +64 -0
- package/dist/storage/tree-staging.js.map +1 -0
- package/dist/storage/types.d.ts +15 -0
- package/dist/symlinks.d.ts +44 -0
- package/dist/symlinks.js +153 -0
- package/dist/symlinks.js.map +1 -0
- package/dist/tar-protocol/controls.d.ts +52 -0
- package/dist/tar-protocol/controls.js +160 -2
- package/dist/tar-protocol/controls.js.map +1 -1
- package/dist/tar-protocol/direct-wire.d.ts +8 -3
- package/dist/tar-protocol/direct-wire.js +14 -3
- package/dist/tar-protocol/direct-wire.js.map +1 -1
- package/dist/tar-protocol/manifest.d.ts +13 -1
- package/dist/tar-protocol/manifest.js +26 -1
- package/dist/tar-protocol/manifest.js.map +1 -1
- package/dist/tar-protocol/tree-extract.d.ts +37 -0
- package/dist/tar-protocol/tree-extract.js +240 -0
- package/dist/tar-protocol/tree-extract.js.map +1 -0
- package/dist/tar-protocol/tree-manifest.d.ts +45 -0
- package/dist/tar-protocol/tree-manifest.js +277 -0
- package/dist/tar-protocol/tree-manifest.js.map +1 -0
- package/dist/tar-protocol/tree-path.d.ts +15 -0
- package/dist/tar-protocol/tree-path.js +68 -0
- package/dist/tar-protocol/tree-path.js.map +1 -0
- package/dist/tar-protocol/tree.d.ts +68 -0
- package/dist/tar-protocol/tree.js +269 -0
- package/dist/tar-protocol/tree.js.map +1 -0
- package/dist/tar-protocol/ustar.d.ts +14 -0
- package/dist/tar-protocol/ustar.js +81 -1
- package/dist/tar-protocol/ustar.js.map +1 -1
- package/dist/types.d.ts +3 -0
- 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 =
|
|
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
|
-
|
|
47
|
-
session.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.
|
|
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(
|
|
155
|
-
return (0, controls_js_1.
|
|
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 !==
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|