Skip to content

assets: browser downloads + loud 404s for file-like paths - #735

Open
gtheys wants to merge 1 commit into
ZenNotes:mainfrom
gtheys:feat/asset-download
Open

assets: browser downloads + loud 404s for file-like paths#735
gtheys wants to merge 1 commit into
ZenNotes:mainfrom
gtheys:feat/asset-download

Conversation

@gtheys

@gtheys gtheys commented Sep 4, 2026

Copy link
Copy Markdown

Closes #716 (PR 1 of the suggested split: server + web/desktop UI; CLI/MCP parity can follow).

What this does

1. Server: ?download=1 on GET /api/assets/raw

When the query flag is set, the handler adds an attachment Content-Disposition (RFC 5987 filename*, so unicode filenames survive):

Content-Disposition: attachment; filename*=UTF-8''pic.png

This unlocks plain <a href> downloads in the web app with zero blob plumbing on the server side. Without the flag, embed-serving behavior is byte-for-byte unchanged (no header added).

2. Server: the SPA false-200 papercut from the issue

Previously serveStatic fell back to index.html for any unknown path — including /files/assets/image.png — with HTTP 200 + text/html. Naive download attempts "succeeded" but yielded an HTML file (reproduced live against a self-hosted pod before the fix). Now unknown paths with a non-.html extension get a real 404; unknown extension-less app routes still hit the SPA fallback, so client-side routing is untouched.

3. Web + Desktop UI (shared AssetsView)

  • New Download item in the asset context menu.
  • Keyboard-first: pressing d on a focused asset row downloads it (next to the existing Enter/Space open action).
  • The shared downloadAsset() helper resolves the asset URL exactly the way embedded images already do — same-origin HTTP on web, the zen-asset:// privileged scheme in the desktop app (which has supportFetchAPI: true, so it works for local and remote/self-hosted vaults alike) — fetches it as a blob and clicks a hidden <a download>. No bridge-contract change, no new IPC.

Verification

  • go test ./... — all packages pass (incl. new TestAssetDownloadDisposition + TestStaticFallback404sAssetLikePaths); go vet clean.
  • npm run typecheck — all TS workspaces pass.
  • @zennotes/web production build passes; new vitest suite covers the helper (anchor naming, unresolvable path, failed fetch).
  • Live-tested against a built server binary: download=1 returns 200 image/png + attachment header; inline serving unchanged; /files/assets/nope.png404 instead of 200 text/html; unknown app route → 200 SPA shell.

Out of scope (follow-up PR per the issue's suggested split)

Desktop save-dialog, zn asset list/get CLI, and MCP get_asset parity — happy to take those next.

Server:
- GET /api/assets/raw accepts ?download=1, serving the asset with an
  attachment Content-Disposition (RFC 5987 filename*) so plain browsers
  and <a href> links save the file under its own name.
- The SPA fallback now returns a real 404 for unknown file-looking paths
  (anything with a non-.html extension, e.g. /files/assets/img.png)
  instead of index.html with HTTP 200, which made naive download attempts
  'succeed' while yielding an HTML file. Unknown app routes still fall
  through to the SPA shell.

Web + Desktop UI (shared app-core Assets view):
- New 'Download' context-menu item and a keyboard 'd' on focused asset
  rows. The shared downloadAsset() helper resolves the asset URL exactly
  like embedded images do (same-origin HTTP on web, zen-asset:// scheme
  in the desktop app for local and remote vaults) and saves it via a
  blob + <a download> click, with no bridge-contract change.

Tests: Go coverage for the download flag and the fallback 404; vitest
coverage for the download helper (resolve failure, read failure, and
anchor naming).
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.

[Feature] Download/export assets — save-as in Web & Desktop UI, plus zn CLI / MCP parity

1 participant