naro-cli 0.15.0 → 0.17.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 +70 -7
- package/dist/{chunk-6WJ3QQ5Y.js → chunk-PYRN5XR6.js} +288 -53
- package/dist/deploy.d.ts +4 -1
- package/dist/deploy.js +1 -1
- package/dist/index.js +592 -97
- package/dist/output.d.ts +5 -0
- package/dist/upload.d.ts +2 -0
- package/package.json +5 -5
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-PYRN5XR6.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}
|
|
@@ -4554,21 +4560,69 @@ function domainLines(domain) {
|
|
|
4554
4560
|
}
|
|
4555
4561
|
return lines;
|
|
4556
4562
|
}
|
|
4563
|
+
function claimLines(claim) {
|
|
4564
|
+
return [
|
|
4565
|
+
` \u23F3 ${claim.hostname} \u2014 another project holds it. Publish this TXT record, then run \`naro domains verify ${claim.hostname}\`:`,
|
|
4566
|
+
` TXT ${claim.record.name}`,
|
|
4567
|
+
` host: ${claim.record.host}`,
|
|
4568
|
+
` value: ${claim.record.value}`
|
|
4569
|
+
];
|
|
4570
|
+
}
|
|
4571
|
+
async function openClaims(client, projectId) {
|
|
4572
|
+
try {
|
|
4573
|
+
return await client.domains.listClaims(projectId);
|
|
4574
|
+
} catch (error) {
|
|
4575
|
+
if (isNaroError(error) && error.code === "NOT_FOUND") return [];
|
|
4576
|
+
throw error;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4557
4579
|
async function runDomainsList(context) {
|
|
4558
4580
|
const client = await context.client();
|
|
4559
4581
|
const projectId = await context.resolveProjectId();
|
|
4560
4582
|
const domains = await client.domains.list(projectId);
|
|
4583
|
+
const claims = await openClaims(client, projectId);
|
|
4561
4584
|
const pending = domains.filter((domain) => !isReady(domain));
|
|
4562
4585
|
context.output.result(
|
|
4563
|
-
{ projectId, domains },
|
|
4564
|
-
domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
4565
|
-
|
|
4566
|
-
|
|
4586
|
+
{ projectId, domains, claims },
|
|
4587
|
+
domains.length === 0 && claims.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
4588
|
+
...domains.length > 0 ? [
|
|
4589
|
+
`Domains (${String(domains.length)}):`,
|
|
4590
|
+
...domains.flatMap(domainLines)
|
|
4591
|
+
] : [],
|
|
4592
|
+
...claims.length > 0 ? [
|
|
4593
|
+
...domains.length > 0 ? [""] : [],
|
|
4594
|
+
`Ownership checks (${String(claims.length)}):`,
|
|
4595
|
+
...claims.flatMap(claimLines)
|
|
4596
|
+
] : [],
|
|
4567
4597
|
// list reads our stored copy; only verify goes and looks again.
|
|
4568
4598
|
...pending.length > 0 ? ["", "Re-check DNS now: naro domains verify"] : []
|
|
4569
4599
|
]
|
|
4570
4600
|
);
|
|
4571
4601
|
}
|
|
4602
|
+
async function verifyClaimFor(context, client, projectId, hostname) {
|
|
4603
|
+
const wanted = hostname.toLowerCase().replace(/\.+$/, "");
|
|
4604
|
+
const claim = (await openClaims(client, projectId)).find(
|
|
4605
|
+
(candidate) => candidate.hostname === wanted
|
|
4606
|
+
);
|
|
4607
|
+
if (!claim) return false;
|
|
4608
|
+
const result = await client.domains.verifyClaim(projectId, claim.id);
|
|
4609
|
+
if (!result.verified) {
|
|
4610
|
+
const cause = result.claim.cause;
|
|
4611
|
+
throw new NaroError(
|
|
4612
|
+
cause?.code ?? "DOMAIN_VERIFICATION_REQUIRED",
|
|
4613
|
+
cause ? `${cause.message}${cause.fix ? ` ${cause.fix}` : ""}` : `${wanted} is not verified yet.`,
|
|
4614
|
+
{ details: { claim: result.claim } }
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
context.output.result({ projectId, verified: true, domain: result.domain }, [
|
|
4618
|
+
`\u2705 Ownership of ${result.domain.hostname} verified \u2014 it now belongs to this project.`,
|
|
4619
|
+
"",
|
|
4620
|
+
...domainLines(result.domain),
|
|
4621
|
+
"",
|
|
4622
|
+
"Status: Waiting for DNS \u2014 add the CNAME above; naro connects it within about five minutes. Check with `naro domains verify`."
|
|
4623
|
+
]);
|
|
4624
|
+
return true;
|
|
4625
|
+
}
|
|
4572
4626
|
async function runDomainsAdd(context) {
|
|
4573
4627
|
const [hostname] = context.positionalOrThrow(
|
|
4574
4628
|
1,
|
|
@@ -4602,7 +4656,7 @@ async function runDomainsAdd(context) {
|
|
|
4602
4656
|
` refuses the record above, run \`naro domains add ${alternative}\`.`
|
|
4603
4657
|
] : [],
|
|
4604
4658
|
"",
|
|
4605
|
-
isReady(domain) ? "The domain is active." : "Status:
|
|
4659
|
+
isReady(domain) ? "The domain is active." : "Status: Waiting for DNS \u2014 add the CNAME above; naro connects it within about five minutes. Check with `naro domains verify`."
|
|
4606
4660
|
]
|
|
4607
4661
|
);
|
|
4608
4662
|
}
|
|
@@ -4626,7 +4680,14 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4626
4680
|
const progress = (line) => {
|
|
4627
4681
|
if (!context.flags.json) context.output.log(line);
|
|
4628
4682
|
};
|
|
4629
|
-
let domains
|
|
4683
|
+
let domains;
|
|
4684
|
+
try {
|
|
4685
|
+
domains = await verifyAll(client, projectId, hostname);
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
if (hostname !== void 0 && isNaroError(error) && error.code === "DOMAIN_NOT_FOUND" && await verifyClaimFor(context, client, projectId, hostname))
|
|
4688
|
+
return;
|
|
4689
|
+
throw error;
|
|
4690
|
+
}
|
|
4630
4691
|
if (domains.length === 0) {
|
|
4631
4692
|
context.output.result({ projectId, domains, ready: false }, [
|
|
4632
4693
|
"No custom domains. Add one with `naro domains add example.com`."
|
|
@@ -4685,7 +4746,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4685
4746
|
const cause = notReady[0]?.cause;
|
|
4686
4747
|
throw new NaroError(
|
|
4687
4748
|
cause?.code ?? "DOMAIN_NOT_READY",
|
|
4688
|
-
`${names} is not live yet.${cause ? ` ${cause.message}` : ""}
|
|
4749
|
+
`${names} is not live yet.${cause ? ` ${cause.message}` : ""} naro connects it within about five minutes of the CNAME appearing; \`naro domains verify --wait\` waits for it.`,
|
|
4689
4750
|
{ details }
|
|
4690
4751
|
);
|
|
4691
4752
|
}
|
|
@@ -5337,7 +5398,7 @@ function meterLines(info) {
|
|
|
5337
5398
|
}
|
|
5338
5399
|
if (today.bytesBlocked)
|
|
5339
5400
|
lines.push(
|
|
5340
|
-
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).` : `\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).`
|
|
5401
|
+
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).`
|
|
5341
5402
|
);
|
|
5342
5403
|
if (today.opsBlocked)
|
|
5343
5404
|
lines.push(
|
|
@@ -5362,7 +5423,7 @@ function storageLines(info) {
|
|
|
5362
5423
|
const { limits, usage } = info;
|
|
5363
5424
|
return [
|
|
5364
5425
|
`Storage: on (R2 bucket ${printable(info.bucket ?? "unknown")})`,
|
|
5365
|
-
`Limits: ${binarySize(limits.bytes)} in all, ${binarySize(limits.objectBytes)} per object, ${binarySize(limits.singleUploadBytes)} per
|
|
5426
|
+
`Limits: ${binarySize(limits.bytes)} in all, ${binarySize(limits.objectBytes)} per object, ${binarySize(limits.singleUploadBytes)} per request (larger files go in parts)`,
|
|
5366
5427
|
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")}`,
|
|
5367
5428
|
...meterLines(info),
|
|
5368
5429
|
info.buckets.length === 0 ? "Buckets: none yet (`naro storage bucket create <name>` makes one)" : `Buckets: ${String(info.buckets.length)}`,
|
|
@@ -5549,6 +5610,12 @@ function requestTimeout(ms) {
|
|
|
5549
5610
|
message: `The Naro backend did not answer within ${String(Math.round(ms / 1e3))} seconds. Check the network and try again.`
|
|
5550
5611
|
};
|
|
5551
5612
|
}
|
|
5613
|
+
function uploadAborted() {
|
|
5614
|
+
return {
|
|
5615
|
+
code: "ABORTED",
|
|
5616
|
+
message: "The upload was stopped before it finished."
|
|
5617
|
+
};
|
|
5618
|
+
}
|
|
5552
5619
|
function invalidResponse(status) {
|
|
5553
5620
|
return {
|
|
5554
5621
|
code: "INVALID_RESPONSE",
|
|
@@ -5662,7 +5729,10 @@ async function call(config, options) {
|
|
|
5662
5729
|
}
|
|
5663
5730
|
}
|
|
5664
5731
|
const token = options.withToken ? await config.tokens.get() : null;
|
|
5665
|
-
const headers = {
|
|
5732
|
+
const headers = {
|
|
5733
|
+
...options.headers,
|
|
5734
|
+
apikey: config.apiKey
|
|
5735
|
+
};
|
|
5666
5736
|
if (token !== null) headers.Authorization = `Bearer ${token}`;
|
|
5667
5737
|
const init = {
|
|
5668
5738
|
method: options.method,
|
|
@@ -5714,6 +5784,12 @@ async function send(config, options) {
|
|
|
5714
5784
|
credentials: "omit"
|
|
5715
5785
|
};
|
|
5716
5786
|
if (options.body !== void 0) init.body = options.body;
|
|
5787
|
+
const { signal } = options;
|
|
5788
|
+
if (signal?.aborted) return { error: uploadAborted(), status: null };
|
|
5789
|
+
const controller = signal ? new AbortController() : null;
|
|
5790
|
+
const stop = () => controller?.abort();
|
|
5791
|
+
if (controller) init.signal = controller.signal;
|
|
5792
|
+
signal?.addEventListener("abort", stop);
|
|
5717
5793
|
let response;
|
|
5718
5794
|
try {
|
|
5719
5795
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -5722,26 +5798,34 @@ async function send(config, options) {
|
|
|
5722
5798
|
init
|
|
5723
5799
|
);
|
|
5724
5800
|
} catch (error2) {
|
|
5725
|
-
return {
|
|
5801
|
+
return {
|
|
5802
|
+
error: signal?.aborted ? uploadAborted() : networkError(error2),
|
|
5803
|
+
status: null
|
|
5804
|
+
};
|
|
5805
|
+
} finally {
|
|
5806
|
+
signal?.removeEventListener("abort", stop);
|
|
5726
5807
|
}
|
|
5727
|
-
|
|
5728
|
-
|
|
5808
|
+
const { status } = response;
|
|
5809
|
+
if (status >= 200 && status < 300) return { error: null, response };
|
|
5729
5810
|
let text;
|
|
5730
5811
|
try {
|
|
5731
5812
|
text = await response.text();
|
|
5732
5813
|
} catch (error2) {
|
|
5733
|
-
return { error: networkError(error2) };
|
|
5814
|
+
return { error: networkError(error2), status: null };
|
|
5734
5815
|
}
|
|
5735
5816
|
let body;
|
|
5736
5817
|
if (text.trim() !== "") {
|
|
5737
5818
|
try {
|
|
5738
5819
|
body = JSON.parse(text);
|
|
5739
5820
|
} catch {
|
|
5740
|
-
return { error: invalidResponse(
|
|
5821
|
+
return { error: invalidResponse(status), status };
|
|
5741
5822
|
}
|
|
5742
5823
|
}
|
|
5743
|
-
const error = errorFromBody(
|
|
5744
|
-
return {
|
|
5824
|
+
const error = errorFromBody(status, body) ?? invalidResponse(status);
|
|
5825
|
+
return {
|
|
5826
|
+
error: await settled(config, error, response.headers, token),
|
|
5827
|
+
status
|
|
5828
|
+
};
|
|
5745
5829
|
}
|
|
5746
5830
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
5747
5831
|
"RATE_LIMITED",
|
|
@@ -7312,12 +7396,187 @@ var TableBuilder = class {
|
|
|
7312
7396
|
});
|
|
7313
7397
|
}
|
|
7314
7398
|
};
|
|
7399
|
+
function fail(error) {
|
|
7400
|
+
return { data: null, error };
|
|
7401
|
+
}
|
|
7402
|
+
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
7403
|
+
function dataOf2(status, body, guard) {
|
|
7404
|
+
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
7405
|
+
return { data: body.data, error: null };
|
|
7406
|
+
return fail(invalidResponse(status));
|
|
7407
|
+
}
|
|
7408
|
+
async function readJson(response, guard) {
|
|
7409
|
+
let text;
|
|
7410
|
+
try {
|
|
7411
|
+
text = await response.text();
|
|
7412
|
+
} catch (error) {
|
|
7413
|
+
return fail(networkError(error));
|
|
7414
|
+
}
|
|
7415
|
+
let body;
|
|
7416
|
+
try {
|
|
7417
|
+
body = JSON.parse(text);
|
|
7418
|
+
} catch {
|
|
7419
|
+
return fail(invalidResponse(response.status));
|
|
7420
|
+
}
|
|
7421
|
+
return dataOf2(response.status, body, guard);
|
|
7422
|
+
}
|
|
7423
|
+
var ROUTE = "/storage/upload/multipart";
|
|
7424
|
+
var UPLOAD_TOKEN_HEADER = "x-naro-upload-token";
|
|
7425
|
+
var CONCURRENCY = 4;
|
|
7426
|
+
var ATTEMPTS = 4;
|
|
7427
|
+
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
7428
|
+
var UPLOAD_ID = /^[\w-][\w.-]*$/;
|
|
7429
|
+
var positive = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
7430
|
+
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;
|
|
7431
|
+
var partAnswer = (n) => (value) => isRecord(value) && value.partNumber === n && typeof value.etag === "string" && value.etag !== "";
|
|
7432
|
+
var retryable = (error, status) => error.code === "NETWORK_ERROR" || error.code === "OBJECT_RATE_LIMITED" || status !== null && status >= 500 && error.code !== "STORAGE_QUOTA_EXCEEDED";
|
|
7433
|
+
function waitAfter(error, attempt22) {
|
|
7434
|
+
const after = isRecord(error.details) ? error.details.retryAfter : void 0;
|
|
7435
|
+
if (typeof after === "number") return after * 1e3;
|
|
7436
|
+
if (error.code === "OBJECT_RATE_LIMITED") return 1e3;
|
|
7437
|
+
return BACKOFF_MS[attempt22 - 1] ?? 4e3;
|
|
7438
|
+
}
|
|
7439
|
+
var sleepFor = (ms, signal) => new Promise((done) => {
|
|
7440
|
+
if (signal?.aborted) {
|
|
7441
|
+
done();
|
|
7442
|
+
return;
|
|
7443
|
+
}
|
|
7444
|
+
const end = () => {
|
|
7445
|
+
clearTimeout(timer);
|
|
7446
|
+
signal?.removeEventListener("abort", end);
|
|
7447
|
+
done();
|
|
7448
|
+
};
|
|
7449
|
+
const timer = setTimeout(end, ms);
|
|
7450
|
+
signal?.addEventListener("abort", end);
|
|
7451
|
+
});
|
|
7452
|
+
async function uploadInParts(a) {
|
|
7453
|
+
const { signal } = a;
|
|
7454
|
+
const sleep2 = a.sleep ?? sleepFor;
|
|
7455
|
+
const signed = a.uploadToken === void 0 ? {} : { [UPLOAD_TOKEN_HEADER]: a.uploadToken };
|
|
7456
|
+
const withToken = a.uploadToken === void 0;
|
|
7457
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
7458
|
+
const started = await call(a.http, {
|
|
7459
|
+
method: a.method,
|
|
7460
|
+
path: ROUTE,
|
|
7461
|
+
body: {
|
|
7462
|
+
bucket: a.bucket,
|
|
7463
|
+
path: a.path,
|
|
7464
|
+
size: a.size,
|
|
7465
|
+
contentType: a.contentType,
|
|
7466
|
+
cacheControl: a.cacheControl,
|
|
7467
|
+
// PUT always replaces: the worker refuses `upsert` on it.
|
|
7468
|
+
...a.method === "POST" && a.upsert ? { upsert: true } : {}
|
|
7469
|
+
},
|
|
7470
|
+
headers: signed,
|
|
7471
|
+
withToken
|
|
7472
|
+
});
|
|
7473
|
+
if (started.error !== null) return fail(started.error);
|
|
7474
|
+
const plan = dataOf2(
|
|
7475
|
+
started.answer.status,
|
|
7476
|
+
started.answer.body,
|
|
7477
|
+
planFor(a.size)
|
|
7478
|
+
);
|
|
7479
|
+
if (plan.error !== null) return fail(plan.error);
|
|
7480
|
+
const { uploadId, partSize, partCount } = plan.data;
|
|
7481
|
+
const base = `${ROUTE}/${uploadId}`;
|
|
7482
|
+
const progress = (sentBytes2) => {
|
|
7483
|
+
if (signal?.aborted) return;
|
|
7484
|
+
try {
|
|
7485
|
+
const result = a.onProgress?.(sentBytes2, a.size);
|
|
7486
|
+
if (isThenable(result)) result.then(void 0, () => void 0);
|
|
7487
|
+
} catch {
|
|
7488
|
+
}
|
|
7489
|
+
};
|
|
7490
|
+
const callOff = async () => {
|
|
7491
|
+
await call(a.http, {
|
|
7492
|
+
method: "DELETE",
|
|
7493
|
+
path: base,
|
|
7494
|
+
headers: signed,
|
|
7495
|
+
withToken
|
|
7496
|
+
}).catch(() => void 0);
|
|
7497
|
+
};
|
|
7498
|
+
const etags = [];
|
|
7499
|
+
const failure = {
|
|
7500
|
+
error: signal?.aborted ? uploadAborted() : null
|
|
7501
|
+
};
|
|
7502
|
+
let next = 1;
|
|
7503
|
+
let sentBytes = 0;
|
|
7504
|
+
const onAbort = () => {
|
|
7505
|
+
failure.error ??= uploadAborted();
|
|
7506
|
+
};
|
|
7507
|
+
const tryPart = async (n, bytes) => {
|
|
7508
|
+
const sent = await send(a.http, {
|
|
7509
|
+
method: "PUT",
|
|
7510
|
+
path: `${base}/${String(n)}`,
|
|
7511
|
+
headers: { "content-type": "application/octet-stream", ...signed },
|
|
7512
|
+
body: bytes,
|
|
7513
|
+
withToken,
|
|
7514
|
+
signal
|
|
7515
|
+
});
|
|
7516
|
+
if (sent.error !== null) return sent;
|
|
7517
|
+
const read = await readJson(sent.response, partAnswer(n));
|
|
7518
|
+
if (read.error !== null)
|
|
7519
|
+
return { error: read.error, status: sent.response.status };
|
|
7520
|
+
return { etag: read.data.etag };
|
|
7521
|
+
};
|
|
7522
|
+
const sendPart = async (n) => {
|
|
7523
|
+
const from = (n - 1) * partSize;
|
|
7524
|
+
const to = Math.min(from + partSize, a.size);
|
|
7525
|
+
const bytes = a.body instanceof Uint8Array ? a.body.subarray(from, to) : a.body.slice(from, to);
|
|
7526
|
+
for (let attempt22 = 1; ; attempt22 += 1) {
|
|
7527
|
+
const tried = await tryPart(n, bytes);
|
|
7528
|
+
if ("etag" in tried) {
|
|
7529
|
+
etags[n - 1] = tried.etag;
|
|
7530
|
+
sentBytes += to - from;
|
|
7531
|
+
progress(sentBytes);
|
|
7532
|
+
return null;
|
|
7533
|
+
}
|
|
7534
|
+
if (attempt22 === ATTEMPTS || failure.error !== null || !retryable(tried.error, tried.status))
|
|
7535
|
+
return tried.error;
|
|
7536
|
+
await sleep2(waitAfter(tried.error, attempt22), signal);
|
|
7537
|
+
if (failure.error !== null) return tried.error;
|
|
7538
|
+
}
|
|
7539
|
+
};
|
|
7540
|
+
const worker = async () => {
|
|
7541
|
+
while (failure.error === null && next <= partCount) {
|
|
7542
|
+
const n = next;
|
|
7543
|
+
next += 1;
|
|
7544
|
+
const error = await sendPart(n);
|
|
7545
|
+
if (error !== null) failure.error ??= error;
|
|
7546
|
+
}
|
|
7547
|
+
};
|
|
7548
|
+
signal?.addEventListener("abort", onAbort);
|
|
7549
|
+
await Promise.all(
|
|
7550
|
+
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
7551
|
+
);
|
|
7552
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7553
|
+
if (failure.error !== null) {
|
|
7554
|
+
await callOff();
|
|
7555
|
+
return fail(failure.error);
|
|
7556
|
+
}
|
|
7557
|
+
const completed = await call(a.http, {
|
|
7558
|
+
method: "POST",
|
|
7559
|
+
path: `${base}/complete`,
|
|
7560
|
+
body: {
|
|
7561
|
+
parts: etags.map((etag, i) => ({ partNumber: i + 1, etag }))
|
|
7562
|
+
},
|
|
7563
|
+
headers: signed,
|
|
7564
|
+
withToken
|
|
7565
|
+
});
|
|
7566
|
+
if (completed.error !== null) {
|
|
7567
|
+
if (completed.answer !== null && completed.error.code !== "INVALID_RESPONSE")
|
|
7568
|
+
await callOff();
|
|
7569
|
+
return fail(completed.error);
|
|
7570
|
+
}
|
|
7571
|
+
return dataOf2(completed.answer.status, completed.answer.body, isUploadResult);
|
|
7572
|
+
}
|
|
7573
|
+
var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
7315
7574
|
var MAX_SINGLE_UPLOAD_BYTES2 = 52428800;
|
|
7316
7575
|
var STORAGE_MESSAGES = {
|
|
7317
7576
|
bucket: "The bucket name is not valid.",
|
|
7318
|
-
tooLarge: "Uploads over 50 MiB are not supported yet.",
|
|
7319
7577
|
request: "The storage request is not valid.",
|
|
7320
|
-
body: "upload takes a Blob, File, ArrayBuffer, typed array or string."
|
|
7578
|
+
body: "upload takes a Blob, File, ArrayBuffer, typed array or string.",
|
|
7579
|
+
signature: "The signed URL is not valid."
|
|
7321
7580
|
};
|
|
7322
7581
|
var RESERVED_BUCKETS = /* @__PURE__ */ new Set([
|
|
7323
7582
|
"authenticated",
|
|
@@ -7340,9 +7599,6 @@ function invalidPath(reason) {
|
|
|
7340
7599
|
message: `The object path is not valid: it ${reason}.`
|
|
7341
7600
|
};
|
|
7342
7601
|
}
|
|
7343
|
-
function objectTooLarge() {
|
|
7344
|
-
return { code: "OBJECT_TOO_LARGE", message: STORAGE_MESSAGES.tooLarge };
|
|
7345
|
-
}
|
|
7346
7602
|
function encodePath(path) {
|
|
7347
7603
|
if (typeof path !== "string" || path === "")
|
|
7348
7604
|
return { ok: false, error: invalidPath("is empty") };
|
|
@@ -7379,6 +7635,16 @@ function encodeBucket(bucket) {
|
|
|
7379
7635
|
};
|
|
7380
7636
|
return { ok: true, encoded: encodeURIComponent(bucket) };
|
|
7381
7637
|
}
|
|
7638
|
+
function objectRoute(bucket, path) {
|
|
7639
|
+
const encodedBucket = encodeBucket(bucket);
|
|
7640
|
+
if (!encodedBucket.ok) return encodedBucket;
|
|
7641
|
+
const encodedPath = encodePath(path);
|
|
7642
|
+
if (!encodedPath.ok) return encodedPath;
|
|
7643
|
+
return {
|
|
7644
|
+
ok: true,
|
|
7645
|
+
encoded: `${encodedBucket.encoded}/${encodedPath.encoded}`
|
|
7646
|
+
};
|
|
7647
|
+
}
|
|
7382
7648
|
var isArrayBuffer = (value) => Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
7383
7649
|
function isDetached(buffer) {
|
|
7384
7650
|
if (!isArrayBuffer(buffer)) return false;
|
|
@@ -7459,36 +7725,25 @@ function payloadOf(body, contentType) {
|
|
|
7459
7725
|
};
|
|
7460
7726
|
}
|
|
7461
7727
|
var SIGNED_PREFIX = "/_naro/v1/storage/object/sign/";
|
|
7462
|
-
|
|
7463
|
-
|
|
7728
|
+
var SIGNED_UPLOAD_PREFIX = "/_naro/v1/storage/object/upload/sign/";
|
|
7729
|
+
var TOKEN_TEXT = /^[\x21-\x7e]+$/;
|
|
7730
|
+
var TOKEN_PARAMETER = /([?&]token=)[^&#\s"'<>]*/g;
|
|
7731
|
+
function withoutToken(error, token) {
|
|
7732
|
+
const { details } = error;
|
|
7733
|
+
if (!isRecord(details) || typeof details.cause !== "string") return error;
|
|
7734
|
+
let cause = details.cause.replace(TOKEN_PARAMETER, "$1[redacted]");
|
|
7735
|
+
for (const form of [encodeURIComponent(token), token])
|
|
7736
|
+
cause = cause.replaceAll(form, "[redacted]");
|
|
7737
|
+
return { ...error, details: { ...details, cause } };
|
|
7464
7738
|
}
|
|
7465
|
-
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
7466
7739
|
var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
|
|
7467
7740
|
var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
|
|
7468
7741
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
7742
|
+
var isSignedUpload = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_UPLOAD_PREFIX) && typeof value.token === "string" && value.token !== "" && typeof value.path === "string";
|
|
7743
|
+
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
7469
7744
|
function listOf(guard) {
|
|
7470
7745
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
7471
7746
|
}
|
|
7472
|
-
function dataOf2(status, body, guard) {
|
|
7473
|
-
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
7474
|
-
return { data: body.data, error: null };
|
|
7475
|
-
return fail(invalidResponse(status));
|
|
7476
|
-
}
|
|
7477
|
-
async function readJson(response, guard) {
|
|
7478
|
-
let text;
|
|
7479
|
-
try {
|
|
7480
|
-
text = await response.text();
|
|
7481
|
-
} catch (error) {
|
|
7482
|
-
return fail(networkError(error));
|
|
7483
|
-
}
|
|
7484
|
-
let body;
|
|
7485
|
-
try {
|
|
7486
|
-
body = JSON.parse(text);
|
|
7487
|
-
} catch {
|
|
7488
|
-
return fail(invalidResponse(response.status));
|
|
7489
|
-
}
|
|
7490
|
-
return dataOf2(response.status, body, guard);
|
|
7491
|
-
}
|
|
7492
7747
|
var StorageFileApi = class {
|
|
7493
7748
|
#http;
|
|
7494
7749
|
#auth;
|
|
@@ -7500,14 +7755,9 @@ var StorageFileApi = class {
|
|
|
7500
7755
|
}
|
|
7501
7756
|
/** `/storage/object/<route><bucket>/<path>`, or the refusal no request needs. */
|
|
7502
7757
|
#objectPath(route2, path) {
|
|
7503
|
-
const
|
|
7504
|
-
if (!
|
|
7505
|
-
|
|
7506
|
-
if (!name.ok) return name;
|
|
7507
|
-
return {
|
|
7508
|
-
ok: true,
|
|
7509
|
-
path: `/storage/object/${route2}${bucket.encoded}/${name.encoded}`
|
|
7510
|
-
};
|
|
7758
|
+
const target = objectRoute(this.#bucket, path);
|
|
7759
|
+
if (!target.ok) return target;
|
|
7760
|
+
return { ok: true, path: `/storage/object/${route2}${target.encoded}` };
|
|
7511
7761
|
}
|
|
7512
7762
|
/** `/storage/object/<route><bucket>`, for the routes that take names in the body. */
|
|
7513
7763
|
#bucketPath(route2) {
|
|
@@ -7529,8 +7779,23 @@ var StorageFileApi = class {
|
|
|
7529
7779
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
7530
7780
|
const unreadable = headerProblem(options.contentType, cache);
|
|
7531
7781
|
if (unreadable !== null) return fail(unreadable);
|
|
7532
|
-
|
|
7782
|
+
const { signal } = options;
|
|
7783
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
7533
7784
|
await this.#auth.ready();
|
|
7785
|
+
if (payload.size > MAX_SINGLE_UPLOAD_BYTES2)
|
|
7786
|
+
return uploadInParts({
|
|
7787
|
+
http: this.#http,
|
|
7788
|
+
method,
|
|
7789
|
+
bucket: this.#bucket,
|
|
7790
|
+
path,
|
|
7791
|
+
body: partsBody(payload.body),
|
|
7792
|
+
size: payload.size,
|
|
7793
|
+
contentType: payload.type,
|
|
7794
|
+
cacheControl: cache,
|
|
7795
|
+
upsert,
|
|
7796
|
+
...options.onProgress ? { onProgress: options.onProgress } : {},
|
|
7797
|
+
signal
|
|
7798
|
+
});
|
|
7534
7799
|
const headers = {
|
|
7535
7800
|
"content-type": payload.type,
|
|
7536
7801
|
"cache-control": cache
|
|
@@ -7541,7 +7806,9 @@ var StorageFileApi = class {
|
|
|
7541
7806
|
path: target.path,
|
|
7542
7807
|
headers,
|
|
7543
7808
|
body: payload.body,
|
|
7544
|
-
withToken: true
|
|
7809
|
+
withToken: true,
|
|
7810
|
+
// The fetch stops with the body: the worker undoes its claim at once.
|
|
7811
|
+
signal
|
|
7545
7812
|
});
|
|
7546
7813
|
if (sent.error !== null) return fail(sent.error);
|
|
7547
7814
|
return readJson(sent.response, isUploadResult);
|
|
@@ -7649,6 +7916,101 @@ var StorageFileApi = class {
|
|
|
7649
7916
|
error: null
|
|
7650
7917
|
};
|
|
7651
7918
|
}
|
|
7919
|
+
/**
|
|
7920
|
+
* A token that lets whoever holds it upload `path` for two hours (Storage
|
|
7921
|
+
* 3-1e-2), as supabase-js's: made where the key is — a server with the
|
|
7922
|
+
* service key for any path, anon or a signed-in user only where the
|
|
7923
|
+
* policies let them upload. With `upsert: true` it may replace a file
|
|
7924
|
+
* that is there.
|
|
7925
|
+
* The token goes to `uploadToSignedUrl`; `signedUrl` is where a client
|
|
7926
|
+
* without naro-js PUTs the bytes.
|
|
7927
|
+
*/
|
|
7928
|
+
async createSignedUploadUrl(path, options = {}) {
|
|
7929
|
+
const target = this.#objectPath("upload/sign/", path);
|
|
7930
|
+
if (!target.ok) return fail(target.error);
|
|
7931
|
+
const upsert = (options ?? {}).upsert === true;
|
|
7932
|
+
const signed = await this.#json(
|
|
7933
|
+
{
|
|
7934
|
+
method: "POST",
|
|
7935
|
+
path: target.path,
|
|
7936
|
+
body: upsert ? { upsert: true } : {}
|
|
7937
|
+
},
|
|
7938
|
+
isSignedUpload
|
|
7939
|
+
);
|
|
7940
|
+
if (signed.error !== null) return signed;
|
|
7941
|
+
return {
|
|
7942
|
+
data: {
|
|
7943
|
+
signedUrl: `${this.#http.baseUrl}${signed.data.signedUrl}`,
|
|
7944
|
+
token: signed.data.token,
|
|
7945
|
+
path: signed.data.path
|
|
7946
|
+
},
|
|
7947
|
+
error: null
|
|
7948
|
+
};
|
|
7949
|
+
}
|
|
7950
|
+
/**
|
|
7951
|
+
* Uploads `body` to `path` with a token from `createSignedUploadUrl`
|
|
7952
|
+
* (Storage 3-1e-2). No session is sent, or needed: the token is the
|
|
7953
|
+
* permission, so a page's anon-key client uploads where only a server
|
|
7954
|
+
* could sign. It works until the token expires, two hours from when it
|
|
7955
|
+
* was made; one made without `upsert` uploads one new file
|
|
7956
|
+
* (`OBJECT_EXISTS` for a name that is there). A body over 50 MiB goes in
|
|
7957
|
+
* parts with the token on every step, and once started may finish after
|
|
7958
|
+
* the two hours. `contentType`, `cacheControl`, `onProgress` and `signal`
|
|
7959
|
+
* are `upload`'s.
|
|
7960
|
+
*/
|
|
7961
|
+
async uploadToSignedUrl(path, token, body, options = {}) {
|
|
7962
|
+
const given = options ?? {};
|
|
7963
|
+
const target = this.#objectPath("upload/sign/", path);
|
|
7964
|
+
if (!target.ok) return fail(target.error);
|
|
7965
|
+
if (typeof token !== "string" || !TOKEN_TEXT.test(token))
|
|
7966
|
+
return fail({
|
|
7967
|
+
code: "INVALID_SIGNATURE",
|
|
7968
|
+
message: STORAGE_MESSAGES.signature
|
|
7969
|
+
});
|
|
7970
|
+
const payload = payloadOf(body, given.contentType);
|
|
7971
|
+
if (!payload.ok) return fail(payload.error);
|
|
7972
|
+
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
7973
|
+
const unreadable = headerProblem(given.contentType, cache);
|
|
7974
|
+
if (unreadable !== null) return fail(unreadable);
|
|
7975
|
+
const { signal } = given;
|
|
7976
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
7977
|
+
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES2 ? await uploadInParts({
|
|
7978
|
+
http: this.#http,
|
|
7979
|
+
method: "POST",
|
|
7980
|
+
bucket: this.#bucket,
|
|
7981
|
+
path,
|
|
7982
|
+
body: partsBody(payload.body),
|
|
7983
|
+
size: payload.size,
|
|
7984
|
+
contentType: payload.type,
|
|
7985
|
+
cacheControl: cache,
|
|
7986
|
+
upsert: false,
|
|
7987
|
+
uploadToken: token,
|
|
7988
|
+
...given.onProgress ? { onProgress: given.onProgress } : {},
|
|
7989
|
+
signal
|
|
7990
|
+
}) : await this.#putSigned(target.path, token, payload, cache, signal);
|
|
7991
|
+
if (uploaded.error !== null) return fail(uploaded.error);
|
|
7992
|
+
return {
|
|
7993
|
+
data: { path: uploaded.data.path, fullPath: uploaded.data.fullPath },
|
|
7994
|
+
error: null
|
|
7995
|
+
};
|
|
7996
|
+
}
|
|
7997
|
+
/**
|
|
7998
|
+
* The one-request signed upload: the token in the query, no session — it
|
|
7999
|
+
* is the permission. A failure's cause never carries the token.
|
|
8000
|
+
*/
|
|
8001
|
+
async #putSigned(path, token, payload, cache, signal) {
|
|
8002
|
+
const sent = await send(this.#http, {
|
|
8003
|
+
method: "PUT",
|
|
8004
|
+
path: `${path}?token=${encodeURIComponent(token)}`,
|
|
8005
|
+
headers: { "content-type": payload.type, "cache-control": cache },
|
|
8006
|
+
body: payload.body,
|
|
8007
|
+
withToken: false,
|
|
8008
|
+
signal
|
|
8009
|
+
});
|
|
8010
|
+
if (sent.error !== null) return fail(withoutToken(sent.error, token));
|
|
8011
|
+
const read = await readJson(sent.response, isUploadResult);
|
|
8012
|
+
return read.error === null ? read : fail(withoutToken(read.error, token));
|
|
8013
|
+
}
|
|
7652
8014
|
/**
|
|
7653
8015
|
* A public bucket's URL for the object. No request is made, so whether
|
|
7654
8016
|
* the bucket is public is not checked (as in supabase-js); a path no URL
|
|
@@ -7787,6 +8149,7 @@ var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<pa
|
|
|
7787
8149
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
7788
8150
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]";
|
|
7789
8151
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
8152
|
+
var FOUR_GIB = 4294967296;
|
|
7790
8153
|
var PAGE = 1e3;
|
|
7791
8154
|
var MAX_OFFSET = 1e5;
|
|
7792
8155
|
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.`;
|
|
@@ -7889,7 +8252,8 @@ async function objects(context) {
|
|
|
7889
8252
|
{ cause: error }
|
|
7890
8253
|
);
|
|
7891
8254
|
}
|
|
7892
|
-
|
|
8255
|
+
const url = new URL(backend.url.trim().replace(/\/+$/, "")).origin;
|
|
8256
|
+
return { projectId, storage: naro.storage, url, serviceKey };
|
|
7893
8257
|
}
|
|
7894
8258
|
function isFolder(entry) {
|
|
7895
8259
|
return entry.id === null;
|
|
@@ -7982,33 +8346,56 @@ async function storageLs(context) {
|
|
|
7982
8346
|
);
|
|
7983
8347
|
}
|
|
7984
8348
|
var INTERRUPTS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
7985
|
-
function
|
|
7986
|
-
|
|
8349
|
+
function endWith(signal) {
|
|
8350
|
+
try {
|
|
8351
|
+
process.kill(process.pid, signal);
|
|
8352
|
+
} catch {
|
|
8353
|
+
process.exit(1);
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
function onInterrupt(onSignal) {
|
|
8357
|
+
for (const signal of INTERRUPTS) process.on(signal, onSignal);
|
|
8358
|
+
return () => {
|
|
7987
8359
|
for (const signal of INTERRUPTS) process.off(signal, onSignal);
|
|
7988
8360
|
};
|
|
7989
|
-
|
|
8361
|
+
}
|
|
8362
|
+
function removeOnInterrupt(partial) {
|
|
8363
|
+
const stop = onInterrupt((signal) => {
|
|
7990
8364
|
stop();
|
|
7991
8365
|
try {
|
|
7992
8366
|
rmSync2(partial, { force: true });
|
|
7993
8367
|
} catch {
|
|
7994
8368
|
}
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
} catch {
|
|
7998
|
-
process.exit(1);
|
|
7999
|
-
}
|
|
8000
|
-
}
|
|
8001
|
-
for (const signal of INTERRUPTS) process.on(signal, onSignal);
|
|
8369
|
+
endWith(signal);
|
|
8370
|
+
});
|
|
8002
8371
|
return stop;
|
|
8003
8372
|
}
|
|
8004
|
-
|
|
8373
|
+
function stopOnInterrupt(onFirst) {
|
|
8374
|
+
const controller = new AbortController();
|
|
8375
|
+
let caught = null;
|
|
8376
|
+
const stop = onInterrupt((signal) => {
|
|
8377
|
+
if (caught !== null) {
|
|
8378
|
+
stop();
|
|
8379
|
+
endWith(signal);
|
|
8380
|
+
return;
|
|
8381
|
+
}
|
|
8382
|
+
caught = signal;
|
|
8383
|
+
onFirst();
|
|
8384
|
+
controller.abort();
|
|
8385
|
+
});
|
|
8386
|
+
return {
|
|
8387
|
+
signal: controller.signal,
|
|
8388
|
+
settle: () => {
|
|
8389
|
+
stop();
|
|
8390
|
+
return caught;
|
|
8391
|
+
}
|
|
8392
|
+
};
|
|
8393
|
+
}
|
|
8394
|
+
async function writeAtomically(bytes, destination) {
|
|
8005
8395
|
const partial = join6(dirname3(destination), `.${randomUUID()}.naro-partial`);
|
|
8006
8396
|
const stopListening = removeOnInterrupt(partial);
|
|
8007
8397
|
try {
|
|
8008
|
-
await pipeline2(
|
|
8009
|
-
Readable.fromWeb(blob.stream()),
|
|
8010
|
-
createWriteStream2(partial, { flags: "wx" })
|
|
8011
|
-
);
|
|
8398
|
+
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
8012
8399
|
await rename(partial, destination);
|
|
8013
8400
|
} catch (error) {
|
|
8014
8401
|
await rm(partial, { force: true }).catch(() => void 0);
|
|
@@ -8017,6 +8404,34 @@ async function writeAtomically(blob, destination) {
|
|
|
8017
8404
|
stopListening();
|
|
8018
8405
|
}
|
|
8019
8406
|
}
|
|
8407
|
+
function progressReporter(output, total) {
|
|
8408
|
+
const tty = process.stderr.isTTY === true;
|
|
8409
|
+
let lastTenth = -1;
|
|
8410
|
+
let lastLength = 0;
|
|
8411
|
+
let open = false;
|
|
8412
|
+
return {
|
|
8413
|
+
report(sent) {
|
|
8414
|
+
const percent2 = Math.floor(sent / total * 100);
|
|
8415
|
+
const line = `\u2191 ${binarySize(sent)} / ${binarySize(total)} (${String(percent2)}%)`;
|
|
8416
|
+
if (tty) {
|
|
8417
|
+
open = sent !== total;
|
|
8418
|
+
output.progress(`\r${line.padEnd(lastLength)}${open ? "" : "\n"}`);
|
|
8419
|
+
lastLength = line.length;
|
|
8420
|
+
return;
|
|
8421
|
+
}
|
|
8422
|
+
const tenth = Math.floor(percent2 / 10);
|
|
8423
|
+
if (tenth === lastTenth && sent !== total) return;
|
|
8424
|
+
lastTenth = tenth;
|
|
8425
|
+
output.progress(`${line}
|
|
8426
|
+
`);
|
|
8427
|
+
},
|
|
8428
|
+
close() {
|
|
8429
|
+
if (!open) return;
|
|
8430
|
+
open = false;
|
|
8431
|
+
output.progress("\n");
|
|
8432
|
+
}
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8020
8435
|
async function upload(context, local, target) {
|
|
8021
8436
|
const file = resolve5(context.cwd, local);
|
|
8022
8437
|
const info = await stat(file).catch(() => null);
|
|
@@ -8035,32 +8450,49 @@ async function upload(context, local, target) {
|
|
|
8035
8450
|
"VALIDATION_FAILED",
|
|
8036
8451
|
`${printable(local)} is not a regular file.`
|
|
8037
8452
|
);
|
|
8038
|
-
if (info.size > MAX_SINGLE_UPLOAD_BYTES)
|
|
8039
|
-
throw new NaroError(
|
|
8040
|
-
"OBJECT_TOO_LARGE",
|
|
8041
|
-
dataError("OBJECT_TOO_LARGE", { single: true }).message
|
|
8042
|
-
);
|
|
8043
8453
|
const { cacheControl } = context.flags;
|
|
8044
8454
|
if (cacheControl !== void 0 && !isMaxAge(cacheControl))
|
|
8045
8455
|
throw new NaroError("VALIDATION_FAILED", CACHE_CONTROL_RULE);
|
|
8046
8456
|
const path = target.path === "" || target.path.endsWith("/") ? `${target.path}${basename2(file)}` : target.path;
|
|
8047
|
-
const { projectId, storage } = await objects(context);
|
|
8048
8457
|
const body = await openAsBlob(file);
|
|
8458
|
+
if (body.size !== info.size)
|
|
8459
|
+
throw new NaroError(
|
|
8460
|
+
"VALIDATION_FAILED",
|
|
8461
|
+
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.`
|
|
8462
|
+
);
|
|
8463
|
+
const { projectId, storage } = await objects(context);
|
|
8049
8464
|
const contentType = context.flags.contentType ?? typeFor(file);
|
|
8050
|
-
const
|
|
8051
|
-
|
|
8465
|
+
const progress = body.size > MAX_SINGLE_UPLOAD_BYTES && !context.output.json ? progressReporter(context.output, body.size) : void 0;
|
|
8466
|
+
const interrupt = stopOnInterrupt(() => {
|
|
8467
|
+
progress?.close();
|
|
8468
|
+
context.output.progress("Stopping the upload\u2026\n");
|
|
8469
|
+
});
|
|
8470
|
+
let answer;
|
|
8471
|
+
let caught;
|
|
8472
|
+
try {
|
|
8473
|
+
answer = await storage.from(target.bucket).upload(path, body, {
|
|
8052
8474
|
...contentType === void 0 ? {} : { contentType },
|
|
8053
8475
|
...cacheControl === void 0 ? {} : { cacheControl },
|
|
8054
|
-
upsert: context.flags.upsert === true
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8476
|
+
upsert: context.flags.upsert === true,
|
|
8477
|
+
...progress === void 0 ? {} : { onProgress: progress.report },
|
|
8478
|
+
signal: interrupt.signal
|
|
8479
|
+
});
|
|
8480
|
+
} finally {
|
|
8481
|
+
progress?.close();
|
|
8482
|
+
caught = interrupt.settle();
|
|
8483
|
+
}
|
|
8484
|
+
try {
|
|
8485
|
+
const result = unwrap(answer);
|
|
8486
|
+
const to = `${NARO_SCHEME}${target.bucket}/${path}`;
|
|
8487
|
+
context.output.result(
|
|
8488
|
+
{ projectId, from: local, to, bytes: body.size, id: result.id },
|
|
8489
|
+
[
|
|
8490
|
+
`\u2705 Uploaded ${printable(local)} \u2192 ${printable(to)} (${binarySize(body.size)})`
|
|
8491
|
+
]
|
|
8492
|
+
);
|
|
8493
|
+
} finally {
|
|
8494
|
+
if (caught !== null) endWith(caught);
|
|
8495
|
+
}
|
|
8064
8496
|
}
|
|
8065
8497
|
var WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
8066
8498
|
var MAX_FILE_NAME_BYTES = 255;
|
|
@@ -8073,6 +8505,62 @@ function savedAs(path) {
|
|
|
8073
8505
|
);
|
|
8074
8506
|
return name;
|
|
8075
8507
|
}
|
|
8508
|
+
var NETWORK_MESSAGE = "The request did not reach the Naro backend. Check the url and the network.";
|
|
8509
|
+
async function failureOf(response) {
|
|
8510
|
+
const { status } = response;
|
|
8511
|
+
const invalid = {
|
|
8512
|
+
code: "INVALID_RESPONSE",
|
|
8513
|
+
message: `The server did not answer like a Naro backend (HTTP ${String(status)}).`,
|
|
8514
|
+
details: { status }
|
|
8515
|
+
};
|
|
8516
|
+
let text;
|
|
8517
|
+
try {
|
|
8518
|
+
text = await response.text();
|
|
8519
|
+
} catch {
|
|
8520
|
+
return { code: "NETWORK_ERROR", message: NETWORK_MESSAGE };
|
|
8521
|
+
}
|
|
8522
|
+
let body;
|
|
8523
|
+
if (text.trim() !== "") {
|
|
8524
|
+
try {
|
|
8525
|
+
body = JSON.parse(text);
|
|
8526
|
+
} catch {
|
|
8527
|
+
return invalid;
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
const error = typeof body === "object" && body !== null && "error" in body ? body.error : void 0;
|
|
8531
|
+
if (typeof error !== "object" || error === null) return invalid;
|
|
8532
|
+
const { code, message, details } = error;
|
|
8533
|
+
if (typeof code !== "string" || code === "" || typeof message !== "string")
|
|
8534
|
+
return invalid;
|
|
8535
|
+
return details === void 0 ? { code, message } : { code, message, details };
|
|
8536
|
+
}
|
|
8537
|
+
async function getObject(url, serviceKey) {
|
|
8538
|
+
let response;
|
|
8539
|
+
try {
|
|
8540
|
+
response = await fetch(url, {
|
|
8541
|
+
headers: { apikey: serviceKey },
|
|
8542
|
+
redirect: "error"
|
|
8543
|
+
});
|
|
8544
|
+
} catch {
|
|
8545
|
+
throw new NaroError("NETWORK_ERROR", NETWORK_MESSAGE);
|
|
8546
|
+
}
|
|
8547
|
+
if (response.ok) return response;
|
|
8548
|
+
return unwrap({ data: null, error: await failureOf(response) });
|
|
8549
|
+
}
|
|
8550
|
+
async function* chunksOf2(response, received) {
|
|
8551
|
+
if (response.body === null) return;
|
|
8552
|
+
try {
|
|
8553
|
+
for await (const chunk2 of Readable.fromWeb(
|
|
8554
|
+
response.body
|
|
8555
|
+
)) {
|
|
8556
|
+
const bytes = chunk2;
|
|
8557
|
+
received.bytes += bytes.byteLength;
|
|
8558
|
+
yield bytes;
|
|
8559
|
+
}
|
|
8560
|
+
} catch {
|
|
8561
|
+
throw new NaroError("NETWORK_ERROR", NETWORK_MESSAGE);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8076
8564
|
async function download(context, source, local) {
|
|
8077
8565
|
if (source.path === "" || source.path.endsWith("/"))
|
|
8078
8566
|
throw new NaroError(
|
|
@@ -8088,14 +8576,20 @@ async function download(context, source, local) {
|
|
|
8088
8576
|
"VALIDATION_FAILED",
|
|
8089
8577
|
`Cannot save to ${printable(local)}: its folder does not exist.`
|
|
8090
8578
|
);
|
|
8091
|
-
const
|
|
8092
|
-
|
|
8093
|
-
await
|
|
8579
|
+
const route2 = objectRoute(source.bucket, source.path);
|
|
8580
|
+
if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
|
|
8581
|
+
const { projectId, url, serviceKey } = await objects(context);
|
|
8582
|
+
const response = await getObject(
|
|
8583
|
+
`${url}/_naro/v1/storage/object/${route2.encoded}`,
|
|
8584
|
+
serviceKey
|
|
8585
|
+
);
|
|
8586
|
+
const received = { bytes: 0 };
|
|
8587
|
+
await writeAtomically(chunksOf2(response, received), destination);
|
|
8094
8588
|
const from = `${NARO_SCHEME}${source.bucket}/${source.path}`;
|
|
8095
8589
|
context.output.result(
|
|
8096
|
-
{ projectId, from, to: destination, bytes:
|
|
8590
|
+
{ projectId, from, to: destination, bytes: received.bytes },
|
|
8097
8591
|
[
|
|
8098
|
-
`\u2705 Downloaded ${printable(from)} \u2192 ${printable(destination)} (${binarySize(
|
|
8592
|
+
`\u2705 Downloaded ${printable(from)} \u2192 ${printable(destination)} (${binarySize(received.bytes)})`
|
|
8099
8593
|
]
|
|
8100
8594
|
);
|
|
8101
8595
|
}
|
|
@@ -8388,8 +8882,9 @@ Usage:
|
|
|
8388
8882
|
naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]
|
|
8389
8883
|
The buckets, or the folders and objects under a prefix
|
|
8390
8884
|
naro storage cp <from> <to> [--upsert] [--content-type <t>] [--cache-control <seconds>]
|
|
8391
|
-
One file between this machine and naro://<bucket>/<path>, 50 MiB
|
|
8885
|
+
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
8392
8886
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
8887
|
+
A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
|
|
8393
8888
|
A download replaces a local file of the same name
|
|
8394
8889
|
naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
|
|
8395
8890
|
Remove objects; --recursive removes what is under a folder (asks first):
|