Skip to content

OpenConceptLab/ocl_issues#2864 | export download saves under the API's name by default - #13

Merged
paynejd merged 5 commits into
mainfrom
feat/2864-export-download-name
Oct 7, 2026
Merged

paynejd merged 5 commits into
mainfrom
feat/2864-export-download-name

Conversation

@paynejd

@paynejd paynejd commented Oct 7, 2026

Copy link
Copy Markdown
Member

Part of OpenConceptLab/ocl_issues#2864 (ocl-cli piece). The API side is OpenConceptLab/oclapi2#929.

What

ocl repo export download no longer needs -o. By default it saves the file under the name the API gives the export, for example orgs_PIH_sources_PIH_1.8.24_2026-09-30_123456.zip.

  • No -o: saved in the current directory under the API's name.
  • -o DIR (an existing directory): saved there under the API's name.
  • -o FILE: saved as FILE, as before.

The name comes from the Content-Disposition header first, then from the signed URL's response-content-disposition, then from the URL's last path segment. Only the base name is used, so a name can't put the file outside the target directory. If none of those gives a name, the command asks for -o.

Until oclapi2#929 is deployed, the default name is the end of the storage key (PIH_PIH_v1.8.24.2026-09-30_123456.zip), which is what TBv2 and curl -O save today.

The ticket said export download already read Content-Disposition. It didn't, so this PR adds that.

Also

  • README: updated the export download usage and example.
  • Removed an unused sys import and an f-string with no placeholders in export.py. Ruff flagged both, and both were already on main.

Tests

tests/test_export_commands.py covers the header, the signed URL, the storage-name fallback, -o DIR, -o FILE, a ../ name, and an unnamed response. pytest: 16 passed. ruff check is clean on the changed files.

🤖 Generated with Claude Code

paynejd and others added 2 commits October 7, 2026 10:29
…s name by default

-o is now optional. Without it, or when it names a directory, the file is
saved under the name the API gives the export: Content-Disposition first,
then the signed URL's response-content-disposition, then the URL's last
path segment. Only the base name is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…laceholder-free f-string (ruff)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@paynejd paynejd left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex adversarial review, pass 1 (codex-cli 0.160.1, commit f69e0e6)

  1. [P1] Automatically selected paths can overwrite files outside the destination. export.py:135
    If downloads/<API_NAME> already symlinks to ../important.txt, the subsequent open(..., "wb") follows it and truncates that target. Basename sanitization does not ensure containment. Ordinary existing files are silently overwritten too. Automatically named downloads should use atomic no-clobber creation, with an explicit overwrite policy.

  2. [P2] filename incorrectly overrides filename*. export.py:91
    Reproduced with attachment; filename="cafe.zip"; filename*=UTF-8''caf%C3%A9.zip: the result is cafe.zip, regardless of parameter order. RFC 6266 says to prefer the extended filename, so this loses the API’s intended non-ASCII name. Both header and signed-query extraction are affected. Explicitly prioritize valid filename*. RFC 6266 §4.3

  3. [P2] A malformed fallback can abort an otherwise valid download. export.py:102
    All candidates are parsed eagerly. A valid header naming good.zip still fails if the query disposition contains filename*=UTF-8''a.zip; filename*0*=UTF-8''b.zip: Python’s email parser raises an uncaught TypeError. A malformed primary header likewise prevents trying a valid query name. Evaluate candidates sequentially and handle parsing failures individually.

  4. [P2] Filename validation accepts unusable names and Windows special targets. export.py:108
    filename*=UTF-8''evil%00.zip produces an embedded NUL, passes validation, and causes ValueError when opened—even with a usable query fallback. Windows path handling also leaves NUL.zip and victim.txt:payload.zip intact; these identify a device or alternate stream rather than an ordinary export file. Reject unusable candidates and continue the fallback chain, or require -o FILE. Windows naming rules

  5. [P3] Clarify that -o DIR requires an existing directory. README.md:194
    A nonexistent -o downloads creates a file named downloads; -o downloads/ fails with an unhandled filesystem error. The README should state the existing-directory requirement. The naming example otherwise matches the companion API implementation; expansion is included only when present.

The new tests omit these failure cases, filename-parameter precedence, collisions/symlinks, and the actual 302 handoff. Read-only probes verified the normal redirect/header/query paths, standalone UTF-8, quoting, and separator stripping. Filesystem-writing tests were not run. No files were modified.

Overall verdict: Request changes. The ordinary API example works, but filename precedence and safe output handling need correction.

…rict names, no clobbering

- filename* wins over filename (RFC 6266); each name source is tried in turn,
  and a malformed one falls through to the next.
- A name must be made of the characters the API uses (letters, digits,
  . _ - @) and not be a Windows device name, which rules out paths, NUL
  bytes and alternate streams.
- A name the API chose is created exclusively, so it never replaces an
  existing file or follows a symlink; -o FILE still overwrites.
- -o DIR/ that doesn't exist is an error; README says DIR must exist.
- Tests for each, plus the API's 302 handoff through OCLAPIClient.

Addresses Codex pass 1 on ocl-cli#13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paynejd

paynejd commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Replies to Codex pass 1. All five are fixed in 65f4ef5:

  1. [P1] Overwrite and symlinks (fixed): a name the API chose is now created with exclusive create (xb). It never replaces an existing file and never follows a symlink; instead the command stops and says how to proceed. An explicit -o FILE still overwrites, as before. Test: test_an_existing_file_is_not_overwritten (a symlink to another file).
  2. filename* precedence (fixed): filename* is read first, as RFC 6266 says, and filename is used only if it's missing or can't be decoded. Test: test_filename_star_wins_over_filename.
  3. A malformed source aborts the others (fixed): the header, the signed URL and the URL path are tried one at a time, and a source that fails to parse or decode falls through to the next. The email parser is gone. Test: test_unusable_header_name_falls_back_to_the_signed_url.
  4. Unusable names (fixed): a name must match [A-Za-z0-9][A-Za-z0-9._@-]*, the only characters the API uses since oclapi2#929 b7a4ac7a, and must not be a Windows device name. That rules out NUL bytes, : streams, paths and NUL.zip. The same test covers each case.
  5. -o DIR (fixed): the README and --help say the directory must exist, and -o missing/ now fails with a clear message.

Also added test_export_download_follows_the_redirect_to_the_named_file, which runs the real OCLAPIClient.export_download 302 handoff through pytest-httpx.

…parsing, names may start with _ - @

- Content-Disposition parameters are parsed in order, so a quoted value
  containing ';' or 'filename=' can't be mistaken for the filename.
- Storage names may start with _, - or @, as OCL owner names can.
- README: an export the API can't name keeps its storage name.

Addresses Codex pass 2 on ocl-cli#13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@paynejd paynejd left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex adversarial review, pass 2 (codex-cli 0.160.1, commit 65f4ef5)

  1. [P2] Valid legacy storage filenames are rejected — export.py:86. OCL allows owner names beginning with _, -, or @, but this pattern requires an initial alphanumeric character. For a timestamp-less legacy export at /orgs/_PIH/_PIH_PIH_v1.8.24.zip, the API deliberately supplies no disposition name. The CLI rejects the safe storage basename and demands -o FILE. Allow these initial characters and add a fallback regression test.

  2. [P3] Filename regexes match inside quoted metadata — export.py:95. The valid header attachment; note="x; filename=wrong.zip; y"; filename="right.zip" produces wrong.zip. Both regexes search without respecting quoted parameter boundaries. Parse parameters before selecting filename* or filename, and test embedded semicolons. The current API’s simple header avoids this case, but the parser mishandles valid RFC 6266 syntax.

  3. [P3] README overstates the naming guarantee — README.md:210. “Every export” is inaccurate: the accompanying API change deliberately leaves timestamp-less legacy exports unnamed. Document the storage-basename fallback and the requirement for -o FILE when no usable name remains.

The earlier precedence, overwrite protection, and missing-directory fixes are present; nine mocked CLI checks passed. Ten pytest tests passed, including the redirect integration test. Eleven filesystem-writing tests were excluded to preserve read-only operation. Ruff and diff checks passed; no files changed.

Overall verdict: changes requested for the legacy-filename compatibility defect.

@paynejd

paynejd commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Replies to Codex pass 2. All three are fixed in ee04c51:

  1. Storage names starting with _, - or @ (fixed): those are now allowed as the first character, as in OCL owner names. A leading . is still refused. Test: test_storage_name_may_start_with_an_underscore.
  2. Quoted parameters (fixed): Content-Disposition parameters are now parsed in order, respecting quotes, before filename* or filename is chosen. Test: test_quoted_parameters_can_contain_semicolons.
  3. README (fixed): it now says an export the API can't name keeps its storage name, and that -o FILE is needed if no usable name comes back.

@paynejd paynejd left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex adversarial review, pass 3 (codex-cli 0.160.1, commit ee04c51)

  • [P2] Handle API filenames exceeding filesystem limits — src/ocl_cli/commands/export.py:180. A valid 220-character repository ID produces a 261-byte download name. _usable_filename() accepts it, but saving on a filesystem with a 255-byte limit raises uncaught ENAMETOOLONG. Both omitted -o and -o DIR fail without the documented guidance. Catch this failure and ask for -o FILE, preserving the rule against inventing names; add a regression test.

Validation: the 302 integration test and 34 in-memory checks passed. Reproduced the length failure using read-only filesystem lookup; no files modified. The full filesystem-writing suite was not run.

Overall verdict: one remaining fix needed.

… the filesystem asks for -o FILE

Addresses Codex pass 3 on ocl-cli#13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paynejd

paynejd commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Reply to Codex pass 3: name too long for the filesystem (fixed in 22bd9cc). When a name the API chose fails with ENAMETOOLONG, the command now stops and asks for -o FILE instead of crashing; it doesn't shorten the name itself, since the CLI shouldn't make up names. Test: test_a_name_too_long_for_the_filesystem_asks_for_output. This was the third and last Codex pass, so I haven't run another review on this one-line fix. 24 tests pass and ruff is clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant