auths_transparency/
fs_store.rs1use std::path::PathBuf;
2
3use crate::error::TransparencyError;
4use crate::store::TileStore;
5
6pub struct FsTileStore {
22 base_path: PathBuf,
23}
24
25impl FsTileStore {
26 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}