1use crate::{errors::IoOperationKind, 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 SaveIfMissing,
45 ErrorIfMissing,
47}
48
49#[derive(Debug, Clone)]
51pub struct FileStorageStrategy {
52 pub format: FormatStrategy,
54 pub atomic_write: AtomicWriteConfig,
56 pub load_behavior: LoadBehavior,
58 pub default_value: Option<JsonValue>,
60}
61
62impl Default for FileStorageStrategy {
63 fn default() -> Self {
64 Self {
65 format: FormatStrategy::Toml,
66 atomic_write: AtomicWriteConfig::default(),
67 load_behavior: LoadBehavior::CreateIfMissing,
68 default_value: None,
69 }
70 }
71}
72
73impl FileStorageStrategy {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn with_format(mut self, format: FormatStrategy) -> Self {
81 self.format = format;
82 self
83 }
84
85 pub fn with_retry_count(mut self, count: usize) -> Self {
87 self.atomic_write.retry_count = count;
88 self
89 }
90
91 pub fn with_cleanup(mut self, cleanup: bool) -> Self {
93 self.atomic_write.cleanup_tmp_files = cleanup;
94 self
95 }
96
97 pub fn with_load_behavior(mut self, behavior: LoadBehavior) -> Self {
99 self.load_behavior = behavior;
100 self
101 }
102
103 pub fn with_default_value(mut self, value: JsonValue) -> Self {
120 self.default_value = Some(value);
121 self
122 }
123}
124
125pub struct FileStorage {
133 path: PathBuf,
134 config: ConfigMigrator,
135 strategy: FileStorageStrategy,
136}
137
138impl FileStorage {
139 pub fn new(
167 path: PathBuf,
168 migrator: Migrator,
169 strategy: FileStorageStrategy,
170 ) -> Result<Self, MigrationError> {
171 let file_was_missing = !path.exists();
173
174 let json_string = if path.exists() {
176 let content = fs::read_to_string(&path).map_err(|e| MigrationError::IoError {
177 operation: IoOperationKind::Read,
178 path: path.display().to_string(),
179 context: None,
180 error: e.to_string(),
181 })?;
182
183 if content.trim().is_empty() {
184 "{}".to_string()
186 } else {
187 match strategy.format {
189 FormatStrategy::Toml => {
190 let toml_value: toml::Value = toml::from_str(&content)
191 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
192 let json_value = toml_to_json(toml_value)?;
193 serde_json::to_string(&json_value)
194 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
195 }
196 FormatStrategy::Json => content,
197 }
198 }
199 } else {
200 match strategy.load_behavior {
202 LoadBehavior::CreateIfMissing | LoadBehavior::SaveIfMissing => {
203 if let Some(ref default_value) = strategy.default_value {
205 serde_json::to_string(default_value)
206 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
207 } else {
208 "{}".to_string()
209 }
210 }
211 LoadBehavior::ErrorIfMissing => {
212 return Err(MigrationError::IoError {
213 operation: IoOperationKind::Read,
214 path: path.display().to_string(),
215 context: None,
216 error: "File not found".to_string(),
217 });
218 }
219 }
220 };
221
222 let config = ConfigMigrator::from(&json_string, migrator)?;
224
225 let storage = Self {
226 path,
227 config,
228 strategy,
229 };
230
231 if file_was_missing && storage.strategy.load_behavior == LoadBehavior::SaveIfMissing {
233 storage.save()?;
234 }
235
236 Ok(storage)
237 }
238
239 pub fn save(&self) -> Result<(), MigrationError> {
244 if let Some(parent) = self.path.parent() {
246 if !parent.exists() {
247 fs::create_dir_all(parent).map_err(|e| MigrationError::IoError {
248 operation: IoOperationKind::CreateDir,
249 path: parent.display().to_string(),
250 context: Some("parent directory".to_string()),
251 error: e.to_string(),
252 })?;
253 }
254 }
255
256 let json_value = self.config.as_value();
258
259 let content = match self.strategy.format {
261 FormatStrategy::Toml => {
262 let toml_value = json_to_toml(json_value)?;
263 toml::to_string_pretty(&toml_value)
264 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
265 }
266 FormatStrategy::Json => serde_json::to_string_pretty(&json_value)
267 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
268 };
269
270 let tmp_path = self.get_temp_path()?;
272 let mut tmp_file = File::create(&tmp_path).map_err(|e| MigrationError::IoError {
273 operation: IoOperationKind::Create,
274 path: tmp_path.display().to_string(),
275 context: Some("temporary file".to_string()),
276 error: e.to_string(),
277 })?;
278
279 tmp_file
280 .write_all(content.as_bytes())
281 .map_err(|e| MigrationError::IoError {
282 operation: IoOperationKind::Write,
283 path: tmp_path.display().to_string(),
284 context: Some("temporary file".to_string()),
285 error: e.to_string(),
286 })?;
287
288 tmp_file.sync_all().map_err(|e| MigrationError::IoError {
290 operation: IoOperationKind::Sync,
291 path: tmp_path.display().to_string(),
292 context: Some("temporary file".to_string()),
293 error: e.to_string(),
294 })?;
295
296 drop(tmp_file);
297
298 self.atomic_rename(&tmp_path)?;
300
301 if self.strategy.atomic_write.cleanup_tmp_files {
303 let _ = self.cleanup_temp_files();
304 }
305
306 Ok(())
307 }
308
309 pub fn config(&self) -> &ConfigMigrator {
311 &self.config
312 }
313
314 pub fn config_mut(&mut self) -> &mut ConfigMigrator {
316 &mut self.config
317 }
318
319 pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
323 where
324 T: Queryable + for<'de> serde::Deserialize<'de>,
325 {
326 self.config.query(key)
327 }
328
329 pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
333 where
334 T: Queryable + serde::Serialize,
335 {
336 self.config.update(key, value)
337 }
338
339 pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
341 where
342 T: Queryable + serde::Serialize,
343 {
344 self.update(key, value)?;
345 self.save()
346 }
347
348 pub fn path(&self) -> &Path {
354 &self.path
355 }
356
357 fn get_temp_path(&self) -> Result<PathBuf, MigrationError> {
359 let parent = self.path.parent().ok_or_else(|| {
360 MigrationError::PathResolution("Path has no parent directory".to_string())
361 })?;
362
363 let file_name = self
364 .path
365 .file_name()
366 .ok_or_else(|| MigrationError::PathResolution("Path has no file name".to_string()))?;
367
368 let tmp_name = format!(
369 ".{}.tmp.{}",
370 file_name.to_string_lossy(),
371 std::process::id()
372 );
373 Ok(parent.join(tmp_name))
374 }
375
376 fn atomic_rename(&self, tmp_path: &Path) -> Result<(), MigrationError> {
378 let mut last_error = None;
379
380 for attempt in 0..self.strategy.atomic_write.retry_count {
381 match fs::rename(tmp_path, &self.path) {
382 Ok(()) => return Ok(()),
383 Err(e) => {
384 last_error = Some(e);
385 if attempt + 1 < self.strategy.atomic_write.retry_count {
386 std::thread::sleep(std::time::Duration::from_millis(10));
388 }
389 }
390 }
391 }
392
393 Err(MigrationError::IoError {
394 operation: IoOperationKind::Rename,
395 path: self.path.display().to_string(),
396 context: Some(format!(
397 "after {} retries",
398 self.strategy.atomic_write.retry_count
399 )),
400 error: last_error.unwrap().to_string(),
401 })
402 }
403
404 fn cleanup_temp_files(&self) -> std::io::Result<()> {
406 let parent = match self.path.parent() {
407 Some(p) => p,
408 None => return Ok(()),
409 };
410
411 let file_name = match self.path.file_name() {
412 Some(f) => f.to_string_lossy(),
413 None => return Ok(()),
414 };
415
416 let prefix = format!(".{}.tmp.", file_name);
417
418 if let Ok(entries) = fs::read_dir(parent) {
419 for entry in entries.flatten() {
420 if let Ok(name) = entry.file_name().into_string() {
421 if name.starts_with(&prefix) {
422 let _ = fs::remove_file(entry.path());
424 }
425 }
426 }
427 }
428
429 Ok(())
430 }
431}
432
433#[allow(dead_code)]
437struct FileLock {
438 file: File,
439 lock_path: PathBuf,
440}
441
442#[allow(dead_code)]
443impl FileLock {
444 fn acquire(path: &Path) -> Result<Self, MigrationError> {
446 let lock_path = path.with_extension("lock");
448
449 if let Some(parent) = lock_path.parent() {
451 if !parent.exists() {
452 fs::create_dir_all(parent).map_err(|e| MigrationError::LockError {
453 path: lock_path.display().to_string(),
454 error: e.to_string(),
455 })?;
456 }
457 }
458
459 let file = OpenOptions::new()
461 .write(true)
462 .create(true)
463 .truncate(false)
464 .open(&lock_path)
465 .map_err(|e| MigrationError::LockError {
466 path: lock_path.display().to_string(),
467 error: e.to_string(),
468 })?;
469
470 #[cfg(unix)]
472 {
473 use fs2::FileExt;
474 file.lock_exclusive()
475 .map_err(|e| MigrationError::LockError {
476 path: lock_path.display().to_string(),
477 error: format!("Failed to acquire exclusive lock: {}", e),
478 })?;
479 }
480
481 #[cfg(not(unix))]
482 {
483 }
487
488 Ok(FileLock { file, lock_path })
489 }
490}
491
492impl Drop for FileLock {
493 fn drop(&mut self) {
494 let _ = fs::remove_file(&self.lock_path);
497 }
498}
499
500fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
502 let json_str = serde_json::to_string(&toml_value)
504 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
505 let json_value: JsonValue = serde_json::from_str(&json_str)
506 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
507 Ok(json_value)
508}
509
510fn json_to_toml(json_value: &JsonValue) -> Result<toml::Value, MigrationError> {
512 let json_str = serde_json::to_string(json_value)
514 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
515 let toml_value: toml::Value = serde_json::from_str(&json_str)
516 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
517 Ok(toml_value)
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::{IntoDomain, MigratesTo, Versioned};
524 use serde::{Deserialize, Serialize};
525 use tempfile::TempDir;
526
527 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528 struct TestEntity {
529 name: String,
530 count: u32,
531 }
532
533 impl Queryable for TestEntity {
534 const ENTITY_NAME: &'static str = "test";
535 }
536
537 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538 struct TestV1 {
539 name: String,
540 }
541
542 impl Versioned for TestV1 {
543 const VERSION: &'static str = "1.0.0";
544 }
545
546 impl MigratesTo<TestV2> for TestV1 {
547 fn migrate(self) -> TestV2 {
548 TestV2 {
549 name: self.name,
550 count: 0,
551 }
552 }
553 }
554
555 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
556 struct TestV2 {
557 name: String,
558 count: u32,
559 }
560
561 impl Versioned for TestV2 {
562 const VERSION: &'static str = "2.0.0";
563 }
564
565 impl IntoDomain<TestEntity> for TestV2 {
566 fn into_domain(self) -> TestEntity {
567 TestEntity {
568 name: self.name,
569 count: self.count,
570 }
571 }
572 }
573
574 fn setup_migrator() -> Migrator {
575 let path = Migrator::define("test")
576 .from::<TestV1>()
577 .step::<TestV2>()
578 .into::<TestEntity>();
579
580 let mut migrator = Migrator::new();
581 migrator.register(path).unwrap();
582 migrator
583 }
584
585 #[test]
586 fn test_file_storage_strategy_builder() {
587 let strategy = FileStorageStrategy::new()
588 .with_format(FormatStrategy::Json)
589 .with_retry_count(5)
590 .with_cleanup(false)
591 .with_load_behavior(LoadBehavior::ErrorIfMissing);
592
593 assert_eq!(strategy.format, FormatStrategy::Json);
594 assert_eq!(strategy.atomic_write.retry_count, 5);
595 assert!(!strategy.atomic_write.cleanup_tmp_files);
596 assert_eq!(strategy.load_behavior, LoadBehavior::ErrorIfMissing);
597 }
598
599 #[test]
600 fn test_save_and_load_toml() {
601 let temp_dir = TempDir::new().unwrap();
602 let file_path = temp_dir.path().join("test.toml");
603 let migrator = setup_migrator();
604 let strategy = FileStorageStrategy::default(); let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
607
608 let entities = vec![TestEntity {
610 name: "test".to_string(),
611 count: 42,
612 }];
613 storage.update_and_save("test", entities).unwrap();
614
615 let migrator2 = setup_migrator();
617 let storage2 =
618 FileStorage::new(file_path, migrator2, FileStorageStrategy::default()).unwrap();
619
620 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
622 assert_eq!(loaded.len(), 1);
623 assert_eq!(loaded[0].name, "test");
624 assert_eq!(loaded[0].count, 42);
625 }
626
627 #[test]
628 fn test_save_and_load_json() {
629 let temp_dir = TempDir::new().unwrap();
630 let file_path = temp_dir.path().join("test.json");
631 let migrator = setup_migrator();
632 let strategy = FileStorageStrategy::new().with_format(FormatStrategy::Json);
633
634 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
635
636 let entities = vec![TestEntity {
638 name: "json_test".to_string(),
639 count: 100,
640 }];
641 storage.update_and_save("test", entities).unwrap();
642
643 let migrator2 = setup_migrator();
645 let strategy2 = FileStorageStrategy::new().with_format(FormatStrategy::Json);
646 let storage2 = FileStorage::new(file_path, migrator2, strategy2).unwrap();
647
648 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
650 assert_eq!(loaded.len(), 1);
651 assert_eq!(loaded[0].name, "json_test");
652 assert_eq!(loaded[0].count, 100);
653 }
654
655 #[test]
656 fn test_load_behavior_create_if_missing() {
657 let temp_dir = TempDir::new().unwrap();
658 let file_path = temp_dir.path().join("nonexistent.toml");
659 let migrator = setup_migrator();
660 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
661
662 let result = FileStorage::new(file_path, migrator, strategy);
663
664 assert!(result.is_ok()); }
666
667 #[test]
668 fn test_load_behavior_error_if_missing() {
669 let temp_dir = TempDir::new().unwrap();
670 let file_path = temp_dir.path().join("nonexistent.toml");
671 let migrator = setup_migrator();
672 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
673
674 let result = FileStorage::new(file_path, migrator, strategy);
675
676 assert!(result.is_err()); assert!(matches!(result, Err(MigrationError::IoError { .. })));
678 }
679
680 #[test]
681 fn test_load_behavior_save_if_missing() {
682 let temp_dir = TempDir::new().unwrap();
683 let file_path = temp_dir.path().join("save_if_missing.toml");
684 let migrator = setup_migrator();
685 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::SaveIfMissing);
686
687 assert!(!file_path.exists());
689
690 let result = FileStorage::new(file_path.clone(), migrator, strategy.clone());
691
692 assert!(result.is_ok());
694 assert!(file_path.exists());
695
696 let _storage = result.unwrap();
698 let reloaded = FileStorage::new(file_path.clone(), setup_migrator(), strategy);
699 assert!(reloaded.is_ok());
700 }
701
702 #[test]
703 fn test_save_if_missing_with_default_value() {
704 let temp_dir = TempDir::new().unwrap();
705 let file_path = temp_dir.path().join("default_value.toml");
706 let migrator = setup_migrator();
707
708 let default_value = serde_json::json!({
710 "test": [
711 {
712 "version": "2.0.0",
713 "name": "default_user",
714 "count": 99
715 }
716 ]
717 });
718
719 let strategy = FileStorageStrategy::new()
720 .with_load_behavior(LoadBehavior::SaveIfMissing)
721 .with_default_value(default_value);
722
723 assert!(!file_path.exists());
725
726 let storage = FileStorage::new(file_path.clone(), migrator, strategy.clone()).unwrap();
727
728 assert!(file_path.exists());
730
731 let loaded: Vec<TestEntity> = storage.query("test").unwrap();
733 assert_eq!(loaded.len(), 1);
734 assert_eq!(loaded[0].name, "default_user");
735 assert_eq!(loaded[0].count, 99);
736
737 let reloaded = FileStorage::new(file_path.clone(), setup_migrator(), strategy).unwrap();
739 let reloaded_entities: Vec<TestEntity> = reloaded.query("test").unwrap();
740 assert_eq!(reloaded_entities.len(), 1);
741 assert_eq!(reloaded_entities[0].name, "default_user");
742 assert_eq!(reloaded_entities[0].count, 99);
743 }
744
745 #[test]
746 fn test_create_if_missing_with_default_value() {
747 let temp_dir = TempDir::new().unwrap();
748 let file_path = temp_dir.path().join("create_default.toml");
749 let migrator = setup_migrator();
750
751 let default_value = serde_json::json!({
752 "test": [{
753 "version": "2.0.0",
754 "name": "created",
755 "count": 42
756 }]
757 });
758
759 let strategy = FileStorageStrategy::new()
760 .with_load_behavior(LoadBehavior::CreateIfMissing)
761 .with_default_value(default_value);
762
763 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
765
766 let loaded: Vec<TestEntity> = storage.query("test").unwrap();
768 assert_eq!(loaded.len(), 1);
769 assert_eq!(loaded[0].name, "created");
770 assert_eq!(loaded[0].count, 42);
771 }
772
773 #[test]
774 fn test_atomic_write_no_tmp_file_left() {
775 let temp_dir = TempDir::new().unwrap();
776 let file_path = temp_dir.path().join("atomic.toml");
777 let migrator = setup_migrator();
778 let strategy = FileStorageStrategy::default();
779
780 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
781
782 let entities = vec![TestEntity {
783 name: "atomic".to_string(),
784 count: 1,
785 }];
786 storage.update_and_save("test", entities).unwrap();
787
788 let entries: Vec<_> = fs::read_dir(temp_dir.path())
790 .unwrap()
791 .filter_map(|e| e.ok())
792 .collect();
793
794 let tmp_files: Vec<_> = entries
795 .iter()
796 .filter(|e| {
797 e.file_name()
798 .to_string_lossy()
799 .starts_with(".atomic.toml.tmp")
800 })
801 .collect();
802
803 assert_eq!(tmp_files.len(), 0, "Temporary files should be cleaned up");
804 }
805
806 #[test]
807 fn test_file_storage_path() {
808 let temp_dir = TempDir::new().unwrap();
809 let file_path = temp_dir.path().join("test_config.toml");
810 let migrator = setup_migrator();
811 let strategy = FileStorageStrategy::default();
812
813 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
814
815 let returned_path = storage.path();
817 assert_eq!(returned_path, file_path.as_path());
818 }
819}