Files
multica/server/internal/handler
Jiayuan Zhang 3e892a359f fix(attachments): prefer locally-served /uploads/<key> over auth-gated download endpoint (#4048)
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>
2026-06-13 04:37:10 +08:00
..