1use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::io::{BufReader, BufWriter, Read, Write};
13use std::path::{Path, PathBuf};
14use uuid::Uuid;
15
16use crate::config::ServerConfig;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StoredProject {
21 pub uuid: Uuid,
22 pub name: String,
23 pub size_bytes: u64,
24 pub file_count: u32,
25 pub encrypted: bool,
26 pub created_at: DateTime<Utc>,
27 pub updated_at: DateTime<Utc>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32struct Registry {
33 version: u32,
34 projects: HashMap<Uuid, StoredProject>,
35}
36
37impl Registry {
38 fn new() -> Self {
39 Self {
40 version: 1,
41 projects: HashMap::new(),
42 }
43 }
44}
45
46pub struct ProjectStorage {
52 config: ServerConfig,
53 registry: Registry,
54}
55
56impl ProjectStorage {
57 pub fn new(config: ServerConfig) -> Result<Self, String> {
63 fs::create_dir_all(&config.storage_base_path)
64 .map_err(|e| format!("Cannot create storage dir: {}", e))?;
65
66 let registry_path = config.registry_path();
67 let registry = if registry_path.exists() {
68 Self::load_registry(®istry_path)?
69 } else {
70 let reg = Registry::new();
71 Self::save_registry(®istry_path, ®)?;
72 reg
73 };
74
75 Ok(Self { config, registry })
76 }
77
78 pub fn add_project(&mut self, project: StoredProject) -> Result<(), String> {
82 if self.registry.projects.contains_key(&project.uuid) {
83 return Err(format!(
84 "Project {} ({}) already exists",
85 project.name, project.uuid
86 ));
87 }
88
89 let project_dir = self.config.project_dir(&project.uuid);
91 fs::create_dir_all(&project_dir)
92 .map_err(|e| format!("Cannot create project dir: {}", e))?;
93
94 self.registry
95 .projects
96 .insert(project.uuid, project);
97 self.flush()
98 }
99
100 pub fn remove_project(&mut self, uuid: &Uuid) -> Result<(), String> {
102 if self.registry.projects.remove(uuid).is_none() {
103 return Err(format!("Project {} not found", uuid));
104 }
105
106 let project_dir = self.config.project_dir(uuid);
108 if project_dir.exists() {
109 fs::remove_dir_all(&project_dir)
110 .map_err(|e| format!("Cannot delete project dir: {}", e))?;
111 }
112
113 self.flush()
114 }
115
116 pub fn get_project(&self, uuid: &Uuid) -> Option<&StoredProject> {
118 self.registry.projects.get(uuid)
119 }
120
121 pub fn list_projects(&self) -> Vec<&StoredProject> {
123 self.registry.projects.values().collect()
124 }
125
126 pub fn update_project(&mut self, project: StoredProject) -> Result<(), String> {
128 if !self.registry.projects.contains_key(&project.uuid) {
129 return Err(format!("Project {} not found", project.uuid));
130 }
131 self.registry
132 .projects
133 .insert(project.uuid, project);
134 self.flush()
135 }
136
137 pub fn archive_path(&self, uuid: &Uuid) -> PathBuf {
139 self.config.project_dir(uuid).join("data.enc")
140 }
141
142 pub fn meta_path(&self, uuid: &Uuid) -> PathBuf {
144 self.config.project_dir(uuid).join("meta.toml")
145 }
146
147 pub fn write_archive(
153 &self,
154 uuid: &Uuid,
155 reader: &mut impl Read,
156 ) -> Result<u64, String> {
157 let path = self.archive_path(uuid);
158 let file = fs::File::create(&path)
159 .map_err(|e| format!("Cannot create archive file: {}", e))?;
160 let mut writer = BufWriter::new(file);
161
162 let bytes_written = std::io::copy(reader, &mut writer)
163 .map_err(|e| format!("Cannot write archive data: {}", e))?;
164
165 writer
166 .flush()
167 .map_err(|e| format!("Cannot flush archive: {}", e))?;
168
169 Ok(bytes_written)
170 }
171
172 pub fn read_archive(&self, uuid: &Uuid) -> Result<BufReader<fs::File>, String> {
176 let path = self.archive_path(uuid);
177 if !path.exists() {
178 return Err(format!(
179 "Archive file not found for project {}",
180 uuid
181 ));
182 }
183 let file = fs::File::open(&path)
184 .map_err(|e| format!("Cannot open archive: {}", e))?;
185 Ok(BufReader::new(file))
186 }
187
188 pub fn archive_exists(&self, uuid: &Uuid) -> bool {
190 self.archive_path(uuid).exists()
191 }
192
193 pub fn write_meta(&self, uuid: &Uuid, project: &StoredProject) -> Result<(), String> {
195 let path = self.meta_path(uuid);
196 let contents = toml::to_string_pretty(project)
197 .map_err(|e| format!("Cannot serialize meta: {}", e))?;
198 fs::write(&path, contents)
199 .map_err(|e| format!("Cannot write meta file: {}", e))?;
200 Ok(())
201 }
202
203 pub fn check_size_limit(&self, additional_bytes: u64) -> Result<(), String> {
205 let current_total: u64 = self
206 .registry
207 .projects
208 .values()
209 .map(|p| p.size_bytes)
210 .sum();
211 let max_bytes = self.config.max_project_size_gb * 1024 * 1024 * 1024;
212 let new_total = current_total + additional_bytes;
213
214 if new_total > max_bytes {
215 return Err(format!(
216 "Storage limit exceeded: would use {} GB (limit {} GB)",
217 new_total / (1024 * 1024 * 1024),
218 self.config.max_project_size_gb
219 ));
220 }
221 Ok(())
222 }
223
224 fn load_registry(path: &Path) -> Result<Registry, String> {
227 let contents = fs::read_to_string(path)
228 .map_err(|e| format!("Cannot read registry: {}", e))?;
229 serde_json::from_str(&contents)
230 .map_err(|e| format!("Cannot parse registry: {}", e))
231 }
232
233 fn save_registry(path: &Path, registry: &Registry) -> Result<(), String> {
234 let tmp_path = path.with_extension("tmp");
235 let contents = serde_json::to_string_pretty(registry)
236 .map_err(|e| format!("Cannot serialize registry: {}", e))?;
237
238 fs::write(&tmp_path, contents)
240 .map_err(|e| format!("Cannot write registry tmp: {}", e))?;
241 fs::rename(&tmp_path, path)
242 .map_err(|e| format!("Cannot rename registry: {}", e))?;
243
244 Ok(())
245 }
246
247 fn flush(&self) -> Result<(), String> {
248 Self::save_registry(&self.config.registry_path(), &self.registry)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use tempfile::TempDir;
256
257 fn make_config(dir: &Path) -> ServerConfig {
258 let mut cfg = ServerConfig::default();
259 cfg.storage_base_path = dir.join("storage");
260 cfg.auth_token = "test-token".into();
261 cfg
262 }
263
264 fn make_project(name: &str) -> StoredProject {
265 StoredProject {
266 uuid: Uuid::new_v4(),
267 name: name.into(),
268 size_bytes: 1_000_000,
269 file_count: 42,
270 encrypted: false,
271 created_at: Utc::now(),
272 updated_at: Utc::now(),
273 }
274 }
275
276 #[test]
277 fn test_create_and_list() {
278 let tmp = TempDir::new().unwrap();
279 let config = make_config(tmp.path());
280 let mut storage = ProjectStorage::new(config).unwrap();
281
282 let p1 = make_project("alpha");
283 let p2 = make_project("beta");
284
285 storage.add_project(p1.clone()).unwrap();
286 storage.add_project(p2.clone()).unwrap();
287
288 let list = storage.list_projects();
289 assert_eq!(list.len(), 2);
290 }
291
292 #[test]
293 fn test_duplicate_rejected() {
294 let tmp = TempDir::new().unwrap();
295 let config = make_config(tmp.path());
296 let mut storage = ProjectStorage::new(config).unwrap();
297
298 let p = make_project("test");
299 storage.add_project(p.clone()).unwrap();
300 assert!(storage.add_project(p).is_err());
301 }
302
303 #[test]
304 fn test_remove_project() {
305 let tmp = TempDir::new().unwrap();
306 let config = make_config(tmp.path());
307 let mut storage = ProjectStorage::new(config).unwrap();
308
309 let p = make_project("removable");
310 let uuid = p.uuid;
311 storage.add_project(p).unwrap();
312 assert!(storage.get_project(&uuid).is_some());
313
314 storage.remove_project(&uuid).unwrap();
315 assert!(storage.get_project(&uuid).is_none());
316 }
317
318 #[test]
319 fn test_write_and_read_archive() {
320 let tmp = TempDir::new().unwrap();
321 let config = make_config(tmp.path());
322 let mut storage = ProjectStorage::new(config).unwrap();
323
324 let p = make_project("data-test");
325 let uuid = p.uuid;
326 storage.add_project(p).unwrap();
327
328 let data = b"encrypted project archive contents here";
329 let written = storage
330 .write_archive(&uuid, &mut &data[..])
331 .unwrap();
332 assert_eq!(written as usize, data.len());
333
334 let mut reader = storage.read_archive(&uuid).unwrap();
335 let mut read_back = Vec::new();
336 reader.read_to_end(&mut read_back).unwrap();
337 assert_eq!(read_back, data);
338 }
339
340 #[test]
341 fn test_persistence() {
342 let tmp = TempDir::new().unwrap();
343 let config = make_config(tmp.path());
344 let uuid;
345
346 {
347 let mut storage = ProjectStorage::new(config.clone()).unwrap();
348 let p = make_project("persistent");
349 uuid = p.uuid;
350 storage.add_project(p).unwrap();
351 }
352
353 {
355 let storage = ProjectStorage::new(config).unwrap();
356 let retrieved = storage.get_project(&uuid).unwrap();
357 assert_eq!(retrieved.name, "persistent");
358 }
359 }
360}