@lightnet/cli 4.6.2 → 4.7.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 +18 -0
- package/package.json +7 -1
- package/src/index.js +24 -15
- package/src/r2.js +450 -205
- package/src/support/r2.js +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @lightnet/cli
|
|
2
2
|
|
|
3
|
+
## 4.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#447](https://github.com/LightNetDev/LightNet/pull/447) [`0d8fece`](https://github.com/LightNetDev/LightNet/commit/0d8fece3527a67a21c6272b422c62152948b32a1) - Only prompt for `lightnet r2 cp` and `lightnet r2 mv` when files would actually be overwritten, and list the destination files before confirmation.
|
|
8
|
+
|
|
9
|
+
## 4.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#445](https://github.com/LightNetDev/LightNet/pull/445) [`53b6a0d`](https://github.com/LightNetDev/LightNet/commit/53b6a0db8d9e6dca38c808d925007cb2ae343076) - Add a `--progress` option to `lightnet r2 cp`, `lightnet r2 mv`, and `lightnet r2 rm`.
|
|
14
|
+
|
|
15
|
+
- [#445](https://github.com/LightNetDev/LightNet/pull/445) [`53b6a0d`](https://github.com/LightNetDev/LightNet/commit/53b6a0db8d9e6dca38c808d925007cb2ae343076) - Update `lightnet r2 cp` to follow Unix-style copy semantics for files and directories.
|
|
16
|
+
|
|
17
|
+
- [#445](https://github.com/LightNetDev/LightNet/pull/445) [`53b6a0d`](https://github.com/LightNetDev/LightNet/commit/53b6a0db8d9e6dca38c808d925007cb2ae343076) - Update `lightnet r2 mv` to use safer Unix-like move semantics inside the configured R2 bucket.
|
|
18
|
+
|
|
19
|
+
- [#445](https://github.com/LightNetDev/LightNet/pull/445) [`53b6a0d`](https://github.com/LightNetDev/LightNet/commit/53b6a0db8d9e6dca38c808d925007cb2ae343076) - Update `lightnet r2 rm` to follow safer Unix-like delete semantics inside the configured R2 bucket.
|
|
20
|
+
|
|
3
21
|
## 4.6.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "CLI for managing LightNet projects",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.7.1",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -28,7 +28,13 @@
|
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
29
|
"commander": "^15.0.0"
|
|
30
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"vitest": "^4.1.9"
|
|
33
|
+
},
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "vitest"
|
|
33
39
|
}
|
|
34
40
|
}
|
package/src/index.js
CHANGED
|
@@ -92,16 +92,20 @@ r2Command
|
|
|
92
92
|
.description(
|
|
93
93
|
'delete an R2 file; use -r to delete a directory/prefix or "/" to clean the bucket',
|
|
94
94
|
)
|
|
95
|
-
.argument(
|
|
95
|
+
.argument(
|
|
96
|
+
"<paths...>",
|
|
97
|
+
'R2 file path(s), directory/prefix paths with -r, or "/" with -r',
|
|
98
|
+
)
|
|
96
99
|
.option("-r, --recursive", "delete a directory/prefix recursively")
|
|
100
|
+
.option("--progress", "show operation progress")
|
|
97
101
|
.option(
|
|
98
102
|
"-f, --force",
|
|
99
103
|
'delete without confirmation; use "--force" to clean the bucket root without confirmation',
|
|
100
104
|
)
|
|
101
|
-
.action(async (
|
|
105
|
+
.action(async (paths, options) => {
|
|
102
106
|
try {
|
|
103
107
|
options.longForce = hasLongForceFlag()
|
|
104
|
-
await removeR2(
|
|
108
|
+
await removeR2(paths, options)
|
|
105
109
|
} catch (error) {
|
|
106
110
|
handleCommandError(error)
|
|
107
111
|
}
|
|
@@ -112,16 +116,20 @@ r2Command
|
|
|
112
116
|
.description(
|
|
113
117
|
'copy files between R2 and local paths, or inside R2; prefix R2 paths with "r2:"',
|
|
114
118
|
)
|
|
115
|
-
.argument(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"-f, --force",
|
|
119
|
-
'overwrite without confirmation; use "--force" to replace the bucket root without confirmation',
|
|
119
|
+
.argument(
|
|
120
|
+
"<paths...>",
|
|
121
|
+
'source path(s) followed by a destination path; use "r2:<path>" for R2',
|
|
120
122
|
)
|
|
121
|
-
.
|
|
123
|
+
.option("-r, --recursive", "copy directories recursively")
|
|
124
|
+
.option("-R", "copy directories recursively")
|
|
125
|
+
.option("-a, --archive", "copy directories recursively")
|
|
126
|
+
.option("-f, --force", "overwrite existing files without confirmation")
|
|
127
|
+
.option("-n, --no-clobber", "skip existing destination files")
|
|
128
|
+
.option("--progress", "show operation progress")
|
|
129
|
+
.action(async (paths, options) => {
|
|
122
130
|
try {
|
|
123
|
-
options.
|
|
124
|
-
await copyR2(
|
|
131
|
+
options.recursive = options.recursive || options.R || options.archive
|
|
132
|
+
await copyR2(paths, options)
|
|
125
133
|
} catch (error) {
|
|
126
134
|
handleCommandError(error)
|
|
127
135
|
}
|
|
@@ -130,12 +138,13 @@ r2Command
|
|
|
130
138
|
r2Command
|
|
131
139
|
.command("mv")
|
|
132
140
|
.description("move or rename an R2 file or directory/prefix")
|
|
133
|
-
.argument("<
|
|
134
|
-
.argument("<destination>", "R2 destination path")
|
|
141
|
+
.argument("<paths...>", "R2 source path(s) followed by a destination path")
|
|
135
142
|
.option("-f, --force", "overwrite existing destination without confirmation")
|
|
136
|
-
.
|
|
143
|
+
.option("-n, --no-clobber", "skip existing destination files")
|
|
144
|
+
.option("--progress", "show operation progress")
|
|
145
|
+
.action(async (paths, options) => {
|
|
137
146
|
try {
|
|
138
|
-
await moveR2(
|
|
147
|
+
await moveR2(paths, options)
|
|
139
148
|
} catch (error) {
|
|
140
149
|
handleCommandError(error)
|
|
141
150
|
}
|
package/src/r2.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { readdir, stat } from "node:fs/promises"
|
|
4
4
|
import { basename, join, posix, resolve } from "node:path"
|
|
5
5
|
import {
|
|
6
6
|
cwd as processCwd,
|
|
@@ -9,7 +9,15 @@ import {
|
|
|
9
9
|
stdout,
|
|
10
10
|
} from "node:process"
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
confirm,
|
|
14
|
+
intro,
|
|
15
|
+
isCancel,
|
|
16
|
+
log,
|
|
17
|
+
outro,
|
|
18
|
+
progress,
|
|
19
|
+
text,
|
|
20
|
+
} from "@clack/prompts"
|
|
13
21
|
|
|
14
22
|
import { CliError } from "./support/cli-error.js"
|
|
15
23
|
import { cancelPrompt } from "./support/prompt-cancel.js"
|
|
@@ -31,13 +39,17 @@ const defaultRcloneOptions = {
|
|
|
31
39
|
/**
|
|
32
40
|
* @typedef {{
|
|
33
41
|
* force?: boolean
|
|
34
|
-
*
|
|
42
|
+
* noClobber?: boolean
|
|
43
|
+
* progress?: boolean
|
|
44
|
+
* recursive?: boolean
|
|
35
45
|
* }} R2CopyOptions
|
|
36
46
|
*/
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
49
|
* @typedef {{
|
|
40
50
|
* force?: boolean
|
|
51
|
+
* noClobber?: boolean
|
|
52
|
+
* progress?: boolean
|
|
41
53
|
* }} R2MoveOptions
|
|
42
54
|
*/
|
|
43
55
|
|
|
@@ -55,6 +67,7 @@ const defaultRcloneOptions = {
|
|
|
55
67
|
* force?: boolean
|
|
56
68
|
* recursive?: boolean
|
|
57
69
|
* longForce?: boolean
|
|
70
|
+
* progress?: boolean
|
|
58
71
|
* }} R2RemoveOptions
|
|
59
72
|
*/
|
|
60
73
|
|
|
@@ -65,6 +78,7 @@ const defaultRcloneOptions = {
|
|
|
65
78
|
* isInteractive?: boolean
|
|
66
79
|
* promptConfirm?: (message: string) => Promise<boolean>
|
|
67
80
|
* promptText?: (message: string) => Promise<string>
|
|
81
|
+
* createR2Client?: typeof createR2Client
|
|
68
82
|
* writeStdout?: (message: string) => void
|
|
69
83
|
* }} R2Runtime
|
|
70
84
|
*/
|
|
@@ -85,297 +99,378 @@ export async function listR2(path, runtime = {}) {
|
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
/**
|
|
88
|
-
* @param {string}
|
|
102
|
+
* @param {string|string[]} paths
|
|
89
103
|
* @param {R2RemoveOptions} options
|
|
90
104
|
* @param {R2Runtime} [runtime]
|
|
91
105
|
*/
|
|
92
|
-
export async function removeR2(
|
|
106
|
+
export async function removeR2(paths, options, runtime = {}) {
|
|
93
107
|
intro("r2 rm")
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
108
|
+
const removePaths = Array.isArray(paths) ? paths : [paths]
|
|
109
|
+
const rootPaths = removePaths.filter((path) => isR2BucketRootPath(path))
|
|
110
|
+
const hasBucketRoot = rootPaths.length > 0
|
|
111
|
+
if (removePaths.length === 0) {
|
|
112
|
+
throw new CliError("R2 deletion requires at least one path.")
|
|
113
|
+
}
|
|
114
|
+
if (hasBucketRoot && removePaths.length > 1) {
|
|
115
|
+
throw new CliError(
|
|
116
|
+
"Refusing to delete the R2 bucket root with other paths.",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
if (hasBucketRoot && !options.recursive) {
|
|
97
120
|
throw new CliError(
|
|
98
121
|
'Refusing to delete the R2 bucket root without "--recursive".',
|
|
99
122
|
)
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
|
|
125
|
+
const r2Paths = removePaths.map((path) => normalizeR2Path(path))
|
|
126
|
+
for (const [index, r2Path] of r2Paths.entries()) {
|
|
127
|
+
if (!r2Path && !isR2BucketRootPath(removePaths[index])) {
|
|
128
|
+
throw new CliError("Refusing to delete the R2 bucket root.")
|
|
129
|
+
}
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
const interactive = getInteractive(runtime)
|
|
106
|
-
if (
|
|
133
|
+
if (hasBucketRoot && options.force && !options.longForce && !interactive) {
|
|
107
134
|
throw new CliError(
|
|
108
135
|
'Ignoring "-f" for R2 bucket-root cleanup. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
109
136
|
)
|
|
110
137
|
}
|
|
111
|
-
if (
|
|
138
|
+
if (hasBucketRoot && !interactive && !options.longForce) {
|
|
112
139
|
throw new CliError(
|
|
113
140
|
'Cleaning the R2 bucket root requires interactive confirmation. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
114
141
|
)
|
|
115
142
|
}
|
|
116
|
-
if (
|
|
143
|
+
if (hasBucketRoot && options.force && !options.longForce) {
|
|
117
144
|
log.warn(
|
|
118
145
|
'Ignoring "-f" for R2 bucket-root cleanup. Use "--force" if you are sure you want to delete the entire R2 bucket.',
|
|
119
146
|
)
|
|
120
147
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
if (hasBucketRoot) {
|
|
149
|
+
log.warn(
|
|
150
|
+
"This will delete ALL files in the configured R2 bucket. This cannot be undone.",
|
|
124
151
|
)
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
const client = createR2CommandClient(runtime)
|
|
155
|
+
const targets = []
|
|
156
|
+
for (const [index, r2Path] of r2Paths.entries()) {
|
|
157
|
+
const originalPath = removePaths[index]
|
|
158
|
+
const isBucketRoot = isR2BucketRootPath(originalPath)
|
|
159
|
+
const pathType = isBucketRoot
|
|
160
|
+
? "directory"
|
|
161
|
+
: await getCopyPathType({ remote: true, path: r2Path }, client, runtime)
|
|
162
|
+
|
|
163
|
+
if (pathType === "missing") {
|
|
164
|
+
if (options.force) {
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
throw new CliError(`R2 path "${r2Path}" does not exist.`)
|
|
168
|
+
}
|
|
169
|
+
if (pathType === "directory" && !options.recursive) {
|
|
170
|
+
throw new CliError(
|
|
171
|
+
`R2 path "${r2Path}" is a directory. Re-run with "--recursive" to delete directories.`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
targets.push({
|
|
175
|
+
isBucketRoot,
|
|
176
|
+
path: r2Path,
|
|
177
|
+
recursive: pathType === "directory",
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (targets.length > 0 && !options.force && !interactive) {
|
|
182
|
+
throw new CliError(
|
|
183
|
+
'R2 deletion requires confirmation. Re-run with "--force" in non-interactive environments.',
|
|
130
184
|
)
|
|
131
185
|
}
|
|
132
186
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
for (const target of targets) {
|
|
188
|
+
const shouldDelete = target.isBucketRoot
|
|
189
|
+
? options.longForce ||
|
|
190
|
+
(await getPromptConfirm(runtime)(
|
|
191
|
+
"Delete all files in the R2 bucket? [y/N] ",
|
|
192
|
+
))
|
|
193
|
+
: options.force ||
|
|
194
|
+
(await getPromptConfirm(runtime)(
|
|
195
|
+
`Delete R2 path "${target.path}"? [y/N] `,
|
|
196
|
+
))
|
|
140
197
|
|
|
141
|
-
|
|
142
|
-
|
|
198
|
+
if (!shouldDelete) {
|
|
199
|
+
cancelPrompt()
|
|
200
|
+
}
|
|
143
201
|
}
|
|
144
202
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
203
|
+
const removeProgress = createR2Progress({
|
|
204
|
+
enabled: options.progress === true && targets.length > 0,
|
|
205
|
+
label: "Deleting R2 paths",
|
|
206
|
+
max: targets.length,
|
|
207
|
+
success: "R2 delete complete.",
|
|
150
208
|
})
|
|
209
|
+
try {
|
|
210
|
+
for (const target of targets) {
|
|
211
|
+
await client.remove(target.path, {
|
|
212
|
+
recursive: target.recursive,
|
|
213
|
+
rcloneOptions: target.recursive ? defaultRcloneOptions : undefined,
|
|
214
|
+
})
|
|
215
|
+
removeProgress.advance()
|
|
216
|
+
}
|
|
217
|
+
removeProgress.stop()
|
|
218
|
+
} catch (error) {
|
|
219
|
+
removeProgress.error("R2 delete failed.")
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
151
222
|
outro("R2 delete complete.")
|
|
152
223
|
}
|
|
153
224
|
|
|
154
225
|
/**
|
|
155
|
-
* @param {string}
|
|
156
|
-
* @param {string} destination
|
|
226
|
+
* @param {string|string[]} paths
|
|
157
227
|
* @param {R2CopyOptions} [options]
|
|
158
228
|
* @param {R2Runtime} [runtime]
|
|
159
229
|
*/
|
|
160
|
-
export async function copyR2(
|
|
230
|
+
export async function copyR2(paths, options = {}, runtime = {}) {
|
|
161
231
|
intro("r2 cp")
|
|
162
|
-
const
|
|
232
|
+
const { sources, destination } = parseCopyArguments(paths)
|
|
233
|
+
if (options.force && options.noClobber) {
|
|
234
|
+
throw new CliError('Cannot use "--force" with "--no-clobber".')
|
|
235
|
+
}
|
|
236
|
+
const sourcePaths = sources.map((source) => parseCopyPath(source))
|
|
163
237
|
const destinationPath = parseCopyPath(destination)
|
|
164
|
-
if (
|
|
238
|
+
if (
|
|
239
|
+
!destinationPath.remote &&
|
|
240
|
+
sourcePaths.every((sourcePath) => !sourcePath.remote)
|
|
241
|
+
) {
|
|
165
242
|
throw new CliError(
|
|
166
243
|
`R2 copy requires at least one R2 path. Prefix R2 paths with "${remotePathPrefix}".`,
|
|
167
244
|
)
|
|
168
245
|
}
|
|
169
246
|
|
|
170
247
|
const client = createR2CommandClient(runtime)
|
|
171
|
-
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
172
|
-
if (sourceType === "missing") {
|
|
173
|
-
throw new CliError(`Copy source "${source}" does not exist.`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
248
|
const destinationType = await getCopyPathType(
|
|
177
249
|
destinationPath,
|
|
178
250
|
client,
|
|
179
251
|
runtime,
|
|
180
252
|
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
destinationType
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
253
|
+
if (
|
|
254
|
+
sourcePaths.length > 1 &&
|
|
255
|
+
destinationType !== "directory" &&
|
|
256
|
+
!isDirectoryLikeCopyPath(destinationPath)
|
|
257
|
+
) {
|
|
258
|
+
throw new CliError(
|
|
259
|
+
"Copying multiple sources requires a directory destination.",
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const operations = []
|
|
264
|
+
for (const [index, sourcePath] of sourcePaths.entries()) {
|
|
265
|
+
const source = sources[index]
|
|
266
|
+
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
267
|
+
if (sourceType === "missing") {
|
|
268
|
+
throw new CliError(`Copy source "${source}" does not exist.`)
|
|
269
|
+
}
|
|
270
|
+
if (sourceType === "directory" && !options.recursive) {
|
|
271
|
+
throw new CliError(
|
|
272
|
+
`Copy source "${source}" is a directory. Re-run with "--recursive" to copy directories.`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const targetPath = getCopyTargetPath({
|
|
277
|
+
destination: destinationPath,
|
|
278
|
+
destinationType,
|
|
279
|
+
runtime,
|
|
280
|
+
source: sourcePath,
|
|
281
|
+
sourceType,
|
|
282
|
+
})
|
|
283
|
+
const targetType = await getCopyPathType(targetPath, client, runtime)
|
|
284
|
+
const shouldUseCopyTo =
|
|
285
|
+
sourceType === "file" &&
|
|
286
|
+
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
192
287
|
|
|
193
|
-
const confirmedRootOverwrite = await confirmCopyRootOverwrite({
|
|
194
|
-
force: options.force === true,
|
|
195
|
-
longForce: options.longForce === true,
|
|
196
|
-
runtime,
|
|
197
|
-
targetPath,
|
|
198
|
-
targetType,
|
|
199
|
-
})
|
|
200
|
-
if (!confirmedRootOverwrite) {
|
|
201
288
|
await confirmCopyOverwrite({
|
|
289
|
+
client,
|
|
202
290
|
destination,
|
|
203
291
|
force: options.force === true,
|
|
292
|
+
noClobber: options.noClobber === true,
|
|
204
293
|
runtime,
|
|
294
|
+
sourcePath,
|
|
205
295
|
sourceType,
|
|
206
296
|
targetPath,
|
|
207
297
|
targetType,
|
|
208
298
|
})
|
|
299
|
+
|
|
300
|
+
operations.push({ shouldUseCopyTo, sourcePath, targetPath })
|
|
209
301
|
}
|
|
210
|
-
await replaceCopyTargetBeforeCopy({
|
|
211
|
-
client,
|
|
212
|
-
rcloneOptions: defaultRcloneOptions,
|
|
213
|
-
runtime,
|
|
214
|
-
sourceType,
|
|
215
|
-
targetPath,
|
|
216
|
-
targetType,
|
|
217
|
-
})
|
|
218
302
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
303
|
+
const copyProgress = createR2Progress({
|
|
304
|
+
enabled: options.progress === true,
|
|
305
|
+
label: "Copying R2 paths",
|
|
306
|
+
max: operations.length,
|
|
307
|
+
success: "R2 copy complete.",
|
|
308
|
+
})
|
|
309
|
+
try {
|
|
310
|
+
for (const operation of operations) {
|
|
311
|
+
await client.copy(
|
|
312
|
+
await toRcloneCopyPath(operation.sourcePath, client, runtime),
|
|
313
|
+
await toRcloneCopyPath(operation.targetPath, client, runtime),
|
|
314
|
+
{
|
|
315
|
+
ignoreExisting: options.noClobber === true,
|
|
316
|
+
rcloneOptions: defaultRcloneOptions,
|
|
317
|
+
to: operation.shouldUseCopyTo,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
copyProgress.advance()
|
|
321
|
+
}
|
|
322
|
+
copyProgress.stop()
|
|
323
|
+
} catch (error) {
|
|
324
|
+
copyProgress.error("R2 copy failed.")
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
224
327
|
outro("R2 copy complete.")
|
|
225
328
|
}
|
|
226
329
|
|
|
227
330
|
/**
|
|
228
|
-
* @param {string}
|
|
229
|
-
* @param {string} destination
|
|
331
|
+
* @param {string|string[]} paths
|
|
230
332
|
* @param {R2MoveOptions} options
|
|
231
333
|
* @param {R2Runtime} [runtime]
|
|
232
334
|
*/
|
|
233
|
-
export async function moveR2(
|
|
335
|
+
export async function moveR2(paths, options = {}, runtime = {}) {
|
|
234
336
|
intro("r2 mv")
|
|
235
|
-
const
|
|
337
|
+
const { sources, destination } = parseCopyArguments(paths)
|
|
338
|
+
if (options.force && options.noClobber) {
|
|
339
|
+
throw new CliError('Cannot use "--force" with "--no-clobber".')
|
|
340
|
+
}
|
|
341
|
+
const sourcePaths = sources.map((source) =>
|
|
342
|
+
parseR2OnlyPath(source, "Move source"),
|
|
343
|
+
)
|
|
236
344
|
const destinationPath = parseR2OnlyPath(destination, "Move destination")
|
|
237
|
-
if (
|
|
345
|
+
if (
|
|
346
|
+
sources.some((source) => isR2BucketRootPath(source)) ||
|
|
347
|
+
isR2BucketRootPath(destination)
|
|
348
|
+
) {
|
|
238
349
|
throw new CliError("Refusing to move the R2 bucket root.")
|
|
239
350
|
}
|
|
240
351
|
|
|
241
352
|
const client = createR2CommandClient(runtime)
|
|
242
|
-
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
243
|
-
if (sourceType === "missing") {
|
|
244
|
-
throw new CliError(`Move source "${source}" does not exist.`)
|
|
245
|
-
}
|
|
246
353
|
const destinationType = await getCopyPathType(
|
|
247
354
|
destinationPath,
|
|
248
355
|
client,
|
|
249
356
|
runtime,
|
|
250
357
|
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
destinationType
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
sourceType === "file" &&
|
|
261
|
-
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
358
|
+
if (
|
|
359
|
+
sourcePaths.length > 1 &&
|
|
360
|
+
destinationType !== "directory" &&
|
|
361
|
+
!isDirectoryLikeCopyPath(destinationPath)
|
|
362
|
+
) {
|
|
363
|
+
throw new CliError(
|
|
364
|
+
"Moving multiple sources requires a directory destination.",
|
|
365
|
+
)
|
|
366
|
+
}
|
|
262
367
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
runtime
|
|
267
|
-
sourceType
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
})
|
|
271
|
-
await replaceCopyTargetBeforeCopy({
|
|
272
|
-
client,
|
|
273
|
-
rcloneOptions: defaultRcloneOptions,
|
|
274
|
-
runtime,
|
|
275
|
-
sourceType,
|
|
276
|
-
targetPath,
|
|
277
|
-
targetType,
|
|
278
|
-
})
|
|
368
|
+
const operations = []
|
|
369
|
+
for (const [index, sourcePath] of sourcePaths.entries()) {
|
|
370
|
+
const source = sources[index]
|
|
371
|
+
const sourceType = await getCopyPathType(sourcePath, client, runtime)
|
|
372
|
+
if (sourceType === "missing") {
|
|
373
|
+
throw new CliError(`Move source "${source}" does not exist.`)
|
|
374
|
+
}
|
|
279
375
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
376
|
+
const targetPath = getCopyTargetPath({
|
|
377
|
+
destination: destinationPath,
|
|
378
|
+
destinationType,
|
|
379
|
+
runtime,
|
|
380
|
+
source: sourcePath,
|
|
381
|
+
sourceType,
|
|
382
|
+
})
|
|
383
|
+
const targetType = await getCopyPathType(targetPath, client, runtime)
|
|
384
|
+
const shouldUseMoveTo =
|
|
385
|
+
sourceType === "file" &&
|
|
386
|
+
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
287
387
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
client,
|
|
300
|
-
rcloneOptions,
|
|
301
|
-
runtime,
|
|
302
|
-
sourceType,
|
|
303
|
-
targetPath,
|
|
304
|
-
targetType,
|
|
305
|
-
}) {
|
|
306
|
-
if (sourceType !== "directory" || targetType !== "directory") {
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
if (targetPath.remote) {
|
|
310
|
-
await client.remove(targetPath.path, { recursive: true, rcloneOptions })
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
await rm(resolveLocalPath(targetPath.path, runtime), {
|
|
314
|
-
force: true,
|
|
315
|
-
recursive: true,
|
|
316
|
-
})
|
|
317
|
-
}
|
|
388
|
+
await confirmCopyOverwrite({
|
|
389
|
+
client,
|
|
390
|
+
destination,
|
|
391
|
+
force: options.force === true,
|
|
392
|
+
noClobber: options.noClobber === true,
|
|
393
|
+
runtime,
|
|
394
|
+
sourcePath,
|
|
395
|
+
sourceType,
|
|
396
|
+
targetPath,
|
|
397
|
+
targetType,
|
|
398
|
+
})
|
|
318
399
|
|
|
319
|
-
|
|
320
|
-
* @param {{
|
|
321
|
-
* force: boolean,
|
|
322
|
-
* longForce: boolean,
|
|
323
|
-
* runtime: R2Runtime,
|
|
324
|
-
* targetPath: CopyPath,
|
|
325
|
-
* targetType: CopyPathType
|
|
326
|
-
* }} args
|
|
327
|
-
*/
|
|
328
|
-
async function confirmCopyRootOverwrite({
|
|
329
|
-
force,
|
|
330
|
-
longForce,
|
|
331
|
-
runtime,
|
|
332
|
-
targetPath,
|
|
333
|
-
targetType,
|
|
334
|
-
}) {
|
|
335
|
-
if (!isR2BucketRootCopyPath(targetPath) || targetType === "missing") {
|
|
336
|
-
return false
|
|
337
|
-
}
|
|
338
|
-
log.warn(
|
|
339
|
-
"This will replace ALL files in the configured R2 bucket. This cannot be undone.",
|
|
340
|
-
)
|
|
341
|
-
if (longForce) {
|
|
342
|
-
return true
|
|
343
|
-
}
|
|
344
|
-
if (force && !getInteractive(runtime)) {
|
|
345
|
-
throw new CliError(
|
|
346
|
-
'Ignoring "-f" for R2 bucket-root replacement. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
if (!getInteractive(runtime)) {
|
|
350
|
-
throw new CliError(
|
|
351
|
-
'Replacing the R2 bucket root requires interactive confirmation. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
if (force) {
|
|
355
|
-
log.warn(
|
|
356
|
-
'Ignoring "-f" for R2 bucket-root replacement. Use "--force" if you are sure you want to replace the entire R2 bucket.',
|
|
357
|
-
)
|
|
400
|
+
operations.push({ shouldUseMoveTo, sourcePath, targetPath })
|
|
358
401
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
402
|
+
|
|
403
|
+
const moveProgress = createR2Progress({
|
|
404
|
+
enabled: options.progress === true,
|
|
405
|
+
label: "Moving R2 paths",
|
|
406
|
+
max: operations.length,
|
|
407
|
+
success: "R2 move complete.",
|
|
408
|
+
})
|
|
409
|
+
try {
|
|
410
|
+
for (const operation of operations) {
|
|
411
|
+
await client.move(
|
|
412
|
+
await toRcloneCopyPath(operation.sourcePath, client, runtime),
|
|
413
|
+
await toRcloneCopyPath(operation.targetPath, client, runtime),
|
|
414
|
+
{
|
|
415
|
+
ignoreExisting: options.noClobber === true,
|
|
416
|
+
rcloneOptions: defaultRcloneOptions,
|
|
417
|
+
to: operation.shouldUseMoveTo,
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
moveProgress.advance()
|
|
421
|
+
}
|
|
422
|
+
moveProgress.stop()
|
|
423
|
+
} catch (error) {
|
|
424
|
+
moveProgress.error("R2 move failed.")
|
|
425
|
+
throw error
|
|
364
426
|
}
|
|
365
|
-
|
|
427
|
+
outro("R2 move complete.")
|
|
366
428
|
}
|
|
367
429
|
|
|
368
430
|
/**
|
|
369
431
|
* @param {R2Runtime} runtime
|
|
370
432
|
*/
|
|
371
433
|
function createR2CommandClient(runtime) {
|
|
372
|
-
return createR2Client({
|
|
434
|
+
return (runtime.createR2Client ?? createR2Client)({
|
|
373
435
|
cwd: runtime.cwd ?? processCwd(),
|
|
374
436
|
interactive: getInteractive(runtime),
|
|
375
437
|
promptText: getPromptText(runtime),
|
|
376
438
|
})
|
|
377
439
|
}
|
|
378
440
|
|
|
441
|
+
/**
|
|
442
|
+
* @param {{enabled: boolean, label: string, max: number, success: string}} options
|
|
443
|
+
*/
|
|
444
|
+
function createR2Progress({ enabled, label, max, success }) {
|
|
445
|
+
if (!enabled || max === 0) {
|
|
446
|
+
return {
|
|
447
|
+
advance() {},
|
|
448
|
+
error() {},
|
|
449
|
+
stop() {},
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const operationProgress = progress({ max })
|
|
454
|
+
let completed = 0
|
|
455
|
+
operationProgress.start(`${label} (0/${max})`)
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
advance() {
|
|
459
|
+
completed += 1
|
|
460
|
+
operationProgress.advance(1, `${label} (${completed}/${max})`)
|
|
461
|
+
},
|
|
462
|
+
/**
|
|
463
|
+
* @param {string} message
|
|
464
|
+
*/
|
|
465
|
+
error(message) {
|
|
466
|
+
operationProgress.error(message)
|
|
467
|
+
},
|
|
468
|
+
stop() {
|
|
469
|
+
operationProgress.stop(success)
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
379
474
|
/**
|
|
380
475
|
* @param {R2Runtime} runtime
|
|
381
476
|
*/
|
|
@@ -407,6 +502,23 @@ function parseCopyPath(value) {
|
|
|
407
502
|
return { remote: true, path: normalizeR2Path(value) }
|
|
408
503
|
}
|
|
409
504
|
|
|
505
|
+
/**
|
|
506
|
+
* @param {string|string[]} paths
|
|
507
|
+
* @returns {{sources: string[], destination: string}}
|
|
508
|
+
*/
|
|
509
|
+
function parseCopyArguments(paths) {
|
|
510
|
+
const copyPaths = Array.isArray(paths) ? paths : [paths]
|
|
511
|
+
if (copyPaths.length < 2) {
|
|
512
|
+
throw new CliError(
|
|
513
|
+
"R2 copy requires at least one source and a destination.",
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
sources: copyPaths.slice(0, -1),
|
|
518
|
+
destination: copyPaths[copyPaths.length - 1],
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
410
522
|
/**
|
|
411
523
|
* @param {string} value
|
|
412
524
|
* @param {string} label
|
|
@@ -443,6 +555,18 @@ function getCopyTargetPath({
|
|
|
443
555
|
if (destinationType === "file") {
|
|
444
556
|
throw new CliError("Cannot copy a directory to an existing file.")
|
|
445
557
|
}
|
|
558
|
+
if (isCopyPathContentsOnly(source)) {
|
|
559
|
+
return destination
|
|
560
|
+
}
|
|
561
|
+
if (
|
|
562
|
+
destinationType === "directory" ||
|
|
563
|
+
isDirectoryLikeCopyPath(destination)
|
|
564
|
+
) {
|
|
565
|
+
return {
|
|
566
|
+
...destination,
|
|
567
|
+
path: joinCopyPath(destination, getCopyPathName(source, runtime)),
|
|
568
|
+
}
|
|
569
|
+
}
|
|
446
570
|
return destination
|
|
447
571
|
}
|
|
448
572
|
|
|
@@ -461,18 +585,24 @@ function getCopyTargetPath({
|
|
|
461
585
|
|
|
462
586
|
/**
|
|
463
587
|
* @param {{
|
|
588
|
+
* client: ReturnType<typeof createR2CommandClient>,
|
|
464
589
|
* destination: string,
|
|
465
590
|
* force: boolean,
|
|
591
|
+
* noClobber: boolean,
|
|
466
592
|
* runtime: R2Runtime,
|
|
593
|
+
* sourcePath: CopyPath,
|
|
467
594
|
* sourceType: CopyPathType,
|
|
468
595
|
* targetPath: CopyPath,
|
|
469
596
|
* targetType: CopyPathType
|
|
470
597
|
* }} args
|
|
471
598
|
*/
|
|
472
599
|
async function confirmCopyOverwrite({
|
|
600
|
+
client,
|
|
473
601
|
destination,
|
|
474
602
|
force,
|
|
603
|
+
noClobber,
|
|
475
604
|
runtime,
|
|
605
|
+
sourcePath,
|
|
476
606
|
sourceType,
|
|
477
607
|
targetPath,
|
|
478
608
|
targetType,
|
|
@@ -480,6 +610,9 @@ async function confirmCopyOverwrite({
|
|
|
480
610
|
if (targetType === "missing") {
|
|
481
611
|
return
|
|
482
612
|
}
|
|
613
|
+
if (sourceType === "directory" && targetType === "file") {
|
|
614
|
+
throw new CliError("Cannot transfer a directory to an existing file.")
|
|
615
|
+
}
|
|
483
616
|
if (
|
|
484
617
|
sourceType === "file" &&
|
|
485
618
|
targetType === "directory" &&
|
|
@@ -490,19 +623,128 @@ async function confirmCopyOverwrite({
|
|
|
490
623
|
if (force) {
|
|
491
624
|
return
|
|
492
625
|
}
|
|
626
|
+
if (noClobber) {
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
const overwrittenPaths =
|
|
630
|
+
sourceType === "directory"
|
|
631
|
+
? await getDirectoryOverwritePaths({
|
|
632
|
+
client,
|
|
633
|
+
runtime,
|
|
634
|
+
sourcePath,
|
|
635
|
+
targetPath,
|
|
636
|
+
})
|
|
637
|
+
: [formatCopyPath(targetPath)]
|
|
638
|
+
if (overwrittenPaths.length === 0) {
|
|
639
|
+
return
|
|
640
|
+
}
|
|
493
641
|
if (!getInteractive(runtime)) {
|
|
494
642
|
throw new CliError(
|
|
495
|
-
'
|
|
643
|
+
'Transfer would overwrite existing files. Re-run with "--force" in non-interactive environments.',
|
|
496
644
|
)
|
|
497
645
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
646
|
+
log.warn(`Files to overwrite (${overwrittenPaths.length})`)
|
|
647
|
+
for (const overwrittenPath of overwrittenPaths) {
|
|
648
|
+
log.message(`• ${overwrittenPath}`)
|
|
649
|
+
}
|
|
650
|
+
const message =
|
|
651
|
+
sourceType === "directory"
|
|
652
|
+
? `Overwrite any existing files under destination "${destination}"? [y/N] `
|
|
653
|
+
: `Overwrite existing destination "${destination}"? [y/N] `
|
|
654
|
+
const shouldOverwrite = await getPromptConfirm(runtime)(message)
|
|
501
655
|
if (!shouldOverwrite) {
|
|
502
656
|
cancelPrompt()
|
|
503
657
|
}
|
|
504
658
|
}
|
|
505
659
|
|
|
660
|
+
/**
|
|
661
|
+
* @param {{
|
|
662
|
+
* client: ReturnType<typeof createR2CommandClient>
|
|
663
|
+
* runtime: R2Runtime
|
|
664
|
+
* sourcePath: CopyPath
|
|
665
|
+
* targetPath: CopyPath
|
|
666
|
+
* }} args
|
|
667
|
+
*/
|
|
668
|
+
async function getDirectoryOverwritePaths({
|
|
669
|
+
client,
|
|
670
|
+
runtime,
|
|
671
|
+
sourcePath,
|
|
672
|
+
targetPath,
|
|
673
|
+
}) {
|
|
674
|
+
const sourceFiles = await listCopyFiles(sourcePath, client, runtime)
|
|
675
|
+
if (sourceFiles.length === 0) {
|
|
676
|
+
return []
|
|
677
|
+
}
|
|
678
|
+
const targetFiles = new Set(await listCopyFiles(targetPath, client, runtime))
|
|
679
|
+
return sourceFiles
|
|
680
|
+
.filter((relativePath) => targetFiles.has(relativePath))
|
|
681
|
+
.map((relativePath) =>
|
|
682
|
+
formatCopyPath({
|
|
683
|
+
...targetPath,
|
|
684
|
+
path: joinCopyPath(targetPath, relativePath),
|
|
685
|
+
}),
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* @param {CopyPath} copyPath
|
|
691
|
+
* @param {ReturnType<typeof createR2CommandClient>} client
|
|
692
|
+
* @param {R2Runtime} runtime
|
|
693
|
+
*/
|
|
694
|
+
async function listCopyFiles(copyPath, client, runtime) {
|
|
695
|
+
if (copyPath.remote) {
|
|
696
|
+
return parseRcloneFileList(await client.list(copyPath.path))
|
|
697
|
+
}
|
|
698
|
+
return listLocalFiles(resolveLocalPath(copyPath.path, runtime))
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {string} root
|
|
703
|
+
* @param {string} [prefix]
|
|
704
|
+
* @returns {Promise<string[]>}
|
|
705
|
+
*/
|
|
706
|
+
async function listLocalFiles(root, prefix = "") {
|
|
707
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
708
|
+
const files = []
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
const relativePath = prefix
|
|
711
|
+
? joinRelativePath(prefix, entry.name)
|
|
712
|
+
: entry.name
|
|
713
|
+
const absolutePath = join(root, entry.name)
|
|
714
|
+
if (entry.isDirectory()) {
|
|
715
|
+
files.push(...(await listLocalFiles(absolutePath, relativePath)))
|
|
716
|
+
} else if (entry.isFile()) {
|
|
717
|
+
files.push(relativePath)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return files
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* @param {string} output
|
|
725
|
+
*/
|
|
726
|
+
function parseRcloneFileList(output) {
|
|
727
|
+
return output
|
|
728
|
+
.split(/\r?\n/)
|
|
729
|
+
.map((line) => line.trim())
|
|
730
|
+
.filter(Boolean)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @param {string} parent
|
|
735
|
+
* @param {string} child
|
|
736
|
+
*/
|
|
737
|
+
function joinRelativePath(parent, child) {
|
|
738
|
+
return parent ? posix.join(parent, child) : child
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* @param {CopyPath} copyPath
|
|
743
|
+
*/
|
|
744
|
+
function formatCopyPath(copyPath) {
|
|
745
|
+
return copyPath.remote ? `${remotePathPrefix}${copyPath.path}` : copyPath.path
|
|
746
|
+
}
|
|
747
|
+
|
|
506
748
|
/**
|
|
507
749
|
* @param {CopyPath} copyPath
|
|
508
750
|
* @param {ReturnType<typeof createR2CommandClient>} client
|
|
@@ -542,6 +784,16 @@ function isDirectoryLikeCopyPath(copyPath) {
|
|
|
542
784
|
return copyPath.path === "." || copyPath.path.endsWith("/")
|
|
543
785
|
}
|
|
544
786
|
|
|
787
|
+
/**
|
|
788
|
+
* @param {CopyPath} copyPath
|
|
789
|
+
*/
|
|
790
|
+
function isCopyPathContentsOnly(copyPath) {
|
|
791
|
+
const path = copyPath.remote
|
|
792
|
+
? copyPath.path
|
|
793
|
+
: copyPath.path.replaceAll("\\", "/")
|
|
794
|
+
return path === "." || path.endsWith("/.")
|
|
795
|
+
}
|
|
796
|
+
|
|
545
797
|
/**
|
|
546
798
|
* @param {CopyPath} parent
|
|
547
799
|
* @param {string} child
|
|
@@ -592,13 +844,6 @@ function isR2BucketRootPath(path) {
|
|
|
592
844
|
return path.trim() === "/"
|
|
593
845
|
}
|
|
594
846
|
|
|
595
|
-
/**
|
|
596
|
-
* @param {CopyPath} path
|
|
597
|
-
*/
|
|
598
|
-
function isR2BucketRootCopyPath(path) {
|
|
599
|
-
return path.remote && path.path === ""
|
|
600
|
-
}
|
|
601
|
-
|
|
602
847
|
/**
|
|
603
848
|
* @param {unknown} error
|
|
604
849
|
*/
|
package/src/support/r2.js
CHANGED
|
@@ -248,12 +248,17 @@ export function createR2Client({ cwd, interactive, promptText }) {
|
|
|
248
248
|
/**
|
|
249
249
|
* @param {string} source
|
|
250
250
|
* @param {string} destination
|
|
251
|
-
* @param {{to?: boolean, rcloneOptions?: RcloneOptions}} [options]
|
|
251
|
+
* @param {{ignoreExisting?: boolean, to?: boolean, rcloneOptions?: RcloneOptions}} [options]
|
|
252
252
|
*/
|
|
253
253
|
async copy(source, destination, options = {}) {
|
|
254
254
|
const r2Config = await getConfig()
|
|
255
255
|
await runConfiguredRclone({
|
|
256
|
-
args: [
|
|
256
|
+
args: [
|
|
257
|
+
options.to ? "copyto" : "copy",
|
|
258
|
+
source,
|
|
259
|
+
destination,
|
|
260
|
+
...(options.ignoreExisting ? ["--ignore-existing"] : []),
|
|
261
|
+
],
|
|
257
262
|
cwd,
|
|
258
263
|
config: r2Config,
|
|
259
264
|
rcloneOptions: options.rcloneOptions,
|
|
@@ -263,12 +268,17 @@ export function createR2Client({ cwd, interactive, promptText }) {
|
|
|
263
268
|
/**
|
|
264
269
|
* @param {string} source
|
|
265
270
|
* @param {string} destination
|
|
266
|
-
* @param {{to?: boolean, rcloneOptions?: RcloneOptions}} [options]
|
|
271
|
+
* @param {{ignoreExisting?: boolean, to?: boolean, rcloneOptions?: RcloneOptions}} [options]
|
|
267
272
|
*/
|
|
268
273
|
async move(source, destination, options = {}) {
|
|
269
274
|
const r2Config = await getConfig()
|
|
270
275
|
await runConfiguredRclone({
|
|
271
|
-
args: [
|
|
276
|
+
args: [
|
|
277
|
+
options.to ? "moveto" : "move",
|
|
278
|
+
source,
|
|
279
|
+
destination,
|
|
280
|
+
...(options.ignoreExisting ? ["--ignore-existing"] : []),
|
|
281
|
+
],
|
|
272
282
|
cwd,
|
|
273
283
|
config: r2Config,
|
|
274
284
|
rcloneOptions: options.rcloneOptions,
|