Repository navigation
feat: add 'wherobots files my-files' commands for the user-files API - #37
Merged
Merged
Conversation
Add a curated `files` -> `my-files` command group (ls, mkdir, upload,
download, cat, mv, rm, rmdir) over the /storage/{storage_id}/... file
routes, with the drive model in internal/files so a future shared drive
only adds a drive kind and a command.
- Path params keep "/": each segment is escaped separately, which also
fixes the generated `api storage ...` commands for nested paths.
- Downloads resolve the 307 without following it and fetch the
presigned URL with no auth or client headers.
- Listing always sends limit=1000 and follows next_page.
- Nested mkdir creates each level in order; 409 means it exists.
- 401/404 map to "not signed in", "Files is not enabled for my-files
in region X" and "no such file or folder".
- rmdir refuses a non-empty folder unless --recursive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lookup Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`upload <local-file> <remote-path>`, matching cp/scp/aws s3 cp. The previous `<remote-path> <local-file>` order was a mistake. `download` is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClayMav
reviewed
Sep 23, 2026
- download over an existing file keeps its permissions; a new file gets 0666 less the umask instead of a fixed 0644 - the presigned GET sends Accept-Encoding: identity so Go does not gunzip a Content-Encoding: gzip object - main runs on a context cancelled by SIGINT/SIGTERM, so an interrupted transfer unwinds through cleanup and leaves no .partial file Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The global signal context in main also cancelled job-runs poll requests, so Ctrl-C during `job-runs logs --follow` printed "context canceled" and exited 1 instead of "Detached.". main is back on context.Background(); upload, download and cat now wrap their own context with signal.NotifyContext, released after the first signal. TestFilesDownloadInterruptLeavesNoFile sends a real SIGINT mid-download and checks the .partial file is removed (unix only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed by Salty Hambot 🤖🧂 — rubric mode
Verdict:
| Dimension | Verdict | Notes |
|---|---|---|
| correctness | ls surfaces the folder self-marker as a phantom / entry, which DeleteDir already filters but List does not. |
|
| security | ✅ pass | Presigned download/upload strip Wherobots credentials (tested); slash-escaping change only affects the caller's own request path. |
| privacy | ✅ pass | No PII handling; transfers go straight to presigned storage with no auth or client headers. |
| reliability | Non-recursive rmdir's emptiness check remains a TOCTOU before a recursive server delete (already documented, prior thread 3 unresolved). | |
| scalability | ✅ pass | Listing paginates with an explicit limit and a hard page cap; no regressions. |
| observability | ✅ pass | Dry-run curl output is thorough and the rmdir delete is now flagged as conditional. |
| clarity/maintainability | ✅ pass | Clean drive/ops separation with well-documented helpers; the parseEntries marker inconsistency is the main readability snag. |
| test quality | Broad coverage of pagination, dry-run, permissions, and credential-stripping, but no test asserts ls of a folder-marker-only listing filters the phantom entry. |
Prior threads mostly landed — the mv guard, slash-escaping scope, and rmdir dry-run caveat all have code and tests now. One genuinely new snag: ls still hands back the folder self-marker as a phantom / entry, even though rmdir already knows to ignore it. Filter it once and this is good to go.
Prior findings: ✅ 4 resolved ·
1 finding(s) posted.
💰 Review cost: $3.3401 · 758.1k in / 10.9k out tokens · ⏱️ 2m21s
💬 To request a re-review, comment @salty-hambot review
- Keep "/" unescaped only in the Files {path} param (BuildRequestMultiSegment);
dynamic API commands escape "/" as %2F again, as on main.
- mv refuses a folder source (trailing "/") before any request.
- mkdir: after a 409, list the parent and fail if the name is a file.
- rmdir: document that the emptiness check is not atomic; dry-run notes
that the DELETE is contingent on the folder being empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a 409, requireNotFile returned the file error on the first non-folder match. S3-style storage can list both a file b and a folder b/; the folder exists, so mkdir now continues when any match is a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List returned the folder's own empty-name marker as an entry, so ls of an empty folder showed a nameless folder. parseEntries now skips it, and DeleteDir's emptiness check relies on that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClayMav
approved these changes
Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this delivers
A new hand-written command group,
wherobots files my-files, for the per-user Files area (the user-files API under/storage/{storage_id}/...):ls,mkdir,upload,download,cat,mv,rm,rmdir— single-file operations only.Examples
The three error kinds:
Details
internal/files/drive.go): today one drive,my-files, resolved touser_files::<region>. A future shared drive only needs a new kind and its storage-id mapping; all operations live on one drive type (internal/files/ops.go).{path}param keeps/: each segment is percent-encoded separately, never%2F(BuildRequestMultiSegmentininternal/executor/request.go). This applies only to the Files commands; plainBuildRequest, used by the dynamicapi ...commands, still escapes/as%2F.internal/executor/download.go). It writes to a temporary file and moves it into place only on success; Ctrl-C cleans up.limitand paginates by cursor; the folder's own marker entry is not shown.--recursive/-ris given.--region, else the organization'sdefaultRegion. No STS /POST /storage/credentials.Look hardest at
internal/files/ops.goDeleteDir: without--recursive, refuses on a non-emptynext_pageor any named entry; a marker-only single page with nonext_pageis treated as empty. The check and the delete are separate requests, so content added in between is deleted too.Mkdirtreats a 409 as "this level already exists" — assumed from the API, not confirmed against the server — then lists the parent and fails only if the name is a file and not also a folder.--dry-runstill issuesGET /organizationto resolve the region when--regionisn't given (documented in README).Left out
Recursive/bulk transfer, streaming uploads beyond the existing 500 MB cap, cross-folder moves, 5xx retries, confirmation prompts.
Verified
make fmt,make test,make build,go vet ./...all pass. Tests use mocked HTTP only; no live credentials.🤖 Generated with Claude Code