Skip to main content

oxios_kernel/image_gen/
store.rs

1//! Persistence for generated image bytes.
2//!
3//! Providers that return only base64 (e.g. `gpt-image-1`) need their output
4//! materialized to disk and served under a stable URL so agents can emit
5//! markdown `![](url)`. The sink is a trait so the provider logic stays
6//! unit-testable with a fake.
7
8use std::path::PathBuf;
9
10use crate::image_gen::ImageGenError;
11
12/// Persists image bytes and returns a URL the frontend can fetch.
13pub trait ImageSink: Send + Sync {
14    /// Write `bytes` with extension `ext`, return the served URL.
15    fn save(&self, bytes: Vec<u8>, ext: &str) -> Result<String, ImageGenError>;
16}
17
18/// Filesystem-backed store. Writes into `dir`, serves under `serve_prefix`.
19///
20/// `serve_prefix` is the binary's API path (default `/api/images/`); the
21/// actual `GET /api/images/:name` route lives in the binary crate.
22pub struct FsImageStore {
23    dir: PathBuf,
24    serve_prefix: String,
25}
26
27impl FsImageStore {
28    /// Create a new store rooted at `dir`, serving under `serve_prefix`.
29    pub fn new(dir: PathBuf, serve_prefix: String) -> Self {
30        Self { dir, serve_prefix }
31    }
32}
33
34impl ImageSink for FsImageStore {
35    fn save(&self, bytes: Vec<u8>, ext: &str) -> Result<String, ImageGenError> {
36        std::fs::create_dir_all(&self.dir).map_err(|e| ImageGenError::Store(e.to_string()))?;
37        let name = format!("{}.{ext}", uuid::Uuid::new_v4());
38        std::fs::write(self.dir.join(&name), &bytes)
39            .map_err(|e| ImageGenError::Store(e.to_string()))?;
40        Ok(format!("{}{name}", self.serve_prefix))
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn save_writes_file_and_returns_prefixed_url() {
50        let tmp = tempfile::tempdir().unwrap();
51        let store = FsImageStore::new(tmp.path().to_path_buf(), "/api/images/".into());
52        let url = store.save(vec![1, 2, 3, 4], "png").unwrap();
53        assert!(url.starts_with("/api/images/"));
54        assert!(url.ends_with(".png"));
55        // File materialized on disk.
56        let name = url.strip_prefix("/api/images/").unwrap();
57        let written = std::fs::read(tmp.path().join(name)).unwrap();
58        assert_eq!(written, vec![1, 2, 3, 4]);
59    }
60
61    #[test]
62    fn save_creates_missing_dir() {
63        let tmp = tempfile::tempdir().unwrap();
64        let nested = tmp.path().join("deeply").join("nested");
65        let store = FsImageStore::new(nested, "/api/images/".into());
66        let url = store.save(vec![0], "png").unwrap();
67        assert!(url.starts_with("/api/images/"));
68    }
69}