naro-cli 0.14.0 → 0.16.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/README.md +48 -3
- package/dist/{chunk-U7AZSAFS.js → chunk-XNT7SINN.js} +150 -41
- package/dist/deploy.d.ts +4 -1
- package/dist/deploy.js +1 -1
- package/dist/index.js +458 -91
- package/dist/output.d.ts +5 -0
- package/dist/upload.d.ts +2 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
controlPlaneUrl,
|
|
27
27
|
countLintedTables,
|
|
28
28
|
createAuthClient,
|
|
29
|
-
dataError,
|
|
30
29
|
deployDirectory,
|
|
31
30
|
detectGitInfo,
|
|
32
31
|
ensureGitignoreHasNaro,
|
|
@@ -83,7 +82,7 @@ import {
|
|
|
83
82
|
toNaroError,
|
|
84
83
|
unreadablePolicyComment,
|
|
85
84
|
writeProjectLink
|
|
86
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-XNT7SINN.js";
|
|
87
86
|
import "./chunk-K3NQKI34.js";
|
|
88
87
|
|
|
89
88
|
// src/args.ts
|
|
@@ -289,6 +288,13 @@ var Output = class {
|
|
|
289
288
|
this.streams.stderr(`${message}
|
|
290
289
|
`);
|
|
291
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Text on stderr exactly as given, with no line break added: a progress
|
|
293
|
+
* line a terminal rewrites in place (`\r…`). Never stdout, in either mode.
|
|
294
|
+
*/
|
|
295
|
+
progress(text) {
|
|
296
|
+
this.streams.stderr(text);
|
|
297
|
+
}
|
|
292
298
|
/** Extra diagnostics, only with --debug. */
|
|
293
299
|
debugLog(message) {
|
|
294
300
|
if (this.debug) this.streams.stderr(`[debug] ${message}
|
|
@@ -5272,7 +5278,7 @@ function parseSize(text) {
|
|
|
5272
5278
|
throw new NaroError("VALIDATION_FAILED", SIZE_MESSAGE);
|
|
5273
5279
|
return bytes;
|
|
5274
5280
|
}
|
|
5275
|
-
function binarySize(bytes) {
|
|
5281
|
+
function binarySize(bytes, decimals = 1) {
|
|
5276
5282
|
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
5277
5283
|
let value = bytes;
|
|
5278
5284
|
let unit = 0;
|
|
@@ -5280,7 +5286,7 @@ function binarySize(bytes) {
|
|
|
5280
5286
|
value /= 1024;
|
|
5281
5287
|
unit += 1;
|
|
5282
5288
|
}
|
|
5283
|
-
const shown = unit === 0 ? String(value) : String(Number(value.toFixed(
|
|
5289
|
+
const shown = unit === 0 ? String(value) : String(Number(value.toFixed(decimals)));
|
|
5284
5290
|
return `${shown} ${units[unit] ?? "B"}`;
|
|
5285
5291
|
}
|
|
5286
5292
|
function count(n, noun) {
|
|
@@ -5308,6 +5314,51 @@ function bucketLine(bucket) {
|
|
|
5308
5314
|
const line = ` ${cells.join(" ")}`;
|
|
5309
5315
|
return bucket.deletingAt === null ? line : `${line} (being removed: \`naro storage bucket remove ${name} --empty\` finishes it)`;
|
|
5310
5316
|
}
|
|
5317
|
+
var WARN_PERCENT = 80;
|
|
5318
|
+
function utcClock(iso) {
|
|
5319
|
+
const at = new Date(iso);
|
|
5320
|
+
return Number.isNaN(at.getTime()) ? null : at.toISOString().slice(11, 16);
|
|
5321
|
+
}
|
|
5322
|
+
function warnedPercent(value, limit) {
|
|
5323
|
+
if (limit === null || !(limit > 0)) return null;
|
|
5324
|
+
const percent2 = Math.floor(value * 100 / limit);
|
|
5325
|
+
return percent2 >= WARN_PERCENT ? percent2 : null;
|
|
5326
|
+
}
|
|
5327
|
+
function meterLines(info) {
|
|
5328
|
+
const { today, limits } = info;
|
|
5329
|
+
if (today === null) return [];
|
|
5330
|
+
const reads = count(today.classBOps, "read");
|
|
5331
|
+
const writes = today.classAOps.toLocaleString("en-US");
|
|
5332
|
+
const dailyOps = limits.dailyOps?.toLocaleString("en-US") ?? null;
|
|
5333
|
+
const lines = [
|
|
5334
|
+
dailyOps === null ? `Today (UTC): ${writes} write operations, ${reads}` : `Today (UTC): ${writes} of ${dailyOps} write operations, ${reads}`
|
|
5335
|
+
];
|
|
5336
|
+
if (today.measuredBytes === null)
|
|
5337
|
+
lines.push("Measured: not yet (the meter reads R2 every 15 minutes)");
|
|
5338
|
+
else {
|
|
5339
|
+
const time = today.measuredAt === null ? null : utcClock(today.measuredAt);
|
|
5340
|
+
lines.push(
|
|
5341
|
+
`Measured: ${binarySize(today.measuredBytes, 2)} in R2${time === null ? "" : ` at ${time} UTC`}`
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
if (today.bytesBlocked)
|
|
5345
|
+
lines.push(
|
|
5346
|
+
today.measuredBytes === null ? `\u2717 Uploads are stopped: the bucket is at the storage limit of ${binarySize(limits.bytes)}. Delete objects; uploads reopen after the next measurement (usually within an hour and a half).` : `\u2717 Uploads are stopped: R2 holds ${binarySize(today.measuredBytes, 2)} of ${binarySize(limits.bytes)}. Delete objects; uploads reopen after the next measurement (usually within an hour and a half).`
|
|
5347
|
+
);
|
|
5348
|
+
if (today.opsBlocked)
|
|
5349
|
+
lines.push(
|
|
5350
|
+
dailyOps === null ? "\u2717 Uploads are stopped until 00:00 UTC: today's write operations are used. Downloads still work." : `\u2717 Uploads are stopped until 00:00 UTC: today's ${dailyOps} write operations are used. Downloads still work.`
|
|
5351
|
+
);
|
|
5352
|
+
const storage = today.bytesBlocked || today.measuredBytes === null ? null : warnedPercent(today.measuredBytes, limits.bytes);
|
|
5353
|
+
if (storage !== null)
|
|
5354
|
+
lines.push(`\u26A0 ${String(storage)}% of the storage limit is used.`);
|
|
5355
|
+
const operations = today.opsBlocked ? null : warnedPercent(today.classAOps, limits.dailyOps);
|
|
5356
|
+
if (operations !== null)
|
|
5357
|
+
lines.push(
|
|
5358
|
+
`\u26A0 ${String(operations)}% of today's write operations are used.`
|
|
5359
|
+
);
|
|
5360
|
+
return lines;
|
|
5361
|
+
}
|
|
5311
5362
|
function storageLines(info) {
|
|
5312
5363
|
if (!info.enabled)
|
|
5313
5364
|
return [
|
|
@@ -5317,8 +5368,9 @@ function storageLines(info) {
|
|
|
5317
5368
|
const { limits, usage } = info;
|
|
5318
5369
|
return [
|
|
5319
5370
|
`Storage: on (R2 bucket ${printable(info.bucket ?? "unknown")})`,
|
|
5320
|
-
`Limits: ${binarySize(limits.bytes)} in all, ${binarySize(limits.objectBytes)} per object, ${binarySize(limits.singleUploadBytes)} per
|
|
5371
|
+
`Limits: ${binarySize(limits.bytes)} in all, ${binarySize(limits.objectBytes)} per object, ${binarySize(limits.singleUploadBytes)} per request (larger files go in parts)`,
|
|
5321
5372
|
usage === null ? "Used: not known yet: the backend is not ready (`naro backend` shows where it got to)" : `Used: ${binarySize(usage.bytes)} in ${count(usage.objects, "object")}`,
|
|
5373
|
+
...meterLines(info),
|
|
5322
5374
|
info.buckets.length === 0 ? "Buckets: none yet (`naro storage bucket create <name>` makes one)" : `Buckets: ${String(info.buckets.length)}`,
|
|
5323
5375
|
...info.buckets.map(bucketLine)
|
|
5324
5376
|
];
|
|
@@ -5503,6 +5555,12 @@ function requestTimeout(ms) {
|
|
|
5503
5555
|
message: `The Naro backend did not answer within ${String(Math.round(ms / 1e3))} seconds. Check the network and try again.`
|
|
5504
5556
|
};
|
|
5505
5557
|
}
|
|
5558
|
+
function uploadAborted() {
|
|
5559
|
+
return {
|
|
5560
|
+
code: "ABORTED",
|
|
5561
|
+
message: "The upload was stopped before it finished."
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5506
5564
|
function invalidResponse(status) {
|
|
5507
5565
|
return {
|
|
5508
5566
|
code: "INVALID_RESPONSE",
|
|
@@ -5668,6 +5726,12 @@ async function send(config, options) {
|
|
|
5668
5726
|
credentials: "omit"
|
|
5669
5727
|
};
|
|
5670
5728
|
if (options.body !== void 0) init.body = options.body;
|
|
5729
|
+
const { signal } = options;
|
|
5730
|
+
if (signal?.aborted) return { error: uploadAborted(), status: null };
|
|
5731
|
+
const controller = signal ? new AbortController() : null;
|
|
5732
|
+
const stop = () => controller?.abort();
|
|
5733
|
+
if (controller) init.signal = controller.signal;
|
|
5734
|
+
signal?.addEventListener("abort", stop);
|
|
5671
5735
|
let response;
|
|
5672
5736
|
try {
|
|
5673
5737
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -5676,26 +5740,34 @@ async function send(config, options) {
|
|
|
5676
5740
|
init
|
|
5677
5741
|
);
|
|
5678
5742
|
} catch (error2) {
|
|
5679
|
-
return {
|
|
5743
|
+
return {
|
|
5744
|
+
error: signal?.aborted ? uploadAborted() : networkError(error2),
|
|
5745
|
+
status: null
|
|
5746
|
+
};
|
|
5747
|
+
} finally {
|
|
5748
|
+
signal?.removeEventListener("abort", stop);
|
|
5680
5749
|
}
|
|
5681
|
-
|
|
5682
|
-
|
|
5750
|
+
const { status } = response;
|
|
5751
|
+
if (status >= 200 && status < 300) return { error: null, response };
|
|
5683
5752
|
let text;
|
|
5684
5753
|
try {
|
|
5685
5754
|
text = await response.text();
|
|
5686
5755
|
} catch (error2) {
|
|
5687
|
-
return { error: networkError(error2) };
|
|
5756
|
+
return { error: networkError(error2), status: null };
|
|
5688
5757
|
}
|
|
5689
5758
|
let body;
|
|
5690
5759
|
if (text.trim() !== "") {
|
|
5691
5760
|
try {
|
|
5692
5761
|
body = JSON.parse(text);
|
|
5693
5762
|
} catch {
|
|
5694
|
-
return { error: invalidResponse(
|
|
5763
|
+
return { error: invalidResponse(status), status };
|
|
5695
5764
|
}
|
|
5696
5765
|
}
|
|
5697
|
-
const error = errorFromBody(
|
|
5698
|
-
return {
|
|
5766
|
+
const error = errorFromBody(status, body) ?? invalidResponse(status);
|
|
5767
|
+
return {
|
|
5768
|
+
error: await settled(config, error, response.headers, token),
|
|
5769
|
+
status
|
|
5770
|
+
};
|
|
5699
5771
|
}
|
|
5700
5772
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
5701
5773
|
"RATE_LIMITED",
|
|
@@ -7266,10 +7338,176 @@ var TableBuilder = class {
|
|
|
7266
7338
|
});
|
|
7267
7339
|
}
|
|
7268
7340
|
};
|
|
7341
|
+
function fail(error) {
|
|
7342
|
+
return { data: null, error };
|
|
7343
|
+
}
|
|
7344
|
+
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
7345
|
+
function dataOf2(status, body, guard) {
|
|
7346
|
+
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
7347
|
+
return { data: body.data, error: null };
|
|
7348
|
+
return fail(invalidResponse(status));
|
|
7349
|
+
}
|
|
7350
|
+
async function readJson(response, guard) {
|
|
7351
|
+
let text;
|
|
7352
|
+
try {
|
|
7353
|
+
text = await response.text();
|
|
7354
|
+
} catch (error) {
|
|
7355
|
+
return fail(networkError(error));
|
|
7356
|
+
}
|
|
7357
|
+
let body;
|
|
7358
|
+
try {
|
|
7359
|
+
body = JSON.parse(text);
|
|
7360
|
+
} catch {
|
|
7361
|
+
return fail(invalidResponse(response.status));
|
|
7362
|
+
}
|
|
7363
|
+
return dataOf2(response.status, body, guard);
|
|
7364
|
+
}
|
|
7365
|
+
var ROUTE = "/storage/upload/multipart";
|
|
7366
|
+
var CONCURRENCY = 4;
|
|
7367
|
+
var ATTEMPTS = 4;
|
|
7368
|
+
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
7369
|
+
var UPLOAD_ID = /^[\w-][\w.-]*$/;
|
|
7370
|
+
var positive = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
7371
|
+
var planFor = (size) => (value) => isRecord(value) && typeof value.uploadId === "string" && UPLOAD_ID.test(value.uploadId) && positive(value.partSize) && positive(value.partCount) && (value.partCount - 1) * value.partSize < size && value.partCount * value.partSize >= size;
|
|
7372
|
+
var partAnswer = (n) => (value) => isRecord(value) && value.partNumber === n && typeof value.etag === "string" && value.etag !== "";
|
|
7373
|
+
var retryable = (error, status) => error.code === "NETWORK_ERROR" || error.code === "OBJECT_RATE_LIMITED" || status !== null && status >= 500 && error.code !== "STORAGE_QUOTA_EXCEEDED";
|
|
7374
|
+
function waitAfter(error, attempt22) {
|
|
7375
|
+
const after = isRecord(error.details) ? error.details.retryAfter : void 0;
|
|
7376
|
+
if (typeof after === "number") return after * 1e3;
|
|
7377
|
+
if (error.code === "OBJECT_RATE_LIMITED") return 1e3;
|
|
7378
|
+
return BACKOFF_MS[attempt22 - 1] ?? 4e3;
|
|
7379
|
+
}
|
|
7380
|
+
var sleepFor = (ms, signal) => new Promise((done) => {
|
|
7381
|
+
if (signal?.aborted) {
|
|
7382
|
+
done();
|
|
7383
|
+
return;
|
|
7384
|
+
}
|
|
7385
|
+
const end = () => {
|
|
7386
|
+
clearTimeout(timer);
|
|
7387
|
+
signal?.removeEventListener("abort", end);
|
|
7388
|
+
done();
|
|
7389
|
+
};
|
|
7390
|
+
const timer = setTimeout(end, ms);
|
|
7391
|
+
signal?.addEventListener("abort", end);
|
|
7392
|
+
});
|
|
7393
|
+
async function uploadInParts(a) {
|
|
7394
|
+
const { signal } = a;
|
|
7395
|
+
const sleep2 = a.sleep ?? sleepFor;
|
|
7396
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
7397
|
+
const started = await call(a.http, {
|
|
7398
|
+
method: a.method,
|
|
7399
|
+
path: ROUTE,
|
|
7400
|
+
body: {
|
|
7401
|
+
bucket: a.bucket,
|
|
7402
|
+
path: a.path,
|
|
7403
|
+
size: a.size,
|
|
7404
|
+
contentType: a.contentType,
|
|
7405
|
+
cacheControl: a.cacheControl,
|
|
7406
|
+
// PUT always replaces: the worker refuses `upsert` on it.
|
|
7407
|
+
...a.method === "POST" && a.upsert ? { upsert: true } : {}
|
|
7408
|
+
},
|
|
7409
|
+
withToken: true
|
|
7410
|
+
});
|
|
7411
|
+
if (started.error !== null) return fail(started.error);
|
|
7412
|
+
const plan = dataOf2(
|
|
7413
|
+
started.answer.status,
|
|
7414
|
+
started.answer.body,
|
|
7415
|
+
planFor(a.size)
|
|
7416
|
+
);
|
|
7417
|
+
if (plan.error !== null) return fail(plan.error);
|
|
7418
|
+
const { uploadId, partSize, partCount } = plan.data;
|
|
7419
|
+
const base = `${ROUTE}/${uploadId}`;
|
|
7420
|
+
const progress = (sentBytes2) => {
|
|
7421
|
+
if (signal?.aborted) return;
|
|
7422
|
+
try {
|
|
7423
|
+
const result = a.onProgress?.(sentBytes2, a.size);
|
|
7424
|
+
if (isThenable(result)) result.then(void 0, () => void 0);
|
|
7425
|
+
} catch {
|
|
7426
|
+
}
|
|
7427
|
+
};
|
|
7428
|
+
const callOff = async () => {
|
|
7429
|
+
await call(a.http, { method: "DELETE", path: base, withToken: true }).catch(
|
|
7430
|
+
() => void 0
|
|
7431
|
+
);
|
|
7432
|
+
};
|
|
7433
|
+
const etags = [];
|
|
7434
|
+
const failure = {
|
|
7435
|
+
error: signal?.aborted ? uploadAborted() : null
|
|
7436
|
+
};
|
|
7437
|
+
let next = 1;
|
|
7438
|
+
let sentBytes = 0;
|
|
7439
|
+
const onAbort = () => {
|
|
7440
|
+
failure.error ??= uploadAborted();
|
|
7441
|
+
};
|
|
7442
|
+
const tryPart = async (n, bytes) => {
|
|
7443
|
+
const sent = await send(a.http, {
|
|
7444
|
+
method: "PUT",
|
|
7445
|
+
path: `${base}/${String(n)}`,
|
|
7446
|
+
headers: { "content-type": "application/octet-stream" },
|
|
7447
|
+
body: bytes,
|
|
7448
|
+
withToken: true,
|
|
7449
|
+
signal
|
|
7450
|
+
});
|
|
7451
|
+
if (sent.error !== null) return sent;
|
|
7452
|
+
const read = await readJson(sent.response, partAnswer(n));
|
|
7453
|
+
if (read.error !== null)
|
|
7454
|
+
return { error: read.error, status: sent.response.status };
|
|
7455
|
+
return { etag: read.data.etag };
|
|
7456
|
+
};
|
|
7457
|
+
const sendPart = async (n) => {
|
|
7458
|
+
const from = (n - 1) * partSize;
|
|
7459
|
+
const to = Math.min(from + partSize, a.size);
|
|
7460
|
+
const bytes = a.body instanceof Uint8Array ? a.body.subarray(from, to) : a.body.slice(from, to);
|
|
7461
|
+
for (let attempt22 = 1; ; attempt22 += 1) {
|
|
7462
|
+
const tried = await tryPart(n, bytes);
|
|
7463
|
+
if ("etag" in tried) {
|
|
7464
|
+
etags[n - 1] = tried.etag;
|
|
7465
|
+
sentBytes += to - from;
|
|
7466
|
+
progress(sentBytes);
|
|
7467
|
+
return null;
|
|
7468
|
+
}
|
|
7469
|
+
if (attempt22 === ATTEMPTS || failure.error !== null || !retryable(tried.error, tried.status))
|
|
7470
|
+
return tried.error;
|
|
7471
|
+
await sleep2(waitAfter(tried.error, attempt22), signal);
|
|
7472
|
+
if (failure.error !== null) return tried.error;
|
|
7473
|
+
}
|
|
7474
|
+
};
|
|
7475
|
+
const worker = async () => {
|
|
7476
|
+
while (failure.error === null && next <= partCount) {
|
|
7477
|
+
const n = next;
|
|
7478
|
+
next += 1;
|
|
7479
|
+
const error = await sendPart(n);
|
|
7480
|
+
if (error !== null) failure.error ??= error;
|
|
7481
|
+
}
|
|
7482
|
+
};
|
|
7483
|
+
signal?.addEventListener("abort", onAbort);
|
|
7484
|
+
await Promise.all(
|
|
7485
|
+
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
7486
|
+
);
|
|
7487
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7488
|
+
if (failure.error !== null) {
|
|
7489
|
+
await callOff();
|
|
7490
|
+
return fail(failure.error);
|
|
7491
|
+
}
|
|
7492
|
+
const completed = await call(a.http, {
|
|
7493
|
+
method: "POST",
|
|
7494
|
+
path: `${base}/complete`,
|
|
7495
|
+
body: {
|
|
7496
|
+
parts: etags.map((etag, i) => ({ partNumber: i + 1, etag }))
|
|
7497
|
+
},
|
|
7498
|
+
withToken: true
|
|
7499
|
+
});
|
|
7500
|
+
if (completed.error !== null) {
|
|
7501
|
+
if (completed.answer !== null && completed.error.code !== "INVALID_RESPONSE")
|
|
7502
|
+
await callOff();
|
|
7503
|
+
return fail(completed.error);
|
|
7504
|
+
}
|
|
7505
|
+
return dataOf2(completed.answer.status, completed.answer.body, isUploadResult);
|
|
7506
|
+
}
|
|
7507
|
+
var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
7269
7508
|
var MAX_SINGLE_UPLOAD_BYTES2 = 52428800;
|
|
7270
7509
|
var STORAGE_MESSAGES = {
|
|
7271
7510
|
bucket: "The bucket name is not valid.",
|
|
7272
|
-
tooLarge: "Uploads over 50 MiB are not supported yet.",
|
|
7273
7511
|
request: "The storage request is not valid.",
|
|
7274
7512
|
body: "upload takes a Blob, File, ArrayBuffer, typed array or string."
|
|
7275
7513
|
};
|
|
@@ -7294,9 +7532,6 @@ function invalidPath(reason) {
|
|
|
7294
7532
|
message: `The object path is not valid: it ${reason}.`
|
|
7295
7533
|
};
|
|
7296
7534
|
}
|
|
7297
|
-
function objectTooLarge() {
|
|
7298
|
-
return { code: "OBJECT_TOO_LARGE", message: STORAGE_MESSAGES.tooLarge };
|
|
7299
|
-
}
|
|
7300
7535
|
function encodePath(path) {
|
|
7301
7536
|
if (typeof path !== "string" || path === "")
|
|
7302
7537
|
return { ok: false, error: invalidPath("is empty") };
|
|
@@ -7333,6 +7568,16 @@ function encodeBucket(bucket) {
|
|
|
7333
7568
|
};
|
|
7334
7569
|
return { ok: true, encoded: encodeURIComponent(bucket) };
|
|
7335
7570
|
}
|
|
7571
|
+
function objectRoute(bucket, path) {
|
|
7572
|
+
const encodedBucket = encodeBucket(bucket);
|
|
7573
|
+
if (!encodedBucket.ok) return encodedBucket;
|
|
7574
|
+
const encodedPath = encodePath(path);
|
|
7575
|
+
if (!encodedPath.ok) return encodedPath;
|
|
7576
|
+
return {
|
|
7577
|
+
ok: true,
|
|
7578
|
+
encoded: `${encodedBucket.encoded}/${encodedPath.encoded}`
|
|
7579
|
+
};
|
|
7580
|
+
}
|
|
7336
7581
|
var isArrayBuffer = (value) => Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
7337
7582
|
function isDetached(buffer) {
|
|
7338
7583
|
if (!isArrayBuffer(buffer)) return false;
|
|
@@ -7413,36 +7658,12 @@ function payloadOf(body, contentType) {
|
|
|
7413
7658
|
};
|
|
7414
7659
|
}
|
|
7415
7660
|
var SIGNED_PREFIX = "/_naro/v1/storage/object/sign/";
|
|
7416
|
-
function fail(error) {
|
|
7417
|
-
return { data: null, error };
|
|
7418
|
-
}
|
|
7419
|
-
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
7420
7661
|
var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
|
|
7421
7662
|
var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
|
|
7422
7663
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
7423
7664
|
function listOf(guard) {
|
|
7424
7665
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
7425
7666
|
}
|
|
7426
|
-
function dataOf2(status, body, guard) {
|
|
7427
|
-
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
7428
|
-
return { data: body.data, error: null };
|
|
7429
|
-
return fail(invalidResponse(status));
|
|
7430
|
-
}
|
|
7431
|
-
async function readJson(response, guard) {
|
|
7432
|
-
let text;
|
|
7433
|
-
try {
|
|
7434
|
-
text = await response.text();
|
|
7435
|
-
} catch (error) {
|
|
7436
|
-
return fail(networkError(error));
|
|
7437
|
-
}
|
|
7438
|
-
let body;
|
|
7439
|
-
try {
|
|
7440
|
-
body = JSON.parse(text);
|
|
7441
|
-
} catch {
|
|
7442
|
-
return fail(invalidResponse(response.status));
|
|
7443
|
-
}
|
|
7444
|
-
return dataOf2(response.status, body, guard);
|
|
7445
|
-
}
|
|
7446
7667
|
var StorageFileApi = class {
|
|
7447
7668
|
#http;
|
|
7448
7669
|
#auth;
|
|
@@ -7454,14 +7675,9 @@ var StorageFileApi = class {
|
|
|
7454
7675
|
}
|
|
7455
7676
|
/** `/storage/object/<route><bucket>/<path>`, or the refusal no request needs. */
|
|
7456
7677
|
#objectPath(route2, path) {
|
|
7457
|
-
const
|
|
7458
|
-
if (!
|
|
7459
|
-
|
|
7460
|
-
if (!name.ok) return name;
|
|
7461
|
-
return {
|
|
7462
|
-
ok: true,
|
|
7463
|
-
path: `/storage/object/${route2}${bucket.encoded}/${name.encoded}`
|
|
7464
|
-
};
|
|
7678
|
+
const target = objectRoute(this.#bucket, path);
|
|
7679
|
+
if (!target.ok) return target;
|
|
7680
|
+
return { ok: true, path: `/storage/object/${route2}${target.encoded}` };
|
|
7465
7681
|
}
|
|
7466
7682
|
/** `/storage/object/<route><bucket>`, for the routes that take names in the body. */
|
|
7467
7683
|
#bucketPath(route2) {
|
|
@@ -7483,8 +7699,24 @@ var StorageFileApi = class {
|
|
|
7483
7699
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
7484
7700
|
const unreadable = headerProblem(options.contentType, cache);
|
|
7485
7701
|
if (unreadable !== null) return fail(unreadable);
|
|
7486
|
-
|
|
7702
|
+
const { signal } = options;
|
|
7703
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
7487
7704
|
await this.#auth.ready();
|
|
7705
|
+
if (payload.size > MAX_SINGLE_UPLOAD_BYTES2)
|
|
7706
|
+
return uploadInParts({
|
|
7707
|
+
http: this.#http,
|
|
7708
|
+
method,
|
|
7709
|
+
bucket: this.#bucket,
|
|
7710
|
+
path,
|
|
7711
|
+
// No copy: a Blob stays lazy, a Uint8Array is viewed, an ArrayBuffer wrapped.
|
|
7712
|
+
body: isArrayBuffer(payload.body) ? new Uint8Array(payload.body) : payload.body,
|
|
7713
|
+
size: payload.size,
|
|
7714
|
+
contentType: payload.type,
|
|
7715
|
+
cacheControl: cache,
|
|
7716
|
+
upsert,
|
|
7717
|
+
...options.onProgress ? { onProgress: options.onProgress } : {},
|
|
7718
|
+
signal
|
|
7719
|
+
});
|
|
7488
7720
|
const headers = {
|
|
7489
7721
|
"content-type": payload.type,
|
|
7490
7722
|
"cache-control": cache
|
|
@@ -7495,7 +7727,9 @@ var StorageFileApi = class {
|
|
|
7495
7727
|
path: target.path,
|
|
7496
7728
|
headers,
|
|
7497
7729
|
body: payload.body,
|
|
7498
|
-
withToken: true
|
|
7730
|
+
withToken: true,
|
|
7731
|
+
// The fetch stops with the body: the worker undoes its claim at once.
|
|
7732
|
+
signal
|
|
7499
7733
|
});
|
|
7500
7734
|
if (sent.error !== null) return fail(sent.error);
|
|
7501
7735
|
return readJson(sent.response, isUploadResult);
|
|
@@ -7741,6 +7975,7 @@ var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<pa
|
|
|
7741
7975
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
7742
7976
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]";
|
|
7743
7977
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
7978
|
+
var FOUR_GIB = 4294967296;
|
|
7744
7979
|
var PAGE = 1e3;
|
|
7745
7980
|
var MAX_OFFSET = 1e5;
|
|
7746
7981
|
var TRUNCATED_WARNING = `\u26A0 A folder's listing stopped at ${(MAX_OFFSET + PAGE).toLocaleString("en-US")} entries, as far as the API's offset reaches; there may be more.`;
|
|
@@ -7843,7 +8078,8 @@ async function objects(context) {
|
|
|
7843
8078
|
{ cause: error }
|
|
7844
8079
|
);
|
|
7845
8080
|
}
|
|
7846
|
-
|
|
8081
|
+
const url = new URL(backend.url.trim().replace(/\/+$/, "")).origin;
|
|
8082
|
+
return { projectId, storage: naro.storage, url, serviceKey };
|
|
7847
8083
|
}
|
|
7848
8084
|
function isFolder(entry) {
|
|
7849
8085
|
return entry.id === null;
|
|
@@ -7936,33 +8172,56 @@ async function storageLs(context) {
|
|
|
7936
8172
|
);
|
|
7937
8173
|
}
|
|
7938
8174
|
var INTERRUPTS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
7939
|
-
function
|
|
7940
|
-
|
|
8175
|
+
function endWith(signal) {
|
|
8176
|
+
try {
|
|
8177
|
+
process.kill(process.pid, signal);
|
|
8178
|
+
} catch {
|
|
8179
|
+
process.exit(1);
|
|
8180
|
+
}
|
|
8181
|
+
}
|
|
8182
|
+
function onInterrupt(onSignal) {
|
|
8183
|
+
for (const signal of INTERRUPTS) process.on(signal, onSignal);
|
|
8184
|
+
return () => {
|
|
7941
8185
|
for (const signal of INTERRUPTS) process.off(signal, onSignal);
|
|
7942
8186
|
};
|
|
7943
|
-
|
|
8187
|
+
}
|
|
8188
|
+
function removeOnInterrupt(partial) {
|
|
8189
|
+
const stop = onInterrupt((signal) => {
|
|
7944
8190
|
stop();
|
|
7945
8191
|
try {
|
|
7946
8192
|
rmSync2(partial, { force: true });
|
|
7947
8193
|
} catch {
|
|
7948
8194
|
}
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
} catch {
|
|
7952
|
-
process.exit(1);
|
|
7953
|
-
}
|
|
7954
|
-
}
|
|
7955
|
-
for (const signal of INTERRUPTS) process.on(signal, onSignal);
|
|
8195
|
+
endWith(signal);
|
|
8196
|
+
});
|
|
7956
8197
|
return stop;
|
|
7957
8198
|
}
|
|
7958
|
-
|
|
8199
|
+
function stopOnInterrupt(onFirst) {
|
|
8200
|
+
const controller = new AbortController();
|
|
8201
|
+
let caught = null;
|
|
8202
|
+
const stop = onInterrupt((signal) => {
|
|
8203
|
+
if (caught !== null) {
|
|
8204
|
+
stop();
|
|
8205
|
+
endWith(signal);
|
|
8206
|
+
return;
|
|
8207
|
+
}
|
|
8208
|
+
caught = signal;
|
|
8209
|
+
onFirst();
|
|
8210
|
+
controller.abort();
|
|
8211
|
+
});
|
|
8212
|
+
return {
|
|
8213
|
+
signal: controller.signal,
|
|
8214
|
+
settle: () => {
|
|
8215
|
+
stop();
|
|
8216
|
+
return caught;
|
|
8217
|
+
}
|
|
8218
|
+
};
|
|
8219
|
+
}
|
|
8220
|
+
async function writeAtomically(bytes, destination) {
|
|
7959
8221
|
const partial = join6(dirname3(destination), `.${randomUUID()}.naro-partial`);
|
|
7960
8222
|
const stopListening = removeOnInterrupt(partial);
|
|
7961
8223
|
try {
|
|
7962
|
-
await pipeline2(
|
|
7963
|
-
Readable.fromWeb(blob.stream()),
|
|
7964
|
-
createWriteStream2(partial, { flags: "wx" })
|
|
7965
|
-
);
|
|
8224
|
+
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
7966
8225
|
await rename(partial, destination);
|
|
7967
8226
|
} catch (error) {
|
|
7968
8227
|
await rm(partial, { force: true }).catch(() => void 0);
|
|
@@ -7971,6 +8230,34 @@ async function writeAtomically(blob, destination) {
|
|
|
7971
8230
|
stopListening();
|
|
7972
8231
|
}
|
|
7973
8232
|
}
|
|
8233
|
+
function progressReporter(output, total) {
|
|
8234
|
+
const tty = process.stderr.isTTY === true;
|
|
8235
|
+
let lastTenth = -1;
|
|
8236
|
+
let lastLength = 0;
|
|
8237
|
+
let open = false;
|
|
8238
|
+
return {
|
|
8239
|
+
report(sent) {
|
|
8240
|
+
const percent2 = Math.floor(sent / total * 100);
|
|
8241
|
+
const line = `\u2191 ${binarySize(sent)} / ${binarySize(total)} (${String(percent2)}%)`;
|
|
8242
|
+
if (tty) {
|
|
8243
|
+
open = sent !== total;
|
|
8244
|
+
output.progress(`\r${line.padEnd(lastLength)}${open ? "" : "\n"}`);
|
|
8245
|
+
lastLength = line.length;
|
|
8246
|
+
return;
|
|
8247
|
+
}
|
|
8248
|
+
const tenth = Math.floor(percent2 / 10);
|
|
8249
|
+
if (tenth === lastTenth && sent !== total) return;
|
|
8250
|
+
lastTenth = tenth;
|
|
8251
|
+
output.progress(`${line}
|
|
8252
|
+
`);
|
|
8253
|
+
},
|
|
8254
|
+
close() {
|
|
8255
|
+
if (!open) return;
|
|
8256
|
+
open = false;
|
|
8257
|
+
output.progress("\n");
|
|
8258
|
+
}
|
|
8259
|
+
};
|
|
8260
|
+
}
|
|
7974
8261
|
async function upload(context, local, target) {
|
|
7975
8262
|
const file = resolve5(context.cwd, local);
|
|
7976
8263
|
const info = await stat(file).catch(() => null);
|
|
@@ -7989,32 +8276,49 @@ async function upload(context, local, target) {
|
|
|
7989
8276
|
"VALIDATION_FAILED",
|
|
7990
8277
|
`${printable(local)} is not a regular file.`
|
|
7991
8278
|
);
|
|
7992
|
-
if (info.size > MAX_SINGLE_UPLOAD_BYTES)
|
|
7993
|
-
throw new NaroError(
|
|
7994
|
-
"OBJECT_TOO_LARGE",
|
|
7995
|
-
dataError("OBJECT_TOO_LARGE", { single: true }).message
|
|
7996
|
-
);
|
|
7997
8279
|
const { cacheControl } = context.flags;
|
|
7998
8280
|
if (cacheControl !== void 0 && !isMaxAge(cacheControl))
|
|
7999
8281
|
throw new NaroError("VALIDATION_FAILED", CACHE_CONTROL_RULE);
|
|
8000
8282
|
const path = target.path === "" || target.path.endsWith("/") ? `${target.path}${basename2(file)}` : target.path;
|
|
8001
|
-
const { projectId, storage } = await objects(context);
|
|
8002
8283
|
const body = await openAsBlob(file);
|
|
8284
|
+
if (body.size !== info.size)
|
|
8285
|
+
throw new NaroError(
|
|
8286
|
+
"VALIDATION_FAILED",
|
|
8287
|
+
info.size >= FOUR_GIB ? `${printable(local)} is ${binarySize(info.size)}, but Node reads it as ${binarySize(body.size)}: naro storage cp cannot upload a file of 4 GiB or more yet.` : `${printable(local)} changed while it was being opened (${binarySize(info.size)}, then ${binarySize(body.size)}). Run the copy again once nothing is writing to it.`
|
|
8288
|
+
);
|
|
8289
|
+
const { projectId, storage } = await objects(context);
|
|
8003
8290
|
const contentType = context.flags.contentType ?? typeFor(file);
|
|
8004
|
-
const
|
|
8005
|
-
|
|
8291
|
+
const progress = body.size > MAX_SINGLE_UPLOAD_BYTES && !context.output.json ? progressReporter(context.output, body.size) : void 0;
|
|
8292
|
+
const interrupt = stopOnInterrupt(() => {
|
|
8293
|
+
progress?.close();
|
|
8294
|
+
context.output.progress("Stopping the upload\u2026\n");
|
|
8295
|
+
});
|
|
8296
|
+
let answer;
|
|
8297
|
+
let caught;
|
|
8298
|
+
try {
|
|
8299
|
+
answer = await storage.from(target.bucket).upload(path, body, {
|
|
8006
8300
|
...contentType === void 0 ? {} : { contentType },
|
|
8007
8301
|
...cacheControl === void 0 ? {} : { cacheControl },
|
|
8008
|
-
upsert: context.flags.upsert === true
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8302
|
+
upsert: context.flags.upsert === true,
|
|
8303
|
+
...progress === void 0 ? {} : { onProgress: progress.report },
|
|
8304
|
+
signal: interrupt.signal
|
|
8305
|
+
});
|
|
8306
|
+
} finally {
|
|
8307
|
+
progress?.close();
|
|
8308
|
+
caught = interrupt.settle();
|
|
8309
|
+
}
|
|
8310
|
+
try {
|
|
8311
|
+
const result = unwrap(answer);
|
|
8312
|
+
const to = `${NARO_SCHEME}${target.bucket}/${path}`;
|
|
8313
|
+
context.output.result(
|
|
8314
|
+
{ projectId, from: local, to, bytes: body.size, id: result.id },
|
|
8315
|
+
[
|
|
8316
|
+
`\u2705 Uploaded ${printable(local)} \u2192 ${printable(to)} (${binarySize(body.size)})`
|
|
8317
|
+
]
|
|
8318
|
+
);
|
|
8319
|
+
} finally {
|
|
8320
|
+
if (caught !== null) endWith(caught);
|
|
8321
|
+
}
|
|
8018
8322
|
}
|
|
8019
8323
|
var WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
8020
8324
|
var MAX_FILE_NAME_BYTES = 255;
|
|
@@ -8027,6 +8331,62 @@ function savedAs(path) {
|
|
|
8027
8331
|
);
|
|
8028
8332
|
return name;
|
|
8029
8333
|
}
|
|
8334
|
+
var NETWORK_MESSAGE = "The request did not reach the Naro backend. Check the url and the network.";
|
|
8335
|
+
async function failureOf(response) {
|
|
8336
|
+
const { status } = response;
|
|
8337
|
+
const invalid = {
|
|
8338
|
+
code: "INVALID_RESPONSE",
|
|
8339
|
+
message: `The server did not answer like a Naro backend (HTTP ${String(status)}).`,
|
|
8340
|
+
details: { status }
|
|
8341
|
+
};
|
|
8342
|
+
let text;
|
|
8343
|
+
try {
|
|
8344
|
+
text = await response.text();
|
|
8345
|
+
} catch {
|
|
8346
|
+
return { code: "NETWORK_ERROR", message: NETWORK_MESSAGE };
|
|
8347
|
+
}
|
|
8348
|
+
let body;
|
|
8349
|
+
if (text.trim() !== "") {
|
|
8350
|
+
try {
|
|
8351
|
+
body = JSON.parse(text);
|
|
8352
|
+
} catch {
|
|
8353
|
+
return invalid;
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
const error = typeof body === "object" && body !== null && "error" in body ? body.error : void 0;
|
|
8357
|
+
if (typeof error !== "object" || error === null) return invalid;
|
|
8358
|
+
const { code, message, details } = error;
|
|
8359
|
+
if (typeof code !== "string" || code === "" || typeof message !== "string")
|
|
8360
|
+
return invalid;
|
|
8361
|
+
return details === void 0 ? { code, message } : { code, message, details };
|
|
8362
|
+
}
|
|
8363
|
+
async function getObject(url, serviceKey) {
|
|
8364
|
+
let response;
|
|
8365
|
+
try {
|
|
8366
|
+
response = await fetch(url, {
|
|
8367
|
+
headers: { apikey: serviceKey },
|
|
8368
|
+
redirect: "error"
|
|
8369
|
+
});
|
|
8370
|
+
} catch {
|
|
8371
|
+
throw new NaroError("NETWORK_ERROR", NETWORK_MESSAGE);
|
|
8372
|
+
}
|
|
8373
|
+
if (response.ok) return response;
|
|
8374
|
+
return unwrap({ data: null, error: await failureOf(response) });
|
|
8375
|
+
}
|
|
8376
|
+
async function* chunksOf2(response, received) {
|
|
8377
|
+
if (response.body === null) return;
|
|
8378
|
+
try {
|
|
8379
|
+
for await (const chunk2 of Readable.fromWeb(
|
|
8380
|
+
response.body
|
|
8381
|
+
)) {
|
|
8382
|
+
const bytes = chunk2;
|
|
8383
|
+
received.bytes += bytes.byteLength;
|
|
8384
|
+
yield bytes;
|
|
8385
|
+
}
|
|
8386
|
+
} catch {
|
|
8387
|
+
throw new NaroError("NETWORK_ERROR", NETWORK_MESSAGE);
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8030
8390
|
async function download(context, source, local) {
|
|
8031
8391
|
if (source.path === "" || source.path.endsWith("/"))
|
|
8032
8392
|
throw new NaroError(
|
|
@@ -8042,14 +8402,20 @@ async function download(context, source, local) {
|
|
|
8042
8402
|
"VALIDATION_FAILED",
|
|
8043
8403
|
`Cannot save to ${printable(local)}: its folder does not exist.`
|
|
8044
8404
|
);
|
|
8045
|
-
const
|
|
8046
|
-
|
|
8047
|
-
await
|
|
8405
|
+
const route2 = objectRoute(source.bucket, source.path);
|
|
8406
|
+
if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
|
|
8407
|
+
const { projectId, url, serviceKey } = await objects(context);
|
|
8408
|
+
const response = await getObject(
|
|
8409
|
+
`${url}/_naro/v1/storage/object/${route2.encoded}`,
|
|
8410
|
+
serviceKey
|
|
8411
|
+
);
|
|
8412
|
+
const received = { bytes: 0 };
|
|
8413
|
+
await writeAtomically(chunksOf2(response, received), destination);
|
|
8048
8414
|
const from = `${NARO_SCHEME}${source.bucket}/${source.path}`;
|
|
8049
8415
|
context.output.result(
|
|
8050
|
-
{ projectId, from, to: destination, bytes:
|
|
8416
|
+
{ projectId, from, to: destination, bytes: received.bytes },
|
|
8051
8417
|
[
|
|
8052
|
-
`\u2705 Downloaded ${printable(from)} \u2192 ${printable(destination)} (${binarySize(
|
|
8418
|
+
`\u2705 Downloaded ${printable(from)} \u2192 ${printable(destination)} (${binarySize(received.bytes)})`
|
|
8053
8419
|
]
|
|
8054
8420
|
);
|
|
8055
8421
|
}
|
|
@@ -8328,7 +8694,7 @@ Usage:
|
|
|
8328
8694
|
naro_insert, naro_update, naro_delete (shorthand for CREATE POLICY)
|
|
8329
8695
|
naro policy remove <table> Drop every policy on it: back to deny (asks first)
|
|
8330
8696
|
|
|
8331
|
-
naro storage [status] Naro Storage: on or off, limits, what is stored, the buckets
|
|
8697
|
+
naro storage [status] Naro Storage: on or off, limits, what is stored, today's operations, the buckets
|
|
8332
8698
|
naro storage enable Turn Storage on: the project's R2 bucket, bound to its backend
|
|
8333
8699
|
(safe to re-run; applies a plan change's limits)
|
|
8334
8700
|
naro storage bucket list The buckets: public or private, objects, bytes, limits
|
|
@@ -8342,8 +8708,9 @@ Usage:
|
|
|
8342
8708
|
naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]
|
|
8343
8709
|
The buckets, or the folders and objects under a prefix
|
|
8344
8710
|
naro storage cp <from> <to> [--upsert] [--content-type <t>] [--cache-control <seconds>]
|
|
8345
|
-
One file between this machine and naro://<bucket>/<path>, 50 MiB
|
|
8711
|
+
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
8346
8712
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
8713
|
+
A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
|
|
8347
8714
|
A download replaces a local file of the same name
|
|
8348
8715
|
naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
|
|
8349
8716
|
Remove objects; --recursive removes what is under a folder (asks first):
|