Skip to main content

auths_transparency/
fs_store.rs

1use std::path::PathBuf;
2
3use crate::error::TransparencyError;
4use crate::store::TileStore;
5
6/// Filesystem-backed tile store.
7///
8/// Stores tiles and checkpoints as plain files under a base directory.
9/// Full tiles (paths without `.p/`) are write-once: subsequent writes
10/// are silently skipped. Partial tiles (paths containing `.p/`) and
11/// the checkpoint file are always overwritable.
12///
13/// Args:
14/// * `base_path` — Root directory for all tile and checkpoint files.
15///
16/// Usage:
17/// ```ignore
18/// let store = FsTileStore::new("/home/user/.auths/tlog".into());
19/// store.write_tile("tile/0/000", &data).await?;
20/// ```
21pub struct FsTileStore {
22    base_path: PathBuf,
23}
24
25impl FsTileStore {
26    /// Creates a new filesystem tile store rooted at the given path.
27    ///
28    /// Args:
29    /// * `base_path` — Directory where tiles and checkpoints are stored.
30    ///
31    /// Usage:
32    /// ```ignore
33    /// let store = FsTileStore::new(PathBuf::from("/tmp/tlog"));
34    /// ```
35    pub fn new(base_path: PathBuf) -> Self {
36        Self { base_path }
37    }
38}
39
40fn is_partial_tile(path: &str) -> bool {
41    path.contains(".p/")
42}
43
44#[async_trait::async_trait]
45impl TileStore for FsTileStore {
46    async fn read_tile(&self, path: &str) -> Result<Vec<u8>, TransparencyError> {
47        let full_path = self.base_path.join(path);
48        tokio::fs::read(&full_path)
49            .await
50            .map_err(|e| TransparencyError::StoreError(e.to_string()))
51    }
52
53    async fn write_tile(&self, path: &str, data: &[u8]) -> Result<(), TransparencyError> {
54        let full_path = self.base_path.join(path);
55
56        if !is_partial_tile(path) && full_path.exists() {
57            return Ok(());
58        }
59
60        if let Some(parent) = full_path.parent() {
61            tokio::fs::create_dir_all(parent)
62                .await
63                .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
64        }
65
66        tokio::fs::write(&full_path, data)
67            .await
68            .map_err(|e| TransparencyError::StoreError(e.to_string()))
69    }
70
71    async fn read_checkpoint(&self) -> Result<Option<Vec<u8>>, TransparencyError> {
72        let path = self.base_path.join("checkpoint");
73        match tokio::fs::read(&path).await {
74            Ok(data) => Ok(Some(data)),
75            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
76            Err(e) => Err(TransparencyError::StoreError(e.to_string())),
77        }
78    }
79
80    async fn write_checkpoint(&self, data: &[u8]) -> Result<(), TransparencyError> {
81        let path = self.base_path.join("checkpoint");
82
83        if let Some(parent) = path.parent() {
84            tokio::fs::create_dir_all(parent)
85                .await
86                .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
87        }
88
89        tokio::fs::write(&path, data)
90            .await
91            .map_err(|e| TransparencyError::StoreError(e.to_string()))
92    }
93}
94
95#[cfg(test)]
96#[allow(clippy::unwrap_used, clippy::expect_used)]
97mod tests {
98    use super::*;
99
100    #[tokio::test]
101    async fn tile_write_read_roundtrip() {
102        let dir = tempfile::tempdir().unwrap();
103        let store = FsTileStore::new(dir.path().to_path_buf());
104
105        let data = b"leaf data";
106        store.write_tile("tile/0/000", data).await.unwrap();
107
108        let read_back = store.read_tile("tile/0/000").await.unwrap();
109        assert_eq!(read_back, data);
110    }
111
112    #[tokio::test]
113    async fn tile_creates_nested_directories() {
114        let dir = tempfile::tempdir().unwrap();
115        let store = FsTileStore::new(dir.path().to_path_buf());
116
117        store.write_tile("tile/2/001/002", b"deep").await.unwrap();
118
119        let on_disk = dir.path().join("tile/2/001/002");
120        assert!(on_disk.exists());
121    }
122
123    #[tokio::test]
124    async fn full_tile_is_immutable() {
125        let dir = tempfile::tempdir().unwrap();
126        let store = FsTileStore::new(dir.path().to_path_buf());
127
128        store.write_tile("tile/0/000", b"first").await.unwrap();
129        store.write_tile("tile/0/000", b"second").await.unwrap();
130
131        let data = store.read_tile("tile/0/000").await.unwrap();
132        assert_eq!(data, b"first");
133    }
134
135    #[tokio::test]
136    async fn partial_tile_is_overwritable() {
137        let dir = tempfile::tempdir().unwrap();
138        let store = FsTileStore::new(dir.path().to_path_buf());
139
140        store
141            .write_tile("tile/0/000.p/5", b"partial-v1")
142            .await
143            .unwrap();
144        store
145            .write_tile("tile/0/000.p/5", b"partial-v2")
146            .await
147            .unwrap();
148
149        let data = store.read_tile("tile/0/000.p/5").await.unwrap();
150        assert_eq!(data, b"partial-v2");
151    }
152
153    #[tokio::test]
154    async fn checkpoint_roundtrip() {
155        let dir = tempfile::tempdir().unwrap();
156        let store = FsTileStore::new(dir.path().to_path_buf());
157
158        let result = store.read_checkpoint().await.unwrap();
159        assert!(result.is_none());
160
161        store.write_checkpoint(b"cp-v1").await.unwrap();
162        let data = store.read_checkpoint().await.unwrap();
163        assert_eq!(data, Some(b"cp-v1".to_vec()));
164
165        store.write_checkpoint(b"cp-v2").await.unwrap();
166        let data = store.read_checkpoint().await.unwrap();
167        assert_eq!(data, Some(b"cp-v2".to_vec()));
168    }
169
170    #[tokio::test]
171    async fn read_nonexistent_tile_returns_error() {
172        let dir = tempfile::tempdir().unwrap();
173        let store = FsTileStore::new(dir.path().to_path_buf());
174
175        let result = store.read_tile("tile/0/999").await;
176        assert!(result.is_err());
177    }
178}