proton-drive-mcp 1.0.35 → 1.0.37
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 +55 -0
- package/Dockerfile +15 -3
- package/README.md +27 -22
- package/dist/cli.js +42 -16
- package/dist/index.js +219 -37
- package/dist/services/drive.js +225 -56
- package/dist/utils/isMainModule.js +7 -1
- package/dist/utils/localguard.js +102 -0
- package/dist/utils/subprocess.js +260 -30
- package/dist/utils/syncfs.js +143 -17
- package/dist/utils/validation.js +33 -9
- package/glama.json +4 -1
- package/package.json +3 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.37 — 2026-09-20
|
|
4
|
+
|
|
5
|
+
A parallel review of the whole project (security, live testing of every tool group, reliability, token cost, CI/supply chain, test-suite gaps — modelled on the Proton Mail Bridge MCP review rounds), then every finding fixed. Every behaviour change was checked live against a real account (39 end-to-end checks on scratch data) besides the automated tests. 1.0.36 was folded into this release.
|
|
6
|
+
|
|
7
|
+
**Behaviour changes to be aware of:** destructive and outward-facing tools now require `confirmed: true`; long lists return `{total, offset, limit, hasMore, items}` instead of a bare array; responses are compact JSON; tool arguments are validated against the published schema (unknown/mistyped arguments are rejected).
|
|
8
|
+
|
|
9
|
+
### Critical
|
|
10
|
+
- **`npx proton-drive-mcp` never served MCP.** The entry-point check compared `import.meta.url` with `process.argv[1]`, which under `npx`, a global install or `node_modules/.bin` is the bin *symlink* — so the published server started, printed nothing and exited 0. Now compares real paths. CI gained a pack-and-install smoke test that launches the real bin symlink (the previous tests imported `dist/` directly and could not see this).
|
|
11
|
+
- **A failing call leaked its full command line to the client**, including the `--password` of `drive_share_set_url` and the binary path (`e.message` from Node's exec). Errors are now built from the CLI's own output; secrets are never echoed. The same change surfaces the real cause when the CLI writes it to stdout behind an `=====` banner (e.g. a missing local file), truncates huge output, tolerates ANSI noise before the JSON, and recognises more "session expired" wording.
|
|
12
|
+
- **`drive_rename` accepted `.` and `..`**, after which `drive_list` returned a path that resolves to the *parent* folder — a follow-up `drive_trash` on it would have trashed the wrong node. Rejected now; `list()` also builds paths by concatenation instead of `posix.join`.
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
- `drive_download` with `remove` (deletes the existing local file/folder), `drive_upload` with `replace` (trashes the remote item), `photos_download` with `remove`, and overwriting a sync-folder file via `drive_write_file` now need `confirmed: true` (CLI: `--confirm`).
|
|
16
|
+
- Also gated: `drive_auth_logout`, `drive_share_invite`, `drive_share_revoke`, `drive_share_set_url` (public links), `drive_share_remove_url`, `drive_share_leave`, `drive_invitation_reject`, `photos_remove_from_album`.
|
|
17
|
+
- `drive_read_file` / `drive_write_file` no longer follow symlinks out of `PROTON_DRIVE_SYNC_PATH` (containment by realpath, `O_NOFOLLOW`), read size is checked before loading, writes are capped at 5 MB.
|
|
18
|
+
- Local paths given to upload/download/photos tools are checked: credential locations (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.claude*`, keychains, `.env`, keys, …) are refused (`PROTON_DRIVE_LOCAL_ROOT` allowlist, `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS=1` to opt out).
|
|
19
|
+
- Arguments are validated against the tool schema (unknown keys, wrong types, enums, integer bounds); validators reject non-strings (an array used to coerce into a path).
|
|
20
|
+
|
|
21
|
+
### Fixed (data correctness)
|
|
22
|
+
- **`drive_share_set_url` silently turned a password-protected link into an open one** when re-run without a password (same URL). It now says so in a `warning`; `drive_share_status` reports `sharePasswordProtected`, `shareUrlExpiresAt` and `editorsCanShare`.
|
|
23
|
+
- **`photos_create_album` created duplicate names**, after which `update`/`delete` acted on an arbitrary one of them. Duplicates are refused, and album operations refuse an ambiguous `/albums/<name>`; `photos_list_albums` returns `uid`.
|
|
24
|
+
- **`drive_share_revoke` reported success for a non-member**, and for a wrong-case address the real member kept access (the CLI matches case-sensitively and exits 0 either way). Now verified against the current members, matched case-insensitively, canonical address sent. `drive_share_remove_all` on an unshared item reports "Nothing to remove".
|
|
25
|
+
- **`drive_list "/"` returned ten `[unnamed]` entries**; it now lists the real roots.
|
|
26
|
+
- **Names with a trailing space were unusable** (paths were trimmed: `list` returned `…/tsp ` but `info` then said "Node not found"; `mkdir "trail "` created `trail`). Paths, names and passwords are no longer trimmed; trailing `/` is stripped from remote paths.
|
|
27
|
+
- **`drive_move` across folders with a rename** could fail with a bogus collision or leave the item half-moved. It now checks the destination first, renames-first when the source's own name is taken there, rolls back on failure and reports partial state explicitly; a `X → X` no-op on a missing source now fails.
|
|
28
|
+
- `mkdir`/`move` split paths on the last *unescaped* slash (items created by other clients can contain `\/`).
|
|
29
|
+
- `{ok:false}` results are also checked for `share_leave`, invitation accept/reject and album add/remove-photo; the "name collision" hint is only added for real collisions (move-into-itself etc. get their own message).
|
|
30
|
+
- `drive_list_trash` returns `uid` (and trash time when the CLI reports it) — names are not unique in trash, so two items could share one path.
|
|
31
|
+
|
|
32
|
+
### Reliability
|
|
33
|
+
- Cancelling a request (`notifications/cancelled`) now kills the running CLI process; SIGTERM/SIGINT, a closed stdout (EPIPE) and client disconnect kill running CLI children instead of orphaning them; a timeout kills the whole process group (grandchildren too); stdin closing mid-call no longer leaves the server lingering for up to 30 minutes.
|
|
34
|
+
- The startup CLI probe runs after the connection is up, so a slow `proton-drive version` can no longer delay `initialize` (hosts time out at ~5 s).
|
|
35
|
+
|
|
36
|
+
### Token cost
|
|
37
|
+
- `drive_list`, `drive_list_trash`, `photos_list_timeline`, `photos_list_album_photos` take `limit`/`offset` and return `{total, offset, limit, hasMore, items}` (defaults 200 / 100 / 50 / 100). Measured on a real account before the change: the trash list alone was ~54k tokens, the photo timeline ~15.6k.
|
|
38
|
+
- Compact JSON (−14–16 % bytes on large lists); `drive_info` unwraps the `{ok,value}` wrappers and drops duplicate fields (`verbose: true` for the raw node).
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `drive_copy`: `newName` (CLI `--name`) — copy under a new name or duplicate inside the same folder. CLI: `copy --name`, `info --verbose`, `album photos --load-details`.
|
|
42
|
+
- `photos_list_album_photos`: `loadDetails`.
|
|
43
|
+
|
|
44
|
+
### CI / supply chain / packaging
|
|
45
|
+
- Actions pinned to commit SHAs, least-privilege `permissions`, concurrency group, `npm audit --audit-level=high` gate, pack-and-install smoke test, Dependabot for npm and Actions (majors of `typescript` / `@types/node` ignored).
|
|
46
|
+
- Publish: `id-token: write`, `npm publish --provenance`, `npm-publish` environment, tag must be reachable from `main`.
|
|
47
|
+
- Lockfile refreshed (6 advisories → 0), SDK range raised to `^1.29.0`.
|
|
48
|
+
- Dockerfile: multi-stage, non-root, production deps only, `.dockerignore`; `SECURITY.md`; `.code-review-graph/` ignored.
|
|
49
|
+
|
|
50
|
+
### Tests and docs
|
|
51
|
+
- ~230 unit tests plus real-process tests that spawn the server/CLI against a fake `PROTON_DRIVE_BIN` over stdio (gates, schema enforcement, error sanitization, lifecycle, symlink launch, tool/README/glama/smithery parity) and an opt-in read-only live smoke test. The old integration test probed a nonexistent `auth status` command and never ran in CI; it is removed.
|
|
52
|
+
- Tool descriptions corrected where they contradicted behaviour (`photos_create_album`, `share_revoke`, `share_set_url`, `photos_delete_album`, `drive_read_file` / `drive_write_file` "requires authentication", timeline shapes). README no longer claims `--skip-thumbnails` (removed in 1.0.34) or full CLI parity; SPEC.md rewritten (it described 11 tools and a layout that never existed).
|
|
53
|
+
|
|
54
|
+
### Investigated, not bugs
|
|
55
|
+
- CLI role `inherited` is rejected by the CLI itself despite its help text — the three roles offered are correct.
|
|
56
|
+
- Stdout hygiene, concurrent calls, the 60 s / 50 MB limits, a missing CLI and unicode/emoji/backslash names all behaved correctly.
|
|
57
|
+
|
|
3
58
|
## 1.0.35 — 2026-09-03
|
|
4
59
|
|
|
5
60
|
Continued the live-testing bug sweep. This round found a serious silent-data-corruption bug in `drive_move`, confirmed by a full live reproduction against the real account, plus the same root cause in `drive_copy`. The same fix hardens three more structurally identical code paths (`trash`, `restore`, `delete`) against the same failure mode.
|
package/Dockerfile
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:24-slim AS build
|
|
2
3
|
WORKDIR /app
|
|
3
|
-
COPY package
|
|
4
|
+
COPY package.json package-lock.json tsconfig.json ./
|
|
4
5
|
RUN npm ci --ignore-scripts
|
|
5
|
-
COPY
|
|
6
|
+
COPY src ./src
|
|
6
7
|
RUN npm run build
|
|
8
|
+
|
|
9
|
+
FROM node:24-slim
|
|
10
|
+
ENV NODE_ENV=production
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY package.json package-lock.json ./
|
|
13
|
+
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
|
14
|
+
COPY --from=build /app/dist ./dist
|
|
15
|
+
# NOTE: this image does NOT include the official `proton-drive` CLI, which the
|
|
16
|
+
# server shells out to. Provide it separately: bind-mount the binary and set
|
|
17
|
+
# PROTON_DRIVE_BIN, or extend this image with a layer that installs it.
|
|
18
|
+
USER node
|
|
7
19
|
CMD ["node", "dist/index.js"]
|
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ proton-drive-cli upload ./dist /my-files/Releases/$(date +%Y-%m-%d) --file-confl
|
|
|
222
222
|
proton-drive-cli download /my-files/Contracts ./audit/contracts
|
|
223
223
|
|
|
224
224
|
# Nightly backup via cron
|
|
225
|
-
0 2 * * * proton-drive-cli upload ~/Documents /my-files/Backups/$(date +%Y-%m-%d) --conflict skip
|
|
225
|
+
0 2 * * * proton-drive-cli upload ~/Documents /my-files/Backups/$(date +%Y-%m-%d) --file-conflict skip --folder-conflict skip
|
|
226
226
|
|
|
227
227
|
# Check who has access before a team change
|
|
228
228
|
proton-drive-cli share status /my-files/Projects
|
|
@@ -263,45 +263,45 @@ proton-drive-cli share status /my-files/Projects
|
|
|
263
263
|
| Tool | Description | Key parameters |
|
|
264
264
|
|------|-------------|----------------|
|
|
265
265
|
| `drive_auth_status` | Check if authenticated (probes `/my-files` — no native status command) | — |
|
|
266
|
-
| `drive_auth_logout` | Log out (clear session) |
|
|
266
|
+
| `drive_auth_logout` | Log out (clear session) ⚠️ | `confirmed: true` |
|
|
267
267
|
| `drive_version` | CLI and SDK version info | — |
|
|
268
|
-
| `drive_list` | List files and folders at a path | `path` |
|
|
269
|
-
| `drive_info` | Get
|
|
268
|
+
| `drive_list` | List files and folders at a path (paginated, default 200; `/` lists the roots) | `path`, `limit?`, `offset?` |
|
|
269
|
+
| `drive_info` | Get metadata for one file/folder, including revision details (noise trimmed) | `path`, `verbose?` (raw CLI node) |
|
|
270
270
|
| `drive_mkdir` | Create a new empty folder | `path` |
|
|
271
|
-
| `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `fileConflictStrategy?` (skip/create-new-revision/rename/replace), `folderConflictStrategy?` (skip/merge/rename/replace) |
|
|
272
|
-
| `drive_download` | Download to local path | `remotePath`, `localPath`, `fileConflictStrategy?` (skip/rename/remove), `folderConflictStrategy?` (skip/merge/rename/remove) |
|
|
271
|
+
| `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `fileConflictStrategy?` (skip/create-new-revision/rename/replace), `folderConflictStrategy?` (skip/merge/rename/replace), `confirmed?` (**required for `replace`** — it trashes the existing remote item) |
|
|
272
|
+
| `drive_download` | Download to local path | `remotePath`, `localPath`, `fileConflictStrategy?` (skip/rename/remove), `folderConflictStrategy?` (skip/merge/rename/remove), `confirmed?` (**required for `remove`** — it deletes the existing local item) |
|
|
273
273
|
| `drive_rename` | Rename in place, no move | `path`, `newName` |
|
|
274
274
|
| `drive_move` | Move and/or rename | `sourcePath`, `destinationPath` |
|
|
275
|
-
| `drive_copy` | Copy file or folder
|
|
275
|
+
| `drive_copy` | Copy file or folder into another Drive folder | `sourcePath`, `destinationPath` (target *parent folder*), `newName?` |
|
|
276
276
|
| `drive_delete` | Permanently delete an item already in trash ⚠️ | `path`, `confirmed: true` |
|
|
277
|
-
| `drive_list_trash` | List items currently in trash
|
|
277
|
+
| `drive_list_trash` | List items currently in trash (paginated, default 100; includes `uid` — names are not unique in trash) | `limit?`, `offset?` |
|
|
278
278
|
| `drive_share_status` | Get sharing members and URL | `path` |
|
|
279
|
-
| `drive_share_invite` | Invite a user | `path`, `email`, `role` (viewer/editor/admin), `message
|
|
280
|
-
| `drive_share_revoke` | Revoke one person's access | `path`, `email` |
|
|
279
|
+
| `drive_share_invite` | Invite a user (sends an email) ⚠️ | `path`, `email`, `role` (viewer/editor/admin), `message?`, `confirmed: true` |
|
|
280
|
+
| `drive_share_revoke` | Revoke one person's access (fails if not a member) ⚠️ | `path`, `email`, `confirmed: true` |
|
|
281
281
|
| `drive_share_remove_all` | Remove every member + pending invitation at once ⚠️ | `path`, `confirmed: true` |
|
|
282
|
-
| `drive_share_set_url` | Create/
|
|
283
|
-
| `drive_share_remove_url` | Remove the public share link ⚠️ | `path` |
|
|
282
|
+
| `drive_share_set_url` | Create/replace a public share link ⚠️ (re-running without `password`/`expiration` removes them; expiry max ~90 days) | `path`, `role?` (viewer/editor), `password?`, `expiration?`, `confirmed: true` |
|
|
283
|
+
| `drive_share_remove_url` | Remove the public share link ⚠️ | `path`, `confirmed: true` |
|
|
284
284
|
| `drive_trash` | Move to trash | `path` |
|
|
285
285
|
| `drive_restore` | Restore from trash | `path` |
|
|
286
286
|
| `drive_empty_trash` | Permanently delete all trash ⚠️ | `confirmed: true` |
|
|
287
287
|
| `drive_read_file` | Read text file from local sync folder | `path` |
|
|
288
|
-
| `drive_write_file` | Write text file to local sync folder ⚠️ | `path`, `content` |
|
|
288
|
+
| `drive_write_file` | Write text file to local sync folder ⚠️ (overwriting an existing file needs confirmation; max 5 MB) | `path`, `content`, `confirmed?` |
|
|
289
289
|
| `drive_list_invitations` | List pending sharing invitations received | — |
|
|
290
290
|
| `drive_invitation_accept` | Accept a pending invitation | `uid` (from `drive_list_invitations`) |
|
|
291
|
-
| `drive_invitation_reject` | Reject a pending invitation ⚠️ | `uid` (from `drive_list_invitations`) |
|
|
292
|
-
| `drive_share_leave` | Leave a shared folder shared with you ⚠️ | `path` |
|
|
291
|
+
| `drive_invitation_reject` | Reject a pending invitation ⚠️ | `uid` (from `drive_list_invitations`), `confirmed: true` |
|
|
292
|
+
| `drive_share_leave` | Leave a shared folder shared with you ⚠️ | `path`, `confirmed: true` |
|
|
293
293
|
| `photos_list_albums` | List all Proton Photos albums | — |
|
|
294
294
|
| `photos_create_album` | Create a new empty album | `name` |
|
|
295
295
|
| `photos_update_album` | Rename an album or change its cover photo | `albumPath`, `name?`, `coverPhotoUid?` |
|
|
296
296
|
| `photos_delete_album` | Delete an album ⚠️ | `albumPath`, `confirmed: true`, `force?`, `save?` |
|
|
297
|
-
| `photos_list_album_photos` | List photos in an album | `albumPath` |
|
|
297
|
+
| `photos_list_album_photos` | List photos in an album (paginated, default 100) | `albumPath`, `loadDetails?`, `limit?`, `offset?` |
|
|
298
298
|
| `photos_add_to_album` | Add a photo from your library to an album | `albumPath`, `photoPath` |
|
|
299
|
-
| `photos_remove_from_album` | Remove a photo from an album (keeps it in library) ⚠️ | `albumPath`, `photoPath` |
|
|
300
|
-
| `photos_list_timeline` | List photos in your full library timeline | `loadDetails?` |
|
|
301
|
-
| `photos_download` | Download photos to a local folder | `photoPaths`, `localFolder`, `conflictStrategy?` (skip/rename/remove) |
|
|
299
|
+
| `photos_remove_from_album` | Remove a photo from an album (keeps it in library) ⚠️ | `albumPath`, `photoPath`, `confirmed: true` |
|
|
300
|
+
| `photos_list_timeline` | List photos in your full library timeline (paginated, default 50) | `loadDetails?`, `limit?`, `offset?` |
|
|
301
|
+
| `photos_download` | Download photos to a local folder | `photoPaths`, `localFolder`, `conflictStrategy?` (skip/rename/remove), `confirmed?` (**required for `remove`**) |
|
|
302
302
|
| `photos_upload` | Upload local files directly into your Photos library | `localPaths`, `conflictStrategy?` (skip/rename) |
|
|
303
303
|
|
|
304
|
-
> ⚠️ **Destructive tools** require `confirmed: true
|
|
304
|
+
> ⚠️ **Destructive and outward-facing tools** (deleting, sharing, public links, logout, and the `replace`/`remove` conflict strategies) require `confirmed: true`, and the CLI requires `--confirm` for the destructive ones. Describe the action to the user first, then pass `confirmed: true`. Tool arguments are validated against the published schema — unknown or mistyped arguments are rejected.
|
|
305
305
|
|
|
306
306
|
---
|
|
307
307
|
|
|
@@ -313,14 +313,17 @@ proton-drive-cli share status /my-files/Projects
|
|
|
313
313
|
| Credential exposure | API keys in config | Zero — OS keychain only |
|
|
314
314
|
| Sharing & invitations | Rarely | Full (invite, revoke, status) |
|
|
315
315
|
| Trash & restore | Rarely | Full |
|
|
316
|
-
| CLI parity | No |
|
|
316
|
+
| CLI parity | No | The CLI mirrors every MCP tool except `drive_read_file` / `drive_write_file` |
|
|
317
317
|
| Shell injection safe | Varies | Yes — `execFile` only |
|
|
318
318
|
|
|
319
319
|
---
|
|
320
320
|
|
|
321
321
|
## Operational notes
|
|
322
322
|
|
|
323
|
-
-
|
|
323
|
+
- Long listings are paginated (`limit` / `offset`, response `{total, offset, limit, hasMore, items}`) to keep responses small. Responses are compact JSON.
|
|
324
|
+
- Local paths passed to upload/download/photos tools are checked: credential locations (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.claude*`, keychains, `.env` files, …) are refused. Set `PROTON_DRIVE_LOCAL_ROOT` to allow only specific directories.
|
|
325
|
+
- The sync-folder tools (`drive_read_file` / `drive_write_file`) refuse to follow symlinks out of `PROTON_DRIVE_SYNC_PATH`.
|
|
326
|
+
- Error messages come from the CLI's own output; the command line (and therefore any `--password`) is never echoed back.
|
|
324
327
|
- `drive_move` accepts a full destination path (parent + new name) for a familiar interface, but the underlying CLI only has separate `move` (change parent) and `rename` (change name) commands — this MCP translates automatically, issuing one or both as needed.
|
|
325
328
|
- `drive_delete` only works on items already in `/trash` or `/photos-trash` — the CLI rejects live paths. Trash an item first with `drive_trash`, or use `drive_empty_trash` to clear everything at once.
|
|
326
329
|
- `drive_auth_status` has no native CLI equivalent — it probes by resolving `/my-files` and reports authenticated based on whether that succeeds.
|
|
@@ -335,6 +338,8 @@ proton-drive-cli share status /my-files/Projects
|
|
|
335
338
|
|---|---|---|
|
|
336
339
|
| `PROTON_DRIVE_SYNC_PATH` | Optional | Absolute path to your local Proton Drive sync folder root (e.g. `/Users/you/Proton Drive`). Required only for `drive_read_file` and `drive_write_file`. The Proton Drive desktop app must be running to sync written files to the cloud. |
|
|
337
340
|
| `PROTON_DRIVE_BIN` | Optional | Override the `proton-drive` binary name or path (default: `proton-drive`). Useful for non-standard installations. |
|
|
341
|
+
| `PROTON_DRIVE_LOCAL_ROOT` | Optional | Path-delimiter-separated list of local directories that upload/download/photos tools may touch. Unset = any path except the built-in credential denylist. |
|
|
342
|
+
| `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS` | Optional | Set to `1` to disable the built-in credential-location denylist (not recommended). |
|
|
338
343
|
|
|
339
344
|
---
|
|
340
345
|
|
package/dist/cli.js
CHANGED
|
@@ -22,12 +22,14 @@ Commands:
|
|
|
22
22
|
auth logout Log out
|
|
23
23
|
version Show CLI/SDK version
|
|
24
24
|
list <path> List files at path
|
|
25
|
-
info <path>
|
|
25
|
+
info <path> [--verbose] Show node metadata (--verbose = raw CLI node)
|
|
26
26
|
mkdir <path> Create a new folder
|
|
27
|
-
upload <local> <remote> [--file-conflict X] [--folder-conflict X]
|
|
28
|
-
Upload file/folder (files: skip/create-new-revision/rename/replace; folders: skip/merge/rename/replace
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
upload <local> <remote> [--file-conflict X] [--folder-conflict X] [--confirm]
|
|
28
|
+
Upload file/folder (files: skip/create-new-revision/rename/replace; folders: skip/merge/rename/replace;
|
|
29
|
+
'replace' trashes the existing remote item and needs --confirm)
|
|
30
|
+
download <remote> <local> [--file-conflict X] [--folder-conflict X] [--confirm]
|
|
31
|
+
Download file/folder (files: skip/rename/remove; folders: skip/merge/rename/remove;
|
|
32
|
+
'remove' deletes the existing LOCAL item and needs --confirm)
|
|
31
33
|
rename <path> <new-name> Rename in place, no move
|
|
32
34
|
move <src> <dst> Move and/or rename
|
|
33
35
|
delete <path> --confirm Delete a file/folder already in trash, permanently
|
|
@@ -38,7 +40,7 @@ Commands:
|
|
|
38
40
|
share set-url <path> [--role] [--password] [--expiration] Create/update public link
|
|
39
41
|
share remove-url <path> Remove public link
|
|
40
42
|
share leave <path> Leave a folder shared with you
|
|
41
|
-
copy <src> <dst>
|
|
43
|
+
copy <src> <dst-parent-folder> [--name N] Copy file/folder into a folder (optionally under a new name)
|
|
42
44
|
trash <path> Move to trash
|
|
43
45
|
trash list List trash contents
|
|
44
46
|
trash empty --confirm Permanently delete all trash
|
|
@@ -50,11 +52,11 @@ Commands:
|
|
|
50
52
|
album create <name> Create a new album
|
|
51
53
|
album update <path> [--name] [--cover-photo-uid] Rename/update album cover
|
|
52
54
|
album delete <path> --confirm [--force] [--save] Delete an album
|
|
53
|
-
album photos <path>
|
|
55
|
+
album photos <path> [--load-details] List photos in an album
|
|
54
56
|
album add-photo <album> <photo> Add a photo to an album
|
|
55
57
|
album remove-photo <album> <photo> Remove a photo from an album
|
|
56
58
|
photo timeline [--load-details] List photos in your timeline
|
|
57
|
-
photo download <photo>... <local> [--conflict X] Download photos (skip/rename/remove)
|
|
59
|
+
photo download <photo>... <local> [--conflict X] [--confirm] Download photos (skip/rename/remove; 'remove' needs --confirm)
|
|
58
60
|
photo upload <local>... [--conflict X] Upload photos to your library (skip/rename)
|
|
59
61
|
|
|
60
62
|
Flags:
|
|
@@ -142,7 +144,7 @@ async function run() {
|
|
|
142
144
|
print(await drive.list(requirePath(sub, "list <path>")));
|
|
143
145
|
break;
|
|
144
146
|
case "info":
|
|
145
|
-
print(await drive.info(requirePath(sub, "info <path>")));
|
|
147
|
+
print(await drive.info(requirePath(sub, "info <path> [--verbose]"), args.includes("--verbose")));
|
|
146
148
|
break;
|
|
147
149
|
case "upload": {
|
|
148
150
|
const rawLocal = sub;
|
|
@@ -170,6 +172,10 @@ async function run() {
|
|
|
170
172
|
console.error(`Invalid --folder-conflict value: ${folderConflictRaw}. Must be skip, merge, rename, or replace.`);
|
|
171
173
|
process.exit(1);
|
|
172
174
|
}
|
|
175
|
+
if ((fileConflictRaw === "replace" || folderConflictRaw === "replace") && !args.includes("--confirm")) {
|
|
176
|
+
console.error("Strategy 'replace' trashes the existing remote item.\nPass --confirm to proceed.");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
173
179
|
print(await drive.upload(local, remote, fileConflictRaw, folderConflictRaw));
|
|
174
180
|
break;
|
|
175
181
|
}
|
|
@@ -200,6 +206,10 @@ async function run() {
|
|
|
200
206
|
console.error(`Invalid --folder-conflict value: ${folderConflictRaw}. Must be skip, merge, rename, or remove.`);
|
|
201
207
|
process.exit(1);
|
|
202
208
|
}
|
|
209
|
+
if ((fileConflictRaw === "remove" || folderConflictRaw === "remove") && !args.includes("--confirm")) {
|
|
210
|
+
console.error("Strategy 'remove' deletes the existing LOCAL file or folder before downloading.\nPass --confirm to proceed.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
203
213
|
print(await drive.download(remote, local, fileConflictRaw, folderConflictRaw));
|
|
204
214
|
break;
|
|
205
215
|
}
|
|
@@ -278,8 +288,10 @@ async function run() {
|
|
|
278
288
|
console.error(`This removes everyone's access to: ${removeAllPath}\nPass --confirm to proceed.`);
|
|
279
289
|
process.exit(1);
|
|
280
290
|
}
|
|
281
|
-
await drive.
|
|
282
|
-
console.log(
|
|
291
|
+
const removedCount = await drive.shareRemoveAll(removeAllPath);
|
|
292
|
+
console.log(removedCount === 0
|
|
293
|
+
? `Nothing to remove: ${removeAllPath} has no members or pending invitations.`
|
|
294
|
+
: `Removed all access (${removedCount} member/invitation(s)) to: ${removeAllPath}`);
|
|
283
295
|
}
|
|
284
296
|
else if (sub === "set-url") {
|
|
285
297
|
const setUrlPath = requirePath(rest[0], "share set-url <path> [--role] [--password] [--expiration]");
|
|
@@ -337,8 +349,18 @@ async function run() {
|
|
|
337
349
|
case "copy": {
|
|
338
350
|
const copySrc = requirePath(sub, "copy <src> <dst>");
|
|
339
351
|
const copyDst = requirePath(rest[0], "copy <src> <dst>");
|
|
340
|
-
|
|
341
|
-
|
|
352
|
+
const rawCopyName = getFlag("--name");
|
|
353
|
+
let copyName;
|
|
354
|
+
try {
|
|
355
|
+
copyName = rawCopyName !== undefined ? validateName(rawCopyName) : undefined;
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
359
|
+
process.exit(1);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await drive.copy(copySrc, copyDst, copyName);
|
|
363
|
+
console.log(`Copied: ${copySrc} → ${copyDst}${copyName ? ` as '${copyName}'` : ""}`);
|
|
342
364
|
break;
|
|
343
365
|
}
|
|
344
366
|
case "invitation":
|
|
@@ -443,7 +465,7 @@ async function run() {
|
|
|
443
465
|
}
|
|
444
466
|
else if (sub === "photos") {
|
|
445
467
|
const albumPath = requirePath(rest[0], "album photos <path>");
|
|
446
|
-
print(await drive.listAlbumPhotos(albumPath));
|
|
468
|
+
print(await drive.listAlbumPhotos(albumPath, args.includes("--load-details")));
|
|
447
469
|
}
|
|
448
470
|
else if (sub === "add-photo") {
|
|
449
471
|
const albumPath = requirePath(rest[0], "album add-photo <album-path> <photo-path>");
|
|
@@ -465,9 +487,9 @@ async function run() {
|
|
|
465
487
|
case "photo": {
|
|
466
488
|
// Strip --conflict <value> out of a variadic positional list.
|
|
467
489
|
const conflictFlagIdx = rest.indexOf("--conflict");
|
|
468
|
-
const positionals = conflictFlagIdx === -1
|
|
490
|
+
const positionals = (conflictFlagIdx === -1
|
|
469
491
|
? rest
|
|
470
|
-
: [...rest.slice(0, conflictFlagIdx), ...rest.slice(conflictFlagIdx + 2)];
|
|
492
|
+
: [...rest.slice(0, conflictFlagIdx), ...rest.slice(conflictFlagIdx + 2)]).filter((x) => x !== "--confirm" && x !== "--load-details");
|
|
471
493
|
const conflictRaw = getFlag("--conflict") ?? "skip";
|
|
472
494
|
if (sub === "timeline") {
|
|
473
495
|
print(await drive.photoTimeline(args.includes("--load-details")));
|
|
@@ -493,6 +515,10 @@ async function run() {
|
|
|
493
515
|
console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip, rename, or remove.`);
|
|
494
516
|
process.exit(1);
|
|
495
517
|
}
|
|
518
|
+
if (conflictRaw === "remove" && !args.includes("--confirm")) {
|
|
519
|
+
console.error("Strategy 'remove' deletes the existing LOCAL file before downloading.\nPass --confirm to proceed.");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
496
522
|
print(await drive.photoDownload(photoPaths, localFolderValidated, conflictRaw));
|
|
497
523
|
}
|
|
498
524
|
else if (sub === "upload") {
|