mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
Self-hosted deployments using LocalStorage WITHOUT `LOCAL_UPLOAD_BASE_URL`
produce a site-relative `/uploads/<key>` URL on upload, but the renderer
was selecting `markdown_url` (the auth-gated
`/api/attachments/<id>/download` endpoint) for inline previews and modal
displays. A native `<img src>` cannot attach an Authorization header, so
the API middleware 401s and the image renders blank.
Two-layer fix:
Server (`server/internal/handler/file.go`):
`buildMarkdownURL` now also returns `a.Url` verbatim when it's a
site-relative `/uploads/<key>` path. On web the Next.js rewrite
proxies `/uploads/*` straight to `LocalStorage.ServeFile` (no auth
roundtrip), and on desktop the renderer prefix pass (`apiBaseUrl`)
prepends the API host. When `MULTICA_PUBLIC_URL` is set the same
path is prefixed with it so non-web clients get an absolute URL.
Pre-fix markdown bodies that landed on the `/api/...`
auth-gated endpoint are repaired client-side.
`download_url` semantics are unchanged — the explicit Download
click still goes through the re-sign / proxy / membership-checked
endpoint. Persisting a short-lived signed URL into markdown bodies
is the original MUL-3130 bug we must not reintroduce.
Client (web + desktop, `packages/views/editor/{attachment,
attachment-preview-modal}.tsx`):
`pickInlineMediaURL` (inline thumbnail) and
`resolvePreviewMediaUrl` (modal preview) now prefer
`record.url` whenever it's natively loadable. The 'natively
loadable' gate expands the existing CDN-match check to include
site-relative `/uploads/<key>` paths. Site-relative picks run
through the existing `absolutizeMediaURL` /`resolvePublicFileUrl`
pass so desktop's `file://` origin gets the API-host prefix.
CDN-signed (CloudFront / S3 presign) URLs still win because the
TTL means the signed redirect beats the durable `markdown_url` on
first paint. CDN-matched absolute URLs (public CDN / cookie mode)
still beat the durable API-shaped `markdown_url` for the same
reason. `markdown_url` is the durable fallback for private-bucket
deployments where the raw `record.url` would 403.
Tests:
- `TestBuildMarkdownURL_PublicURLUnsetKeepsLocalStorageRelativePath` —
web: site-relative `/uploads/<key>` is persisted verbatim.
- `TestBuildMarkdownURL_RelativeStorageURLPrefixedWithPublicURL` —
desktop/non-web: `MULTICA_PUBLIC_URL` prefixes the same path so
clients without a same-origin proxy can resolve it.
- `TestBuildMarkdownURL_PublicURLUnsetFallsBackToSiteRelativeAPI...`
— pre-fix legacy rows still get the API endpoint fallback (no
regression on the backfill branch).
- `TestIsLocalStorageRelativePath` — traversal hardening for
`/uploads/../etc/passwd` etc.
- Inline renderer (`attachment.test.tsx`): `/uploads/<key>` is
preferred over the auth-gated endpoint on web + desktop;
explicit Download button still hits the re-sign / proxy path.
- Modal (`attachment-preview-modal.test.tsx`): same coverage plus a
CDN-matched `record.url` preference check, and the four
MUL-2976 'prefix server-relative download_url' tests are
re-anchored to the legacy-fallback case (`markdown_url` empty)
so the prefix pass is still verified without conflating it with
the new primary path.
Backward compatibility:
- `/api/attachments/<id>/download` endpoint shape unchanged.
- `download_url` semantics unchanged (still the explicit-Download
flow).
- Existing markdown bodies that reference the API endpoint load
through the client-side fix; new uploads land on `/uploads/<key>`.
- CDN-signed / S3-presign / private-bucket deployments unchanged.
Co-authored-by: multica-agent <github@multica.ai>