@lightnet/cli 4.6.0 → 4.6.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/index.js +11 -2
- package/src/r2.js +86 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @lightnet/cli
|
|
2
2
|
|
|
3
|
+
## 4.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#441](https://github.com/LightNetDev/LightNet/pull/441) [`b643414`](https://github.com/LightNetDev/LightNet/commit/b643414852ed1e6d80fa013dff9e1189c9f7ba35) - Protect R2 bucket-root delete and copy replacement with a visible warning and require the long `--force` flag to bypass confirmation.
|
|
8
|
+
|
|
3
9
|
## 4.6.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -96,10 +96,11 @@ r2Command
|
|
|
96
96
|
.option("-r, --recursive", "delete a directory/prefix recursively")
|
|
97
97
|
.option(
|
|
98
98
|
"-f, --force",
|
|
99
|
-
|
|
99
|
+
'delete without confirmation; use "--force" to clean the bucket root without confirmation',
|
|
100
100
|
)
|
|
101
101
|
.action(async (path, options) => {
|
|
102
102
|
try {
|
|
103
|
+
options.longForce = hasLongForceFlag()
|
|
103
104
|
await removeR2(path, options)
|
|
104
105
|
} catch (error) {
|
|
105
106
|
handleCommandError(error)
|
|
@@ -113,9 +114,13 @@ r2Command
|
|
|
113
114
|
)
|
|
114
115
|
.argument("<source>", 'source path; use "r2:<path>" for R2')
|
|
115
116
|
.argument("<destination>", 'destination path; use "r2:<path>" for R2')
|
|
116
|
-
.option(
|
|
117
|
+
.option(
|
|
118
|
+
"-f, --force",
|
|
119
|
+
'overwrite without confirmation; use "--force" to replace the bucket root without confirmation',
|
|
120
|
+
)
|
|
117
121
|
.action(async (source, destination, options) => {
|
|
118
122
|
try {
|
|
123
|
+
options.longForce = hasLongForceFlag()
|
|
119
124
|
await copyR2(source, destination, options)
|
|
120
125
|
} catch (error) {
|
|
121
126
|
handleCommandError(error)
|
|
@@ -154,3 +159,7 @@ function handleCommandError(error) {
|
|
|
154
159
|
}
|
|
155
160
|
throw error
|
|
156
161
|
}
|
|
162
|
+
|
|
163
|
+
function hasLongForceFlag() {
|
|
164
|
+
return process.argv.includes("--force")
|
|
165
|
+
}
|
package/src/r2.js
CHANGED
|
@@ -20,6 +20,7 @@ const remotePathPrefix = "r2:"
|
|
|
20
20
|
/**
|
|
21
21
|
* @typedef {{
|
|
22
22
|
* force?: boolean
|
|
23
|
+
* longForce?: boolean
|
|
23
24
|
* }} R2CopyOptions
|
|
24
25
|
*/
|
|
25
26
|
|
|
@@ -42,6 +43,7 @@ const remotePathPrefix = "r2:"
|
|
|
42
43
|
* @typedef {{
|
|
43
44
|
* force?: boolean
|
|
44
45
|
* recursive?: boolean
|
|
46
|
+
* longForce?: boolean
|
|
45
47
|
* }} R2RemoveOptions
|
|
46
48
|
*/
|
|
47
49
|
|
|
@@ -89,9 +91,19 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
const interactive = getInteractive(runtime)
|
|
92
|
-
if (isBucketRoot && !interactive) {
|
|
94
|
+
if (isBucketRoot && options.force && !options.longForce && !interactive) {
|
|
93
95
|
throw new CliError(
|
|
94
|
-
"
|
|
96
|
+
'Ignoring "-f" for R2 bucket-root cleanup. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
if (isBucketRoot && !interactive && !options.longForce) {
|
|
100
|
+
throw new CliError(
|
|
101
|
+
'Cleaning the R2 bucket root requires interactive confirmation. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (isBucketRoot && options.force && !options.longForce) {
|
|
105
|
+
log.warn(
|
|
106
|
+
'Ignoring "-f" for R2 bucket-root cleanup. Use "--force" if you are sure you want to delete the entire R2 bucket.',
|
|
95
107
|
)
|
|
96
108
|
}
|
|
97
109
|
if (!options.force && !interactive) {
|
|
@@ -107,14 +119,15 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
const shouldDelete = isBucketRoot
|
|
110
|
-
?
|
|
122
|
+
? options.longForce ||
|
|
123
|
+
(await getPromptConfirm(runtime)(
|
|
111
124
|
"Delete all files in the R2 bucket? [y/N] ",
|
|
112
|
-
)
|
|
125
|
+
))
|
|
113
126
|
: options.force ||
|
|
114
127
|
(await getPromptConfirm(runtime)(`Delete R2 path "${r2Path}"? [y/N] `))
|
|
115
128
|
|
|
116
129
|
if (!shouldDelete) {
|
|
117
|
-
|
|
130
|
+
cancelPrompt()
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
const client = createR2CommandClient(runtime)
|
|
@@ -161,14 +174,23 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
|
161
174
|
sourceType === "file" &&
|
|
162
175
|
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
163
176
|
|
|
164
|
-
await
|
|
165
|
-
destination,
|
|
177
|
+
const confirmedRootOverwrite = await confirmCopyRootOverwrite({
|
|
166
178
|
force: options.force === true,
|
|
179
|
+
longForce: options.longForce === true,
|
|
167
180
|
runtime,
|
|
168
|
-
sourceType,
|
|
169
181
|
targetPath,
|
|
170
182
|
targetType,
|
|
171
183
|
})
|
|
184
|
+
if (!confirmedRootOverwrite) {
|
|
185
|
+
await confirmCopyOverwrite({
|
|
186
|
+
destination,
|
|
187
|
+
force: options.force === true,
|
|
188
|
+
runtime,
|
|
189
|
+
sourceType,
|
|
190
|
+
targetPath,
|
|
191
|
+
targetType,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
172
194
|
await replaceCopyTargetBeforeCopy({
|
|
173
195
|
client,
|
|
174
196
|
runtime,
|
|
@@ -271,6 +293,55 @@ async function replaceCopyTargetBeforeCopy({
|
|
|
271
293
|
})
|
|
272
294
|
}
|
|
273
295
|
|
|
296
|
+
/**
|
|
297
|
+
* @param {{
|
|
298
|
+
* force: boolean,
|
|
299
|
+
* longForce: boolean,
|
|
300
|
+
* runtime: R2Runtime,
|
|
301
|
+
* targetPath: CopyPath,
|
|
302
|
+
* targetType: CopyPathType
|
|
303
|
+
* }} args
|
|
304
|
+
*/
|
|
305
|
+
async function confirmCopyRootOverwrite({
|
|
306
|
+
force,
|
|
307
|
+
longForce,
|
|
308
|
+
runtime,
|
|
309
|
+
targetPath,
|
|
310
|
+
targetType,
|
|
311
|
+
}) {
|
|
312
|
+
if (!isR2BucketRootCopyPath(targetPath) || targetType === "missing") {
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
log.warn(
|
|
316
|
+
"This will replace ALL files in the configured R2 bucket. This cannot be undone.",
|
|
317
|
+
)
|
|
318
|
+
if (longForce) {
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
if (force && !getInteractive(runtime)) {
|
|
322
|
+
throw new CliError(
|
|
323
|
+
'Ignoring "-f" for R2 bucket-root replacement. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
if (!getInteractive(runtime)) {
|
|
327
|
+
throw new CliError(
|
|
328
|
+
'Replacing the R2 bucket root requires interactive confirmation. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
if (force) {
|
|
332
|
+
log.warn(
|
|
333
|
+
'Ignoring "-f" for R2 bucket-root replacement. Use "--force" if you are sure you want to replace the entire R2 bucket.',
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
const shouldOverwrite = await getPromptConfirm(runtime)(
|
|
337
|
+
"Replace all files in the R2 bucket? [y/N] ",
|
|
338
|
+
)
|
|
339
|
+
if (!shouldOverwrite) {
|
|
340
|
+
cancelPrompt()
|
|
341
|
+
}
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
|
|
274
345
|
/**
|
|
275
346
|
* @param {R2Runtime} runtime
|
|
276
347
|
*/
|
|
@@ -498,6 +569,13 @@ function isR2BucketRootPath(path) {
|
|
|
498
569
|
return path.trim() === "/"
|
|
499
570
|
}
|
|
500
571
|
|
|
572
|
+
/**
|
|
573
|
+
* @param {CopyPath} path
|
|
574
|
+
*/
|
|
575
|
+
function isR2BucketRootCopyPath(path) {
|
|
576
|
+
return path.remote && path.path === ""
|
|
577
|
+
}
|
|
578
|
+
|
|
501
579
|
/**
|
|
502
580
|
* @param {unknown} error
|
|
503
581
|
*/
|