@lightnet/cli 4.5.0 → 4.6.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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/index.js +27 -7
- package/src/r2.js +356 -16
- package/src/support/r2.js +94 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @lightnet/cli
|
|
2
2
|
|
|
3
|
+
## 4.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#439](https://github.com/LightNetDev/LightNet/pull/439) [`2572437`](https://github.com/LightNetDev/LightNet/commit/25724372a47ac8fcac1fd5959c442d6619f5b758) - Update `lightnet r2 cp` to support Unix-like file overwrite behavior and prompt before overwriting unless `-f` is provided.
|
|
8
|
+
|
|
9
|
+
- [#439](https://github.com/LightNetDev/LightNet/pull/439) [`2572437`](https://github.com/LightNetDev/LightNet/commit/25724372a47ac8fcac1fd5959c442d6619f5b758) - Allow `lightnet r2 cp` to copy files and prefixes inside the configured R2 bucket with `r2:` source and destination paths.
|
|
10
|
+
|
|
11
|
+
- [#439](https://github.com/LightNetDev/LightNet/pull/439) [`2572437`](https://github.com/LightNetDev/LightNet/commit/25724372a47ac8fcac1fd5959c442d6619f5b758) - Add `lightnet r2 mv` for moving and renaming files and prefixes inside the configured R2 bucket.
|
|
12
|
+
|
|
13
|
+
- [#439](https://github.com/LightNetDev/LightNet/pull/439) [`2572437`](https://github.com/LightNetDev/LightNet/commit/25724372a47ac8fcac1fd5959c442d6619f5b758) - Update `lightnet r2 rm` to use OS-style deletion flags with `-r` for recursive prefix removal and `-f` for skipping confirmation.
|
|
14
|
+
|
|
3
15
|
## 4.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import pkg from "../package.json" with { type: "json" }
|
|
|
7
7
|
import { checkFiles } from "./check-files.js"
|
|
8
8
|
import { checkLinks } from "./check-links.js"
|
|
9
9
|
import { checkTranslations } from "./check-translations.js"
|
|
10
|
-
import { copyR2, listR2, removeR2 } from "./r2.js"
|
|
10
|
+
import { copyR2, listR2, moveR2, removeR2 } from "./r2.js"
|
|
11
11
|
import { CliError } from "./support/cli-error.js"
|
|
12
12
|
import { PromptCancelled } from "./support/prompt-cancel.js"
|
|
13
13
|
const { version } = pkg
|
|
@@ -89,10 +89,15 @@ r2Command
|
|
|
89
89
|
|
|
90
90
|
r2Command
|
|
91
91
|
.command("rm")
|
|
92
|
-
.description(
|
|
93
|
-
|
|
92
|
+
.description(
|
|
93
|
+
'delete an R2 file; use -r to delete a directory/prefix or "/" to clean the bucket',
|
|
94
|
+
)
|
|
95
|
+
.argument("<path>", 'R2 file path, directory/prefix with -r, or "/" with -r')
|
|
94
96
|
.option("-r, --recursive", "delete a directory/prefix recursively")
|
|
95
|
-
.option(
|
|
97
|
+
.option(
|
|
98
|
+
"-f, --force",
|
|
99
|
+
"delete without confirmation, except when cleaning the bucket root",
|
|
100
|
+
)
|
|
96
101
|
.action(async (path, options) => {
|
|
97
102
|
try {
|
|
98
103
|
await removeR2(path, options)
|
|
@@ -104,13 +109,28 @@ r2Command
|
|
|
104
109
|
r2Command
|
|
105
110
|
.command("cp")
|
|
106
111
|
.description(
|
|
107
|
-
'copy files between R2 and
|
|
112
|
+
'copy files between R2 and local paths, or inside R2; prefix R2 paths with "r2:"',
|
|
108
113
|
)
|
|
109
114
|
.argument("<source>", 'source path; use "r2:<path>" for R2')
|
|
110
115
|
.argument("<destination>", 'destination path; use "r2:<path>" for R2')
|
|
111
|
-
.
|
|
116
|
+
.option("-f, --force", "overwrite existing files without confirmation")
|
|
117
|
+
.action(async (source, destination, options) => {
|
|
118
|
+
try {
|
|
119
|
+
await copyR2(source, destination, options)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
handleCommandError(error)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
r2Command
|
|
126
|
+
.command("mv")
|
|
127
|
+
.description("move or rename an R2 file or directory/prefix")
|
|
128
|
+
.argument("<source>", "R2 source path")
|
|
129
|
+
.argument("<destination>", "R2 destination path")
|
|
130
|
+
.option("-f, --force", "overwrite existing destination without confirmation")
|
|
131
|
+
.action(async (source, destination, options) => {
|
|
112
132
|
try {
|
|
113
|
-
await
|
|
133
|
+
await moveR2(source, destination, options)
|
|
114
134
|
} catch (error) {
|
|
115
135
|
handleCommandError(error)
|
|
116
136
|
}
|
package/src/r2.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { rm, stat } from "node:fs/promises"
|
|
4
|
+
import { basename, join, posix, resolve } from "node:path"
|
|
4
5
|
import {
|
|
5
6
|
cwd as processCwd,
|
|
6
7
|
env as processEnv,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
stdout,
|
|
9
10
|
} from "node:process"
|
|
10
11
|
|
|
11
|
-
import { confirm, isCancel, text } from "@clack/prompts"
|
|
12
|
+
import { confirm, isCancel, log, text } from "@clack/prompts"
|
|
12
13
|
|
|
13
14
|
import { CliError } from "./support/cli-error.js"
|
|
14
15
|
import { cancelPrompt } from "./support/prompt-cancel.js"
|
|
@@ -16,6 +17,27 @@ import { createR2Client } from "./support/r2.js"
|
|
|
16
17
|
|
|
17
18
|
const remotePathPrefix = "r2:"
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* force?: boolean
|
|
23
|
+
* }} R2CopyOptions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{
|
|
28
|
+
* force?: boolean
|
|
29
|
+
* }} R2MoveOptions
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{
|
|
34
|
+
* remote: boolean
|
|
35
|
+
* path: string
|
|
36
|
+
* }} CopyPath
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @typedef {"file"|"directory"|"missing"} CopyPathType */
|
|
40
|
+
|
|
19
41
|
/**
|
|
20
42
|
* @typedef {{
|
|
21
43
|
* force?: boolean
|
|
@@ -56,20 +78,40 @@ export async function listR2(path, runtime = {}) {
|
|
|
56
78
|
*/
|
|
57
79
|
export async function removeR2(path, options, runtime = {}) {
|
|
58
80
|
const r2Path = normalizeR2Path(path)
|
|
59
|
-
|
|
81
|
+
const isBucketRoot = isR2BucketRootPath(path)
|
|
82
|
+
if (isBucketRoot && !options.recursive) {
|
|
83
|
+
throw new CliError(
|
|
84
|
+
'Refusing to delete the R2 bucket root without "--recursive".',
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
if (!r2Path && !isBucketRoot) {
|
|
60
88
|
throw new CliError("Refusing to delete the R2 bucket root.")
|
|
61
89
|
}
|
|
62
90
|
|
|
63
91
|
const interactive = getInteractive(runtime)
|
|
92
|
+
if (isBucketRoot && !interactive) {
|
|
93
|
+
throw new CliError(
|
|
94
|
+
"Cleaning the R2 bucket root requires interactive confirmation.",
|
|
95
|
+
)
|
|
96
|
+
}
|
|
64
97
|
if (!options.force && !interactive) {
|
|
65
98
|
throw new CliError(
|
|
66
99
|
'R2 deletion requires confirmation. Re-run with "--force" in non-interactive environments.',
|
|
67
100
|
)
|
|
68
101
|
}
|
|
69
102
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
if (isBucketRoot) {
|
|
104
|
+
log.warn(
|
|
105
|
+
"This will delete ALL files in the configured R2 bucket. This cannot be undone.",
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const shouldDelete = isBucketRoot
|
|
110
|
+
? await getPromptConfirm(runtime)(
|
|
111
|
+
"Delete all files in the R2 bucket? [y/N] ",
|
|
112
|
+
)
|
|
113
|
+
: options.force ||
|
|
114
|
+
(await getPromptConfirm(runtime)(`Delete R2 path "${r2Path}"? [y/N] `))
|
|
73
115
|
|
|
74
116
|
if (!shouldDelete) {
|
|
75
117
|
return
|
|
@@ -84,26 +126,149 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
84
126
|
/**
|
|
85
127
|
* @param {string} source
|
|
86
128
|
* @param {string} destination
|
|
129
|
+
* @param {R2CopyOptions} [options]
|
|
87
130
|
* @param {R2Runtime} [runtime]
|
|
88
131
|
*/
|
|
89
|
-
export async function copyR2(source, destination, runtime = {}) {
|
|
132
|
+
export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
90
133
|
const sourcePath = parseCopyPath(source)
|
|
91
134
|
const destinationPath = parseCopyPath(destination)
|
|
92
|
-
if (sourcePath.remote
|
|
135
|
+
if (!sourcePath.remote && !destinationPath.remote) {
|
|
93
136
|
throw new CliError(
|
|
94
|
-
`R2 copy requires
|
|
137
|
+
`R2 copy requires at least one R2 path. Prefix R2 paths with "${remotePathPrefix}".`,
|
|
95
138
|
)
|
|
96
139
|
}
|
|
97
140
|
|
|
98
141
|
const client = createR2CommandClient(runtime)
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
143
|
+
if (sourceType === "missing") {
|
|
144
|
+
throw new CliError(`Copy source "${source}" does not exist.`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const destinationType = await getCopyPathType(
|
|
148
|
+
destinationPath,
|
|
149
|
+
client,
|
|
150
|
+
runtime,
|
|
151
|
+
)
|
|
152
|
+
const targetPath = getCopyTargetPath({
|
|
153
|
+
destination: destinationPath,
|
|
154
|
+
destinationType,
|
|
155
|
+
runtime,
|
|
156
|
+
source: sourcePath,
|
|
157
|
+
sourceType,
|
|
158
|
+
})
|
|
159
|
+
const targetType = await getCopyPathType(targetPath, client, runtime)
|
|
160
|
+
const shouldUseCopyTo =
|
|
161
|
+
sourceType === "file" &&
|
|
162
|
+
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
163
|
+
|
|
164
|
+
await confirmCopyOverwrite({
|
|
165
|
+
destination,
|
|
166
|
+
force: options.force === true,
|
|
167
|
+
runtime,
|
|
168
|
+
sourceType,
|
|
169
|
+
targetPath,
|
|
170
|
+
targetType,
|
|
171
|
+
})
|
|
172
|
+
await replaceCopyTargetBeforeCopy({
|
|
173
|
+
client,
|
|
174
|
+
runtime,
|
|
175
|
+
sourceType,
|
|
176
|
+
targetPath,
|
|
177
|
+
targetType,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
await client.copy(
|
|
181
|
+
await toRcloneCopyPath(sourcePath, client, runtime),
|
|
182
|
+
await toRcloneCopyPath(targetPath, client, runtime),
|
|
183
|
+
{ to: shouldUseCopyTo },
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {string} source
|
|
189
|
+
* @param {string} destination
|
|
190
|
+
* @param {R2MoveOptions} options
|
|
191
|
+
* @param {R2Runtime} [runtime]
|
|
192
|
+
*/
|
|
193
|
+
export async function moveR2(source, destination, options, runtime = {}) {
|
|
194
|
+
const sourcePath = parseR2OnlyPath(source, "Move source")
|
|
195
|
+
const destinationPath = parseR2OnlyPath(destination, "Move destination")
|
|
196
|
+
if (isR2BucketRootPath(source) || isR2BucketRootPath(destination)) {
|
|
197
|
+
throw new CliError("Refusing to move the R2 bucket root.")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const client = createR2CommandClient(runtime)
|
|
201
|
+
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
202
|
+
if (sourceType === "missing") {
|
|
203
|
+
throw new CliError(`Move source "${source}" does not exist.`)
|
|
204
|
+
}
|
|
205
|
+
const destinationType = await getCopyPathType(
|
|
206
|
+
destinationPath,
|
|
207
|
+
client,
|
|
208
|
+
runtime,
|
|
209
|
+
)
|
|
210
|
+
const targetPath = getCopyTargetPath({
|
|
211
|
+
destination: destinationPath,
|
|
212
|
+
destinationType,
|
|
213
|
+
runtime,
|
|
214
|
+
source: sourcePath,
|
|
215
|
+
sourceType,
|
|
216
|
+
})
|
|
217
|
+
const targetType = await getCopyPathType(targetPath, client, runtime)
|
|
218
|
+
const shouldUseMoveTo =
|
|
219
|
+
sourceType === "file" &&
|
|
220
|
+
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
221
|
+
|
|
222
|
+
await confirmCopyOverwrite({
|
|
223
|
+
destination,
|
|
224
|
+
force: options.force === true,
|
|
225
|
+
runtime,
|
|
226
|
+
sourceType,
|
|
227
|
+
targetPath,
|
|
228
|
+
targetType,
|
|
229
|
+
})
|
|
230
|
+
await replaceCopyTargetBeforeCopy({
|
|
231
|
+
client,
|
|
232
|
+
runtime,
|
|
233
|
+
sourceType,
|
|
234
|
+
targetPath,
|
|
235
|
+
targetType,
|
|
236
|
+
})
|
|
105
237
|
|
|
106
|
-
await client.
|
|
238
|
+
await client.move(
|
|
239
|
+
await toRcloneCopyPath(sourcePath, client, runtime),
|
|
240
|
+
await toRcloneCopyPath(targetPath, client, runtime),
|
|
241
|
+
{ to: shouldUseMoveTo },
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {{
|
|
247
|
+
* client: ReturnType<typeof createR2CommandClient>,
|
|
248
|
+
* runtime: R2Runtime,
|
|
249
|
+
* sourceType: CopyPathType,
|
|
250
|
+
* targetPath: CopyPath,
|
|
251
|
+
* targetType: CopyPathType
|
|
252
|
+
* }} args
|
|
253
|
+
*/
|
|
254
|
+
async function replaceCopyTargetBeforeCopy({
|
|
255
|
+
client,
|
|
256
|
+
runtime,
|
|
257
|
+
sourceType,
|
|
258
|
+
targetPath,
|
|
259
|
+
targetType,
|
|
260
|
+
}) {
|
|
261
|
+
if (sourceType !== "directory" || targetType !== "directory") {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (targetPath.remote) {
|
|
265
|
+
await client.remove(targetPath.path, { recursive: true })
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
await rm(resolveLocalPath(targetPath.path, runtime), {
|
|
269
|
+
force: true,
|
|
270
|
+
recursive: true,
|
|
271
|
+
})
|
|
107
272
|
}
|
|
108
273
|
|
|
109
274
|
/**
|
|
@@ -148,6 +313,162 @@ function parseCopyPath(value) {
|
|
|
148
313
|
return { remote: true, path: normalizeR2Path(value) }
|
|
149
314
|
}
|
|
150
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @param {string} value
|
|
318
|
+
* @param {string} label
|
|
319
|
+
* @returns {CopyPath}
|
|
320
|
+
*/
|
|
321
|
+
function parseR2OnlyPath(value, label) {
|
|
322
|
+
if (value.startsWith(remotePathPrefix)) {
|
|
323
|
+
return { remote: true, path: normalizeR2Path(value) }
|
|
324
|
+
}
|
|
325
|
+
if (value.startsWith("/") || value.startsWith("./") || value === ".") {
|
|
326
|
+
throw new CliError(`${label} must be an R2 path, not a local path.`)
|
|
327
|
+
}
|
|
328
|
+
return { remote: true, path: normalizeR2Path(value) }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {{
|
|
333
|
+
* destination: CopyPath,
|
|
334
|
+
* destinationType: CopyPathType,
|
|
335
|
+
* runtime: R2Runtime,
|
|
336
|
+
* source: CopyPath,
|
|
337
|
+
* sourceType: CopyPathType
|
|
338
|
+
* }} args
|
|
339
|
+
* @returns {CopyPath}
|
|
340
|
+
*/
|
|
341
|
+
function getCopyTargetPath({
|
|
342
|
+
destination,
|
|
343
|
+
destinationType,
|
|
344
|
+
runtime,
|
|
345
|
+
source,
|
|
346
|
+
sourceType,
|
|
347
|
+
}) {
|
|
348
|
+
if (sourceType === "directory") {
|
|
349
|
+
if (destinationType === "file") {
|
|
350
|
+
throw new CliError("Cannot copy a directory to an existing file.")
|
|
351
|
+
}
|
|
352
|
+
return destination
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
destinationType === "directory" ||
|
|
357
|
+
(destinationType === "missing" && isDirectoryLikeCopyPath(destination))
|
|
358
|
+
) {
|
|
359
|
+
return {
|
|
360
|
+
...destination,
|
|
361
|
+
path: joinCopyPath(destination, getCopyPathName(source, runtime)),
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return destination
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @param {{
|
|
370
|
+
* destination: string,
|
|
371
|
+
* force: boolean,
|
|
372
|
+
* runtime: R2Runtime,
|
|
373
|
+
* sourceType: CopyPathType,
|
|
374
|
+
* targetPath: CopyPath,
|
|
375
|
+
* targetType: CopyPathType
|
|
376
|
+
* }} args
|
|
377
|
+
*/
|
|
378
|
+
async function confirmCopyOverwrite({
|
|
379
|
+
destination,
|
|
380
|
+
force,
|
|
381
|
+
runtime,
|
|
382
|
+
sourceType,
|
|
383
|
+
targetPath,
|
|
384
|
+
targetType,
|
|
385
|
+
}) {
|
|
386
|
+
if (targetType === "missing") {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
sourceType === "file" &&
|
|
391
|
+
targetType === "directory" &&
|
|
392
|
+
!isDirectoryLikeCopyPath(targetPath)
|
|
393
|
+
) {
|
|
394
|
+
throw new CliError("Cannot overwrite an existing directory with a file.")
|
|
395
|
+
}
|
|
396
|
+
if (force) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if (!getInteractive(runtime)) {
|
|
400
|
+
throw new CliError(
|
|
401
|
+
'Copy would overwrite existing files. Re-run with "--force" in non-interactive environments.',
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
const shouldOverwrite = await getPromptConfirm(runtime)(
|
|
405
|
+
`Overwrite existing destination "${destination}"? [y/N] `,
|
|
406
|
+
)
|
|
407
|
+
if (!shouldOverwrite) {
|
|
408
|
+
cancelPrompt()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @param {CopyPath} copyPath
|
|
414
|
+
* @param {ReturnType<typeof createR2CommandClient>} client
|
|
415
|
+
* @param {R2Runtime} runtime
|
|
416
|
+
* @returns {Promise<CopyPathType>}
|
|
417
|
+
*/
|
|
418
|
+
async function getCopyPathType(copyPath, client, runtime) {
|
|
419
|
+
if (copyPath.remote) {
|
|
420
|
+
return client.getPathType(copyPath.path)
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const stats = await stat(resolveLocalPath(copyPath.path, runtime))
|
|
424
|
+
return stats.isDirectory() ? "directory" : "file"
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (isLocalNotFoundError(error)) {
|
|
427
|
+
return "missing"
|
|
428
|
+
}
|
|
429
|
+
throw error
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @param {CopyPath} copyPath
|
|
435
|
+
* @param {ReturnType<typeof createR2CommandClient>} client
|
|
436
|
+
* @param {R2Runtime} runtime
|
|
437
|
+
*/
|
|
438
|
+
async function toRcloneCopyPath(copyPath, client, runtime) {
|
|
439
|
+
return copyPath.remote
|
|
440
|
+
? await client.toRemotePath(copyPath.path)
|
|
441
|
+
: resolveLocalPath(copyPath.path, runtime)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @param {CopyPath} copyPath
|
|
446
|
+
*/
|
|
447
|
+
function isDirectoryLikeCopyPath(copyPath) {
|
|
448
|
+
return copyPath.path === "." || copyPath.path.endsWith("/")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @param {CopyPath} parent
|
|
453
|
+
* @param {string} child
|
|
454
|
+
*/
|
|
455
|
+
function joinCopyPath(parent, child) {
|
|
456
|
+
if (parent.remote) {
|
|
457
|
+
return parent.path ? posix.join(parent.path, child) : child
|
|
458
|
+
}
|
|
459
|
+
return join(parent.path, child)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {CopyPath} copyPath
|
|
464
|
+
* @param {R2Runtime} runtime
|
|
465
|
+
*/
|
|
466
|
+
function getCopyPathName(copyPath, runtime) {
|
|
467
|
+
return copyPath.remote
|
|
468
|
+
? posix.basename(copyPath.path.replace(/\/+$/, ""))
|
|
469
|
+
: basename(resolveLocalPath(copyPath.path, runtime))
|
|
470
|
+
}
|
|
471
|
+
|
|
151
472
|
/**
|
|
152
473
|
* @param {string} path
|
|
153
474
|
* @param {R2Runtime} runtime
|
|
@@ -170,6 +491,25 @@ function normalizeR2Path(path) {
|
|
|
170
491
|
return path?.replace(/^r2:/, "").replace(/^\/+/, "") ?? ""
|
|
171
492
|
}
|
|
172
493
|
|
|
494
|
+
/**
|
|
495
|
+
* @param {string} path
|
|
496
|
+
*/
|
|
497
|
+
function isR2BucketRootPath(path) {
|
|
498
|
+
return path.trim() === "/"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @param {unknown} error
|
|
503
|
+
*/
|
|
504
|
+
function isLocalNotFoundError(error) {
|
|
505
|
+
return (
|
|
506
|
+
typeof error === "object" &&
|
|
507
|
+
error !== null &&
|
|
508
|
+
"code" in error &&
|
|
509
|
+
error.code === "ENOENT"
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
173
513
|
/**
|
|
174
514
|
* @param {string} message
|
|
175
515
|
*/
|
package/src/support/r2.js
CHANGED
|
@@ -240,16 +240,74 @@ export function createR2Client({ cwd, interactive, promptText }) {
|
|
|
240
240
|
/**
|
|
241
241
|
* @param {string} source
|
|
242
242
|
* @param {string} destination
|
|
243
|
+
* @param {{to?: boolean}} [options]
|
|
243
244
|
*/
|
|
244
|
-
async copy(source, destination) {
|
|
245
|
+
async copy(source, destination, options = {}) {
|
|
245
246
|
const r2Config = await getConfig()
|
|
246
247
|
await runConfiguredRclone({
|
|
247
|
-
args: ["copy", source, destination],
|
|
248
|
+
args: [options.to ? "copyto" : "copy", source, destination],
|
|
248
249
|
cwd,
|
|
249
250
|
config: r2Config,
|
|
250
251
|
secretAccessKey: await getSecretAccessKey(),
|
|
251
252
|
})
|
|
252
253
|
},
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} source
|
|
256
|
+
* @param {string} destination
|
|
257
|
+
* @param {{to?: boolean}} [options]
|
|
258
|
+
*/
|
|
259
|
+
async move(source, destination, options = {}) {
|
|
260
|
+
const r2Config = await getConfig()
|
|
261
|
+
await runConfiguredRclone({
|
|
262
|
+
args: [options.to ? "moveto" : "move", source, destination],
|
|
263
|
+
cwd,
|
|
264
|
+
config: r2Config,
|
|
265
|
+
secretAccessKey: await getSecretAccessKey(),
|
|
266
|
+
})
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} path
|
|
270
|
+
* @returns {Promise<"file"|"directory"|"missing">}
|
|
271
|
+
*/
|
|
272
|
+
async getPathType(path) {
|
|
273
|
+
const normalizedPath = normalizeR2ObjectPath(path)
|
|
274
|
+
if (!normalizedPath) {
|
|
275
|
+
return "directory"
|
|
276
|
+
}
|
|
277
|
+
const r2Config = await getConfig()
|
|
278
|
+
try {
|
|
279
|
+
const result = await runConfiguredRclone({
|
|
280
|
+
args: [
|
|
281
|
+
"lsjson",
|
|
282
|
+
makeR2Path(r2Config, normalizedPath),
|
|
283
|
+
"--max-depth",
|
|
284
|
+
"1",
|
|
285
|
+
"--no-mimetype",
|
|
286
|
+
"--no-modtime",
|
|
287
|
+
],
|
|
288
|
+
cwd,
|
|
289
|
+
config: r2Config,
|
|
290
|
+
secretAccessKey: await getSecretAccessKey(),
|
|
291
|
+
})
|
|
292
|
+
const entries = parseRcloneJsonList(result.stdout)
|
|
293
|
+
if (entries.length === 0) {
|
|
294
|
+
return "missing"
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
entries.length === 1 &&
|
|
298
|
+
!entries[0].isDir &&
|
|
299
|
+
entries[0].name === getR2PathName(normalizedPath)
|
|
300
|
+
) {
|
|
301
|
+
return "file"
|
|
302
|
+
}
|
|
303
|
+
return "directory"
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (isRcloneNotFoundError(error)) {
|
|
306
|
+
return "missing"
|
|
307
|
+
}
|
|
308
|
+
throw error
|
|
309
|
+
}
|
|
310
|
+
},
|
|
253
311
|
/**
|
|
254
312
|
* @param {string} path
|
|
255
313
|
*/
|
|
@@ -530,6 +588,40 @@ function normalizeR2ObjectPath(path) {
|
|
|
530
588
|
return path?.replace(/^r2:/, "").replace(/^\/+/, "") ?? ""
|
|
531
589
|
}
|
|
532
590
|
|
|
591
|
+
/**
|
|
592
|
+
* @param {string} output
|
|
593
|
+
* @returns {{name:string, isDir:boolean}[]}
|
|
594
|
+
*/
|
|
595
|
+
function parseRcloneJsonList(output) {
|
|
596
|
+
const parsed = JSON.parse(output)
|
|
597
|
+
if (!Array.isArray(parsed)) {
|
|
598
|
+
throw new CliError("Unexpected rclone JSON listing output.")
|
|
599
|
+
}
|
|
600
|
+
return parsed
|
|
601
|
+
.filter((item) => isPlainObject(item) && typeof item.Name === "string")
|
|
602
|
+
.map((item) => ({
|
|
603
|
+
name: String(item.Name),
|
|
604
|
+
isDir: item.IsDir === true,
|
|
605
|
+
}))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @param {string} path
|
|
610
|
+
*/
|
|
611
|
+
function getR2PathName(path) {
|
|
612
|
+
return path.replace(/\/+$/, "").split("/").at(-1) ?? ""
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param {unknown} error
|
|
617
|
+
*/
|
|
618
|
+
function isRcloneNotFoundError(error) {
|
|
619
|
+
return (
|
|
620
|
+
error instanceof Error &&
|
|
621
|
+
/not found|directory not found|object not found/i.test(error.message)
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
|
|
533
625
|
/**
|
|
534
626
|
* @param {(message:string)=>Promise<string>} promptText
|
|
535
627
|
* @param {string} message
|