cheogram-www
created pr with
cheogram-www/tauri-media-handling.1
commented
The following changes since commit 62ea2af732f009407e7d2de6588cd10657f4e89b:
npm run tauri init (2026-10-28 10:28:04 -0400)
are available in the Git repository at:
https://tangled.org/mrry.band/cheogram-www tauri-media-handling
for you to fetch changes up to d1d3a238ce8271bab76def99868cfaf5402e1a54:
Tauri file system media store (2026-10-28 11:11:55 -0400)
----------------------------------------------------------------
Eric Roberts (1):
Tauri file system media store
package.json | 1 +
src-tauri/Cargo.lock | 231 +++++++-----
src-tauri/Cargo.toml | 9 +
src-tauri/src/lib.rs | 50 ++-
src-tauri/src/media.rs | 460 ++++++++++++++++++++++
src-tauri/src/media/store.rs | 588 +++++++++++++++++++++++++++++
src-tauri/tauri.conf.json | 1 +
src/App.tsx | 2 +-
src/components/MessageAttachments.test.tsx | 25 ++
src/components/MessageAttachments.tsx | 10 +-
src/media-store-tauri.test.ts | 89 +++++
src/media-store-tauri.ts | 49 +++
src/media-store.ts | 7 +
13 files changed, 1421 insertions(+), 101 deletions(-)
create mode 100644 src-tauri/src/media.rs
create mode 100644 src-tauri/src/media/store.rs
create mode 100644 src/media-store-tauri.test.ts
create mode 100644 src/media-store-tauri.ts
create mode 100644 src/media-store.ts
added cheogram-www/tauri-media-handling.2
1: d1d3a23 ! 1: 5c9c8ae Tauri file system media store
added cheogram-www/tauri-media-handling.3
1: 5c9c8ae < -: ------- Tauri file system media store
-: ------- > 1: 615caaf Review comments
added cheogram-www/tauri-media-handling.4
1: 615caaf < -: ------- Review comments
-: ------- > 1: 58e4016 Tauri file system media store
added cheogram-www/tauri-media-handling.5
1: 58e4016 = 1: b0c5136 Tauri file system media store
added cheogram-www/tauri-media-handling.6
1: b0c5136 ! 1: f6bda1a Tauri file system media store
added cheogram-www/tauri-media-handling.7
1: f6bda1a ! 1: efe5296 Tauri file system media store
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:tauri-media-handling | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:tauri-media-handling [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:tauri-media-handlingadd comment to patch request:
ssh pr.pico.sh comment cheogram-www:tauri-media-handling "lgtm!"
Patchset
cheogram-www/tauri-media-handling.3
Review comments
Stephen Paul Weber
2026-10-28T16:07:49ZSemantic diff summary
0 added,
13 modified,
0 signature changed,
0 removed
across 5 analyzed files
(1 file skipped: unsupported file type)
+5
-0
src-tauri/src/lib.rs
#
| ... | ... | @@ -12,12 +12,17 @@ pub fn run() { | |
| 12 | 12 | media::has_media, | |
| 13 | 13 | media::remove_media | |
| 14 | 14 | ]) | |
| 15 | + | // Is this not the protocol that tauri uses by default? Do we need to register it manually? | |
| 15 | 16 | .register_asynchronous_uri_scheme_protocol("tauri", media::protocol) | |
| 16 | 17 | .setup(|app| { | |
| 17 | 18 | app.manage(std::sync::Arc::new(media::MediaCache::new( | |
| 19 | + | // This is fine for now, but maybe we'll want some to be in cache dir and some in | |
| 20 | + | // more "permanent" dir eventually? We do than on Android anyway | |
| 18 | 21 | app.path().app_cache_dir()?.join("media"), | |
| 19 | 22 | )?)); | |
| 20 | 23 | if cfg!(debug_assertions) { | |
| 24 | + | // Should we have this kind of debug log controlled by an env var so it can be | |
| 25 | + | // enabled at runtime instead of compile time? | |
| 21 | 26 | app.handle().plugin( | |
| 22 | 27 | tauri_plugin_log::Builder::default() | |
| 23 | 28 | .level(log::LevelFilter::Info) |
+16
-0
src-tauri/src/media.rs
#
| ... | ... | @@ -22,9 +22,14 @@ struct MediaUpload { | |
| 22 | 22 | ||
| 23 | 23 | impl tauri::Resource for MediaUpload {} | |
| 24 | 24 | ||
| 25 | + | // I'm not sure this whole wrapper is needed. It seems basically equivalent to | |
| 26 | + | // Mutex<Option<Upload>> and I'n not even sure we need the option, the Upload could know if it is | |
| 27 | + | // closed or whatever, it's already a mutable thing. If we even need to care about it being closed | |
| 28 | + | // anywhere. | |
| 25 | 29 | impl MediaUpload { | |
| 26 | 30 | fn new(cache: Arc<MediaCache>, mime: HeaderValue) -> io::Result<Self> { | |
| 27 | 31 | let upload = Upload::new(&cache, mime)?; | |
| 32 | + | // currently the Upload knows abut the cache but we store it alongside as well? | |
| 28 | 33 | Ok(Self { | |
| 29 | 34 | cache, | |
| 30 | 35 | pending: Mutex::new(Some(upload)), |
| ... | ... | @@ -62,6 +67,9 @@ pub async fn begin_media<R: tauri::Runtime>( | |
| 62 | 67 | ) -> Result<tauri::ResourceId, String> { | |
| 63 | 68 | let mime = HeaderValue::from_str(&mime).map_err(|error| error.to_string())?; | |
| 64 | 69 | let cache = cache.inner().clone(); | |
| 70 | + | // This spawn_blocking moves the MediaUpload::new call to a thread pool so it won't block | |
| 71 | + | // the async runtime, then waits on it. Fine. Except that doesn't seem to do any blocking I/O | |
| 72 | + | // work? And if it did maybe we could write it to be async native? | |
| 65 | 73 | let upload = tauri::async_runtime::spawn_blocking(move || MediaUpload::new(cache, mime)) | |
| 66 | 74 | .await | |
| 67 | 75 | .map_err(|error| error.to_string())? |
| ... | ... | @@ -83,6 +91,7 @@ pub async fn write_media<R: tauri::Runtime>( | |
| 83 | 91 | let data: Vec<u8> = match request.body() { | |
| 84 | 92 | tauri::ipc::InvokeBody::Raw(data) => data.clone(), | |
| 85 | 93 | tauri::ipc::InvokeBody::Json(value) => { | |
| 94 | + | // We allow uploading a json array of numbers? | |
| 86 | 95 | serde_json::from_value(value.clone()).map_err(|error| error.to_string())? | |
| 87 | 96 | } | |
| 88 | 97 | }; |
| ... | ... | @@ -90,6 +99,8 @@ pub async fn write_media<R: tauri::Runtime>( | |
| 90 | 99 | .resources_table() | |
| 91 | 100 | .get::<MediaUpload>(rid) | |
| 92 | 101 | .map_err(|error| error.to_string())?; | |
| 102 | + | // So this blocking is legit, but I do wonder if we couldn't make the write call async native | |
| 103 | + | // instead of wrapping it in a spawn_blocking from outside. | |
| 93 | 104 | tauri::async_runtime::spawn_blocking(move || upload.write(&data)) | |
| 94 | 105 | .await | |
| 95 | 106 | .map_err(|error| error.to_string())? |
| ... | ... | @@ -105,6 +116,7 @@ pub async fn finish_media<R: tauri::Runtime>( | |
| 105 | 116 | .resources_table() | |
| 106 | 117 | .get::<MediaUpload>(rid) | |
| 107 | 118 | .map_err(|error| error.to_string())?; | |
| 119 | + | // Same here: can finish be async native? | |
| 108 | 120 | tauri::async_runtime::spawn_blocking(move || upload.finish()) | |
| 109 | 121 | .await | |
| 110 | 122 | .map_err(|error| error.to_string())? |
| ... | ... | @@ -122,6 +134,8 @@ pub async fn has_media( | |
| 122 | 134 | } | |
| 123 | 135 | let hash = MediaHash::from_hex(&algorithm, &hex).map_err(|error| error.to_string())?; | |
| 124 | 136 | let cache = cache.inner().clone(); | |
| 137 | + | // I'm not going to comment in every place. But in general it seems we built a sync/blocking | |
| 138 | + | // intrface which we never use and then we wrap it in spawn_blocking at every call site. | |
| 125 | 139 | tauri::async_runtime::spawn_blocking(move || { | |
| 126 | 140 | cache.has(&hash).map(|found| found.then(|| hash.url())) | |
| 127 | 141 | }) |
| ... | ... | @@ -209,6 +224,7 @@ pub fn protocol<R: tauri::Runtime>( | |
| 209 | 224 | let app = context.app_handle().clone(); | |
| 210 | 225 | let path = request.uri().path().to_owned(); | |
| 211 | 226 | let head = request.method() == tauri::http::Method::HEAD; | |
| 227 | + | // Does path ever legitimately == /.well-known/ni ? I don't think so | |
| 212 | 228 | if path.starts_with("/.well-known/ni/") || path == "/.well-known/ni" { | |
| 213 | 229 | let Some(hash) = parse_path(&path) else { | |
| 214 | 230 | responder.respond(error_response(StatusCode::BAD_REQUEST)); |
+5
-1
src-tauri/src/media/store.rs
#
| ... | ... | @@ -14,6 +14,9 @@ pub(super) enum MediaHash { | |
| 14 | 14 | ||
| 15 | 15 | impl MediaHash { | |
| 16 | 16 | pub(super) fn from_bytes(algorithm: &str, bytes: &[u8]) -> Option<Self> { | |
| 17 | + | // I thilnk the native format should be bytes, so Sha1(Vec<u8>) or whatever above, | |
| 18 | + | // rather than hex strings we never actually use and just decode to bytes again to make the | |
| 19 | + | // url... | |
| 17 | 20 | match (algorithm, bytes.len()) { | |
| 18 | 21 | ("sha-1", 20) => Some(Self::Sha1(hex::encode(bytes))), | |
| 19 | 22 | ("sha-256", 32) => Some(Self::Sha256(hex::encode(bytes))), |
| ... | ... | @@ -115,7 +119,7 @@ pub(super) struct Upload { | |
| 115 | 119 | impl Upload { | |
| 116 | 120 | pub(super) fn new(cache: &MediaCache, mime: HeaderValue) -> io::Result<Self> { | |
| 117 | 121 | Ok(Self { | |
| 118 | - | file: NamedTempFile::new_in(cache.root.join("tmp"))?, | |
| 122 | + | file: NamedTempFile::new_in(cache.root.join("tmp"))?, // this should probably go in /tmp or another actual temp store? | |
| 119 | 123 | mime, | |
| 120 | 124 | sha256: Sha256::new(), | |
| 121 | 125 | sha1: Sha1::new(), |
+1
-1
src-tauri/tauri.conf.json
#
+3
-0
src/components/MessageAttachments.tsx
#
| ... | ... | @@ -121,6 +121,9 @@ const AttachmentContent = ({ | |
| 121 | 121 | }) => { | |
| 122 | 122 | const { uri, embeddable } = fetchedAttachment; | |
| 123 | 123 | const href = | |
| 124 | + | // Hmm, I guess this is fine. I wonder if we shouldn't just have | |
| 125 | + | // Borogove always append this to the .well-known version | |
| 126 | + | // Along with maybe ct (which is actually standard, though not needed for our case right now) | |
| 124 | 127 | attachment.name && uri.startsWith("/.well-known/ni/") | |
| 125 | 128 | ? `${uri}?name=${encodeURIComponent(attachment.name)}` | |
| 126 | 129 | : uri; |
+2
-0
src/media-store-tauri.ts
#
| ... | ... | @@ -1,6 +1,8 @@ | |
| 1 | 1 | import { Hash, type persistence } from "borogove"; | |
| 2 | 2 | import { invoke, Resource } from "@tauri-apps/api/core"; | |
| 3 | 3 | ||
| 4 | + | // Any reason we're a function that returns an object we declare as | |
| 5 | + | // conforming to an interface rather than a class implementing the interface? | |
| 4 | 6 | export async function MediaStoreTauri() { | |
| 5 | 7 | return { | |
| 6 | 8 | // Instead of using KV we store data on the filesystem because the rust |