@elaraai/e3-cloud-cli 1.1.0 → 1.1.2
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/README.md +44 -2
- package/dist/src/cli.js +22 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/files.d.ts +8 -1
- package/dist/src/commands/files.d.ts.map +1 -1
- package/dist/src/commands/files.js +44 -28
- package/dist/src/commands/files.js.map +1 -1
- package/dist/src/commands/records.d.ts +9 -0
- package/dist/src/commands/records.d.ts.map +1 -1
- package/dist/src/commands/records.js +7 -4
- package/dist/src/commands/records.js.map +1 -1
- package/dist/src/commands/whoami.d.ts +18 -0
- package/dist/src/commands/whoami.d.ts.map +1 -1
- package/dist/src/commands/whoami.js +29 -5
- package/dist/src/commands/whoami.js.map +1 -1
- package/dist/src/dataset-source.d.ts +102 -0
- package/dist/src/dataset-source.d.ts.map +1 -0
- package/dist/src/dataset-source.js +412 -0
- package/dist/src/dataset-source.js.map +1 -0
- package/dist/src/files-local.d.ts +12 -17
- package/dist/src/files-local.d.ts.map +1 -1
- package/dist/src/files-local.js +35 -11
- package/dist/src/files-local.js.map +1 -1
- package/dist/src/files-push.d.ts +129 -31
- package/dist/src/files-push.d.ts.map +1 -1
- package/dist/src/files-push.js +297 -139
- package/dist/src/files-push.js.map +1 -1
- package/dist/src/versions.d.ts +36 -0
- package/dist/src/versions.d.ts.map +1 -0
- package/dist/src/versions.js +83 -0
- package/dist/src/versions.js.map +1 -0
- package/package.json +9 -9
package/dist/src/files-push.js
CHANGED
|
@@ -3,34 +3,46 @@
|
|
|
3
3
|
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* `e3-cloud files push`: files from disk
|
|
6
|
+
* `e3-cloud files push`: files from disk, and a repository's datasets as the
|
|
7
|
+
* files of their values, into a source, and taken in.
|
|
7
8
|
*
|
|
8
9
|
* Each file offered is read for its size, its SHA-256 (streamed) and the
|
|
9
10
|
* root type its beast2 header declares (through ranged reads of its head; a
|
|
10
11
|
* file that is no beast2 offers no type); a directory is walked, its
|
|
11
12
|
* `.`-prefixed entries skipped, each file offered at its path relative to
|
|
12
|
-
* the directory, with the directory's name.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
13
|
+
* the directory, with the directory's name. A dataset (`dataset:<ws>.<name>`)
|
|
14
|
+
* is read from its repository, local or deployed, for the same, as the file
|
|
15
|
+
* of its value (`./dataset-source.ts`); one unassigned, not in its workspace
|
|
16
|
+
* or named twice there is refused in its own row and never planned. Rows keep
|
|
17
|
+
* the order the paths were given.
|
|
18
|
+
*
|
|
19
|
+
* The plan comes from the platform (`POST …/files/push`) for a repository
|
|
20
|
+
* URL, or from the same engine over the local repository's declarations,
|
|
21
|
+
* bindings and ledgers for a path; it says, per file, its target and key and
|
|
22
|
+
* how to put it. A `put` is uploaded: `single` by one PUT with the plan's
|
|
23
|
+
* headers, or `multipart` part by part then completed through the API, both
|
|
24
|
+
* by the client's `uploadFilesPush` (a file's parts four in flight, each read
|
|
25
|
+
* by range; a dataset's one after another from one read); `external` through
|
|
26
|
+
* the bucket's own client with the developer's own credentials (`--profile`,
|
|
27
|
+
* the binding's, or the ambient chain), in parts past 64 MiB, a dataset's
|
|
28
|
+
* file with the metadata naming it, which its seed's or feed's history
|
|
29
|
+
* records. Then one sync of the targets put, waited on, and each row
|
|
30
|
+
* completed from the sync's report. Nothing is put for a dry run.
|
|
31
|
+
*
|
|
32
|
+
* A push to a local repository is {@link pushLocal} whole: the plan, the puts
|
|
33
|
+
* and the sync, over the repository's own declarations, bindings and ledgers.
|
|
23
34
|
*/
|
|
24
35
|
import { createHash } from 'node:crypto';
|
|
25
|
-
import { createReadStream, openSync, closeSync, readSync, statSync, readdirSync
|
|
36
|
+
import { createReadStream, openSync, closeSync, readSync, statSync, readdirSync } from 'node:fs';
|
|
26
37
|
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
27
|
-
import { Readable } from 'node:stream';
|
|
28
38
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, PutObjectCommand, S3Client, UploadPartCommand, } from '@aws-sdk/client-s3';
|
|
29
39
|
import { none, some } from '@elaraai/east';
|
|
30
|
-
import { FILES_PUSH_PART_BYTES, filesPushRefusalText, filesTargetText, filesTypeWords, sameFilesTarget, } from '@elaraai/e3-cloud-types';
|
|
40
|
+
import { FILES_PUSH_PART_BYTES, filesDatasetMetadata, filesPushRefusalText, filesTargetText, filesTypeWords, sameFilesTarget, } from '@elaraai/e3-cloud-types';
|
|
31
41
|
import { parseBucketUrl } from '@elaraai/e3-cloud-files/check';
|
|
32
42
|
import { beast2HeaderTypeOf } from '@elaraai/e3-cloud-files/sync';
|
|
33
|
-
|
|
43
|
+
import { DatasetMovedError, datasetArgOf, measuredDataset } from './dataset-source.js';
|
|
44
|
+
import { planLocal, syncLocal } from './files-local.js';
|
|
45
|
+
/** Uploads of an external multipart upload's parts in flight at once. */
|
|
34
46
|
const PARTS_IN_FLIGHT = 4;
|
|
35
47
|
/**
|
|
36
48
|
* The root type a file's beast2 header declares, through reads of its head
|
|
@@ -70,6 +82,15 @@ async function sha256Of(path) {
|
|
|
70
82
|
hash.update(chunk);
|
|
71
83
|
return hash.digest('hex');
|
|
72
84
|
}
|
|
85
|
+
/** A file's bytes from an offset to its end. */
|
|
86
|
+
async function* fileFrom(path, start) {
|
|
87
|
+
for await (const chunk of createReadStream(path, { start }))
|
|
88
|
+
yield chunk;
|
|
89
|
+
}
|
|
90
|
+
/** A file's bytes as an upload reads them: from any offset, for nothing. */
|
|
91
|
+
function fileBytes(path, size) {
|
|
92
|
+
return { size, ranged: true, read: (start) => fileFrom(path, start) };
|
|
93
|
+
}
|
|
73
94
|
/** The files under a directory, walked, `.`-prefixed entries skipped, each path relative to it. */
|
|
74
95
|
function walk(dir) {
|
|
75
96
|
const files = [];
|
|
@@ -105,7 +126,12 @@ export async function offeredFiles(paths) {
|
|
|
105
126
|
const one = async (file, name, directory) => {
|
|
106
127
|
const size = statSync(file).size;
|
|
107
128
|
const type = await fileBeast2Type(file, size);
|
|
108
|
-
offered.push({
|
|
129
|
+
offered.push({
|
|
130
|
+
label: directory === null ? name : `${directory}/${name}`,
|
|
131
|
+
offered: { name, directory: directory === null ? none : some(directory), size: BigInt(size), sha256: await sha256Of(file), type: type === null ? none : some(type), dataset: none },
|
|
132
|
+
bytes: fileBytes(file, size),
|
|
133
|
+
origin: null,
|
|
134
|
+
});
|
|
109
135
|
};
|
|
110
136
|
if (stat.isDirectory()) {
|
|
111
137
|
for (const name of walk(path))
|
|
@@ -120,50 +146,84 @@ export async function offeredFiles(paths) {
|
|
|
120
146
|
}
|
|
121
147
|
return offered;
|
|
122
148
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
/**
|
|
150
|
+
* A dataset found, as a push offers it: its value read once for its size,
|
|
151
|
+
* its SHA-256 and its type, and named by its path and what it is.
|
|
152
|
+
*
|
|
153
|
+
* @param found - The dataset
|
|
154
|
+
* @returns The file of its value
|
|
155
|
+
* @throws {DatasetMovedError} When it moves while it is read
|
|
156
|
+
*/
|
|
157
|
+
export async function offeredDataset(found) {
|
|
158
|
+
const measured = await measuredDataset(found);
|
|
159
|
+
return {
|
|
160
|
+
label: `${found.arg.arg} (${found.kind})`,
|
|
161
|
+
offered: {
|
|
162
|
+
name: found.arg.arg,
|
|
163
|
+
directory: none,
|
|
164
|
+
size: BigInt(measured.size),
|
|
165
|
+
sha256: measured.sha256,
|
|
166
|
+
type: some(measured.type),
|
|
167
|
+
dataset: some({ workspace: found.arg.workspace, name: found.arg.name, hash: found.hash }),
|
|
168
|
+
},
|
|
169
|
+
bytes: measured.bytes,
|
|
170
|
+
origin: measured.origin,
|
|
171
|
+
};
|
|
126
172
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
method: 'PUT',
|
|
131
|
-
headers: { ...Object.fromEntries(headers), 'content-length': String(size) },
|
|
132
|
-
body: body(path),
|
|
133
|
-
// A streamed request body
|
|
134
|
-
duplex: 'half',
|
|
135
|
-
});
|
|
136
|
-
if (!response.ok)
|
|
137
|
-
throw new Error(`the upload answered HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
|
|
173
|
+
/** A row refusing what was offered before any plan. */
|
|
174
|
+
function refusedRow(label, size, put) {
|
|
175
|
+
return { label, size, target: null, kind: '', put, sync: '' };
|
|
138
176
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (etag === null)
|
|
161
|
-
throw new Error(`part ${part.number} answered no ETag`);
|
|
162
|
-
etags.push({ number: part.number, etag: etag.replace(/^"|"$/g, '') });
|
|
177
|
+
/**
|
|
178
|
+
* What a push offers, in the order its paths were given: each file, each
|
|
179
|
+
* directory's files, and each dataset (`dataset:<ws>.<name>`) read from the
|
|
180
|
+
* repository `--from` names; a dataset that is unassigned, not in its
|
|
181
|
+
* workspace or named twice there, or that moves while it is read, refused in
|
|
182
|
+
* its own row.
|
|
183
|
+
*
|
|
184
|
+
* @param paths - Files, directories and datasets
|
|
185
|
+
* @param repository - Where datasets are read, opened once, only when a path names one
|
|
186
|
+
* @returns Each offering, in order
|
|
187
|
+
* @throws {Error} When a path is neither a file, a directory nor a dataset's name
|
|
188
|
+
*/
|
|
189
|
+
export async function offerings(paths, repository) {
|
|
190
|
+
let opened = null;
|
|
191
|
+
const found = [];
|
|
192
|
+
for (const given of paths) {
|
|
193
|
+
const arg = datasetArgOf(given);
|
|
194
|
+
if (arg === null) {
|
|
195
|
+
for (const file of await offeredFiles([given]))
|
|
196
|
+
found.push({ file });
|
|
197
|
+
continue;
|
|
163
198
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
opened ??= await repository();
|
|
200
|
+
const [lookup] = await opened.find([arg]);
|
|
201
|
+
if ('refused' in lookup) {
|
|
202
|
+
found.push({ refused: refusedRow(given, null, `refused: ${lookup.message}`) });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
found.push({ file: await offeredDataset(lookup.found) });
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (!(err instanceof DatasetMovedError))
|
|
210
|
+
throw err;
|
|
211
|
+
found.push({ refused: refusedRow(`${given} (${lookup.found.kind})`, null, `failed: ${err.message}`) });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return found;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Each offering's row, in the order the paths were given: a planned file's
|
|
218
|
+
* from the plan's rows, in turn; a refused dataset's its own.
|
|
219
|
+
*
|
|
220
|
+
* @param offered - The offerings
|
|
221
|
+
* @param planned - The rows of the files planned, in the plan's order
|
|
222
|
+
* @returns The rows
|
|
223
|
+
*/
|
|
224
|
+
export function rowsInOrder(offered, planned) {
|
|
225
|
+
let next = 0;
|
|
226
|
+
return offered.map((o) => ('file' in o ? planned[next++] : o.refused));
|
|
167
227
|
}
|
|
168
228
|
/**
|
|
169
229
|
* A client of the developer's own credentials for a bucket: the profile
|
|
@@ -178,88 +238,129 @@ export function ownClient(binding, profile) {
|
|
|
178
238
|
const chosen = profile ?? (binding.profile.type === 'some' ? binding.profile.value : undefined);
|
|
179
239
|
return new S3Client({ ...(chosen !== undefined && { profile: chosen }), ...(region !== undefined && { region }), followRegionRedirects: true });
|
|
180
240
|
}
|
|
241
|
+
/** Bytes, one after another, in one buffer. */
|
|
242
|
+
function joined(parts, length) {
|
|
243
|
+
const out = new Uint8Array(length);
|
|
244
|
+
let at = 0;
|
|
245
|
+
for (const part of parts) {
|
|
246
|
+
out.set(part, at);
|
|
247
|
+
at += part.length;
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A source's bytes in parts of `partBytes` (the last the rest), in order, from
|
|
253
|
+
* one read: each part whole, for its checksum. A whole part is given once the
|
|
254
|
+
* byte after it has come, so the last is given only after the read has ended,
|
|
255
|
+
* and a source that checks its bytes as they end (a dataset's) is heard
|
|
256
|
+
* before an upload of them is completed.
|
|
257
|
+
*
|
|
258
|
+
* @throws {Error} When the bytes are not as many as planned: a file that changed while it was read
|
|
259
|
+
*/
|
|
260
|
+
async function* partsOf(bytes, partBytes) {
|
|
261
|
+
let held = [];
|
|
262
|
+
let heldBytes = 0;
|
|
263
|
+
let total = 0;
|
|
264
|
+
for await (const chunk of bytes.read(0)) {
|
|
265
|
+
total += chunk.length;
|
|
266
|
+
if (total > bytes.size)
|
|
267
|
+
throw new Error(`the bytes run past the ${bytes.size} planned: it changed while it was read`);
|
|
268
|
+
let at = 0;
|
|
269
|
+
while (at < chunk.length) {
|
|
270
|
+
if (heldBytes === partBytes) {
|
|
271
|
+
// A whole part, with more to come
|
|
272
|
+
yield joined(held, heldBytes);
|
|
273
|
+
held = [];
|
|
274
|
+
heldBytes = 0;
|
|
275
|
+
}
|
|
276
|
+
const take = Math.min(chunk.length - at, partBytes - heldBytes);
|
|
277
|
+
held.push(chunk.subarray(at, at + take));
|
|
278
|
+
heldBytes += take;
|
|
279
|
+
at += take;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (total !== bytes.size)
|
|
283
|
+
throw new Error(`the bytes end ${bytes.size - total} short of the ${bytes.size} planned: it changed while it was read`);
|
|
284
|
+
if (heldBytes > 0 || bytes.size === 0)
|
|
285
|
+
yield joined(held, heldBytes);
|
|
286
|
+
}
|
|
287
|
+
/** A SHA-256 as the bucket's checksum takes it: base64. */
|
|
288
|
+
const checksumOf = (sha256) => Buffer.from(sha256, 'hex').toString('base64');
|
|
181
289
|
/**
|
|
182
290
|
* A put with the developer's own credentials: one PUT up to the part size,
|
|
183
|
-
*
|
|
291
|
+
* its bytes held to the SHA-256 the plan was given; else a multipart upload in
|
|
292
|
+
* parts, each with its own checksum, read from one read and four sent at once.
|
|
293
|
+
* A dataset's file carries the metadata naming the dataset.
|
|
184
294
|
*
|
|
185
295
|
* @param client - The client
|
|
186
296
|
* @param bucket - The bucket
|
|
187
297
|
* @param key - The object's key
|
|
188
|
-
* @param
|
|
189
|
-
* @param size - Its size
|
|
298
|
+
* @param file - The file or dataset
|
|
190
299
|
*/
|
|
191
|
-
export async function putExternal(client, bucket, key,
|
|
300
|
+
export async function putExternal(client, bucket, key, file) {
|
|
301
|
+
const size = file.bytes.size;
|
|
302
|
+
const metadata = file.origin === null ? undefined : filesDatasetMetadata(file.origin);
|
|
192
303
|
if (size <= FILES_PUSH_PART_BYTES) {
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
break;
|
|
201
|
-
done += read;
|
|
202
|
-
}
|
|
203
|
-
if (fstatSync(fd).size !== size)
|
|
204
|
-
throw new Error(`${path} changed while it was read`);
|
|
205
|
-
}
|
|
206
|
-
finally {
|
|
207
|
-
closeSync(fd);
|
|
208
|
-
}
|
|
209
|
-
await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: bytes, ContentLength: size, ChecksumAlgorithm: 'SHA256' }));
|
|
304
|
+
let body = null;
|
|
305
|
+
for await (const part of partsOf(file.bytes, Math.max(size, 1)))
|
|
306
|
+
body = part;
|
|
307
|
+
await client.send(new PutObjectCommand({
|
|
308
|
+
Bucket: bucket, Key: key, Body: body, ContentLength: size, ChecksumSHA256: checksumOf(file.offered.sha256),
|
|
309
|
+
...(metadata !== undefined && { Metadata: metadata }),
|
|
310
|
+
}));
|
|
210
311
|
return;
|
|
211
312
|
}
|
|
212
|
-
const created = await client.send(new CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ChecksumAlgorithm: 'SHA256' }));
|
|
313
|
+
const created = await client.send(new CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ChecksumAlgorithm: 'SHA256', ...(metadata !== undefined && { Metadata: metadata }) }));
|
|
213
314
|
const uploadId = created.UploadId;
|
|
315
|
+
const sent = [];
|
|
316
|
+
const inFlight = new Set();
|
|
317
|
+
let failure = null;
|
|
214
318
|
try {
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
};
|
|
235
|
-
await Promise.all(Array.from({ length: Math.min(PARTS_IN_FLIGHT, count) }, worker));
|
|
319
|
+
let number = 0;
|
|
320
|
+
for await (const part of partsOf(file.bytes, FILES_PUSH_PART_BYTES)) {
|
|
321
|
+
if (failure !== null)
|
|
322
|
+
break;
|
|
323
|
+
const partNumber = ++number;
|
|
324
|
+
// An upload created with a checksum algorithm completes with each part's checksum
|
|
325
|
+
const checksum = createHash('sha256').update(part).digest('base64');
|
|
326
|
+
const sending = client.send(new UploadPartCommand({
|
|
327
|
+
Bucket: bucket, Key: key, UploadId: uploadId, PartNumber: partNumber, Body: part, ContentLength: part.length, ChecksumSHA256: checksum,
|
|
328
|
+
})).then((uploaded) => { sent.push({ PartNumber: partNumber, ETag: uploaded.ETag, ChecksumSHA256: checksum }); }, (err) => { failure ??= err; })
|
|
329
|
+
.finally(() => { inFlight.delete(sending); });
|
|
330
|
+
inFlight.add(sending);
|
|
331
|
+
// A part read only while fewer than four are being sent: four parts held at most
|
|
332
|
+
if (inFlight.size >= PARTS_IN_FLIGHT)
|
|
333
|
+
await Promise.race(inFlight);
|
|
334
|
+
}
|
|
335
|
+
await Promise.all(inFlight);
|
|
336
|
+
if (failure !== null)
|
|
337
|
+
throw failure;
|
|
236
338
|
await client.send(new CompleteMultipartUploadCommand({
|
|
237
339
|
Bucket: bucket, Key: key, UploadId: uploadId,
|
|
238
|
-
MultipartUpload: { Parts:
|
|
340
|
+
MultipartUpload: { Parts: sent.sort((a, b) => a.PartNumber - b.PartNumber) },
|
|
239
341
|
}));
|
|
240
342
|
}
|
|
241
343
|
catch (err) {
|
|
344
|
+
await Promise.allSettled(inFlight);
|
|
242
345
|
await client.send(new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId: uploadId })).catch(() => undefined);
|
|
243
346
|
throw err;
|
|
244
347
|
}
|
|
245
348
|
}
|
|
246
|
-
/** The kind a mapping fills, as a row says it: the type, `seed`,
|
|
247
|
-
function kindOf(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return 'seed';
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
void plan;
|
|
254
|
-
return 'input';
|
|
349
|
+
/** The kind a mapping fills, as a row says it: the type, `seed`, or the mutation. */
|
|
350
|
+
function kindOf(file, target) {
|
|
351
|
+
switch (target.type) {
|
|
352
|
+
case 'input': return typeOfOffered(file);
|
|
353
|
+
case 'record': return 'seed';
|
|
354
|
+
case 'mutation': return target.value.mutation;
|
|
355
|
+
}
|
|
255
356
|
}
|
|
256
357
|
/**
|
|
257
|
-
* Uploads each planned put
|
|
258
|
-
*
|
|
358
|
+
* Uploads each planned put and answers a row per file: put, unchanged,
|
|
359
|
+
* refused or failed.
|
|
259
360
|
*
|
|
260
361
|
* @param plan - The plan
|
|
261
362
|
* @param files - The files offered, in the plan's order
|
|
262
|
-
* @param uploads - The
|
|
363
|
+
* @param uploads - The platform's uploads, and the credentials for external puts
|
|
263
364
|
* @param dryRun - Put nothing
|
|
264
365
|
* @returns A row per file, and the targets put
|
|
265
366
|
*/
|
|
@@ -269,30 +370,30 @@ export async function uploadPlanned(plan, files, uploads, dryRun) {
|
|
|
269
370
|
const bindingOf = (source) => plan.bindings.find((b) => b.source === source);
|
|
270
371
|
for (const [i, planned] of plan.files.entries()) {
|
|
271
372
|
const file = files[i];
|
|
373
|
+
const row = (target, kind, put) => ({ label: file.label, size: file.offered.size, target, kind, put, sync: '' });
|
|
272
374
|
const { outcome } = planned;
|
|
273
375
|
if (outcome.type === 'refused') {
|
|
274
|
-
rows.push(
|
|
376
|
+
rows.push(row(null, '', `refused: ${filesPushRefusalText(outcome.value)}`));
|
|
275
377
|
continue;
|
|
276
378
|
}
|
|
277
379
|
const { target } = outcome.value;
|
|
278
|
-
const kind =
|
|
380
|
+
const kind = kindOf(file, target);
|
|
279
381
|
if (outcome.type === 'unchanged') {
|
|
280
|
-
rows.push(
|
|
382
|
+
rows.push(row(target, kind, 'unchanged'));
|
|
281
383
|
continue;
|
|
282
384
|
}
|
|
283
385
|
if (dryRun) {
|
|
284
|
-
rows.push(
|
|
386
|
+
rows.push(row(target, kind, 'would put'));
|
|
285
387
|
continue;
|
|
286
388
|
}
|
|
287
|
-
const size = Number(file.offered.size);
|
|
288
389
|
try {
|
|
289
|
-
await upload(
|
|
290
|
-
rows.push(
|
|
390
|
+
await upload(plan, outcome.value, bindingOf(outcome.value.source), file, uploads);
|
|
391
|
+
rows.push(row(target, kind, 'put'));
|
|
291
392
|
if (!targets.some((t) => sameFilesTarget(t, target)))
|
|
292
393
|
targets.push(target);
|
|
293
394
|
}
|
|
294
395
|
catch (err) {
|
|
295
|
-
rows.push(
|
|
396
|
+
rows.push(row(target, kind, `failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
296
397
|
}
|
|
297
398
|
}
|
|
298
399
|
return { rows, targets };
|
|
@@ -305,27 +406,36 @@ function typeOfOffered(file) {
|
|
|
305
406
|
return words.length <= 40 ? words : `${words.slice(0, 39)}…`;
|
|
306
407
|
}
|
|
307
408
|
/** One put, as the plan says to upload it. */
|
|
308
|
-
async function upload(
|
|
309
|
-
switch (
|
|
409
|
+
async function upload(plan, put, binding, file, uploads) {
|
|
410
|
+
switch (put.upload.type) {
|
|
310
411
|
case 'single':
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
await uploads.complete(key, how.value.uploadId, etags);
|
|
412
|
+
case 'multipart':
|
|
413
|
+
if (uploads.signed === undefined)
|
|
414
|
+
throw new Error('the plan signed an upload, which only a deployed repository\'s push makes');
|
|
415
|
+
await uploads.signed(plan, put, file.bytes);
|
|
316
416
|
return;
|
|
317
|
-
}
|
|
318
417
|
case 'external': {
|
|
319
418
|
if (binding === undefined)
|
|
320
419
|
throw new Error('the plan names no binding to put to');
|
|
420
|
+
if (uploads.external !== undefined) {
|
|
421
|
+
await uploads.external(binding, put.key, file);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
321
424
|
const location = parseBucketUrl(binding.url);
|
|
322
425
|
if ('problem' in location)
|
|
323
426
|
throw new Error(`${binding.url} ${location.problem}`);
|
|
324
|
-
await putExternal(ownClient(binding, uploads.profile), location.bucket, `${location.prefix}${key}`,
|
|
427
|
+
await putExternal(ownClient(binding, uploads.profile), location.bucket, `${location.prefix}${put.key}`, file);
|
|
325
428
|
return;
|
|
326
429
|
}
|
|
327
430
|
}
|
|
328
431
|
}
|
|
432
|
+
/** A seed's commit as a row says it: `$init #1`, `$seed #2`; `$seed` where its ledger did not count. */
|
|
433
|
+
function seedWords(m) {
|
|
434
|
+
if (m.seed.type === 'none')
|
|
435
|
+
return null;
|
|
436
|
+
const { commit, number } = m.seed.value;
|
|
437
|
+
return number.type === 'some' ? `${commit} #${number.value}` : commit;
|
|
438
|
+
}
|
|
329
439
|
/** What a sync did to a row's target, in words. */
|
|
330
440
|
export function syncWords(report, target) {
|
|
331
441
|
const m = report?.mappings.find((r) => sameFilesTarget(r.target, target));
|
|
@@ -337,13 +447,61 @@ export function syncWords(report, target) {
|
|
|
337
447
|
return `waiting: ${m.waiting.value}`;
|
|
338
448
|
if (m.kept.type === 'some')
|
|
339
449
|
return `kept: ${m.kept.value}`;
|
|
340
|
-
const
|
|
450
|
+
const count = m.taken.type === 'some' && m.taken.value.rows.type === 'some' ? m.taken.value.rows.value : null;
|
|
451
|
+
const rows = count === null ? '' : ` ${count.toLocaleString('en-US')} ${count === 1n ? 'row' : 'rows'}`;
|
|
452
|
+
const seed = seedWords(m);
|
|
453
|
+
if (seed !== null)
|
|
454
|
+
return `${seed}${rows}`;
|
|
341
455
|
if (m.commit.type === 'some')
|
|
342
456
|
return `committed ${m.commit.value.slice(0, 12)}${rows}`;
|
|
343
457
|
if (m.updated)
|
|
344
458
|
return `taken in${rows}`;
|
|
345
459
|
return 'unchanged';
|
|
346
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Each offering's row, in the order the paths were given, each put's with
|
|
463
|
+
* what the sync did to its target.
|
|
464
|
+
*
|
|
465
|
+
* @param offered - The offerings
|
|
466
|
+
* @param planned - The rows of the files planned, in the plan's order
|
|
467
|
+
* @param report - The sync of what was put; null when nothing was
|
|
468
|
+
* @returns The rows
|
|
469
|
+
*/
|
|
470
|
+
export function completedRows(offered, planned, report) {
|
|
471
|
+
const rows = rowsInOrder(offered, planned);
|
|
472
|
+
for (const row of rows)
|
|
473
|
+
if (row.target !== null && row.put === 'put')
|
|
474
|
+
row.sync = syncWords(report, row.target);
|
|
475
|
+
return rows;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A push to a local repository's workspace: the plan over its declarations,
|
|
479
|
+
* bindings and ledgers ({@link planLocal}), each put with the developer's own
|
|
480
|
+
* credentials, then one sync of the targets put ({@link syncLocal}), as a
|
|
481
|
+
* deployed workspace's push does through its API.
|
|
482
|
+
*
|
|
483
|
+
* @param repoPath - The repository, opened (`openLocalRepository`)
|
|
484
|
+
* @param ws - The workspace
|
|
485
|
+
* @param offered - What the push offers ({@link offerings})
|
|
486
|
+
* @param options - The target named, the records reset, a dry run, the credentials
|
|
487
|
+
* @returns The bindings, the rows and the sync's report
|
|
488
|
+
* @throws {Error} When the workspace has no declarations deployed
|
|
489
|
+
*/
|
|
490
|
+
export async function pushLocal(repoPath, ws, offered, options = {}) {
|
|
491
|
+
const files = offered.flatMap((o) => ('file' in o ? [o.file] : []));
|
|
492
|
+
if (files.length === 0)
|
|
493
|
+
return { bindings: [], rows: completedRows(offered, [], null), report: null };
|
|
494
|
+
const plan = await planLocal(repoPath, ws, files, { ...(options.as !== undefined && { as: options.as }), ...(options.reader !== undefined && { reader: options.reader }) });
|
|
495
|
+
const dryRun = options.dryRun === true;
|
|
496
|
+
const { rows, targets } = await uploadPlanned(plan, files, {
|
|
497
|
+
...(options.profile !== undefined && { profile: options.profile }),
|
|
498
|
+
...(options.external !== undefined && { external: options.external }),
|
|
499
|
+
}, dryRun);
|
|
500
|
+
const report = !dryRun && targets.length > 0
|
|
501
|
+
? await syncLocal(repoPath, ws, { targets, ...(options.reset !== undefined && { reset: options.reset }), ...(options.reader !== undefined && { reader: options.reader }) })
|
|
502
|
+
: null;
|
|
503
|
+
return { bindings: plan.bindings, rows: completedRows(offered, rows, report), report };
|
|
504
|
+
}
|
|
347
505
|
/** A count of bytes, as the rows show it. */
|
|
348
506
|
function sizeWords(bytes) {
|
|
349
507
|
const n = Number(bytes);
|
|
@@ -358,22 +516,22 @@ function sizeWords(bytes) {
|
|
|
358
516
|
}
|
|
359
517
|
/**
|
|
360
518
|
* A push's rows, as the CLI prints them: the sources' bucket lines, a row
|
|
361
|
-
* per file, and the dataflow hint when anything was taken.
|
|
519
|
+
* per file or dataset, and the dataflow hint when anything was taken.
|
|
362
520
|
*
|
|
363
|
-
* @param
|
|
364
|
-
* @param rows - The rows
|
|
521
|
+
* @param bindings - The bindings the plan put to; none when nothing was planned
|
|
522
|
+
* @param rows - The rows, in the order the paths were given
|
|
365
523
|
* @param next - The dataflow run hint's command, when anything changed; null for none
|
|
366
524
|
* @returns The lines
|
|
367
525
|
*/
|
|
368
|
-
export function pushText(
|
|
526
|
+
export function pushText(bindings, rows, next) {
|
|
369
527
|
const lines = [];
|
|
370
|
-
for (const binding of
|
|
528
|
+
for (const binding of bindings)
|
|
371
529
|
lines.push(`${binding.source} ${binding.url}`);
|
|
372
530
|
const cells = rows.map((row) => [
|
|
373
|
-
row.
|
|
531
|
+
row.label,
|
|
374
532
|
row.target === null ? '' : `→ ${filesTargetText(row.target)}`,
|
|
375
533
|
row.kind,
|
|
376
|
-
sizeWords(row.
|
|
534
|
+
row.size === null ? '' : sizeWords(row.size),
|
|
377
535
|
row.put,
|
|
378
536
|
row.sync,
|
|
379
537
|
]);
|