//! Resource I/O on the [`Gdd`] handle: the content-addressed byte store that backs raster images, //! fonts, embedded WASM, and proto-node declarations. Registration goes through the session as an //! `AddResource` delta; the bytes live in the working copy's `resources/` directory. #[cfg(not(target_family = "wasm"))] use std::path::Path; use std::sync::Arc; use document_container::{AnyContainer, AsyncContainer, ByteHolder, ContainerError}; use graphene_resource::ResourceFuture; use graphene_resource::{LoadResource, Resource, ResourceHash, ResourceStorage}; use crate::Gdd; use crate::error::Error; use crate::layout::Layout; impl Gdd { pub async fn read_resource(&self, hash: &ResourceHash) -> Result { self.working.read(&self.layout.resource_path(hash)).await } /// Register a resource under `id` and store its bytes. Commits an `AddResource` delta (a single /// `DataSource::Embedded` source resolved to the content hash) through the session so the registry /// records the resource and the entry replicates, then writes the bytes into the working copy's /// content-addressed store. The caller owns `id` allocation. pub fn add_resource(&mut self, id: graph_storage::ResourceId, bytes: &[u8]) -> Result<(), Error> { let hash = ResourceHash::from(bytes); self.working.write_non_blocking(&self.layout.resource_path(&hash), bytes)?; let hot_ops = self.session.stage_embedded_resource(id, hash)?; self.append_and_retire(&hot_ops, false)?; Ok(()) } /// Like [`add_resource`](Self::add_resource) but copies the bytes from a filesystem `src` rather /// than buffering them. Folder backends use `fs::copy` (CoW on supported filesystems); other /// backends fall back to read-then-write. Native-only: there is no filesystem source path on wasm. #[cfg(not(target_family = "wasm"))] pub fn add_resource_from_path(&mut self, id: graph_storage::ResourceId, hash: ResourceHash, src: &Path) -> Result<(), Error> { let dest_path = self.layout.resource_path(&hash); if let AnyContainer::Folder(folder) = self.working.as_ref() { let full = folder.root().join(&dest_path); if let Some(parent) = full.parent() { std::fs::create_dir_all(parent).map_err(ContainerError::Io)?; } std::fs::copy(src, &full).map_err(ContainerError::Io)?; } else { let bytes = std::fs::read(src).map_err(ContainerError::Io)?; // The folder fast path trusts the caller's hash to avoid reading the file; here we've read // the bytes anyway, so verify the hash matches and flag a content-addressing bug in debug. debug_assert_eq!(hash, ResourceHash::from(bytes.as_slice()), "add_resource_from_path hash does not match the file at {src:?}"); self.working.write_non_blocking(&dest_path, &bytes)?; } let hot_ops = self.session.stage_embedded_resource(id, hash)?; self.append_and_retire(&hot_ops, false)?; Ok(()) } pub async fn has_resource(&self, hash: &ResourceHash) -> bool { self.working.exists(&self.layout.resource_path(hash)).await } pub fn remove_resource(&self, hash: &ResourceHash) -> Result<(), ContainerError> { self.working.remove_non_blocking(&self.layout.resource_path(hash)) } /// Enumerate every resource currently in the working copy. Paths that don't parse as a /// `ResourceHash` (foreign files dropped into the resources directory) are silently skipped. pub async fn resource_hashes(&self) -> Result, ContainerError> { let dir = self.layout.resources_dir(); if !self.working.list_dirs("").await?.iter().any(|d| d == dir) { return Ok(Vec::new()); } let entries = self.working.list(dir).await?; let prefix = format!("{dir}/"); let mut hashes = Vec::with_capacity(entries.len()); for entry in entries { let Some(name) = entry.strip_prefix(&prefix) else { continue }; if let Ok(hash) = name.parse::() { hashes.push(hash); } } Ok(hashes) } pub fn resource_proxy(&self) -> ResourceProxy where L: Clone, { ResourceProxy(self.working.clone(), self.layout.clone()) } } impl LoadResource for Gdd { fn load(&self, hash: ResourceHash) -> ResourceFuture<'_> { Box::pin(async move { let bytes = self.working.read(&self.layout.resource_path(&hash)).await.ok()?; Some(Resource::new(bytes)) }) } } pub struct ResourceProxy(Arc, T); impl LoadResource for ResourceProxy { fn load(&self, hash: ResourceHash) -> ResourceFuture<'_> { Box::pin(async move { let bytes = self.0.read(&self.1.resource_path(&hash)).await.ok()?; Some(Resource::new(bytes)) }) } } impl ResourceStorage for Gdd { fn store(&self, data: &[u8]) -> ResourceHash { let hash = ResourceHash::from(data); if let Err(error) = self.working.write_non_blocking(&self.layout.resource_path(&hash), data) { log::error!("ResourceStorage::store failed for {hash}: {error}"); } hash } fn contains(&self, hash: &ResourceHash) -> bool { self.working.exists_non_blocking(&self.layout.resource_path(hash)) } fn garbage_collect(&self, used: &[ResourceHash]) { // `garbage_collect` is synchronous but listing resources is async, so the native path blocks on // it. That's unavailable on wasm (single-threaded; `block_on` would deadlock). The editor never // uses `Gdd` as the runtime `ResourceStorage` on wasm (it GCs the app-global cache instead), so // this is an unreachable configuration there rather than a missing feature. #[cfg(target_family = "wasm")] { let _ = used; log::error!("ResourceStorage::garbage_collect is not supported for Gdd on wasm"); } #[cfg(not(target_family = "wasm"))] { let kept: std::collections::HashSet<&ResourceHash> = used.iter().collect(); let hashes = match futures::executor::block_on(self.resource_hashes()) { Ok(hashes) => hashes, Err(error) => { log::error!("Failed to list resources during garbage_collect: {error}"); return; } }; for hash in hashes { if kept.contains(&hash) { continue; } if let Err(error) = self.working.remove_non_blocking(&self.layout.resource_path(&hash)) { log::error!("ResourceStorage::garbage_collect failed to remove {hash}: {error}"); } } } } }