naro-cli 0.19.0 → 0.19.1
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 +196 -9
- package/dist/{chunk-DEEFHVDZ.js → chunk-ZO2BEC46.js} +1935 -449
- package/dist/deploy.d.ts +3 -3
- package/dist/deploy.js +2 -2
- package/dist/index.js +261 -28
- package/package.json +4 -4
package/dist/deploy.d.ts
CHANGED
|
@@ -55,9 +55,9 @@ export interface DeployOptions {
|
|
|
55
55
|
production?: boolean;
|
|
56
56
|
/**
|
|
57
57
|
* Deploy to this named environment (its slug) instead of preview. Not with
|
|
58
|
-
* `production
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
58
|
+
* `production`, and not `production` or `preview` themselves: those are
|
|
59
|
+
* `production: true` and the default. Each is refused with
|
|
60
|
+
* VALIDATION_FAILED before any request.
|
|
61
61
|
*/
|
|
62
62
|
environment?: string | undefined;
|
|
63
63
|
env?: Record<string, string> | undefined;
|
package/dist/deploy.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
deployDirectory,
|
|
4
4
|
eventsObserver,
|
|
5
5
|
localDependencies,
|
|
6
|
-
|
|
6
|
+
parseDotenv,
|
|
7
7
|
readProjectEnv,
|
|
8
8
|
slugify,
|
|
9
9
|
workingTreeNotices
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ZO2BEC46.js";
|
|
11
11
|
import "./chunk-K3NQKI34.js";
|
|
12
12
|
export {
|
|
13
13
|
deployDirectory,
|
package/dist/index.js
CHANGED
|
@@ -18,14 +18,17 @@ import {
|
|
|
18
18
|
SPEED_INSIGHTS_METRICS,
|
|
19
19
|
SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
|
|
20
20
|
STORAGE_OBJECTS_TABLE,
|
|
21
|
+
STORAGE_SUBQUERIES_SQL,
|
|
21
22
|
buildResetPlan,
|
|
22
23
|
cellText,
|
|
24
|
+
checkObjectName,
|
|
23
25
|
clearSession,
|
|
24
26
|
commentText,
|
|
25
27
|
compareMigrations,
|
|
26
28
|
controlPlaneUrl,
|
|
27
29
|
countLintedTables,
|
|
28
30
|
createAuthClient,
|
|
31
|
+
dataError,
|
|
29
32
|
deployDirectory,
|
|
30
33
|
detectGitInfo,
|
|
31
34
|
ensureGitignoreHasNaro,
|
|
@@ -50,7 +53,7 @@ import {
|
|
|
50
53
|
nextMigrationFileName,
|
|
51
54
|
openBrowser,
|
|
52
55
|
parseCacheControl,
|
|
53
|
-
|
|
56
|
+
parseDotenvDetailed,
|
|
54
57
|
parseGitHubRemote,
|
|
55
58
|
parsePolicyNode,
|
|
56
59
|
parsePolicyStatement,
|
|
@@ -71,6 +74,7 @@ import {
|
|
|
71
74
|
resetFileRefusedMessage,
|
|
72
75
|
resetPoliciesLostMessage,
|
|
73
76
|
resetPoliciesWithTablesMessage,
|
|
77
|
+
resetStorageOutdatedMessage,
|
|
74
78
|
resolveAuth,
|
|
75
79
|
resolveMigrationsDir,
|
|
76
80
|
saveSession,
|
|
@@ -80,6 +84,7 @@ import {
|
|
|
80
84
|
snapshotFromRows,
|
|
81
85
|
splitPolicyStatements,
|
|
82
86
|
splitStatements,
|
|
87
|
+
storageRefusalOfFiles,
|
|
83
88
|
storedPolicySql,
|
|
84
89
|
summarizeStatement,
|
|
85
90
|
summarizeTeamUsage,
|
|
@@ -91,7 +96,7 @@ import {
|
|
|
91
96
|
usageMonthOf,
|
|
92
97
|
userForAccessToken,
|
|
93
98
|
writeProjectLink
|
|
94
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-ZO2BEC46.js";
|
|
95
100
|
import "./chunk-K3NQKI34.js";
|
|
96
101
|
|
|
97
102
|
// src/args.ts
|
|
@@ -2644,12 +2649,45 @@ async function hasBackend(client, projectId) {
|
|
|
2644
2649
|
throw error;
|
|
2645
2650
|
}
|
|
2646
2651
|
}
|
|
2647
|
-
async function
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
+
async function refuseStorageOutdated(client, projectId, storageTable, files, texts) {
|
|
2653
|
+
let refused = storageRefusalOfFiles(texts, {
|
|
2654
|
+
storage: storageTable,
|
|
2655
|
+
storageSubqueries: false
|
|
2656
|
+
});
|
|
2657
|
+
if (refused?.needs === "subqueries" && storageTable) {
|
|
2658
|
+
const answer = await client.db.query(projectId, {
|
|
2659
|
+
sql: STORAGE_SUBQUERIES_SQL
|
|
2660
|
+
});
|
|
2661
|
+
if ((answer.statements[0]?.rows.length ?? 0) > 0)
|
|
2662
|
+
refused = storageRefusalOfFiles(texts, {
|
|
2663
|
+
storage: true,
|
|
2664
|
+
storageSubqueries: true
|
|
2665
|
+
});
|
|
2652
2666
|
}
|
|
2667
|
+
if (refused)
|
|
2668
|
+
throw new NaroError(
|
|
2669
|
+
"BACKEND_OUTDATED",
|
|
2670
|
+
resetStorageOutdatedMessage(files[refused.file] ?? "", refused),
|
|
2671
|
+
{
|
|
2672
|
+
details: {
|
|
2673
|
+
migration: files[refused.file] ?? "",
|
|
2674
|
+
statement: refused.statement
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
async function policyPlan(client, projectId, schema, files, texts) {
|
|
2680
|
+
if (!schema.policyTable && await hasBackend(client, projectId))
|
|
2681
|
+
throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
|
|
2682
|
+
await refuseStorageOutdated(
|
|
2683
|
+
client,
|
|
2684
|
+
projectId,
|
|
2685
|
+
schema.storageTable,
|
|
2686
|
+
files,
|
|
2687
|
+
texts
|
|
2688
|
+
);
|
|
2689
|
+
if (!schema.policyTable)
|
|
2690
|
+
return { clearPolicies: false, lost: [], withTables: [] };
|
|
2653
2691
|
const answer = await client.db.query(projectId, {
|
|
2654
2692
|
sql: NAMED_POLICY_SQL.list
|
|
2655
2693
|
});
|
|
@@ -2678,7 +2716,7 @@ async function policyPlan(client, projectId, schema, dir, files) {
|
|
|
2678
2716
|
...policiesLostByReset({
|
|
2679
2717
|
current,
|
|
2680
2718
|
tables: schema.tables,
|
|
2681
|
-
files:
|
|
2719
|
+
files: texts,
|
|
2682
2720
|
// Storage's table stays; its policies are emptied with the rest (PR
|
|
2683
2721
|
// #85 리뷰). Realtime's messages need no flag: a virtual target, which
|
|
2684
2722
|
// reset-plan counts as a table always there (Realtime 3-2a).
|
|
@@ -2716,6 +2754,7 @@ async function runDbReset(context) {
|
|
|
2716
2754
|
${problemLines.join("\n")}`,
|
|
2717
2755
|
{ details: { problems } }
|
|
2718
2756
|
);
|
|
2757
|
+
const texts = files.map((name) => readFileSync3(join3(dir, name), "utf8"));
|
|
2719
2758
|
const client = await context.client();
|
|
2720
2759
|
const projectId = await context.resolveProjectId();
|
|
2721
2760
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
@@ -2731,8 +2770,8 @@ ${problemLines.join("\n")}`,
|
|
|
2731
2770
|
client,
|
|
2732
2771
|
projectId,
|
|
2733
2772
|
{ policyTable: policyTable2, storageTable, tables },
|
|
2734
|
-
|
|
2735
|
-
|
|
2773
|
+
files,
|
|
2774
|
+
texts
|
|
2736
2775
|
);
|
|
2737
2776
|
const policiesNotInFiles = policies.lost.map((policy) => ({
|
|
2738
2777
|
name: `${policy.table}.${policy.name}`,
|
|
@@ -4074,13 +4113,16 @@ async function runGitConnect(context, overrides = {}) {
|
|
|
4074
4113
|
if (cannotAsk(context)) {
|
|
4075
4114
|
throw new NaroError(
|
|
4076
4115
|
"REPOSITORY_NOT_ACCESSIBLE",
|
|
4077
|
-
`Naro cannot reach ${fullName} yet.
|
|
4116
|
+
`Naro cannot reach ${fullName} yet. Open ${first.installUrl} to give Naro access (if the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first), then run: naro git connect`,
|
|
4078
4117
|
{ details: { projectId, installUrl: first.installUrl } }
|
|
4079
4118
|
);
|
|
4080
4119
|
}
|
|
4081
4120
|
output.log(`
|
|
4082
4121
|
\u25B8 Naro needs access to ${fullName}.`);
|
|
4083
4122
|
output.log(` Opening ${first.installUrl}`);
|
|
4123
|
+
output.log(
|
|
4124
|
+
" If the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first."
|
|
4125
|
+
);
|
|
4084
4126
|
deps.openBrowser(first.installUrl);
|
|
4085
4127
|
output.log(" Waiting for the installation to finish...\n");
|
|
4086
4128
|
await waitForInstall(context, projectId, fullName, deps);
|
|
@@ -4848,6 +4890,7 @@ var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
4848
4890
|
var KEY_MAX_LENGTH = 256;
|
|
4849
4891
|
var IMPORT_BATCH_SIZE = 100;
|
|
4850
4892
|
var VALUE_MAX_BYTES = 5 * 1024;
|
|
4893
|
+
var STATIC_ASSETS_BINDING = "ASSETS";
|
|
4851
4894
|
var TARGET_USAGE = "[production|preview|all|<environment>]";
|
|
4852
4895
|
function validKey(key) {
|
|
4853
4896
|
return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
|
|
@@ -4960,7 +5003,14 @@ async function runEnvImport(context) {
|
|
|
4960
5003
|
} catch {
|
|
4961
5004
|
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
4962
5005
|
}
|
|
4963
|
-
const
|
|
5006
|
+
const parsed = parseDotenvDetailed(text);
|
|
5007
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5008
|
+
if (unclosed !== void 0)
|
|
5009
|
+
throw new NaroError(
|
|
5010
|
+
"VALIDATION_FAILED",
|
|
5011
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5012
|
+
);
|
|
5013
|
+
const vars = Object.entries(parsed.vars);
|
|
4964
5014
|
if (vars.length === 0)
|
|
4965
5015
|
throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
|
|
4966
5016
|
const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
|
|
@@ -4969,6 +5019,11 @@ async function runEnvImport(context) {
|
|
|
4969
5019
|
"VALIDATION_FAILED",
|
|
4970
5020
|
`Invalid keys in ${file}: ${invalid.join(", ")}.`
|
|
4971
5021
|
);
|
|
5022
|
+
if (vars.some(([key]) => key === STATIC_ASSETS_BINDING))
|
|
5023
|
+
throw new NaroError(
|
|
5024
|
+
"VALIDATION_FAILED",
|
|
5025
|
+
`${STATIC_ASSETS_BINDING} is reserved for the project's static files; pick another name.`
|
|
5026
|
+
);
|
|
4972
5027
|
const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
|
|
4973
5028
|
if (oversized.length > 0)
|
|
4974
5029
|
throw new NaroError(
|
|
@@ -4978,7 +5033,9 @@ async function runEnvImport(context) {
|
|
|
4978
5033
|
const projectId = await context.resolveProjectId();
|
|
4979
5034
|
context.output.log(`Variables in ${file}:`);
|
|
4980
5035
|
for (const [key, value] of vars)
|
|
4981
|
-
context.output.log(
|
|
5036
|
+
context.output.log(
|
|
5037
|
+
` ${key.padEnd(32)} ${value === "" ? "(empty)" : maskEnvValue(key, value)}`
|
|
5038
|
+
);
|
|
4982
5039
|
await confirmOrAbort(
|
|
4983
5040
|
context,
|
|
4984
5041
|
`Upload ${variables(vars.length)} to ${target}?`
|
|
@@ -4990,6 +5047,15 @@ async function runEnvImport(context) {
|
|
|
4990
5047
|
target,
|
|
4991
5048
|
isSecret: isSecretKey(key)
|
|
4992
5049
|
}));
|
|
5050
|
+
if (entries.length > IMPORT_BATCH_SIZE) {
|
|
5051
|
+
const taken = new Set(await client.env.takenBindingNames(projectId));
|
|
5052
|
+
const conflict = entries.find((entry) => taken.has(entry.key));
|
|
5053
|
+
if (conflict)
|
|
5054
|
+
throw new NaroError(
|
|
5055
|
+
"VALIDATION_FAILED",
|
|
5056
|
+
`${conflict.key} is already used by a storage binding.`
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
4993
5059
|
for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
|
|
4994
5060
|
await client.env.set(
|
|
4995
5061
|
projectId,
|
|
@@ -5017,17 +5083,14 @@ async function runEnvPull(context) {
|
|
|
5017
5083
|
const rows = await client.env.list(projectId, { reveal: true });
|
|
5018
5084
|
const vars = pullEnv(rows, surface);
|
|
5019
5085
|
const count2 = Object.keys(vars).length;
|
|
5086
|
+
const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
5087
|
+
${serializeDotenv(vars)}`;
|
|
5020
5088
|
const path = resolve4(context.cwd, file);
|
|
5021
5089
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
5022
5090
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
5023
5091
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
5024
5092
|
}
|
|
5025
|
-
writeFileSync4(
|
|
5026
|
-
path,
|
|
5027
|
-
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
5028
|
-
${serializeDotenv(vars)}`,
|
|
5029
|
-
{ mode: 384 }
|
|
5030
|
-
);
|
|
5093
|
+
writeFileSync4(path, text, { mode: 384 });
|
|
5031
5094
|
chmodSync(path, 384);
|
|
5032
5095
|
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5033
5096
|
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
@@ -8055,6 +8118,7 @@ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (
|
|
|
8055
8118
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
8056
8119
|
var isSignedEntry = (value) => isRecord(value) && typeof value.path === "string" && (value.signedUrl === null || typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX)) && (value.error === null || typeof value.error === "string");
|
|
8057
8120
|
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";
|
|
8121
|
+
var isMoved = (value) => isRecord(value) && typeof value.message === "string";
|
|
8058
8122
|
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
8059
8123
|
function listOf(guard) {
|
|
8060
8124
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
@@ -8210,9 +8274,76 @@ var StorageFileApi = class {
|
|
|
8210
8274
|
listOf(isFileObject)
|
|
8211
8275
|
);
|
|
8212
8276
|
}
|
|
8277
|
+
/**
|
|
8278
|
+
* The body a move and a copy send (Storage 3-1i): the bucket and both
|
|
8279
|
+
* names, as supabase-js sends them. The names are not checked here — they
|
|
8280
|
+
* are in the body, and the backend answers a bad one `INVALID_REQUEST`
|
|
8281
|
+
* (`details.path` "sourceKey" or "destinationKey"), as `remove` does.
|
|
8282
|
+
*/
|
|
8283
|
+
#transferBody(fromPath, toPath, options) {
|
|
8284
|
+
const { destinationBucket, upsert } = options;
|
|
8285
|
+
return {
|
|
8286
|
+
bucketId: this.#bucket,
|
|
8287
|
+
sourceKey: fromPath,
|
|
8288
|
+
destinationKey: toPath,
|
|
8289
|
+
...destinationBucket === void 0 ? {} : { destinationBucket },
|
|
8290
|
+
...upsert === true ? { upsert: true } : {}
|
|
8291
|
+
};
|
|
8292
|
+
}
|
|
8293
|
+
/**
|
|
8294
|
+
* Renames an object inside its bucket (Storage 3-1i): the same object — its
|
|
8295
|
+
* id and owner — under `toPath`, its old name gone. It never replaces an
|
|
8296
|
+
* object that is there (`OBJECT_EXISTS`), takes files up to 5 GiB − 5 MiB
|
|
8297
|
+
* (`OBJECT_TOO_LARGE`), and needs room in the project for a second copy
|
|
8298
|
+
* until it is done (`STORAGE_QUOTA_EXCEEDED`). Its signed links stop
|
|
8299
|
+
* working at the old name: a link is bound to the name, the object's id
|
|
8300
|
+
* and its bytes, so moving it back can open them again. A backend whose
|
|
8301
|
+
* Storage was turned on before this method came answers `NOT_FOUND`:
|
|
8302
|
+
* update it with `naro backend enable`.
|
|
8303
|
+
*/
|
|
8304
|
+
async move(fromPath, toPath, options = {}) {
|
|
8305
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8306
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8307
|
+
const { destinationBucket } = options ?? {};
|
|
8308
|
+
return this.#json(
|
|
8309
|
+
{
|
|
8310
|
+
method: "POST",
|
|
8311
|
+
path: "/storage/object/move",
|
|
8312
|
+
body: this.#transferBody(
|
|
8313
|
+
fromPath,
|
|
8314
|
+
toPath,
|
|
8315
|
+
destinationBucket === void 0 ? {} : { destinationBucket }
|
|
8316
|
+
)
|
|
8317
|
+
},
|
|
8318
|
+
isMoved
|
|
8319
|
+
);
|
|
8320
|
+
}
|
|
8321
|
+
/**
|
|
8322
|
+
* Copies an object inside its bucket (Storage 3-1i): the source's bytes,
|
|
8323
|
+
* type and cache-control under `toPath` — a new object of the caller's,
|
|
8324
|
+
* or with `upsert: true` the one that is there, which keeps its owner.
|
|
8325
|
+
* Files up to 5 GiB − 5 MiB, within the bucket's limits as they are now.
|
|
8326
|
+
* It answers `{ id, path, fullPath }` — `path` is the name inside the
|
|
8327
|
+
* bucket; supabase-js's copy puts the bucket in front of it. A backend
|
|
8328
|
+
* whose Storage was turned on before this method came answers
|
|
8329
|
+
* `NOT_FOUND`: update it with `naro backend enable`.
|
|
8330
|
+
*/
|
|
8331
|
+
async copy(fromPath, toPath, options = {}) {
|
|
8332
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8333
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8334
|
+
return this.#json(
|
|
8335
|
+
{
|
|
8336
|
+
method: "POST",
|
|
8337
|
+
path: "/storage/object/copy",
|
|
8338
|
+
body: this.#transferBody(fromPath, toPath, options ?? {})
|
|
8339
|
+
},
|
|
8340
|
+
isUploadResult
|
|
8341
|
+
);
|
|
8342
|
+
}
|
|
8213
8343
|
/**
|
|
8214
8344
|
* A link to the object for `expiresIn` seconds (1–604,800). It stops
|
|
8215
|
-
* working once the object is replaced or removed
|
|
8345
|
+
* working once the object is replaced or removed, and while the object is
|
|
8346
|
+
* moved away from `path` (moved back, it can open it again).
|
|
8216
8347
|
*/
|
|
8217
8348
|
async createSignedUrl(path, expiresIn, options = {}) {
|
|
8218
8349
|
const target = this.#objectPath("sign/", path);
|
|
@@ -8503,7 +8634,8 @@ function createClient(options) {
|
|
|
8503
8634
|
|
|
8504
8635
|
// src/commands/storage-objects.ts
|
|
8505
8636
|
var NARO_SCHEME = "naro://";
|
|
8506
|
-
var CP_USAGE = "Usage: naro storage cp <from> <to> (
|
|
8637
|
+
var CP_USAGE = "Usage: naro storage cp <from> <to> (a local file and naro://<bucket>/<path>, or two naro://<bucket>/<path> in one bucket)";
|
|
8638
|
+
var MV_USAGE = "Usage: naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>";
|
|
8507
8639
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
8508
8640
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
|
|
8509
8641
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
@@ -8516,6 +8648,10 @@ function isOldSearchRefusal(error) {
|
|
|
8516
8648
|
const { path, reason } = details;
|
|
8517
8649
|
return path === "search" && reason === OLD_SEARCH_REASON;
|
|
8518
8650
|
}
|
|
8651
|
+
var NO_ENDPOINT = "There is no Naro endpoint at this path.";
|
|
8652
|
+
function isOldRouteRefusal(error) {
|
|
8653
|
+
return error !== null && error.code === "NOT_FOUND" && error.message === NO_ENDPOINT;
|
|
8654
|
+
}
|
|
8519
8655
|
var FOUR_GIB = 4294967296;
|
|
8520
8656
|
var PAGE = 1e3;
|
|
8521
8657
|
var MAX_OFFSET = 1e5;
|
|
@@ -8982,17 +9118,106 @@ async function download(context, source, local) {
|
|
|
8982
9118
|
]
|
|
8983
9119
|
);
|
|
8984
9120
|
}
|
|
8985
|
-
|
|
9121
|
+
function unwrapTransfer(result) {
|
|
9122
|
+
if (isOldRouteRefusal(result.error))
|
|
9123
|
+
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
9124
|
+
return unwrap(result);
|
|
9125
|
+
}
|
|
9126
|
+
function refuseBadName(bucket, name) {
|
|
9127
|
+
const route2 = objectRoute(bucket, name);
|
|
9128
|
+
if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
|
|
9129
|
+
const checked = checkObjectName(name);
|
|
9130
|
+
if (!checked.ok) {
|
|
9131
|
+
const { code, message } = dataError("INVALID_PATH", {
|
|
9132
|
+
reason: checked.reason
|
|
9133
|
+
});
|
|
9134
|
+
throw new NaroError(code, message);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
function insideStorage(source, target, command) {
|
|
9138
|
+
if (source.bucket !== target.bucket)
|
|
9139
|
+
throw new NaroError(
|
|
9140
|
+
"VALIDATION_FAILED",
|
|
9141
|
+
"naro storage cp and mv work inside one bucket: both sides must be in naro://<bucket>."
|
|
9142
|
+
);
|
|
9143
|
+
if (source.path === "" || source.path.endsWith("/"))
|
|
9144
|
+
throw new NaroError(
|
|
9145
|
+
"VALIDATION_FAILED",
|
|
9146
|
+
`naro storage ${command} ${command === "mv" ? "moves" : "copies"} one object: name it, naro://<bucket>/<path>.`
|
|
9147
|
+
);
|
|
9148
|
+
const name = source.path.slice(source.path.lastIndexOf("/") + 1);
|
|
9149
|
+
const to = target.path === "" || target.path.endsWith("/") ? `${target.path}${name}` : target.path;
|
|
9150
|
+
refuseBadName(source.bucket, source.path);
|
|
9151
|
+
refuseBadName(source.bucket, to);
|
|
9152
|
+
if (to === source.path)
|
|
9153
|
+
throw new NaroError(
|
|
9154
|
+
"VALIDATION_FAILED",
|
|
9155
|
+
`naro storage ${command} needs a different destination: ${printable(`${NARO_SCHEME}${source.bucket}/${source.path}`)} is the source.`
|
|
9156
|
+
);
|
|
9157
|
+
return { bucket: source.bucket, from: source.path, to };
|
|
9158
|
+
}
|
|
9159
|
+
async function copyInside(context, source, target) {
|
|
9160
|
+
const { contentType, cacheControl } = context.flags;
|
|
9161
|
+
if (contentType !== void 0 || cacheControl !== void 0)
|
|
9162
|
+
throw new NaroError(
|
|
9163
|
+
"VALIDATION_FAILED",
|
|
9164
|
+
"A copy inside Storage keeps the object's type and cache-control: --content-type and --cache-control are for uploads."
|
|
9165
|
+
);
|
|
9166
|
+
if (context.flags.recursive === true)
|
|
9167
|
+
throw new NaroError(
|
|
9168
|
+
"VALIDATION_FAILED",
|
|
9169
|
+
"A copy inside Storage takes no --recursive: it copies one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
9170
|
+
);
|
|
9171
|
+
const { bucket, from, to } = insideStorage(source, target, "cp");
|
|
9172
|
+
const { projectId, storage } = await objects(context);
|
|
9173
|
+
const object = unwrapTransfer(
|
|
9174
|
+
await storage.from(bucket).copy(from, to, { upsert: context.flags.upsert === true })
|
|
9175
|
+
);
|
|
9176
|
+
const fromUrl = `${NARO_SCHEME}${bucket}/${from}`;
|
|
9177
|
+
const toUrl = `${NARO_SCHEME}${bucket}/${to}`;
|
|
9178
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl, object }, [
|
|
9179
|
+
`\u2705 Copied ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9180
|
+
]);
|
|
9181
|
+
}
|
|
9182
|
+
async function storageMv(context) {
|
|
8986
9183
|
const [from, to, ...extra] = context.positionals;
|
|
8987
9184
|
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
8988
|
-
throw new NaroError("VALIDATION_FAILED",
|
|
9185
|
+
throw new NaroError("VALIDATION_FAILED", MV_USAGE);
|
|
8989
9186
|
const source = parseStorageUrl(from);
|
|
8990
9187
|
const target = parseStorageUrl(to);
|
|
8991
|
-
if (source
|
|
9188
|
+
if (source === null || target === null)
|
|
9189
|
+
throw new NaroError(
|
|
9190
|
+
"VALIDATION_FAILED",
|
|
9191
|
+
"naro storage mv moves inside Storage: both sides are naro://<bucket>/<path>."
|
|
9192
|
+
);
|
|
9193
|
+
const { upsert, contentType, cacheControl } = context.flags;
|
|
9194
|
+
if (upsert === true || contentType !== void 0 || cacheControl !== void 0)
|
|
8992
9195
|
throw new NaroError(
|
|
8993
9196
|
"VALIDATION_FAILED",
|
|
8994
|
-
"naro storage
|
|
9197
|
+
"naro storage mv takes no --upsert, --content-type or --cache-control: a move keeps the object as it is and never replaces one."
|
|
8995
9198
|
);
|
|
9199
|
+
if (context.flags.recursive === true)
|
|
9200
|
+
throw new NaroError(
|
|
9201
|
+
"VALIDATION_FAILED",
|
|
9202
|
+
"naro storage mv takes no --recursive: it moves one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
9203
|
+
);
|
|
9204
|
+
const moved = insideStorage(source, target, "mv");
|
|
9205
|
+
const { projectId, storage } = await objects(context);
|
|
9206
|
+
unwrapTransfer(await storage.from(moved.bucket).move(moved.from, moved.to));
|
|
9207
|
+
const fromUrl = `${NARO_SCHEME}${moved.bucket}/${moved.from}`;
|
|
9208
|
+
const toUrl = `${NARO_SCHEME}${moved.bucket}/${moved.to}`;
|
|
9209
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl }, [
|
|
9210
|
+
`\u2705 Moved ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9211
|
+
]);
|
|
9212
|
+
}
|
|
9213
|
+
async function storageCp(context) {
|
|
9214
|
+
const [from, to, ...extra] = context.positionals;
|
|
9215
|
+
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
9216
|
+
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
9217
|
+
const source = parseStorageUrl(from);
|
|
9218
|
+
const target = parseStorageUrl(to);
|
|
9219
|
+
if (source !== null && target !== null)
|
|
9220
|
+
return copyInside(context, source, target);
|
|
8996
9221
|
if (target !== null) return upload(context, from, target);
|
|
8997
9222
|
if (source !== null) return download(context, source, to);
|
|
8998
9223
|
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
@@ -9106,6 +9331,9 @@ function runStorageLs(context) {
|
|
|
9106
9331
|
function runStorageCp(context) {
|
|
9107
9332
|
return explainingKey(context, storageCp);
|
|
9108
9333
|
}
|
|
9334
|
+
function runStorageMv(context) {
|
|
9335
|
+
return explainingKey(context, storageMv);
|
|
9336
|
+
}
|
|
9109
9337
|
function runStorageRm(context) {
|
|
9110
9338
|
return explainingKey(context, storageRm);
|
|
9111
9339
|
}
|
|
@@ -9388,7 +9616,10 @@ Usage:
|
|
|
9388
9616
|
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
9389
9617
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
9390
9618
|
A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
|
|
9391
|
-
A download replaces a local file of the same name
|
|
9619
|
+
A download replaces a local file of the same name.
|
|
9620
|
+
Two naro:// paths in one bucket: a copy inside Storage, up to 5 GiB \u2212 5 MiB (--upsert replaces)
|
|
9621
|
+
naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>
|
|
9622
|
+
Move an object inside its bucket, up to 5 GiB \u2212 5 MiB; it never replaces one that is there
|
|
9392
9623
|
naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
|
|
9393
9624
|
Remove objects; --recursive removes what is under a folder (asks first):
|
|
9394
9625
|
<path>/ is the folder alone, <path> also an object of that name
|
|
@@ -9458,8 +9689,8 @@ Global options:
|
|
|
9458
9689
|
--recursive, -r Everything under a folder (naro storage ls, naro storage rm)
|
|
9459
9690
|
--search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
|
|
9460
9691
|
--upsert Replace an object that is there (naro storage cp)
|
|
9461
|
-
--content-type <t> The object's type; by default from the file's extension (naro storage cp)
|
|
9462
|
-
--cache-control <seconds> How long a browser may keep
|
|
9692
|
+
--content-type <t> The uploaded object's type; by default from the file's extension (naro storage cp, uploads only)
|
|
9693
|
+
--cache-control <seconds> How long a browser may keep an upload; 3600 by default (naro storage cp, uploads only)
|
|
9463
9694
|
--select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
|
|
9464
9695
|
--owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
|
|
9465
9696
|
--select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
|
|
@@ -9597,6 +9828,8 @@ function routeStorage(words) {
|
|
|
9597
9828
|
return { handler: runStorageLs, positionals: words.slice(1) };
|
|
9598
9829
|
case "cp":
|
|
9599
9830
|
return { handler: runStorageCp, positionals: words.slice(1) };
|
|
9831
|
+
case "mv":
|
|
9832
|
+
return { handler: runStorageMv, positionals: words.slice(1) };
|
|
9600
9833
|
case "rm":
|
|
9601
9834
|
return { handler: runStorageRm, positionals: words.slice(1) };
|
|
9602
9835
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"tsx": "4.23.12",
|
|
34
34
|
"typescript": "7.0.2",
|
|
35
35
|
"vitest": "3.2.7",
|
|
36
|
-
"naro-js": "0.
|
|
37
|
-
"@next-deploy/
|
|
36
|
+
"naro-js": "0.6.0",
|
|
37
|
+
"@next-deploy/build": "0.1.0",
|
|
38
38
|
"@next-deploy/core": "0.1.0",
|
|
39
39
|
"@next-deploy/config": "0.1.0",
|
|
40
|
-
"@next-deploy/
|
|
40
|
+
"@next-deploy/sdk": "0.1.0",
|
|
41
41
|
"@next-deploy/data-core": "0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|