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 pub fn path(&self) -> &Path {
298 &self.path
299 }
300
301 fn get_temp_path(&self) -> Result<PathBuf, MigrationError> {
303 let parent = self.path.parent().ok_or_else(|| {
304 MigrationError::PathResolution("Path has no parent directory".to_string())
305 })?;
306
307 let file_name = self
308 .path
309 .file_name()
310 .ok_or_else(|| MigrationError::PathResolution("Path has no file name".to_string()))?;
311
312 let tmp_name = format!(
313 ".{}.tmp.{}",
314 file_name.to_string_lossy(),
315 std::process::id()
316 );
317 Ok(parent.join(tmp_name))
318 }
319
320 fn atomic_rename(&self, tmp_path: &Path) -> Result<(), MigrationError> {
322 let mut last_error = None;
323
324 for attempt in 0..self.strategy.atomic_write.retry_count {
325 match fs::rename(tmp_path, &self.path) {
326 Ok(()) => return Ok(()),
327 Err(e) => {
328 last_error = Some(e);
329 if attempt + 1 < self.strategy.atomic_write.retry_count {
330 std::thread::sleep(std::time::Duration::from_millis(10));
332 }
333 }
334 }
335 }
336
337 Err(MigrationError::IoError {
338 path: self.path.display().to_string(),
339 error: format!(
340 "Failed to rename after {} attempts: {}",
341 self.strategy.atomic_write.retry_count,
342 last_error.unwrap()
343 ),
344 })
345 }
346
347 fn cleanup_temp_files(&self) -> std::io::Result<()> {
349 let parent = match self.path.parent() {
350 Some(p) => p,
351 None => return Ok(()),
352 };
353
354 let file_name = match self.path.file_name() {
355 Some(f) => f.to_string_lossy(),
356 None => return Ok(()),
357 };
358
359 let prefix = format!(".{}.tmp.", file_name);
360
361 if let Ok(entries) = fs::read_dir(parent) {
362 for entry in entries.flatten() {
363 if let Ok(name) = entry.file_name().into_string() {
364 if name.starts_with(&prefix) {
365 let _ = fs::remove_file(entry.path());
367 }
368 }
369 }
370 }
371
372 Ok(())
373 }
374}
375
376#[allow(dead_code)]
380struct FileLock {
381 file: File,
382 lock_path: PathBuf,
383}
384
385#[allow(dead_code)]
386impl FileLock {
387 fn acquire(path: &Path) -> Result<Self, MigrationError> {
389 let lock_path = path.with_extension("lock");
391
392 if let Some(parent) = lock_path.parent() {
394 if !parent.exists() {
395 fs::create_dir_all(parent).map_err(|e| MigrationError::LockError {
396 path: lock_path.display().to_string(),
397 error: e.to_string(),
398 })?;
399 }
400 }
401
402 let file = OpenOptions::new()
404 .write(true)
405 .create(true)
406 .truncate(false)
407 .open(&lock_path)
408 .map_err(|e| MigrationError::LockError {
409 path: lock_path.display().to_string(),
410 error: e.to_string(),
411 })?;
412
413 #[cfg(unix)]
415 {
416 use fs2::FileExt;
417 file.lock_exclusive()
418 .map_err(|e| MigrationError::LockError {
419 path: lock_path.display().to_string(),
420 error: format!("Failed to acquire exclusive lock: {}", e),
421 })?;
422 }
423
424 #[cfg(not(unix))]
425 {
426 }
430
431 Ok(FileLock { file, lock_path })
432 }
433}
434
435impl Drop for FileLock {
436 fn drop(&mut self) {
437 let _ = fs::remove_file(&self.lock_path);
440 }
441}
442
443fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
445 let json_str = serde_json::to_string(&toml_value)
447 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
448 let json_value: JsonValue = serde_json::from_str(&json_str)
449 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
450 Ok(json_value)
451}
452
453fn json_to_toml(json_value: &JsonValue) -> Result<toml::Value, MigrationError> {
455 let json_str = serde_json::to_string(json_value)
457 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
458 let toml_value: toml::Value = serde_json::from_str(&json_str)
459 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
460 Ok(toml_value)
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::{IntoDomain, MigratesTo, Versioned};
467 use serde::{Deserialize, Serialize};
468 use tempfile::TempDir;
469
470 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
471 struct TestEntity {
472 name: String,
473 count: u32,
474 }
475
476 impl Queryable for TestEntity {
477 const ENTITY_NAME: &'static str = "test";
478 }
479
480 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481 struct TestV1 {
482 name: String,
483 }
484
485 impl Versioned for TestV1 {
486 const VERSION: &'static str = "1.0.0";
487 }
488
489 impl MigratesTo<TestV2> for TestV1 {
490 fn migrate(self) -> TestV2 {
491 TestV2 {
492 name: self.name,
493 count: 0,
494 }
495 }
496 }
497
498 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
499 struct TestV2 {
500 name: String,
501 count: u32,
502 }
503
504 impl Versioned for TestV2 {
505 const VERSION: &'static str = "2.0.0";
506 }
507
508 impl IntoDomain<TestEntity> for TestV2 {
509 fn into_domain(self) -> TestEntity {
510 TestEntity {
511 name: self.name,
512 count: self.count,
513 }
514 }
515 }
516
517 fn setup_migrator() -> Migrator {
518 let path = Migrator::define("test")
519 .from::<TestV1>()
520 .step::<TestV2>()
521 .into::<TestEntity>();
522
523 let mut migrator = Migrator::new();
524 migrator.register(path).unwrap();
525 migrator
526 }
527
528 #[test]
529 fn test_file_storage_strategy_builder() {
530 let strategy = FileStorageStrategy::new()
531 .with_format(FormatStrategy::Json)
532 .with_retry_count(5)
533 .with_cleanup(false)
534 .with_load_behavior(LoadBehavior::ErrorIfMissing);
535
536 assert_eq!(strategy.format, FormatStrategy::Json);
537 assert_eq!(strategy.atomic_write.retry_count, 5);
538 assert!(!strategy.atomic_write.cleanup_tmp_files);
539 assert_eq!(strategy.load_behavior, LoadBehavior::ErrorIfMissing);
540 }
541
542 #[test]
543 fn test_save_and_load_toml() {
544 let temp_dir = TempDir::new().unwrap();
545 let file_path = temp_dir.path().join("test.toml");
546 let migrator = setup_migrator();
547 let strategy = FileStorageStrategy::default(); let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
550
551 let entities = vec![TestEntity {
553 name: "test".to_string(),
554 count: 42,
555 }];
556 storage.update_and_save("test", entities).unwrap();
557
558 let migrator2 = setup_migrator();
560 let storage2 =
561 FileStorage::new(file_path, migrator2, FileStorageStrategy::default()).unwrap();
562
563 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
565 assert_eq!(loaded.len(), 1);
566 assert_eq!(loaded[0].name, "test");
567 assert_eq!(loaded[0].count, 42);
568 }
569
570 #[test]
571 fn test_save_and_load_json() {
572 let temp_dir = TempDir::new().unwrap();
573 let file_path = temp_dir.path().join("test.json");
574 let migrator = setup_migrator();
575 let strategy = FileStorageStrategy::new().with_format(FormatStrategy::Json);
576
577 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
578
579 let entities = vec![TestEntity {
581 name: "json_test".to_string(),
582 count: 100,
583 }];
584 storage.update_and_save("test", entities).unwrap();
585
586 let migrator2 = setup_migrator();
588 let strategy2 = FileStorageStrategy::new().with_format(FormatStrategy::Json);
589 let storage2 = FileStorage::new(file_path, migrator2, strategy2).unwrap();
590
591 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
593 assert_eq!(loaded.len(), 1);
594 assert_eq!(loaded[0].name, "json_test");
595 assert_eq!(loaded[0].count, 100);
596 }
597
598 #[test]
599 fn test_load_behavior_create_if_missing() {
600 let temp_dir = TempDir::new().unwrap();
601 let file_path = temp_dir.path().join("nonexistent.toml");
602 let migrator = setup_migrator();
603 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
604
605 let result = FileStorage::new(file_path, migrator, strategy);
606
607 assert!(result.is_ok()); }
609
610 #[test]
611 fn test_load_behavior_error_if_missing() {
612 let temp_dir = TempDir::new().unwrap();
613 let file_path = temp_dir.path().join("nonexistent.toml");
614 let migrator = setup_migrator();
615 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
616
617 let result = FileStorage::new(file_path, migrator, strategy);
618
619 assert!(result.is_err()); assert!(matches!(result, Err(MigrationError::IoError { .. })));
621 }
622
623 #[test]
624 fn test_atomic_write_no_tmp_file_left() {
625 let temp_dir = TempDir::new().unwrap();
626 let file_path = temp_dir.path().join("atomic.toml");
627 let migrator = setup_migrator();
628 let strategy = FileStorageStrategy::default();
629
630 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
631
632 let entities = vec![TestEntity {
633 name: "atomic".to_string(),
634 count: 1,
635 }];
636 storage.update_and_save("test", entities).unwrap();
637
638 let entries: Vec<_> = fs::read_dir(temp_dir.path())
640 .unwrap()
641 .filter_map(|e| e.ok())
642 .collect();
643
644 let tmp_files: Vec<_> = entries
645 .iter()
646 .filter(|e| {
647 e.file_name()
648 .to_string_lossy()
649 .starts_with(".atomic.toml.tmp")
650 })
651 .collect();
652
653 assert_eq!(tmp_files.len(), 0, "Temporary files should be cleaned up");
654 }
655
656 #[test]
657 fn test_file_storage_path() {
658 let temp_dir = TempDir::new().unwrap();
659 let file_path = temp_dir.path().join("test_config.toml");
660 let migrator = setup_migrator();
661 let strategy = FileStorageStrategy::default();
662
663 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
664
665 let returned_path = storage.path();
667 assert_eq!(returned_path, file_path.as_path());
668 }
669}