proton-drive-mcp 1.2.0 → 1.3.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 +26 -0
- package/README.md +49 -7
- package/dist/cli.js +145 -3
- package/dist/index.js +389 -52
- package/dist/prompts.js +73 -0
- package/dist/services/analytics.js +300 -0
- package/dist/services/drive.js +145 -24
- package/dist/services/find.js +207 -0
- package/dist/services/plan.js +248 -0
- package/dist/services/walk.js +209 -0
- package/dist/utils/errors.js +7 -0
- package/dist/utils/glob.js +48 -0
- package/dist/utils/localguard.js +61 -17
- package/dist/utils/validation.js +11 -2
- package/glama.json +32 -0
- package/package.json +1 -1
- package/smithery.yaml +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
Proton Drive has no search, and its end-to-end encryption rules out server-side indexing. This release adds the client-side layer: find things, understand what uses your space, and clean up safely. 4 builders worked in parallel, then 3 verifiers (live on a real account, an independent security review, packaging/CI) and a re-check of every fix. Review findings fixed before release include two ReDoS vectors in search and a local-path guard gap.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`drive_tree`** and **`drive_search`** (also in the `core` tier): a bounded, parallel walk of the drive (8 concurrent calls, `.git` and `node_modules` skipped, call cap, cancellable) with a 5-minute in-memory cache that the server's own writes invalidate. Search by name, glob (relative to the search folder when it contains `/`), extension, media type, size and date, with stable pagination. The first walk of a large drive takes minutes (about 25 s per 1,200 files); later calls are instant.
|
|
9
|
+
- **`drive_usage`**: largest files and folders, breakdown by type, old files and trash stats (sums of file sizes, not the account quota).
|
|
10
|
+
- **`drive_find_duplicates`**: groups by the uploader-claimed sha1 or by size; `verify` downloads and hashes candidates within a size and total budget. It never deletes.
|
|
11
|
+
- **`drive_sharing_audit`**: public links (role, expiry, password; the URL is never printed), invitees, pending invitations and risk flags.
|
|
12
|
+
- **`drive_sync_plan`**: compares a local folder with a Drive folder (plan only).
|
|
13
|
+
- **`drive_bulk_move`** and **`drive_bulk_trash`**: up to 200 items in one batched call. Always two steps: without `confirmed` you get a plan and any problems; `confirmed:true` applies only if there are none. Trash stays reversible.
|
|
14
|
+
- **MCP prompts** (`organise-folder`, `find-files`, `storage-audit`, `sharing-audit`), listed only when the tools they need are in the active tier; also declared in the MCPB manifest.
|
|
15
|
+
- **Human confirmation through elicitation** for clients that support it (Claude Code CLI; Claude Desktop is unverified): gated calls and bulk applies ask the person instead of only failing. Clients without it behave as before. It does not protect against a model that passes `confirmed:true` itself.
|
|
16
|
+
- `drive_list` and `drive_info` now return `mtime`, `uploadedAt` and `sha1` (claimed, often absent); `drive_list` takes a `type` filter; `drive_share_status` adds `shareUrlRole`.
|
|
17
|
+
- Tool titles on every tool, corrected annotations (for example `drive_trash` is not destructive), `maxResultSizeChars` hints on the large-result tools.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- The full tool list is now 46 tools (about 39 KB, was 29.5 KB); `core` is 18 tools (16.8 KB). Use `PROTON_DRIVE_TOOL_TIER=core` to keep the token cost down.
|
|
21
|
+
- Folder uploads and `drive_sync_plan` refuse a local folder that contains protected locations (home directory, `/`, `~/.ssh`, ...); scans skip secret-named files and report `protectedSkipped`. Downloads into your home folder still work. Note that for uploads the CLI does the recursion, so a `.env` inside an uploaded subfolder is not filtered.
|
|
22
|
+
- `.gitignore` covers `node_modules` as a symlink too.
|
|
23
|
+
|
|
24
|
+
### Known limitations
|
|
25
|
+
- Windows is not supported. The CLI has no search, version history, quota or change notifications, so none of those are offered.
|
|
26
|
+
- The walk cache does not see changes made by other clients until it expires or `refresh:true`.
|
|
27
|
+
- `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested live (no second Proton account). Elicitation and prompts are verified with the official SDK client and Claude Code semantics, not with Claude Desktop.
|
|
28
|
+
|
|
3
29
|
## 1.2.0 — 2026-09-28
|
|
4
30
|
|
|
5
31
|
Setup, packaging and reliability round. Five features were built in parallel on separate branches, merged, then verified live (real account, 3 concurrent servers), on Node 22/24 and on the new Ubuntu + macOS CI matrix, plus an independent code review. Review and CI findings were fixed before release.
|
package/README.md
CHANGED
|
@@ -30,8 +30,8 @@ Give Claude Desktop (or any MCP client) full access to your Proton Drive and Pro
|
|
|
30
30
|
|
|
31
31
|
- **Claude manages your Proton Drive** — list, upload, download, move, share, trash, restore
|
|
32
32
|
- **Proton Photos album management** — list albums, create/delete albums, add and remove photos
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
33
|
+
- **Companion CLI** — 44 of the 46 operations, scriptable and pipeable, works in cron and shell scripts (the two sync-folder tools, `drive_read_file` and `drive_write_file`, are MCP-only)
|
|
34
|
+
- **Full Proton Drive CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
|
|
35
35
|
- **Zero credential exposure** — auth is handled entirely by the official Proton Drive CLI; this MCP never touches your password or session token
|
|
36
36
|
- **Shell injection safe** — all CLI calls use `execFile` with discrete argument arrays, never string interpolation
|
|
37
37
|
- **Privacy-native** — end-to-end encryption is handled by Proton's own CLI; this server is just a thin MCP wrapper
|
|
@@ -229,6 +229,28 @@ proton-drive-cli restore /my-files/old-draft.pdf # restore from trash
|
|
|
229
229
|
proton-drive-cli trash empty --confirm # permanently delete all trashed items
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
### Search, analytics and cleanup
|
|
233
|
+
`drive_search` · `drive_tree` · `drive_usage` · `drive_find_duplicates` · `drive_sharing_audit` · `drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
proton-drive-cli tree /my-files --depth 2
|
|
237
|
+
proton-drive-cli search /my-files --ext pdf --min-size 1000000
|
|
238
|
+
proton-drive-cli usage /my-files --top 20 --older-than 365
|
|
239
|
+
proton-drive-cli duplicates /my-files --min-size 1000000 [--verify]
|
|
240
|
+
proton-drive-cli share audit /my-files
|
|
241
|
+
proton-drive-cli sync-plan ./local /my-files/backup --direction up
|
|
242
|
+
proton-drive-cli bulk-move /my-files/Archive /my-files/a.pdf /my-files/b.pdf # plan only
|
|
243
|
+
proton-drive-cli bulk-trash /my-files/old1.txt /my-files/old2.txt --confirm # apply
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- The first walk of a large drive can take minutes (measured ~25 s per 1,200 files at concurrency 8). Later calls reuse a 5-minute cache; `refresh` bypasses it.
|
|
247
|
+
- Walks skip `.git` and `node_modules` by default.
|
|
248
|
+
- Read-only tools (`drive_search`, `drive_tree`, `drive_usage`, `drive_find_duplicates`, `drive_sharing_audit`, `drive_sync_plan`) change nothing. `drive_bulk_move` and `drive_bulk_trash` are two-step: call without `confirmed` for the plan, then with `confirmed: true` to apply. Bulk trash is reversible with `drive_restore`.
|
|
249
|
+
- Duplicate detection uses the sha1 the uploader claimed, which is unverified. `verify: true` downloads the candidates and hashes them.
|
|
250
|
+
- The sharing audit flags an invitee as external when the address is outside Proton domains, so Proton users on a custom domain are over-flagged.
|
|
251
|
+
- Sizes are sums of file sizes, not your account quota.
|
|
252
|
+
- `drive_usage` trash stats always make one uncached `/trash` listing (~2.5 s), even when the walk is cached.
|
|
253
|
+
|
|
232
254
|
### Photos
|
|
233
255
|
|
|
234
256
|
```bash
|
|
@@ -260,13 +282,19 @@ proton-drive-cli share status /my-files/Projects
|
|
|
260
282
|
|
|
261
283
|
---
|
|
262
284
|
|
|
285
|
+
## Prompts
|
|
286
|
+
|
|
287
|
+
The server offers four MCP prompts (also declared in the MCPB manifest): `organise-folder` (`path`), `find-files` (`description`), `storage-audit` and `sharing-audit`. In Claude Code they are listed as `/servername:promptname (MCP)`, and `/mcp__servername__promptname` also runs them. Other clients show them as prompts. A prompt is hidden when a tool it needs is outside the active tool tier: with `PROTON_DRIVE_TOOL_TIER=core` only `find-files` is offered.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
263
291
|
## Tool surface
|
|
264
292
|
|
|
265
293
|
### Auth
|
|
266
294
|
`drive_auth_status` · `drive_auth_logout` · `drive_version`
|
|
267
295
|
|
|
268
296
|
### Filesystem
|
|
269
|
-
`drive_list` · `drive_info` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
|
|
297
|
+
`drive_list` · `drive_info` · `drive_tree` · `drive_search` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
|
|
270
298
|
|
|
271
299
|
### Sharing
|
|
272
300
|
`drive_share_status` · `drive_share_invite` · `drive_share_revoke` · `drive_share_remove_all` · `drive_share_set_url` · `drive_share_remove_url`
|
|
@@ -280,6 +308,9 @@ proton-drive-cli share status /my-files/Projects
|
|
|
280
308
|
### Copy
|
|
281
309
|
`drive_copy`
|
|
282
310
|
|
|
311
|
+
### Plan & bulk
|
|
312
|
+
`drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
|
|
313
|
+
|
|
283
314
|
### Invitations
|
|
284
315
|
`drive_list_invitations` · `drive_invitation_accept` · `drive_invitation_reject` · `drive_share_leave`
|
|
285
316
|
|
|
@@ -297,6 +328,8 @@ proton-drive-cli share status /my-files/Projects
|
|
|
297
328
|
| `drive_version` | CLI and SDK version info | — |
|
|
298
329
|
| `drive_list` | List files and folders at a path (paginated, default 200; `/` lists the roots) | `path`, `limit?`, `offset?` |
|
|
299
330
|
| `drive_info` | Get metadata for one file/folder, including revision details (noise trimmed) | `path`, `verbose?` (raw CLI node) |
|
|
331
|
+
| `drive_tree` | Folder overview: per-folder file counts and size totals (largest first), depth-limited, cached 5 min | `path?`, `depth?` (default 2, max 10), `limit?`, `foldersOnly?`, `refresh?` |
|
|
332
|
+
| `drive_search` | Find files/folders under a path in one cached walk: name (substring/glob; a glob containing `/` matches the path relative to `path`), type, extension, MIME prefix, size, modified date; sortable, paginated. Reports `walk.complete` — false means the walk was cut short and matches may be missing | `query?`, `glob?`, `path?`, `type?`, `mediaType?`, `extensions?`, `minSize?`, `maxSize?`, `modifiedAfter?`, `modifiedBefore?`, `sort?`, `limit?` (default 50, max 500), `offset?`, `refresh?` |
|
|
300
333
|
| `drive_mkdir` | Create a new empty folder | `path` |
|
|
301
334
|
| `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) |
|
|
302
335
|
| `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) |
|
|
@@ -306,12 +339,18 @@ proton-drive-cli share status /my-files/Projects
|
|
|
306
339
|
| `drive_delete` | Permanently delete an item already in trash ⚠️ | `path`, `confirmed: true` |
|
|
307
340
|
| `drive_list_trash` | List items currently in trash (paginated, default 100; includes `uid` — names are not unique in trash) | `limit?`, `offset?` |
|
|
308
341
|
| `drive_share_status` | Get sharing members and URL | `path` |
|
|
342
|
+
| `drive_usage` | Storage analytics for a subtree: totals, largest files/folders, extension and media-type breakdown, old files, trash stats (sum of file sizes, not the account quota) | `path?`, `top?`, `olderThanDays?`, `refresh?` |
|
|
343
|
+
| `drive_find_duplicates` | Likely duplicate groups (claimed sha1 / same size; `verify` downloads and sha256s candidates), wasted bytes, suggested keeper — never deletes | `path?`, `minSize?`, `verify?`, `maxVerifyBytes?`, `maxVerifyTotalBytes?` (default 500 MB, max 2 GB), `limit?`, `refresh?` |
|
|
344
|
+
| `drive_sharing_audit` | Public links (no URL), invitees, pending invitations and risk flags for shared items (max 100) | `path?`, `refresh?` |
|
|
309
345
|
| `drive_share_invite` | Invite a user (sends an email) ⚠️ | `path`, `email`, `role` (viewer/editor/admin), `message?`, `confirmed: true` |
|
|
310
346
|
| `drive_share_revoke` | Revoke one person's access (fails if not a member) ⚠️ | `path`, `email`, `confirmed: true` |
|
|
311
347
|
| `drive_share_remove_all` | Remove every member + pending invitation at once ⚠️ | `path`, `confirmed: true` |
|
|
312
348
|
| `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` |
|
|
313
349
|
| `drive_share_remove_url` | Remove the public share link ⚠️ | `path`, `confirmed: true` |
|
|
314
350
|
| `drive_trash` | Move to trash | `path` |
|
|
351
|
+
| `drive_sync_plan` | Read-only diff of a local folder vs a Drive folder (nothing transferred) | `localPath`, `drivePath`, `direction?` (up/down/both), `ignore?`, `compare?` (size-mtime/sha1), `limit?` |
|
|
352
|
+
| `drive_bulk_move` | Move up to 200 items into an existing folder; without `confirmed` returns the plan and problems only | `sources`, `destinationFolder`, `confirmed?` (**required to apply**) |
|
|
353
|
+
| `drive_bulk_trash` | Trash up to 200 items (recoverable); without `confirmed` returns the plan and problems only | `paths`, `confirmed?` (**required to apply**) |
|
|
315
354
|
| `drive_restore` | Restore from trash | `path` |
|
|
316
355
|
| `drive_empty_trash` | Permanently delete all trash ⚠️ | `confirmed: true` |
|
|
317
356
|
| `drive_read_file` | Read text file from local sync folder | `path` |
|
|
@@ -356,15 +395,17 @@ proton-drive-cli share status /my-files/Projects
|
|
|
356
395
|
- Error messages come from the CLI's own output; the command line (and therefore any `--password`) is never echoed back.
|
|
357
396
|
- `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.
|
|
358
397
|
- `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.
|
|
398
|
+
- If the client declares MCP elicitation (form mode), a refused `confirmed` gate is put to the human instead, and `drive_bulk_move` / `drive_bulk_trash` ask before applying a `confirmed: true` call. Only an explicit approval runs the call; decline, cancel, timeout or error keep the refusal, and after 5 declined prompts in a minute the server stops prompting. This only upgrades refusals and bulk applies: a compromised model that supplies `confirmed: true` on any other tool is **not** stopped by it. Clients without elicitation behave as before. Confirmation prompts need a client that supports elicitation (Claude Code CLI today; Claude Desktop is unverified).
|
|
399
|
+
- Walk-based tools (`drive_tree`, `drive_search`, `drive_usage`, `drive_find_duplicates`) read from a cache up to 5 minutes old; changes made by other clients or processes are not seen until it expires or you pass `refresh: true`. The first walk of a large drive can take minutes (measured about 25 s per 1200 files at concurrency 8).
|
|
359
400
|
- `drive_auth_status` has no native CLI equivalent — it probes by resolving `/my-files` and reports authenticated based on whether that succeeds.
|
|
360
401
|
- Paths are always Drive-absolute: `/my-files/folder/file.pdf`. Relative paths are not supported.
|
|
361
402
|
- All calls include `--json` automatically, except `drive_version`, whose underlying CLI command ignores `--json` and always prints plain text — this MCP parses it directly.
|
|
362
403
|
|
|
363
404
|
### Token cost
|
|
364
405
|
|
|
365
|
-
`tools/list` is sent to the model in every session. Measured payload (JSON bytes): `full`
|
|
406
|
+
`tools/list` is sent to the model in every session. Measured payload (JSON bytes): `full` 39.0 KB (46 tools), `core` 16.8 KB (18 tools). Set `PROTON_DRIVE_TOOL_TIER=core` to load only:
|
|
366
407
|
|
|
367
|
-
`drive_auth_status`, `drive_version`, `drive_list`, `drive_info`, `drive_list_trash`, `drive_mkdir`, `drive_upload`, `drive_download`, `drive_rename`, `drive_move`, `drive_copy`, `drive_trash`, `drive_restore`, `drive_share_status`, `photos_list_timeline`, `photos_download`.
|
|
408
|
+
`drive_auth_status`, `drive_version`, `drive_list`, `drive_info`, `drive_list_trash`, `drive_search`, `drive_tree`, `drive_mkdir`, `drive_upload`, `drive_download`, `drive_rename`, `drive_move`, `drive_copy`, `drive_trash`, `drive_restore`, `drive_share_status`, `photos_list_timeline`, `photos_download`.
|
|
368
409
|
|
|
369
410
|
Left out of `core` (use `full`): permanent deletion (`drive_delete`, `drive_empty_trash`), `drive_auth_logout`, public links, invitations and invites, album management, `photos_upload` and the sync-file tools. A call to a hidden tool returns an error asking for `PROTON_DRIVE_TOOL_TIER=full`; no CLI command runs.
|
|
370
411
|
|
|
@@ -386,7 +427,7 @@ These come from the upstream `proton-drive` CLI (v0.8.0), not from this server:
|
|
|
386
427
|
|
|
387
428
|
## Platform support
|
|
388
429
|
|
|
389
|
-
Developed and live-tested on **macOS**. CI runs the test suite on Ubuntu and macOS (Node 22 and 24). **Windows is not supported yet**: the test suite has never passed there
|
|
430
|
+
Developed and live-tested on **macOS**. CI runs the test suite on Ubuntu and macOS (Node 22 and 24). **Windows is not supported yet**: the test suite has never passed there and CI does not run it, and the Windows paths in `doctor` / `setup-claude-desktop` (`%APPDATA%`, `PATHEXT` lookup) are untested. The system-PATH warning in `doctor` only knows POSIX directories. Linux is covered by CI with the fake CLI but has not been live-tested against a real Proton account.
|
|
390
431
|
|
|
391
432
|
## Testing status
|
|
392
433
|
|
|
@@ -404,7 +445,8 @@ Every tool group was live-tested on 2026-09-28 against a real Proton account, **
|
|
|
404
445
|
| `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS` | Optional | Set to `1` to disable the built-in credential-location denylist (not recommended). |
|
|
405
446
|
| `CLAUDE_DESKTOP_CONFIG` | Optional | Path of the Claude Desktop config that `doctor` and `setup-claude-desktop` read/write instead of the per-OS default. |
|
|
406
447
|
| `PROTON_DRIVE_RETRY_BASE_MS` | Optional | Test hook: base backoff in ms for retrying read-only calls (default 250). |
|
|
407
|
-
| `
|
|
448
|
+
| `PROTON_DRIVE_WALK_TTL_MS` | Optional | How long `drive_tree`/`drive_search` reuse a cached folder walk, in ms (default 300000; 0 disables the cache). Results can be up to this old, and changes made by other clients or processes are not seen until it expires or `refresh: true`. The first walk of a large drive can take minutes. The cache is dropped for any path this server writes to. |
|
|
449
|
+
| `PROTON_DRIVE_TOOL_TIER` | Optional | `full` (default, all 46 tools) or `core` (18 everyday tools; see [Token cost](#token-cost)). Tools outside the active tier are hidden from `tools/list` and refused at call time. Read once at startup; an unknown value falls back to `full` with a warning on stderr. |
|
|
408
450
|
|
|
409
451
|
---
|
|
410
452
|
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,12 @@ import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, Drive
|
|
|
11
11
|
import { checkCliAvailable } from "./utils/subprocess.js";
|
|
12
12
|
import { runDoctor, formatDoctor } from "./utils/doctor.js";
|
|
13
13
|
import { buildEntry, defaultConfigPath, isDirectory, resolveDriveCli, writeEntry, SERVER_KEY } from "./utils/claudeConfig.js";
|
|
14
|
+
import { syncPlan, planBulkMove, planBulkTrash, loadListing, foldersToList } from "./services/plan.js";
|
|
15
|
+
import { invalidatePath } from "./services/walk.js";
|
|
14
16
|
import { resolve } from "node:path";
|
|
15
|
-
import {
|
|
17
|
+
import { driveSearch, driveTree } from "./services/find.js";
|
|
18
|
+
import { driveUsage, driveFindDuplicates, driveSharingAudit } from "./services/analytics.js";
|
|
19
|
+
import { validateRemotePath, validateRemotePathList, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
|
|
16
20
|
const drive = new DriveService();
|
|
17
21
|
const args = process.argv.slice(2);
|
|
18
22
|
const jsonMode = args.includes("--json");
|
|
@@ -26,6 +30,12 @@ Commands:
|
|
|
26
30
|
version Show CLI/SDK version
|
|
27
31
|
list <path> List files at path
|
|
28
32
|
info <path> [--verbose] Show node metadata (--verbose = raw CLI node)
|
|
33
|
+
tree [path] [--depth N] [--limit N] [--folders-only] [--refresh]
|
|
34
|
+
Folder overview with per-folder counts and sizes (default /my-files, depth 2)
|
|
35
|
+
search [path] [--query S] [--glob G] [--type file|folder] [--media-type P] [--ext pdf,docx]
|
|
36
|
+
[--min-size B] [--max-size B] [--after DATE] [--before DATE] [--sort name|size|mtime]
|
|
37
|
+
[--limit N] [--offset N] [--refresh]
|
|
38
|
+
Find files/folders under path (default /my-files) in one walk
|
|
29
39
|
mkdir <path> Create a new folder
|
|
30
40
|
upload <local> <remote> [--file-conflict X] [--folder-conflict X] [--confirm]
|
|
31
41
|
Upload file/folder (files: skip/create-new-revision/rename/replace; folders: skip/merge/rename/replace;
|
|
@@ -37,6 +47,7 @@ Commands:
|
|
|
37
47
|
move <src> <dst> Move and/or rename
|
|
38
48
|
delete <path>|--uid <uid> --confirm Delete a file/folder already in trash, permanently
|
|
39
49
|
share status <path> Show sharing info
|
|
50
|
+
share audit [path] [--refresh] Audit public links, invitees and risk flags under a path (default /my-files)
|
|
40
51
|
share invite <path> <email> <role> Invite user (viewer/editor/admin)
|
|
41
52
|
share revoke <path> <email> Revoke one user's access
|
|
42
53
|
share remove-all <path> --confirm Remove everyone's access + pending invitations
|
|
@@ -45,7 +56,15 @@ Commands:
|
|
|
45
56
|
share leave <path> Leave a folder shared with you
|
|
46
57
|
copy <src> <dst-parent-folder> [--name N] Copy file/folder into a folder (optionally under a new name)
|
|
47
58
|
trash <path> Move to trash
|
|
59
|
+
sync-plan <local> <remote> [--direction up|down|both] [--compare size-mtime|sha1] [--ignore g1,g2] [--limit N]
|
|
60
|
+
Read-only diff of a local folder vs a Drive folder (nothing transferred)
|
|
61
|
+
bulk-move <dst-folder> <src>... [--confirm] Move many items into an existing folder; without --confirm prints the plan only
|
|
62
|
+
bulk-trash <path>... [--confirm] Trash many items; without --confirm prints the plan only
|
|
48
63
|
trash list List trash contents
|
|
64
|
+
usage [path] [--top N] [--older-than DAYS] [--refresh]
|
|
65
|
+
Storage analytics: totals, largest files/folders, breakdowns, trash (sum of file sizes, not the quota)
|
|
66
|
+
duplicates [path] [--min-size B] [--verify] [--max-verify-bytes B] [--max-verify-total-bytes B] [--limit N] [--refresh]
|
|
67
|
+
Likely duplicate files; --verify downloads and hashes candidates. Never deletes.
|
|
49
68
|
trash empty --confirm Permanently delete all trash
|
|
50
69
|
restore <path>|--uid <uid> Restore from trash (uid from 'trash list')
|
|
51
70
|
invitation list List pending invitations
|
|
@@ -86,6 +105,19 @@ function requirePath(value, usage) {
|
|
|
86
105
|
}
|
|
87
106
|
return ""; // unreachable; process.exit(1) above always terminates
|
|
88
107
|
}
|
|
108
|
+
function pathOrRoot(value) {
|
|
109
|
+
return value && !value.startsWith("--") ? requirePath(value, "<command> [path]") : "/my-files";
|
|
110
|
+
}
|
|
111
|
+
function intFlag(flag) {
|
|
112
|
+
const v = getFlag(flag);
|
|
113
|
+
if (v === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (!/^\d+$/.test(v)) {
|
|
116
|
+
console.error(`${flag} must be a non-negative integer`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
return Number(v);
|
|
120
|
+
}
|
|
89
121
|
function requireEmail(value) {
|
|
90
122
|
try {
|
|
91
123
|
return validateEmail(value);
|
|
@@ -106,6 +138,19 @@ function getFlag(flag) {
|
|
|
106
138
|
}
|
|
107
139
|
return args[idx + 1];
|
|
108
140
|
}
|
|
141
|
+
function numFlag(flag) {
|
|
142
|
+
const v = getFlag(flag);
|
|
143
|
+
if (v === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!/^\d+$/.test(v)) {
|
|
146
|
+
console.error(`${flag} must be a non-negative integer`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
return Number(v);
|
|
150
|
+
}
|
|
151
|
+
function numFlags(...names) {
|
|
152
|
+
return Object.fromEntries(names.map((n) => [n, numFlag(`--${n}`)]));
|
|
153
|
+
}
|
|
109
154
|
function print(data) {
|
|
110
155
|
console.log(jsonMode ? JSON.stringify(data) : JSON.stringify(data, null, 2));
|
|
111
156
|
}
|
|
@@ -184,6 +229,21 @@ async function run() {
|
|
|
184
229
|
case "list":
|
|
185
230
|
print(await drive.list(requirePath(sub, "list <path>")));
|
|
186
231
|
break;
|
|
232
|
+
case "tree":
|
|
233
|
+
print(await driveTree(drive, { path: sub && !sub.startsWith("--") ? sub : undefined, ...numFlags("depth", "limit"), foldersOnly: args.includes("--folders-only") || undefined, refresh: args.includes("--refresh") || undefined }));
|
|
234
|
+
break;
|
|
235
|
+
case "search":
|
|
236
|
+
print(await driveSearch(drive, {
|
|
237
|
+
path: sub && !sub.startsWith("--") ? sub : undefined,
|
|
238
|
+
query: getFlag("--query"), glob: getFlag("--glob"),
|
|
239
|
+
type: getFlag("--type"), mediaType: getFlag("--media-type"),
|
|
240
|
+
extensions: getFlag("--ext")?.split(","),
|
|
241
|
+
...numFlags("limit", "offset"),
|
|
242
|
+
minSize: numFlag("--min-size"), maxSize: numFlag("--max-size"),
|
|
243
|
+
modifiedAfter: getFlag("--after"), modifiedBefore: getFlag("--before"),
|
|
244
|
+
sort: getFlag("--sort"), refresh: args.includes("--refresh") || undefined,
|
|
245
|
+
}));
|
|
246
|
+
break;
|
|
187
247
|
case "info":
|
|
188
248
|
print(await drive.info(requirePath(sub, "info <path> [--verbose]"), args.includes("--verbose")));
|
|
189
249
|
break;
|
|
@@ -195,7 +255,7 @@ async function run() {
|
|
|
195
255
|
}
|
|
196
256
|
let local;
|
|
197
257
|
try {
|
|
198
|
-
local = validateLocalPath(rawLocal);
|
|
258
|
+
local = validateLocalPath(rawLocal, { scan: true });
|
|
199
259
|
}
|
|
200
260
|
catch (e) {
|
|
201
261
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -297,6 +357,9 @@ async function run() {
|
|
|
297
357
|
if (sub === "status") {
|
|
298
358
|
print(await drive.shareStatus(requirePath(rest[0], "share status <path>")));
|
|
299
359
|
}
|
|
360
|
+
else if (sub === "audit") {
|
|
361
|
+
print(await driveSharingAudit(drive, { path: pathOrRoot(rest[0]), refresh: args.includes("--refresh") }));
|
|
362
|
+
}
|
|
300
363
|
else if (sub === "invite") {
|
|
301
364
|
const [rawPath, rawEmail, role] = rest;
|
|
302
365
|
if (!rawPath || !rawEmail || !role) {
|
|
@@ -365,6 +428,25 @@ async function run() {
|
|
|
365
428
|
process.exit(1);
|
|
366
429
|
}
|
|
367
430
|
break;
|
|
431
|
+
case "usage":
|
|
432
|
+
print(await driveUsage(drive, {
|
|
433
|
+
path: pathOrRoot(sub),
|
|
434
|
+
top: intFlag("--top"),
|
|
435
|
+
olderThanDays: intFlag("--older-than"),
|
|
436
|
+
refresh: args.includes("--refresh"),
|
|
437
|
+
}));
|
|
438
|
+
break;
|
|
439
|
+
case "duplicates":
|
|
440
|
+
print(await driveFindDuplicates(drive, {
|
|
441
|
+
path: pathOrRoot(sub),
|
|
442
|
+
minSize: intFlag("--min-size"),
|
|
443
|
+
verify: args.includes("--verify"),
|
|
444
|
+
maxVerifyBytes: intFlag("--max-verify-bytes"),
|
|
445
|
+
maxVerifyTotalBytes: intFlag("--max-verify-total-bytes"),
|
|
446
|
+
limit: intFlag("--limit"),
|
|
447
|
+
refresh: args.includes("--refresh"),
|
|
448
|
+
}));
|
|
449
|
+
break;
|
|
368
450
|
case "trash":
|
|
369
451
|
if (sub === "empty") {
|
|
370
452
|
if (!args.includes("--confirm")) {
|
|
@@ -386,6 +468,66 @@ async function run() {
|
|
|
386
468
|
process.exit(1);
|
|
387
469
|
}
|
|
388
470
|
break;
|
|
471
|
+
case "sync-plan": {
|
|
472
|
+
const direction = getFlag("--direction"), compare = getFlag("--compare"), ignore = getFlag("--ignore"), limit = getFlag("--limit");
|
|
473
|
+
if (direction !== undefined && !["up", "down", "both"].includes(direction)) {
|
|
474
|
+
console.error("--direction must be up, down or both");
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
if (compare !== undefined && !["size-mtime", "sha1"].includes(compare)) {
|
|
478
|
+
console.error("--compare must be size-mtime or sha1");
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
if (!sub || !rest[0]) {
|
|
482
|
+
console.error("Usage: sync-plan <local> <remote> [--direction X] [--compare X] [--ignore g1,g2] [--limit N]");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
print(await syncPlan(drive, {
|
|
486
|
+
localPath: sub, drivePath: rest[0], direction: direction, compare: compare,
|
|
487
|
+
ignore: ignore?.split(","), limit: limit !== undefined ? Number(limit) : undefined,
|
|
488
|
+
}));
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case "bulk-move": {
|
|
492
|
+
const dst = requirePath(sub, "bulk-move <dst-folder> <src>... [--confirm]");
|
|
493
|
+
const srcs = validateRemotePathList(rest.filter((r) => !r.startsWith("--")), "sources");
|
|
494
|
+
const plan = planBulkMove(srcs, dst, await loadListing(drive, foldersToList(srcs, dst)));
|
|
495
|
+
if (!args.includes("--confirm") || plan.problems.length) {
|
|
496
|
+
print({ applied: false, ...plan });
|
|
497
|
+
if (plan.problems.length)
|
|
498
|
+
process.exit(1);
|
|
499
|
+
console.error("Plan only. Pass --confirm to apply.");
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
await drive.bulkMove(srcs, dst);
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
invalidatePath(dst);
|
|
507
|
+
srcs.forEach(invalidatePath);
|
|
508
|
+
}
|
|
509
|
+
console.log(`Moved ${srcs.length} item(s) to ${dst}.`);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
case "bulk-trash": {
|
|
513
|
+
const paths = validateRemotePathList([sub, ...rest].filter((r) => r !== undefined && !r.startsWith("--")), "paths");
|
|
514
|
+
const plan = planBulkTrash(paths, await loadListing(drive, foldersToList(paths)));
|
|
515
|
+
if (!args.includes("--confirm") || plan.problems.length) {
|
|
516
|
+
print({ applied: false, ...plan });
|
|
517
|
+
if (plan.problems.length)
|
|
518
|
+
process.exit(1);
|
|
519
|
+
console.error("Plan only. Pass --confirm to apply.");
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
await drive.bulkTrash(paths);
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
paths.forEach(invalidatePath);
|
|
527
|
+
}
|
|
528
|
+
console.log(`Moved ${paths.length} item(s) to trash.`);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
389
531
|
case "restore": {
|
|
390
532
|
const restoreUid = getFlag("--uid");
|
|
391
533
|
const restorePath = restoreUid && (!sub || sub.startsWith("--")) ? undefined : requirePath(sub, "restore <path>|--uid <uid>");
|
|
@@ -577,7 +719,7 @@ async function run() {
|
|
|
577
719
|
const localPaths = [];
|
|
578
720
|
for (const p of positionals) {
|
|
579
721
|
try {
|
|
580
|
-
localPaths.push(validateLocalPath(p));
|
|
722
|
+
localPaths.push(validateLocalPath(p, { scan: true }));
|
|
581
723
|
}
|
|
582
724
|
catch (e) {
|
|
583
725
|
console.error(e instanceof Error ? e.message : String(e));
|