1use crate::{ConfigMigrator, MigrationError, Migrator, Queryable};
6use serde_json::Value as JsonValue;
7use std::fs::{self, File, OpenOptions};
8use std::io::Write as IoWrite;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum FormatStrategy {
14 Toml,
16 Json,
18}
19
20#[derive(Debug, Clone)]
22pub struct AtomicWriteConfig {
23 pub retry_count: usize,
25 pub cleanup_tmp_files: bool,
27}
28
29impl Default for AtomicWriteConfig {
30 fn default() -> Self {
31 Self {
32 retry_count: 3,
33 cleanup_tmp_files: true,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum LoadBehavior {
41 CreateIfMissing,
43 ErrorIfMissing,
45}
46
47#[derive(Debug, Clone)]
49pub struct FileStorageStrategy {
50 pub format: FormatStrategy,
52 pub atomic_write: AtomicWriteConfig,
54 pub load_behavior: LoadBehavior,
56}
57
58impl Default for FileStorageStrategy {
59 fn default() -> Self {
60 Self {
61 format: FormatStrategy::Toml,
62 atomic_write: AtomicWriteConfig::default(),
63 load_behavior: LoadBehavior::CreateIfMissing,
64 }
65 }
66}
67
68impl FileStorageStrategy {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn with_format(mut self, format: FormatStrategy) -> Self {
76 self.format = format;
77 self
78 }
79
80 pub fn with_retry_count(mut self, count: usize) -> Self {
82 self.atomic_write.retry_count = count;
83 self
84 }
85
86 pub fn with_cleanup(mut self, cleanup: bool) -> Self {
88 self.atomic_write.cleanup_tmp_files = cleanup;
89 self
90 }
91
92 pub fn with_load_behavior(mut self, behavior: LoadBehavior) -> Self {
94 self.load_behavior = behavior;
95 self
96 }
97}
98
99pub struct FileStorage {
107 path: PathBuf,
108 config: ConfigMigrator,
109 strategy: FileStorageStrategy,
110}
111
112impl FileStorage {
113 pub fn new(
141 path: PathBuf,
142 migrator: Migrator,
143 strategy: FileStorageStrategy,
144 ) -> Result<Self, MigrationError> {
145 let json_string = if path.exists() {
147 let content = fs::read_to_string(&path).map_err(|e| MigrationError::IoError {
148 path: path.display().to_string(),
149 error: e.to_string(),
150 })?;
151
152 if content.trim().is_empty() {
153 "{}".to_string()
155 } else {
156 match strategy.format {
158 FormatStrategy::Toml => {
159 let toml_value: toml::Value = toml::from_str(&content)
160 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
161 let json_value = toml_to_json(toml_value)?;
162 serde_json::to_string(&json_value)
163 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
164 }
165 FormatStrategy::Json => content,
166 }
167 }
168 } else {
169 match strategy.load_behavior {
171 LoadBehavior::CreateIfMissing => "{}".to_string(),
172 LoadBehavior::ErrorIfMissing => {
173 return Err(MigrationError::IoError {
174 path: path.display().to_string(),
175 error: "File not found".to_string(),
176 });
177 }
178 }
179 };
180
181 let config = ConfigMigrator::from(&json_string, migrator)?;
183
184 Ok(Self {
185 path,
186 config,
187 strategy,
188 })
189 }
190
191 pub fn save(&self) -> Result<(), MigrationError> {
196 if let Some(parent) = self.path.parent() {
198 if !parent.exists() {
199 fs::create_dir_all(parent).map_err(|e| MigrationError::IoError {
200 path: parent.display().to_string(),
201 error: e.to_string(),
202 })?;
203 }
204 }
205
206 let json_value = self.config.as_value();
208
209 let content = match self.strategy.format {
211 FormatStrategy::Toml => {
212 let toml_value = json_to_toml(json_value)?;
213 toml::to_string_pretty(&toml_value)
214 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
215 }
216 FormatStrategy::Json => serde_json::to_string_pretty(&json_value)
217 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
218 };
219
220 let tmp_path = self.get_temp_path()?;
222 let mut tmp_file = File::create(&tmp_path).map_err(|e| MigrationError::IoError {
223 path: tmp_path.display().to_string(),
224 error: e.to_string(),
225 })?;
226
227 tmp_file
228 .write_all(content.as_bytes())
229 .map_err(|e| MigrationError::IoError {
230 path: tmp_path.display().to_string(),
231 error: e.to_string(),
232 })?;
233
234 tmp_file.sync_all().map_err(|e| MigrationError::IoError {
236 path: tmp_path.display().to_string(),
237 error: e.to_string(),
238 })?;
239
240 drop(tmp_file);
241
242 self.atomic_rename(&tmp_path)?;
244
245 if self.strategy.atomic_write.cleanup_tmp_files {
247 let _ = self.cleanup_temp_files();
248 }
249
250 Ok(())
251 }
252
253 pub fn config(&self) -> &ConfigMigrator {
255 &self.config
256 }
257
258 pub fn config_mut(&mut self) -> &mut ConfigMigrator {
260 &mut self.config
261 }
262
263 pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
267 where
268 T: Queryable + for<'de> serde::Deserialize<'de>,
269 {
270 self.config.query(key)
271 }
272
273 pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
277 where
278 T: Queryable + serde::Serialize,
279 {
280 self.config.update(key, value)
281 }
282
283 pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
285 where
286 T: Queryable + serde::Serialize,
287 {
288 self.update(key, value)?;
289 self.save()
290 }
291
292 fn get_temp_path(&self) -> Result<PathBuf, MigrationError> {
294 let parent = self.path.parent().ok_or_else(|| {
295 MigrationError::PathResolution("Path has no parent directory".to_string())
296 })?;
297
298 let file_name = self
299 .path
300 .file_name()
301 .ok_or_else(|| MigrationError::PathResolution("Path has no file name".to_string()))?;
302
303 let tmp_name = format!(
304 ".{}.tmp.{}",
305 file_name.to_string_lossy(),
306 std::process::id()
307 );
308 Ok(parent.join(tmp_name))
309 }
310
311 fn atomic_rename(&self, tmp_path: &Path) -> Result<(), MigrationError> {
313 let mut last_error = None;
314
315 for attempt in 0..self.strategy.atomic_write.retry_count {
316 match fs::rename(tmp_path, &self.path) {
317 Ok(()) => return Ok(()),
318 Err(e) => {
319 last_error = Some(e);
320 if attempt + 1 < self.strategy.atomic_write.retry_count {
321 std::thread::sleep(std::time::Duration::from_millis(10));
323 }
324 }
325 }
326 }
327
328 Err(MigrationError::IoError {
329 path: self.path.display().to_string(),
330 error: format!(
331 "Failed to rename after {} attempts: {}",
332 self.strategy.atomic_write.retry_count,
333 last_error.unwrap()
334 ),
335 })
336 }
337
338 fn cleanup_temp_files(&self) -> std::io::Result<()> {
340 let parent = match self.path.parent() {
341 Some(p) => p,
342 None => return Ok(()),
343 };
344
345 let file_name = match self.path.file_name() {
346 Some(f) => f.to_string_lossy(),
347 None => return Ok(()),
348 };
349
350 let prefix = format!(".{}.tmp.", file_name);
351
352 if let Ok(entries) = fs::read_dir(parent) {
353 for entry in entries.flatten() {
354 if let Ok(name) = entry.file_name().into_string() {
355 if name.starts_with(&prefix) {
356 let _ = fs::remove_file(entry.path());
358 }
359 }
360 }
361 }
362
363 Ok(())
364 }
365}
366
367#[allow(dead_code)]
371struct FileLock {
372 file: File,
373 lock_path: PathBuf,
374}
375
376#[allow(dead_code)]
377impl FileLock {
378 fn acquire(path: &Path) -> Result<Self, MigrationError> {
380 let lock_path = path.with_extension("lock");
382
383 if let Some(parent) = lock_path.parent() {
385 if !parent.exists() {
386 fs::create_dir_all(parent).map_err(|e| MigrationError::LockError {
387 path: lock_path.display().to_string(),
388 error: e.to_string(),
389 })?;
390 }
391 }
392
393 let file = OpenOptions::new()
395 .write(true)
396 .create(true)
397 .truncate(false)
398 .open(&lock_path)
399 .map_err(|e| MigrationError::LockError {
400 path: lock_path.display().to_string(),
401 error: e.to_string(),
402 })?;
403
404 #[cfg(unix)]
406 {
407 use fs2::FileExt;
408 file.lock_exclusive()
409 .map_err(|e| MigrationError::LockError {
410 path: lock_path.display().to_string(),
411 error: format!("Failed to acquire exclusive lock: {}", e),
412 })?;
413 }
414
415 #[cfg(not(unix))]
416 {
417 }
421
422 Ok(FileLock { file, lock_path })
423 }
424}
425
426impl Drop for FileLock {
427 fn drop(&mut self) {
428 let _ = fs::remove_file(&self.lock_path);
431 }
432}
433
434fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
436 let json_str = serde_json::to_string(&toml_value)
438 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
439 let json_value: JsonValue = serde_json::from_str(&json_str)
440 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
441 Ok(json_value)
442}
443
444fn json_to_toml(json_value: &JsonValue) -> Result<toml::Value, MigrationError> {
446 let json_str = serde_json::to_string(json_value)
448 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
449 let toml_value: toml::Value = serde_json::from_str(&json_str)
450 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
451 Ok(toml_value)
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::{IntoDomain, MigratesTo, Versioned};
458 use serde::{Deserialize, Serialize};
459 use tempfile::TempDir;
460
461 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462 struct TestEntity {
463 name: String,
464 count: u32,
465 }
466
467 impl Queryable for TestEntity {
468 const ENTITY_NAME: &'static str = "test";
469 }
470
471 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
472 struct TestV1 {
473 name: String,
474 }
475
476 impl Versioned for TestV1 {
477 const VERSION: &'static str = "1.0.0";
478 }
479
480 impl MigratesTo<TestV2> for TestV1 {
481 fn migrate(self) -> TestV2 {
482 TestV2 {
483 name: self.name,
484 count: 0,
485 }
486 }
487 }
488
489 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
490 struct TestV2 {
491 name: String,
492 count: u32,
493 }
494
495 impl Versioned for TestV2 {
496 const VERSION: &'static str = "2.0.0";
497 }
498
499 impl IntoDomain<TestEntity> for TestV2 {
500 fn into_domain(self) -> TestEntity {
501 TestEntity {
502 name: self.name,
503 count: self.count,
504 }
505 }
506 }
507
508 fn setup_migrator() -> Migrator {
509 let path = Migrator::define("test")
510 .from::<TestV1>()
511 .step::<TestV2>()
512 .into::<TestEntity>();
513
514 let mut migrator = Migrator::new();
515 migrator.register(path).unwrap();
516 migrator
517 }
518
519 #[test]
520 fn test_file_storage_strategy_builder() {
521 let strategy = FileStorageStrategy::new()
522 .with_format(FormatStrategy::Json)
523 .with_retry_count(5)
524 .with_cleanup(false)
525 .with_load_behavior(LoadBehavior::ErrorIfMissing);
526
527 assert_eq!(strategy.format, FormatStrategy::Json);
528 assert_eq!(strategy.atomic_write.retry_count, 5);
529 assert!(!strategy.atomic_write.cleanup_tmp_files);
530 assert_eq!(strategy.load_behavior, LoadBehavior::ErrorIfMissing);
531 }
532
533 #[test]
534 fn test_save_and_load_toml() {
535 let temp_dir = TempDir::new().unwrap();
536 let file_path = temp_dir.path().join("test.toml");
537 let migrator = setup_migrator();
538 let strategy = FileStorageStrategy::default(); let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
541
542 let entities = vec![TestEntity {
544 name: "test".to_string(),
545 count: 42,
546 }];
547 storage.update_and_save("test", entities).unwrap();
548
549 let migrator2 = setup_migrator();
551 let storage2 =
552 FileStorage::new(file_path, migrator2, FileStorageStrategy::default()).unwrap();
553
554 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
556 assert_eq!(loaded.len(), 1);
557 assert_eq!(loaded[0].name, "test");
558 assert_eq!(loaded[0].count, 42);
559 }
560
561 #[test]
562 fn test_save_and_load_json() {
563 let temp_dir = TempDir::new().unwrap();
564 let file_path = temp_dir.path().join("test.json");
565 let migrator = setup_migrator();
566 let strategy = FileStorageStrategy::new().with_format(FormatStrategy::Json);
567
568 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
569
570 let entities = vec![TestEntity {
572 name: "json_test".to_string(),
573 count: 100,
574 }];
575 storage.update_and_save("test", entities).unwrap();
576
577 let migrator2 = setup_migrator();
579 let strategy2 = FileStorageStrategy::new().with_format(FormatStrategy::Json);
580 let storage2 = FileStorage::new(file_path, migrator2, strategy2).unwrap();
581
582 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
584 assert_eq!(loaded.len(), 1);
585 assert_eq!(loaded[0].name, "json_test");
586 assert_eq!(loaded[0].count, 100);
587 }
588
589 #[test]
590 fn test_load_behavior_create_if_missing() {
591 let temp_dir = TempDir::new().unwrap();
592 let file_path = temp_dir.path().join("nonexistent.toml");
593 let migrator = setup_migrator();
594 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
595
596 let result = FileStorage::new(file_path, migrator, strategy);
597
598 assert!(result.is_ok()); }
600
601 #[test]
602 fn test_load_behavior_error_if_missing() {
603 let temp_dir = TempDir::new().unwrap();
604 let file_path = temp_dir.path().join("nonexistent.toml");
605 let migrator = setup_migrator();
606 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
607
608 let result = FileStorage::new(file_path, migrator, strategy);
609
610 assert!(result.is_err()); assert!(matches!(result, Err(MigrationError::IoError { .. })));
612 }
613
614 #[test]
615 fn test_atomic_write_no_tmp_file_left() {
616 let temp_dir = TempDir::new().unwrap();
617 let file_path = temp_dir.path().join("atomic.toml");
618 let migrator = setup_migrator();
619 let strategy = FileStorageStrategy::default();
620
621 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
622
623 let entities = vec![TestEntity {
624 name: "atomic".to_string(),
625 count: 1,
626 }];
627 storage.update_and_save("test", entities).unwrap();
628
629 let entries: Vec<_> = fs::read_dir(temp_dir.path())
631 .unwrap()
632 .filter_map(|e| e.ok())
633 .collect();
634
635 let tmp_files: Vec<_> = entries
636 .iter()
637 .filter(|e| {
638 e.file_name()
639 .to_string_lossy()
640 .starts_with(".atomic.toml.tmp")
641 })
642 .collect();
643
644 assert_eq!(tmp_files.len(), 0, "Temporary files should be cleaned up");
645 }
646}