neonctl 2.27.1 → 2.29.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 +35 -3
- package/dist/analytics.js +52 -34
- package/dist/api.js +643 -13
- package/dist/auth.js +50 -44
- package/dist/cli.js +8 -1
- package/dist/commands/auth.js +64 -51
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +160 -150
- package/dist/commands/bucket.js +183 -146
- package/dist/commands/checkout.js +51 -51
- package/dist/commands/config.js +228 -82
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +100 -101
- package/dist/commands/databases.js +29 -26
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +101 -104
- package/dist/commands/index.js +27 -25
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +232 -182
- package/dist/commands/neon_auth.js +385 -370
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +103 -101
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +27 -24
- package/dist/commands/schema_diff.js +25 -26
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +37 -14
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +33 -33
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +68 -5
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +110 -107
- package/dist/log.js +2 -2
- package/dist/parameters.gen.js +14 -14
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +22 -23
- package/dist/test_utils/fixtures.js +74 -41
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +28 -16
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +10 -12
package/dist/commands/bucket.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { createReadStream, createWriteStream } from
|
|
2
|
-
import { stat, unlink } from
|
|
3
|
-
import { basename } from
|
|
4
|
-
import { pipeline } from
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { writer } from
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const ACCESS_LEVELS = ['private', 'public_read'];
|
|
1
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
+
import { stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { isNeonApiError, retryOnLock } from "../api.js";
|
|
6
|
+
import { log } from "../log.js";
|
|
7
|
+
import { createProjectBranchBucket, deleteProjectBranchBucket, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, getProjectBranchBucketObject, listProjectBranchBucketObjects, listProjectBranchBuckets, presignUpload, } from "../storage_api.js";
|
|
8
|
+
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
9
|
+
import { writer } from "../writer.js";
|
|
10
|
+
const OBJECT_FIELDS = ["key", "size", "last_modified", "etag"];
|
|
11
|
+
const BUCKET_FIELDS = ["name", "access_level"];
|
|
12
|
+
const ACCESS_LEVELS = ["private", "public_read"];
|
|
14
13
|
// Single-PUT upload cap. Objects larger than this must use multipart upload,
|
|
15
14
|
// which is out of scope for v1; we reject them client-side before any HTTP so
|
|
16
15
|
// the user gets an immediate, clear error rather than a server-side rejection
|
|
@@ -19,13 +18,13 @@ const MAX_OBJECT_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
|
19
18
|
// Ambient scope shared by every bucket sub-command. The bucket name (and the
|
|
20
19
|
// object key/prefix) is always a positional, never a flag.
|
|
21
20
|
const scopeOptions = {
|
|
22
|
-
|
|
23
|
-
describe:
|
|
24
|
-
type:
|
|
21
|
+
"project-id": {
|
|
22
|
+
describe: "Project ID",
|
|
23
|
+
type: "string",
|
|
25
24
|
},
|
|
26
25
|
branch: {
|
|
27
|
-
describe:
|
|
28
|
-
type:
|
|
26
|
+
describe: "Branch ID or name",
|
|
27
|
+
type: "string",
|
|
29
28
|
},
|
|
30
29
|
};
|
|
31
30
|
// Split an object target into its bucket and the remainder (key or prefix) on
|
|
@@ -33,156 +32,156 @@ const scopeOptions = {
|
|
|
33
32
|
// remainder may contain further slashes and is returned verbatim. When the
|
|
34
33
|
// target has no slash, `rest` is the empty string.
|
|
35
34
|
export const splitBucketTarget = (target) => {
|
|
36
|
-
const slash = target.indexOf(
|
|
35
|
+
const slash = target.indexOf("/");
|
|
37
36
|
if (slash === -1) {
|
|
38
|
-
return { bucket: target, rest:
|
|
37
|
+
return { bucket: target, rest: "" };
|
|
39
38
|
}
|
|
40
39
|
return {
|
|
41
40
|
bucket: target.slice(0, slash),
|
|
42
41
|
rest: target.slice(slash + 1),
|
|
43
42
|
};
|
|
44
43
|
};
|
|
45
|
-
export const command =
|
|
46
|
-
export const describe =
|
|
47
|
-
export const aliases = [
|
|
44
|
+
export const command = "buckets";
|
|
45
|
+
export const describe = "Manage branch object-storage buckets and their objects";
|
|
46
|
+
export const aliases = ["bucket"];
|
|
48
47
|
export const builder = (argv) => argv
|
|
49
|
-
.usage(
|
|
48
|
+
.usage("$0 bucket <sub-command> [options]")
|
|
50
49
|
.options({
|
|
51
|
-
|
|
52
|
-
describe:
|
|
53
|
-
type:
|
|
50
|
+
"project-id": {
|
|
51
|
+
describe: "Project ID",
|
|
52
|
+
type: "string",
|
|
54
53
|
},
|
|
55
54
|
})
|
|
56
55
|
.middleware(fillSingleProject)
|
|
57
|
-
.command(
|
|
58
|
-
.usage(
|
|
59
|
-
.positional(
|
|
60
|
-
describe:
|
|
61
|
-
type:
|
|
56
|
+
.command("create <name>", "Create a bucket on a branch", (yargs) => yargs
|
|
57
|
+
.usage("$0 bucket create <name> [options]")
|
|
58
|
+
.positional("name", {
|
|
59
|
+
describe: "The bucket name to create",
|
|
60
|
+
type: "string",
|
|
62
61
|
demandOption: true,
|
|
63
62
|
})
|
|
64
63
|
.options({
|
|
65
64
|
...scopeOptions,
|
|
66
|
-
|
|
67
|
-
describe:
|
|
68
|
-
type:
|
|
65
|
+
"access-level": {
|
|
66
|
+
describe: "The visibility of the bucket",
|
|
67
|
+
type: "string",
|
|
69
68
|
choices: ACCESS_LEVELS,
|
|
70
|
-
default:
|
|
69
|
+
default: "private",
|
|
71
70
|
},
|
|
72
71
|
}), (args) => createBucket(args))
|
|
73
72
|
.command({
|
|
74
|
-
command:
|
|
75
|
-
aliases: [
|
|
76
|
-
describe:
|
|
77
|
-
builder: (yargs) => yargs.usage(
|
|
73
|
+
command: "list",
|
|
74
|
+
aliases: ["ls"],
|
|
75
|
+
describe: "List the buckets on a branch",
|
|
76
|
+
builder: (yargs) => yargs.usage("$0 bucket list [options]").options(scopeOptions),
|
|
78
77
|
handler: (args) => listBuckets(args),
|
|
79
78
|
})
|
|
80
79
|
.command({
|
|
81
|
-
command:
|
|
82
|
-
aliases: [
|
|
83
|
-
describe:
|
|
80
|
+
command: "delete <name>",
|
|
81
|
+
aliases: ["rm"],
|
|
82
|
+
describe: "Delete a bucket from a branch",
|
|
84
83
|
builder: (yargs) => yargs
|
|
85
|
-
.usage(
|
|
86
|
-
.positional(
|
|
87
|
-
describe:
|
|
88
|
-
type:
|
|
84
|
+
.usage("$0 bucket delete <name> [options]")
|
|
85
|
+
.positional("name", {
|
|
86
|
+
describe: "The bucket name to delete",
|
|
87
|
+
type: "string",
|
|
89
88
|
demandOption: true,
|
|
90
89
|
})
|
|
91
90
|
.options(scopeOptions),
|
|
92
91
|
handler: (args) => deleteBucket(args),
|
|
93
92
|
})
|
|
94
|
-
.command(
|
|
95
|
-
.usage(
|
|
93
|
+
.command("object <sub-command>", "List, download, upload or delete objects in a bucket", (yargs) => yargs
|
|
94
|
+
.usage("$0 bucket object <sub-command> [options]")
|
|
96
95
|
.command({
|
|
97
|
-
command:
|
|
98
|
-
aliases: [
|
|
96
|
+
command: "list <target>",
|
|
97
|
+
aliases: ["ls"],
|
|
99
98
|
describe: 'List objects in a bucket. By default folders are collapsed (like "aws s3 ls"); pass --recursive for a flat listing of every key',
|
|
100
99
|
builder: (yargs) => yargs
|
|
101
|
-
.usage(
|
|
102
|
-
.positional(
|
|
103
|
-
describe:
|
|
104
|
-
type:
|
|
100
|
+
.usage("$0 bucket object list <bucket>[/<prefix>] [options]")
|
|
101
|
+
.positional("target", {
|
|
102
|
+
describe: "The bucket to list, optionally with a key prefix: <bucket>[/<prefix>]",
|
|
103
|
+
type: "string",
|
|
105
104
|
demandOption: true,
|
|
106
105
|
})
|
|
107
106
|
.options({
|
|
108
107
|
...scopeOptions,
|
|
109
108
|
recursive: {
|
|
110
109
|
describe: 'List every key flat, descending into nested folders (no delimiter). Mutually exclusive with --delimiter. Mirrors "aws s3 ls --recursive"',
|
|
111
|
-
type:
|
|
110
|
+
type: "boolean",
|
|
112
111
|
default: false,
|
|
113
112
|
},
|
|
114
113
|
delimiter: {
|
|
115
114
|
describe: 'Collapse keys sharing this prefix separator into folders. Defaults to "/" (folder view); ignored when --recursive is set',
|
|
116
|
-
type:
|
|
115
|
+
type: "string",
|
|
117
116
|
},
|
|
118
117
|
cursor: {
|
|
119
|
-
describe:
|
|
120
|
-
type:
|
|
118
|
+
describe: "Pagination cursor returned as next_cursor by a previous call",
|
|
119
|
+
type: "string",
|
|
121
120
|
},
|
|
122
121
|
limit: {
|
|
123
|
-
describe:
|
|
124
|
-
type:
|
|
122
|
+
describe: "Maximum number of items (objects + folders) to return",
|
|
123
|
+
type: "number",
|
|
125
124
|
},
|
|
126
125
|
}),
|
|
127
126
|
handler: (args) => listObjects(args),
|
|
128
127
|
})
|
|
129
|
-
.command(
|
|
130
|
-
.usage(
|
|
131
|
-
.positional(
|
|
132
|
-
describe:
|
|
133
|
-
type:
|
|
128
|
+
.command("get <target>", "Download an object from a bucket to a local file", (yargs) => yargs
|
|
129
|
+
.usage("$0 bucket object get <bucket>/<key> [options]")
|
|
130
|
+
.positional("target", {
|
|
131
|
+
describe: "The object to download: <bucket>/<key>",
|
|
132
|
+
type: "string",
|
|
134
133
|
demandOption: true,
|
|
135
134
|
})
|
|
136
135
|
.options({
|
|
137
136
|
...scopeOptions,
|
|
138
137
|
file: {
|
|
139
|
-
describe:
|
|
140
|
-
type:
|
|
138
|
+
describe: "Path to write the downloaded object to (defaults to the object filename in the current directory)",
|
|
139
|
+
type: "string",
|
|
141
140
|
},
|
|
142
141
|
}), (args) => getObject(args))
|
|
143
|
-
.command(
|
|
144
|
-
.usage(
|
|
145
|
-
.positional(
|
|
146
|
-
describe:
|
|
147
|
-
type:
|
|
142
|
+
.command("put <target>", "Upload a local file to a bucket as an object", (yargs) => yargs
|
|
143
|
+
.usage("$0 bucket object put <bucket>/<key> [options]")
|
|
144
|
+
.positional("target", {
|
|
145
|
+
describe: "The object to upload to: <bucket>/<key>",
|
|
146
|
+
type: "string",
|
|
148
147
|
demandOption: true,
|
|
149
148
|
})
|
|
150
149
|
.options({
|
|
151
150
|
...scopeOptions,
|
|
152
151
|
file: {
|
|
153
|
-
describe:
|
|
154
|
-
type:
|
|
152
|
+
describe: "Path to the local file to upload",
|
|
153
|
+
type: "string",
|
|
155
154
|
demandOption: true,
|
|
156
155
|
},
|
|
157
|
-
|
|
158
|
-
describe:
|
|
159
|
-
type:
|
|
156
|
+
"content-type": {
|
|
157
|
+
describe: "Content-Type to store the object with (e.g. text/plain)",
|
|
158
|
+
type: "string",
|
|
160
159
|
},
|
|
161
160
|
}), (args) => putObject(args))
|
|
162
161
|
.command({
|
|
163
|
-
command:
|
|
164
|
-
aliases: [
|
|
165
|
-
describe:
|
|
162
|
+
command: "delete <target>",
|
|
163
|
+
aliases: ["rm"],
|
|
164
|
+
describe: "Delete an object, or every object under a prefix",
|
|
166
165
|
builder: (yargs) => yargs
|
|
167
|
-
.usage(
|
|
168
|
-
.positional(
|
|
169
|
-
describe:
|
|
170
|
-
type:
|
|
166
|
+
.usage("$0 bucket object delete <bucket>/<key> [options]")
|
|
167
|
+
.positional("target", {
|
|
168
|
+
describe: "The object to delete: <bucket>/<key>, or <bucket>/<prefix>/ with --recursive",
|
|
169
|
+
type: "string",
|
|
171
170
|
demandOption: true,
|
|
172
171
|
})
|
|
173
172
|
.options({
|
|
174
173
|
...scopeOptions,
|
|
175
174
|
recursive: {
|
|
176
175
|
describe: 'Delete every object under the given prefix. The prefix must end with "/"',
|
|
177
|
-
type:
|
|
176
|
+
type: "boolean",
|
|
178
177
|
default: false,
|
|
179
178
|
},
|
|
180
179
|
}),
|
|
181
180
|
handler: (args) => deleteObject(args),
|
|
182
181
|
})
|
|
183
|
-
.demandCommand(1,
|
|
182
|
+
.demandCommand(1, "")
|
|
184
183
|
.strictCommands())
|
|
185
|
-
.demandCommand(1,
|
|
184
|
+
.demandCommand(1, "");
|
|
186
185
|
export const handler = (args) => {
|
|
187
186
|
return args;
|
|
188
187
|
};
|
|
@@ -202,14 +201,14 @@ const listBuckets = async (props) => {
|
|
|
202
201
|
projectId: props.projectId,
|
|
203
202
|
branchId,
|
|
204
203
|
});
|
|
205
|
-
if (props.output ===
|
|
204
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
206
205
|
writer(props).end(data.buckets, { fields: BUCKET_FIELDS });
|
|
207
206
|
return;
|
|
208
207
|
}
|
|
209
208
|
writer(props).end(data.buckets, {
|
|
210
209
|
fields: BUCKET_FIELDS,
|
|
211
|
-
title:
|
|
212
|
-
emptyMessage:
|
|
210
|
+
title: "buckets",
|
|
211
|
+
emptyMessage: "No buckets found.",
|
|
213
212
|
});
|
|
214
213
|
};
|
|
215
214
|
const deleteBucket = async (props) => {
|
|
@@ -222,7 +221,7 @@ const deleteBucket = async (props) => {
|
|
|
222
221
|
}));
|
|
223
222
|
}
|
|
224
223
|
catch (err) {
|
|
225
|
-
if (
|
|
224
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
226
225
|
throw new Error(`Bucket "${props.name}" not found on branch ${branchId}.`);
|
|
227
226
|
}
|
|
228
227
|
throw err;
|
|
@@ -237,7 +236,7 @@ const deleteBucket = async (props) => {
|
|
|
237
236
|
// rejected client-side before any HTTP request is made.
|
|
238
237
|
export const resolveListDelimiter = (props) => {
|
|
239
238
|
if (props.recursive && props.delimiter !== undefined) {
|
|
240
|
-
throw new Error(
|
|
239
|
+
throw new Error("--recursive and --delimiter cannot be used together. Use --recursive for a flat listing, or --delimiter to collapse on a separator.");
|
|
241
240
|
}
|
|
242
241
|
if (props.recursive) {
|
|
243
242
|
return undefined;
|
|
@@ -245,7 +244,7 @@ export const resolveListDelimiter = (props) => {
|
|
|
245
244
|
if (props.delimiter !== undefined) {
|
|
246
245
|
return props.delimiter;
|
|
247
246
|
}
|
|
248
|
-
return
|
|
247
|
+
return "/";
|
|
249
248
|
};
|
|
250
249
|
const listObjects = async (props) => {
|
|
251
250
|
const delimiter = resolveListDelimiter(props);
|
|
@@ -255,25 +254,31 @@ const listObjects = async (props) => {
|
|
|
255
254
|
projectId: props.projectId,
|
|
256
255
|
branchId,
|
|
257
256
|
bucketName: bucket,
|
|
258
|
-
prefix: rest ===
|
|
257
|
+
prefix: rest === "" ? undefined : rest,
|
|
259
258
|
delimiter,
|
|
260
259
|
cursor: props.cursor,
|
|
261
260
|
limit: props.limit,
|
|
262
261
|
});
|
|
263
|
-
if (props.output ===
|
|
262
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
264
263
|
writer(props).end(data, {
|
|
265
|
-
fields: [
|
|
264
|
+
fields: [
|
|
265
|
+
"folders",
|
|
266
|
+
"objects",
|
|
267
|
+
"prefix",
|
|
268
|
+
"next_cursor",
|
|
269
|
+
"is_truncated",
|
|
270
|
+
],
|
|
266
271
|
});
|
|
267
272
|
return;
|
|
268
273
|
}
|
|
269
274
|
const w = writer(props);
|
|
270
275
|
if (data.folders.length > 0) {
|
|
271
|
-
w.write(data.folders.map((name) => ({ name })), { fields: [
|
|
276
|
+
w.write(data.folders.map((name) => ({ name })), { fields: ["name"], title: "folders" });
|
|
272
277
|
}
|
|
273
278
|
w.write(data.objects, {
|
|
274
279
|
fields: OBJECT_FIELDS,
|
|
275
|
-
title:
|
|
276
|
-
emptyMessage:
|
|
280
|
+
title: "objects",
|
|
281
|
+
emptyMessage: "No objects found.",
|
|
277
282
|
});
|
|
278
283
|
w.end();
|
|
279
284
|
if (data.is_truncated && data.next_cursor) {
|
|
@@ -305,7 +310,7 @@ const filenameFromContentDisposition = (contentDisposition, key) => {
|
|
|
305
310
|
// the body is absent, not an object, or carries no usable message.
|
|
306
311
|
const serverErrorMessage = (body) => {
|
|
307
312
|
const message = body?.message;
|
|
308
|
-
return typeof message ===
|
|
313
|
+
return typeof message === "string" && message.trim() !== ""
|
|
309
314
|
? message
|
|
310
315
|
: undefined;
|
|
311
316
|
};
|
|
@@ -313,7 +318,7 @@ const serverErrorMessage = (body) => {
|
|
|
313
318
|
// and parse its `message`. Returns undefined on any read/parse failure so the
|
|
314
319
|
// caller falls back to its default message.
|
|
315
320
|
const streamErrorMessage = async (stream) => {
|
|
316
|
-
if (typeof stream?.[Symbol.asyncIterator] !==
|
|
321
|
+
if (typeof stream?.[Symbol.asyncIterator] !== "function") {
|
|
317
322
|
return undefined;
|
|
318
323
|
}
|
|
319
324
|
try {
|
|
@@ -332,19 +337,47 @@ const objectNotFoundFallback = (key, bucket, branchId) => `Object "${key}" not f
|
|
|
332
337
|
// misreported as a missing object; otherwise fall back to a clean default. Used
|
|
333
338
|
// for the JSON (non-streamed) endpoints where the body is already parsed.
|
|
334
339
|
const objectNotFoundMessage = (err, key, bucket, branchId) => {
|
|
335
|
-
if (
|
|
336
|
-
const serverMessage = serverErrorMessage(err.
|
|
340
|
+
if (isNeonApiError(err)) {
|
|
341
|
+
const serverMessage = serverErrorMessage(err.data);
|
|
337
342
|
if (serverMessage !== undefined) {
|
|
338
343
|
return serverMessage;
|
|
339
344
|
}
|
|
340
345
|
}
|
|
341
346
|
return objectNotFoundFallback(key, bucket, branchId);
|
|
342
347
|
};
|
|
348
|
+
// Stream a file from disk as a WHATWG `ReadableStream` suitable for a `fetch`
|
|
349
|
+
// request body, applying backpressure so we never read faster than the upload
|
|
350
|
+
// drains. Uses the global `ReadableStream` (what `fetch` expects) directly, so
|
|
351
|
+
// there's no Node-vs-DOM stream type bridging.
|
|
352
|
+
const fileToWebStream = (path) => {
|
|
353
|
+
const source = createReadStream(path);
|
|
354
|
+
return new ReadableStream({
|
|
355
|
+
start(controller) {
|
|
356
|
+
source.on("data", (chunk) => {
|
|
357
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
358
|
+
if ((controller.desiredSize ?? 0) <= 0)
|
|
359
|
+
source.pause();
|
|
360
|
+
});
|
|
361
|
+
source.on("end", () => {
|
|
362
|
+
controller.close();
|
|
363
|
+
});
|
|
364
|
+
source.on("error", (err) => {
|
|
365
|
+
controller.error(err instanceof Error ? err : new Error(String(err)));
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
pull() {
|
|
369
|
+
source.resume();
|
|
370
|
+
},
|
|
371
|
+
cancel() {
|
|
372
|
+
source.destroy();
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
};
|
|
343
376
|
const getObject = async (props) => {
|
|
344
377
|
const branchId = await branchIdFromProps(props);
|
|
345
378
|
const { bucket, rest: key } = splitBucketTarget(props.target);
|
|
346
|
-
if (key ===
|
|
347
|
-
throw new Error(
|
|
379
|
+
if (key === "") {
|
|
380
|
+
throw new Error("Object target must be in the form <bucket>/<key>.");
|
|
348
381
|
}
|
|
349
382
|
let response;
|
|
350
383
|
try {
|
|
@@ -356,16 +389,16 @@ const getObject = async (props) => {
|
|
|
356
389
|
});
|
|
357
390
|
}
|
|
358
391
|
catch (err) {
|
|
359
|
-
if (
|
|
392
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
360
393
|
// The download response is a stream, so a 404 body arrives as a stream
|
|
361
394
|
// too; drain and parse it to recover the server's message (which
|
|
362
395
|
// distinguishes a missing bucket from a missing object).
|
|
363
|
-
const serverMessage = await streamErrorMessage(err.
|
|
396
|
+
const serverMessage = await streamErrorMessage(err.data);
|
|
364
397
|
throw new Error(serverMessage ?? objectNotFoundFallback(key, bucket, branchId));
|
|
365
398
|
}
|
|
366
399
|
throw err;
|
|
367
400
|
}
|
|
368
|
-
const contentDisposition = response.headers[
|
|
401
|
+
const contentDisposition = response.headers["content-disposition"];
|
|
369
402
|
const destination = props.file ?? filenameFromContentDisposition(contentDisposition, key);
|
|
370
403
|
try {
|
|
371
404
|
await pipeline(response.data, createWriteStream(destination));
|
|
@@ -380,8 +413,8 @@ const getObject = async (props) => {
|
|
|
380
413
|
const putObject = async (props) => {
|
|
381
414
|
const branchId = await branchIdFromProps(props);
|
|
382
415
|
const { bucket, rest: key } = splitBucketTarget(props.target);
|
|
383
|
-
if (bucket ===
|
|
384
|
-
throw new Error(
|
|
416
|
+
if (bucket === "" || key === "") {
|
|
417
|
+
throw new Error("Object target must be in the form <bucket>/<key>.");
|
|
385
418
|
}
|
|
386
419
|
// Stat the file first so we fail fast on a missing/unreadable file and can
|
|
387
420
|
// enforce the single-PUT size cap BEFORE any network round-trip. We also
|
|
@@ -396,7 +429,7 @@ const putObject = async (props) => {
|
|
|
396
429
|
fileSize = fileStat.size;
|
|
397
430
|
}
|
|
398
431
|
catch (err) {
|
|
399
|
-
if (err?.code ===
|
|
432
|
+
if (err?.code === "ENOENT") {
|
|
400
433
|
throw new Error(`File "${props.file}" does not exist.`);
|
|
401
434
|
}
|
|
402
435
|
throw err;
|
|
@@ -417,52 +450,56 @@ const putObject = async (props) => {
|
|
|
417
450
|
}));
|
|
418
451
|
}
|
|
419
452
|
catch (err) {
|
|
420
|
-
if (
|
|
421
|
-
const status = err.
|
|
453
|
+
if (isNeonApiError(err)) {
|
|
454
|
+
const status = err.status;
|
|
422
455
|
if (status === 404) {
|
|
423
456
|
throw new Error(objectNotFoundMessage(err, key, bucket, branchId));
|
|
424
457
|
}
|
|
425
458
|
// Any other HTTP error from the console (e.g. 403 when the caller lacks
|
|
426
459
|
// write permission on the bucket) carries the same JSON `{ message }`
|
|
427
|
-
// body, so surface that rather than a bare
|
|
428
|
-
//
|
|
429
|
-
const serverMessage = serverErrorMessage(err.
|
|
460
|
+
// body, so surface that rather than a bare error. When the body has no
|
|
461
|
+
// usable message, fall back to a clean status-bearing error.
|
|
462
|
+
const serverMessage = serverErrorMessage(err.data);
|
|
430
463
|
throw new Error(serverMessage ??
|
|
431
|
-
`Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` :
|
|
464
|
+
`Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ""}: ${err.message}`);
|
|
432
465
|
}
|
|
433
466
|
throw err;
|
|
434
467
|
}
|
|
435
|
-
// Stream the file straight into the PUT body
|
|
436
|
-
// The presigned URL targets the branch S3 data-plane endpoint
|
|
437
|
-
// this PUT
|
|
468
|
+
// Stream the file straight into the PUT body via `fetch`; never buffer the
|
|
469
|
+
// whole file. The presigned URL targets the branch S3 data-plane endpoint
|
|
470
|
+
// directly, so this PUT bypasses the console API entirely.
|
|
438
471
|
//
|
|
439
472
|
// `presign.headers` carries the signature-relevant headers (e.g. host,
|
|
440
473
|
// content-type); the server does not sign Content-Length, so we set it
|
|
441
|
-
// ourselves from the stat'd size
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
474
|
+
// ourselves from the stat'd size. `redirect: 'error'` ensures we never resend
|
|
475
|
+
// the file bytes and signed headers to a different host if the data-plane
|
|
476
|
+
// endpoint were to answer with a redirect. `duplex: 'half'` is required by
|
|
477
|
+
// fetch when streaming a request body.
|
|
478
|
+
const upload = {
|
|
479
|
+
method: "PUT",
|
|
480
|
+
headers: {
|
|
481
|
+
...presign.headers,
|
|
482
|
+
"Content-Length": String(fileSize),
|
|
483
|
+
},
|
|
484
|
+
body: fileToWebStream(props.file),
|
|
485
|
+
redirect: "error",
|
|
486
|
+
duplex: "half",
|
|
487
|
+
};
|
|
488
|
+
let uploadResponse;
|
|
445
489
|
try {
|
|
446
|
-
await
|
|
447
|
-
headers: {
|
|
448
|
-
...presign.headers,
|
|
449
|
-
'Content-Length': fileSize,
|
|
450
|
-
},
|
|
451
|
-
maxBodyLength: Infinity,
|
|
452
|
-
maxContentLength: Infinity,
|
|
453
|
-
maxRedirects: 0,
|
|
454
|
-
});
|
|
490
|
+
uploadResponse = await fetch(presign.url, upload);
|
|
455
491
|
}
|
|
456
492
|
catch (err) {
|
|
493
|
+
// A transport-level failure (DNS, connection reset, redirect when none is
|
|
494
|
+
// allowed). Surface a clean message without leaking the signed URL.
|
|
495
|
+
throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
496
|
+
}
|
|
497
|
+
if (!uploadResponse.ok) {
|
|
457
498
|
// The upload targets the S3 data plane, whose error bodies are XML rather
|
|
458
499
|
// than the JSON `{ message }` the console returns, so surface the status
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
const status = err.response?.status;
|
|
463
|
-
throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ''}: ${err.message}`);
|
|
464
|
-
}
|
|
465
|
-
throw err;
|
|
500
|
+
// rather than the body. Never include the presigned URL, which carries the
|
|
501
|
+
// signature.
|
|
502
|
+
throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId} (HTTP ${uploadResponse.status}): Request failed with status code ${uploadResponse.status}`);
|
|
466
503
|
}
|
|
467
504
|
log.info(`File "${props.file}" uploaded to "${key}" in bucket "${bucket}" on branch ${branchId}`);
|
|
468
505
|
};
|
|
@@ -470,10 +507,10 @@ const deleteObject = async (props) => {
|
|
|
470
507
|
const branchId = await branchIdFromProps(props);
|
|
471
508
|
const { bucket, rest } = splitBucketTarget(props.target);
|
|
472
509
|
if (props.recursive) {
|
|
473
|
-
if (rest ===
|
|
510
|
+
if (rest === "") {
|
|
474
511
|
throw new Error('Recursive delete requires a non-empty prefix ending in "/".');
|
|
475
512
|
}
|
|
476
|
-
if (!rest.endsWith(
|
|
513
|
+
if (!rest.endsWith("/")) {
|
|
477
514
|
throw new Error(`Recursive delete requires a prefix ending in "/" (got "${rest}").`);
|
|
478
515
|
}
|
|
479
516
|
const { data } = await retryOnLock(() => deleteProjectBranchBucketObjectsByPrefix(props.apiClient, {
|
|
@@ -485,8 +522,8 @@ const deleteObject = async (props) => {
|
|
|
485
522
|
log.info(`Deleted ${data.deleted} object(s) under prefix "${rest}" from bucket "${bucket}" on branch ${branchId}`);
|
|
486
523
|
return;
|
|
487
524
|
}
|
|
488
|
-
if (rest ===
|
|
489
|
-
throw new Error(
|
|
525
|
+
if (rest === "") {
|
|
526
|
+
throw new Error("Object target must be in the form <bucket>/<key>.");
|
|
490
527
|
}
|
|
491
528
|
try {
|
|
492
529
|
await retryOnLock(() => deleteProjectBranchBucketObject(props.apiClient, {
|
|
@@ -497,7 +534,7 @@ const deleteObject = async (props) => {
|
|
|
497
534
|
}));
|
|
498
535
|
}
|
|
499
536
|
catch (err) {
|
|
500
|
-
if (
|
|
537
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
501
538
|
throw new Error(objectNotFoundMessage(err, rest, bucket, branchId));
|
|
502
539
|
}
|
|
503
540
|
throw err;
|