gettext_mcp/io/mod.rs
1//! Filesystem abstraction used by the store and manager.
2//!
3//! Production code uses [`FsFileStore`] from [`fs`]; tests can plug in
4//! their own [`FileStore`] impl for in-memory or fault-injected scenarios.
5
6pub mod fs;
7
8use std::path::Path;
9use std::time::SystemTime;
10
11use crate::error::GettextError;
12
13pub use fs::{cleanup_orphan_tmps, FsFileStore};
14
15/// File I/O contract used by [`crate::service::store::GettextStore`].
16///
17/// Implementations are expected to be `Send + Sync` so they can be shared
18/// behind an `Arc` across async tasks. Reads and writes are blocking by
19/// design (called from `spawn_blocking` in the store layer).
20pub trait FileStore: Send + Sync {
21 /// Read the file at `path` as a UTF-8 string. Implementations should
22 /// strip any leading UTF-8 BOM before returning.
23 fn read(&self, path: &Path) -> Result<String, GettextError>;
24
25 /// Atomically write `content` to `path`. Implementations should hold
26 /// an advisory lock on the target during the write where possible.
27 fn write(&self, path: &Path, content: &str) -> Result<(), GettextError>;
28
29 /// Atomically write a raw byte buffer to `path`. Used for binary
30 /// artifacts like `.mo` files. Default impl just routes through
31 /// [`FileStore::write`] by treating the bytes as UTF-8 — implementors
32 /// that need to honour arbitrary bytes (the production
33 /// [`FsFileStore`]) should override.
34 fn write_bytes(&self, path: &Path, bytes: &[u8]) -> Result<(), GettextError> {
35 let s = std::str::from_utf8(bytes).map_err(|e| {
36 GettextError::InvalidInput(format!("write_bytes default impl expects UTF-8: {e}"))
37 })?;
38 self.write(path, s)
39 }
40
41 /// Return the on-disk modified time of `path`, used by the store
42 /// manager to detect external edits and invalidate caches.
43 fn modified_time(&self, path: &Path) -> Result<SystemTime, GettextError>;
44
45 /// `true` if `path` exists on disk.
46 fn exists(&self, path: &Path) -> bool;
47}