feat: serve blobs/icons over a Unix domain socket with native media bridges#193
Open
gmaclennan wants to merge 4 commits into
Open
feat: serve blobs/icons over a Unix domain socket with native media bridges#193gmaclennan wants to merge 4 commits into
gmaclennan wants to merge 4 commits into
Conversation
…ridges Rework of the PR #44 approach on top of current main. The backend's blob/icon Fastify server previously bound to 127.0.0.1:<random>, so any app on the device could read media URLs and share-sheet flows had no usable URL form. It now binds to a third Unix domain socket (media.sock, alongside comapeo.sock / control.sock) inside the app sandbox, and per-platform native bridges serve the bytes to the app. @comapeo/core keeps zero knowledge of URLs or how media is served: a one-line patch-package patch makes getFastifyServerAddress return an empty base for UDS binds, so $blobs.getUrl() / $icons.getIconUrl() return *relative* paths (/blobs/...). The RN side owns URL composition: - getMediaBaseUrl() / toMediaUrl(): compose the platform-native in-app URL (Android content://<applicationId>.comapeo.media, iOS comapeo://media). Host apps or @comapeo/core-react append these to data records in the frontend. - getShareableMediaUrl(): snapshots a blob to a cache file (atomic rename, Content-Type-derived extension, 24h pruning) and returns a file:// URL for the Android/iOS share sheets — in-app media URLs cannot cross the process boundary. Native serving: - Android: MediaContentProvider streams HTTP/1.0-over-UDS responses through a ParcelFileDescriptor pipe (bounded thread pool, buffered header parse, error propagation via closeWithError, Content-Length truncation detection). - iOS: MediaFetcher (shared UDS fetch pipeline) backs ComapeoMediaImageLoader (RCTImageURLLoader for RN <Image>) and a globally-registered streaming MediaURLProtocol (URLSession.shared consumers). Tests at four layers, all black-box against the wire/API contract: - backend/lib/media-serving.test.mjs boots the real backend entry over real sockets: init handshake, blob create via RPC, relative getUrl, raw HTTP/1.0 GET over media.sock returns the stored bytes. - ios/Tests/MediaFetcherTests.swift exercises fetch/stream/share paths against an in-test UDS HTTP server (truncation, 404, cancellation). - android MediaContentProviderTest drives the provider through ContentResolver against a fake UDS backend; MediaHttpClientTest covers the shared MIME-extension mapping on the JVM. - apps/e2e media suite: blob → relative URL → native URL → Image.getSize decodes through the real platform pipeline, plus the shareable file:// snapshot. BREAKING CHANGE: $blobs.getUrl() / $icons.getIconUrl() now return relative paths; compose them with getMediaBaseUrl() (or toMediaUrl()) before handing them to <Image> or other consumers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkeJPhPnipj9JNwYXsD6gU
…napshot Cache-file share snapshots were observed being evicted on low-storage devices before the share completed. On Android the snapshot is unnecessary: the MediaContentProvider already streams from the UDS-bound media server, the socket is served by the :ComapeoCore foreground service (which outlives app switches), and grantUriPermissions was already in place — so getShareableMediaUrl now returns the same content:// URI the app renders with. Zero copy, nothing on disk to evict. It HEAD-validates the path first so a missing blob rejects at the call site rather than inside the receiving app. Share receivers resolve metadata through the ContentResolver, not HTTP, so the provider now answers those from the served headers: - getType() returns the real Content-Type (header cache warmed by every open/HEAD, else a bounded 2-attempt HEAD over the UDS, degrading to application/octet-stream when the backend is down — metadata calls run on other apps' binder threads and must not block on the boot-covering retry budget). - query() serves OpenableColumns.DISPLAY_NAME (path segment plus an extension derived from the MIME type). SIZE is null: the backend streams without Content-Length (pinned by the backend e2e test; if upstream core adds the header, SIZE and the existing truncation checks both light up). iOS keeps the file snapshot (share extensions run out-of-process where comapeo:// means nothing) but moves it from the purgeable Caches directory to Application Support (excluded from backup, still pruned after 24h) so the same eviction failure mode can't hit it. Known Android trade-offs, documented: receivers that defer reading until after the backend stops fail, as do receivers requiring a seekable descriptor; the share Intent must carry FLAG_GRANT_READ_URI_PERMISSION (react-native-share does; expo-sharing does not support content://). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkeJPhPnipj9JNwYXsD6gU
Swift folds a "\r\n" pair into one grapheme-cluster Character, so the Character-level split(whereSeparator:) never matched the line breaks and returned the whole header block as a single line. The status line still parsed (Int() reads the code before the CRLF), which masked the bug for status but dropped every subsequent header — Content-Type and Content-Length were lost, breaking MIME detection, shareable-file extensions, and truncated-body detection. Split on unicode scalars, where \r and \n are distinct.
…s-serving-cehnc7 * origin/main: fix(scripts): fail the unit-test runner when the Gradle test task fails ci(deps): bump actions/cache in the actions group across 1 directory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rework of the PR #44 approach on top of current main. The backend's
blob/icon Fastify server previously bound to 127.0.0.1:, so any
app on the device could read media URLs and share-sheet flows had no
usable URL form. It now binds to a third Unix domain socket
(media.sock, alongside comapeo.sock / control.sock) inside the app
sandbox, and per-platform native bridges serve the bytes to the app.
@comapeo/core keeps zero knowledge of URLs or how media is served: a
one-line patch-package patch makes getFastifyServerAddress return an
empty base for UDS binds, so $blobs.getUrl() / $icons.getIconUrl()
return relative paths (/blobs/...). The RN side owns URL composition:
URL (Android content://.comapeo.media, iOS
comapeo://media). Host apps or @comapeo/core-react append these to
data records in the frontend.
rename, Content-Type-derived extension, 24h pruning) and returns a
file:// URL for the Android/iOS share sheets — in-app media URLs
cannot cross the process boundary.
Native serving:
through a ParcelFileDescriptor pipe (bounded thread pool, buffered
header parse, error propagation via closeWithError, Content-Length
truncation detection).
ComapeoMediaImageLoader (RCTImageURLLoader for RN
globally-registered streaming MediaURLProtocol (URLSession.shared
consumers).
Tests at four layers, all black-box against the wire/API contract:
real sockets: init handshake, blob create via RPC, relative getUrl,
raw HTTP/1.0 GET over media.sock returns the stored bytes.
against an in-test UDS HTTP server (truncation, 404, cancellation).
ContentResolver against a fake UDS backend; MediaHttpClientTest
covers the shared MIME-extension mapping on the JVM.
Image.getSize decodes through the real platform pipeline, plus the
shareable file:// snapshot.
BREAKING CHANGE: $blobs.getUrl() / $icons.getIconUrl() now return
or other consumers.
relative paths; compose them with getMediaBaseUrl() (or toMediaUrl())
before handing them to
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LkeJPhPnipj9JNwYXsD6gU