Fase 5: serve and accept assets over HTTP on the daemon

Closes the gap the plan flagged as blocking the sync deletion: a web/mobile
client had no way to read or write asset bytes at all (only db-sync's now
end-of-life handlers touched platform/asset-*). Adds GET/PUT
/v1/assets/:repo/:name on db_worker_node.cljs, backed by three new
thread-api ops (asset-read-bytes/write-bytes/stat) that just call the
existing Node platform.cljs asset-*-bytes! functions -- reusing the same
JSON+transit /v1/invoke path already proven for export-db-binary, so no new
binary RPC mechanism was needed.

GET responses carry a real Content-Type (guessed from extension), a sha1
ETag, and immutable Cache-Control -- assets are content-addressed by
filename, so this is safe to cache forever once fetched. Auth accepts
?token= as a fallback for a direct <img src>/browser navigation, same
rationale as the SSE interim token, but as a genuine fallback alongside the
Authorization header rather than replacing it (an early version of this
made query-token exclusive with header auth, which 401'd every JS-driven
fetch -- caught before committing, real end-to-end curl testing against a
live daemon also caught a 500-instead-of-404 on missing assets and a
never-firing 304 from a hyphenated JS property access that needed `aget`
instead of `.-`).

Client side: frontend.handler.assets/resolve-asset-real-path-url and
<make-asset-url now resolve to the daemon's asset URL (with the server
token) instead of an unreachable file:// path when a web/mobile client has
a server configured (frontend.config/get-server-base-url) -- covers viewing
existing assets. frontend.fs/write-asset-file! gets the matching upload
path via a new frontend.persist-db.remote/upload-asset!, so pasting an
asset from a web/mobile client no longer silently lands in
frontend.fs.memory-fs and vanishes on reload.

Server-side routes and round-trip verified end-to-end against a real
daemon (curl) and with new automated tests (byte equality, content-type,
etag/304, 404, repo-mismatch, auth-required). The client-side URL
resolution changes could not be visually verified -- no browser in this
environment -- only reviewed and compiled clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 10:33:42 +02:00
parent cf61c46c35
commit 902bf7f568
6 changed files with 361 additions and 14 deletions

View File

@@ -23,6 +23,14 @@
"/v1/import-db-binary?repo="
(js/encodeURIComponent repo)))
(defn- asset-url
[base-url repo file-name]
(str (normalize-base-url base-url)
"/v1/assets/"
(js/encodeURIComponent repo)
"/"
(js/encodeURIComponent file-name)))
(defn- events-url
"EventSource has no header API, so the bearer token travels as a query
param here instead of the `Authorization` header every other request
@@ -102,6 +110,26 @@
:code (normalize-code (:code error))}
error (assoc :error error))))))))
(defn upload-asset!
"PUTs raw asset bytes to a db-worker-node daemon's /v1/assets/:repo/:name
(see Fase 5 in docs/agent-guide). Used by frontend.fs/write-asset-file! for
a web/mobile client, which has no local filesystem to write assets to --
Electron keeps writing straight to disk via its own IPC instead."
[{:keys [base-url auth-token fetch-fn]} repo file-name data]
(p/let [{:keys [status body]} ((or fetch-fn default-fetch-json!)
{:method "PUT"
:url (asset-url base-url repo file-name)
:headers (binary-headers auth-token)
:body data})]
(if (<= 200 status 299)
true
(let [parsed (parse-response-body body)
error (:error parsed)]
(throw (ex-info (or (:message error) "asset upload failed")
(cond-> {:status status
:code (normalize-code (:code error))}
error (assoc :error error))))))))
(defn- decode-event
[{:keys [type payload]}]
(let [decoded (when (some? payload)