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}
59
60impl Default for FileStorageStrategy {
61 fn default() -> Self {
62 Self {
63 format: FormatStrategy::Toml,
64 atomic_write: AtomicWriteConfig::default(),
65 load_behavior: LoadBehavior::CreateIfMissing,
66 }
67 }
68}
69
70impl FileStorageStrategy {
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn with_format(mut self, format: FormatStrategy) -> Self {
78 self.format = format;
79 self
80 }
81
82 pub fn with_retry_count(mut self, count: usize) -> Self {
84 self.atomic_write.retry_count = count;
85 self
86 }
87
88 pub fn with_cleanup(mut self, cleanup: bool) -> Self {
90 self.atomic_write.cleanup_tmp_files = cleanup;
91 self
92 }
93
94 pub fn with_load_behavior(mut self, behavior: LoadBehavior) -> Self {
96 self.load_behavior = behavior;
97 self
98 }
99}
100
101pub struct FileStorage {
109 path: PathBuf,
110 config: ConfigMigrator,
111 strategy: FileStorageStrategy,
112}
113
114impl FileStorage {
115 pub fn new(
143 path: PathBuf,
144 migrator: Migrator,
145 strategy: FileStorageStrategy,
146 ) -> Result<Self, MigrationError> {
147 let file_was_missing = !path.exists();
149
150 let json_string = if path.exists() {
152 let content = fs::read_to_string(&path).map_err(|e| MigrationError::IoError {
153 operation: IoOperationKind::Read,
154 path: path.display().to_string(),
155 context: None,
156 error: e.to_string(),
157 })?;
158
159 if content.trim().is_empty() {
160 "{}".to_string()
162 } else {
163 match strategy.format {
165 FormatStrategy::Toml => {
166 let toml_value: toml::Value = toml::from_str(&content)
167 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
168 let json_value = toml_to_json(toml_value)?;
169 serde_json::to_string(&json_value)
170 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
171 }
172 FormatStrategy::Json => content,
173 }
174 }
175 } else {
176 match strategy.load_behavior {
178 LoadBehavior::CreateIfMissing | LoadBehavior::SaveIfMissing => "{}".to_string(),
179 LoadBehavior::ErrorIfMissing => {
180 return Err(MigrationError::IoError {
181 operation: IoOperationKind::Read,
182 path: path.display().to_string(),
183 context: None,
184 error: "File not found".to_string(),
185 });
186 }
187 }
188 };
189
190 let config = ConfigMigrator::from(&json_string, migrator)?;
192
193 let storage = Self {
194 path,
195 config,
196 strategy,
197 };
198
199 if file_was_missing && storage.strategy.load_behavior == LoadBehavior::SaveIfMissing {
201 storage.save()?;
202 }
203
204 Ok(storage)
205 }
206
207 pub fn save(&self) -> Result<(), MigrationError> {
212 if let Some(parent) = self.path.parent() {
214 if !parent.exists() {
215 fs::create_dir_all(parent).map_err(|e| MigrationError::IoError {
216 operation: IoOperationKind::CreateDir,
217 path: parent.display().to_string(),
218 context: Some("parent directory".to_string()),
219 error: e.to_string(),
220 })?;
221 }
222 }
223
224 let json_value = self.config.as_value();
226
227 let content = match self.strategy.format {
229 FormatStrategy::Toml => {
230 let toml_value = json_to_toml(json_value)?;
231 toml::to_string_pretty(&toml_value)
232 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
233 }
234 FormatStrategy::Json => serde_json::to_string_pretty(&json_value)
235 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
236 };
237
238 let tmp_path = self.get_temp_path()?;
240 let mut tmp_file = File::create(&tmp_path).map_err(|e| MigrationError::IoError {
241 operation: IoOperationKind::Create,
242 path: tmp_path.display().to_string(),
243 context: Some("temporary file".to_string()),
244 error: e.to_string(),
245 })?;
246
247 tmp_file
248 .write_all(content.as_bytes())
249 .map_err(|e| MigrationError::IoError {
250 operation: IoOperationKind::Write,
251 path: tmp_path.display().to_string(),
252 context: Some("temporary file".to_string()),
253 error: e.to_string(),
254 })?;
255
256 tmp_file.sync_all().map_err(|e| MigrationError::IoError {
258 operation: IoOperationKind::Sync,
259 path: tmp_path.display().to_string(),
260 context: Some("temporary file".to_string()),
261 error: e.to_string(),
262 })?;
263
264 drop(tmp_file);
265
266 self.atomic_rename(&tmp_path)?;
268
269 if self.strategy.atomic_write.cleanup_tmp_files {
271 let _ = self.cleanup_temp_files();
272 }
273
274 Ok(())
275 }
276
277 pub fn config(&self) -> &ConfigMigrator {
279 &self.config
280 }
281
282 pub fn config_mut(&mut self) -> &mut ConfigMigrator {
284 &mut self.config
285 }
286
287 pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
291 where
292 T: Queryable + for<'de> serde::Deserialize<'de>,
293 {
294 self.config.query(key)
295 }
296
297 pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
301 where
302 T: Queryable + serde::Serialize,
303 {
304 self.config.update(key, value)
305 }
306
307 pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
309 where
310 T: Queryable + serde::Serialize,
311 {
312 self.update(key, value)?;
313 self.save()
314 }
315
316 pub fn path(&self) -> &Path {
322 &self.path
323 }
324
325 fn get_temp_path(&self) -> Result<PathBuf, MigrationError> {
327 let parent = self.path.parent().ok_or_else(|| {
328 MigrationError::PathResolution("Path has no parent directory".to_string())
329 })?;
330
331 let file_name = self
332 .path
333 .file_name()
334 .ok_or_else(|| MigrationError::PathResolution("Path has no file name".to_string()))?;
335
336 let tmp_name = format!(
337 ".{}.tmp.{}",
338 file_name.to_string_lossy(),
339 std::process::id()
340 );
341 Ok(parent.join(tmp_name))
342 }
343
344 fn atomic_rename(&self, tmp_path: &Path) -> Result<(), MigrationError> {
346 let mut last_error = None;
347
348 for attempt in 0..self.strategy.atomic_write.retry_count {
349 match fs::rename(tmp_path, &self.path) {
350 Ok(()) => return Ok(()),
351 Err(e) => {
352 last_error = Some(e);
353 if attempt + 1 < self.strategy.atomic_write.retry_count {
354 std::thread::sleep(std::time::Duration::from_millis(10));
356 }
357 }
358 }
359 }
360
361 Err(MigrationError::IoError {
362 operation: IoOperationKind::Rename,
363 path: self.path.display().to_string(),
364 context: Some(format!(
365 "after {} retries",
366 self.strategy.atomic_write.retry_count
367 )),
368 error: last_error.unwrap().to_string(),
369 })
370 }
371
372 fn cleanup_temp_files(&self) -> std::io::Result<()> {
374 let parent = match self.path.parent() {
375 Some(p) => p,
376 None => return Ok(()),
377 };
378
379 let file_name = match self.path.file_name() {
380 Some(f) => f.to_string_lossy(),
381 None => return Ok(()),
382 };
383
384 let prefix = format!(".{}.tmp.", file_name);
385
386 if let Ok(entries) = fs::read_dir(parent) {
387 for entry in entries.flatten() {
388 if let Ok(name) = entry.file_name().into_string() {
389 if name.starts_with(&prefix) {
390 let _ = fs::remove_file(entry.path());
392 }
393 }
394 }
395 }
396
397 Ok(())
398 }
399}
400
401#[allow(dead_code)]
405struct FileLock {
406 file: File,
407 lock_path: PathBuf,
408}
409
410#[allow(dead_code)]
411impl FileLock {
412 fn acquire(path: &Path) -> Result<Self, MigrationError> {
414 let lock_path = path.with_extension("lock");
416
417 if let Some(parent) = lock_path.parent() {
419 if !parent.exists() {
420 fs::create_dir_all(parent).map_err(|e| MigrationError::LockError {
421 path: lock_path.display().to_string(),
422 error: e.to_string(),
423 })?;
424 }
425 }
426
427 let file = OpenOptions::new()
429 .write(true)
430 .create(true)
431 .truncate(false)
432 .open(&lock_path)
433 .map_err(|e| MigrationError::LockError {
434 path: lock_path.display().to_string(),
435 error: e.to_string(),
436 })?;
437
438 #[cfg(unix)]
440 {
441 use fs2::FileExt;
442 file.lock_exclusive()
443 .map_err(|e| MigrationError::LockError {
444 path: lock_path.display().to_string(),
445 error: format!("Failed to acquire exclusive lock: {}", e),
446 })?;
447 }
448
449 #[cfg(not(unix))]
450 {
451 }
455
456 Ok(FileLock { file, lock_path })
457 }
458}
459
460impl Drop for FileLock {
461 fn drop(&mut self) {
462 let _ = fs::remove_file(&self.lock_path);
465 }
466}
467
468fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
470 let json_str = serde_json::to_string(&toml_value)
472 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
473 let json_value: JsonValue = serde_json::from_str(&json_str)
474 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
475 Ok(json_value)
476}
477
478fn json_to_toml(json_value: &JsonValue) -> Result<toml::Value, MigrationError> {
480 let json_str = serde_json::to_string(json_value)
482 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
483 let toml_value: toml::Value = serde_json::from_str(&json_str)
484 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
485 Ok(toml_value)
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::{IntoDomain, MigratesTo, Versioned};
492 use serde::{Deserialize, Serialize};
493 use tempfile::TempDir;
494
495 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496 struct TestEntity {
497 name: String,
498 count: u32,
499 }
500
501 impl Queryable for TestEntity {
502 const ENTITY_NAME: &'static str = "test";
503 }
504
505 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506 struct TestV1 {
507 name: String,
508 }
509
510 impl Versioned for TestV1 {
511 const VERSION: &'static str = "1.0.0";
512 }
513
514 impl MigratesTo<TestV2> for TestV1 {
515 fn migrate(self) -> TestV2 {
516 TestV2 {
517 name: self.name,
518 count: 0,
519 }
520 }
521 }
522
523 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
524 struct TestV2 {
525 name: String,
526 count: u32,
527 }
528
529 impl Versioned for TestV2 {
530 const VERSION: &'static str = "2.0.0";
531 }
532
533 impl IntoDomain<TestEntity> for TestV2 {
534 fn into_domain(self) -> TestEntity {
535 TestEntity {
536 name: self.name,
537 count: self.count,
538 }
539 }
540 }
541
542 fn setup_migrator() -> Migrator {
543 let path = Migrator::define("test")
544 .from::<TestV1>()
545 .step::<TestV2>()
546 .into::<TestEntity>();
547
548 let mut migrator = Migrator::new();
549 migrator.register(path).unwrap();
550 migrator
551 }
552
553 #[test]
554 fn test_file_storage_strategy_builder() {
555 let strategy = FileStorageStrategy::new()
556 .with_format(FormatStrategy::Json)
557 .with_retry_count(5)
558 .with_cleanup(false)
559 .with_load_behavior(LoadBehavior::ErrorIfMissing);
560
561 assert_eq!(strategy.format, FormatStrategy::Json);
562 assert_eq!(strategy.atomic_write.retry_count, 5);
563 assert!(!strategy.atomic_write.cleanup_tmp_files);
564 assert_eq!(strategy.load_behavior, LoadBehavior::ErrorIfMissing);
565 }
566
567 #[test]
568 fn test_save_and_load_toml() {
569 let temp_dir = TempDir::new().unwrap();
570 let file_path = temp_dir.path().join("test.toml");
571 let migrator = setup_migrator();
572 let strategy = FileStorageStrategy::default(); let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
575
576 let entities = vec![TestEntity {
578 name: "test".to_string(),
579 count: 42,
580 }];
581 storage.update_and_save("test", entities).unwrap();
582
583 let migrator2 = setup_migrator();
585 let storage2 =
586 FileStorage::new(file_path, migrator2, FileStorageStrategy::default()).unwrap();
587
588 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
590 assert_eq!(loaded.len(), 1);
591 assert_eq!(loaded[0].name, "test");
592 assert_eq!(loaded[0].count, 42);
593 }
594
595 #[test]
596 fn test_save_and_load_json() {
597 let temp_dir = TempDir::new().unwrap();
598 let file_path = temp_dir.path().join("test.json");
599 let migrator = setup_migrator();
600 let strategy = FileStorageStrategy::new().with_format(FormatStrategy::Json);
601
602 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
603
604 let entities = vec![TestEntity {
606 name: "json_test".to_string(),
607 count: 100,
608 }];
609 storage.update_and_save("test", entities).unwrap();
610
611 let migrator2 = setup_migrator();
613 let strategy2 = FileStorageStrategy::new().with_format(FormatStrategy::Json);
614 let storage2 = FileStorage::new(file_path, migrator2, strategy2).unwrap();
615
616 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
618 assert_eq!(loaded.len(), 1);
619 assert_eq!(loaded[0].name, "json_test");
620 assert_eq!(loaded[0].count, 100);
621 }
622
623 #[test]
624 fn test_load_behavior_create_if_missing() {
625 let temp_dir = TempDir::new().unwrap();
626 let file_path = temp_dir.path().join("nonexistent.toml");
627 let migrator = setup_migrator();
628 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
629
630 let result = FileStorage::new(file_path, migrator, strategy);
631
632 assert!(result.is_ok()); }
634
635 #[test]
636 fn test_load_behavior_error_if_missing() {
637 let temp_dir = TempDir::new().unwrap();
638 let file_path = temp_dir.path().join("nonexistent.toml");
639 let migrator = setup_migrator();
640 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
641
642 let result = FileStorage::new(file_path, migrator, strategy);
643
644 assert!(result.is_err()); assert!(matches!(result, Err(MigrationError::IoError { .. })));
646 }
647
648 #[test]
649 fn test_load_behavior_save_if_missing() {
650 let temp_dir = TempDir::new().unwrap();
651 let file_path = temp_dir.path().join("save_if_missing.toml");
652 let migrator = setup_migrator();
653 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::SaveIfMissing);
654
655 assert!(!file_path.exists());
657
658 let result = FileStorage::new(file_path.clone(), migrator, strategy.clone());
659
660 assert!(result.is_ok());
662 assert!(file_path.exists());
663
664 let _storage = result.unwrap();
666 let reloaded = FileStorage::new(file_path.clone(), setup_migrator(), strategy);
667 assert!(reloaded.is_ok());
668 }
669
670 #[test]
671 fn test_atomic_write_no_tmp_file_left() {
672 let temp_dir = TempDir::new().unwrap();
673 let file_path = temp_dir.path().join("atomic.toml");
674 let migrator = setup_migrator();
675 let strategy = FileStorageStrategy::default();
676
677 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
678
679 let entities = vec![TestEntity {
680 name: "atomic".to_string(),
681 count: 1,
682 }];
683 storage.update_and_save("test", entities).unwrap();
684
685 let entries: Vec<_> = fs::read_dir(temp_dir.path())
687 .unwrap()
688 .filter_map(|e| e.ok())
689 .collect();
690
691 let tmp_files: Vec<_> = entries
692 .iter()
693 .filter(|e| {
694 e.file_name()
695 .to_string_lossy()
696 .starts_with(".atomic.toml.tmp")
697 })
698 .collect();
699
700 assert_eq!(tmp_files.len(), 0, "Temporary files should be cleaned up");
701 }
702
703 #[test]
704 fn test_file_storage_path() {
705 let temp_dir = TempDir::new().unwrap();
706 let file_path = temp_dir.path().join("test_config.toml");
707 let migrator = setup_migrator();
708 let strategy = FileStorageStrategy::default();
709
710 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
711
712 let returned_path = storage.path();
714 assert_eq!(returned_path, file_path.as_path());
715 }
716}