desktop: render synced attachments instead of broken images (task 2114)
A synced note carried the SERVER's relative attachment path (/api/notes/<id>/attachments/<aid>). In the webview that resolves against the app origin and 404s, so every synced image rendered broken even though the bytes were already on disk from M10.7d. The absolute server URL wouldn't have worked either: that route wants a bearer token the webview never sends, and it would put an offline app on the network to show a file it already has. The bytes now come off disk over a custom URI scheme, served straight from the content-addressed blob store. The webview caches and range-requests them like any other resource — which a data: URI would have thrown away — and the URL is immutable-cacheable because a content address can never describe different bytes. Two things worth knowing about the shape of this: The URL is rewritten in `load_attachments`, the single place the desktop builds an attachment for the UI. NoteCard and NoteEditor are untouched, so there's no second render site to drift. The scheme's URL form is NOT the same on every platform: `scheme://localhost/` on Linux and macOS, `http://scheme.localhost/` on Windows and Android. Getting it wrong breaks exactly one channel, silently, and a headless CI runner can never tell you. The mime rides in the URL, and this scheme is an origin of its own, so an attachment claiming to be text/html would run as a document there. Only media families are echoed back; everything else is served as an opaque download, which is the right treatment for an arbitrary file anyway. Path safety is inherited rather than re-implemented — the handler reads through BlobStore, which already refuses anything that isn't a bare sha256. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
This commit is contained in:
@@ -33,6 +33,27 @@ pub fn run() {
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
// Attachment bytes are served to the webview from the local blob store
|
||||
// (M10.7f). Registered on the BUILDER because a scheme has to exist before
|
||||
// the webview is created; the directory it reads from arrives later, in
|
||||
// `setup`, via `blobs::publish_root`.
|
||||
.register_uri_scheme_protocol(sync::blobs::BLOB_SCHEME, |_ctx, request| {
|
||||
let (status, content_type, body) =
|
||||
sync::blobs::serve(request.uri().path(), request.uri().query());
|
||||
tauri::http::Response::builder()
|
||||
.status(status)
|
||||
.header("Content-Type", content_type)
|
||||
// The bytes are content-addressed: a given URL can never describe
|
||||
// different bytes, so the webview may keep them indefinitely.
|
||||
.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
.body(body)
|
||||
.unwrap_or_else(|_| {
|
||||
tauri::http::Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new())
|
||||
.expect("a bodiless 500 always builds")
|
||||
})
|
||||
})
|
||||
.setup(|app| {
|
||||
use tauri::Manager;
|
||||
log_environment(app);
|
||||
@@ -51,6 +72,9 @@ pub fn run() {
|
||||
// synced image is readable with no network (M10.7d).
|
||||
let blobs = sync::blobs::BlobStore::new(dir.join("blobs"))?;
|
||||
log::info!("attachment store ready: {}", blobs.root().display());
|
||||
// Hand the directory to the URI-scheme handler registered below, which
|
||||
// was built before this path could be resolved.
|
||||
sync::blobs::publish_root(blobs.root().to_path_buf());
|
||||
app.manage(blobs);
|
||||
Ok(())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user