mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes #3721. **Server** - New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time. - `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign. - `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs. - `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`. **Clients** - CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip. - Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`. **Docs** - `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores. **Tests** - `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser). - `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through. - `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case.
45 lines
1.9 KiB
TypeScript
45 lines
1.9 KiB
TypeScript
/**
|
|
* Resolve a server-relative attachment URL against the configured API base.
|
|
*
|
|
* Background: when the backend has no CloudFront signer configured (e.g.
|
|
* the self-hosted RustFS / private-S3 case in MUL-2976), `attachment.url`
|
|
* and `attachment.download_url` come back as server-relative paths like
|
|
* `/api/attachments/{id}/download`. Web is happy with that — same-origin
|
|
* `<img src="/api/...">` resolves against the document base — but RN
|
|
* needs an absolute http(s) URL for both `Linking.openURL` (`Cannot open
|
|
* URL` otherwise) and `<Image source={{ uri }}>` (no document origin to
|
|
* resolve against; the request is silently dropped).
|
|
*
|
|
* Mirrors `packages/core/workspace/avatar-url.ts:resolvePublicFileUrl`
|
|
* exactly. We don't import the core helper because its `getBaseUrl()`
|
|
* pulls from a singleton ApiClient that lives in `@multica/core/api` —
|
|
* not on the mobile sharing whitelist (apps/mobile/CLAUDE.md "mirror,
|
|
* don't import"). Mobile reads its own `EXPO_PUBLIC_API_URL` from the
|
|
* Expo env, the same value the rest of `data/api.ts` uses.
|
|
*
|
|
* Contract:
|
|
* - null / undefined / "" → null (caller should treat as "no URL").
|
|
* - already-absolute URL → returned unchanged.
|
|
* - server-relative path → API base + path, with a single boundary
|
|
* slash (we trim trailing slashes from the
|
|
* base before joining).
|
|
*/
|
|
|
|
const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "";
|
|
|
|
export function resolveAttachmentUrlWithBase(
|
|
rawUrl: string | null | undefined,
|
|
baseUrl: string,
|
|
): string | null {
|
|
if (!rawUrl) return null;
|
|
if (!rawUrl.startsWith("/")) return rawUrl;
|
|
const trimmedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
return `${trimmedBaseUrl}${rawUrl}`;
|
|
}
|
|
|
|
export function resolveAttachmentUrl(
|
|
rawUrl: string | null | undefined,
|
|
): string | null {
|
|
return resolveAttachmentUrlWithBase(rawUrl, API_URL);
|
|
}
|