Skip to main content

scrobble_scrubber/persistence/
file_storage.rs

1use async_trait::async_trait;
2use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
3use std::path::Path;
4
5use super::{
6    PendingEditsState, PendingRewriteRulesState, RewriteRulesState, SettingsState, StateStorage,
7    TimestampState,
8};
9use crate::rewrite::load_comprehensive_default_rules;
10
11/// PickleDB-based file storage implementation
12pub struct FileStorage {
13    db: PickleDb,
14}
15
16#[derive(Debug, thiserror::Error)]
17#[allow(clippy::enum_variant_names)]
18pub enum FileStorageError {
19    #[error("Database error: {0}")]
20    DatabaseError(String),
21    #[error("Serialization error: {0}")]
22    SerializationError(String),
23    #[error("IO error: {0}")]
24    IoError(#[from] std::io::Error),
25}
26
27impl FileStorage {
28    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, FileStorageError> {
29        let path_ref = path.as_ref();
30
31        // Check if this is a new database
32        let is_new_database = !path_ref.exists();
33
34        // Create parent directory if it doesn't exist
35        if let Some(parent) = path_ref.parent() {
36            std::fs::create_dir_all(parent)?;
37        }
38
39        let db = match PickleDb::load(
40            path_ref,
41            PickleDbDumpPolicy::AutoDump,
42            SerializationMethod::Json,
43        ) {
44            Ok(db) => db,
45            Err(_) => PickleDb::new(
46                path_ref,
47                PickleDbDumpPolicy::AutoDump,
48                SerializationMethod::Json,
49            ),
50        };
51
52        let mut storage = Self { db };
53
54        // Initialize default rules for new databases
55        if is_new_database {
56            if let Err(e) = storage.initialize_default_rules() {
57                log::warn!("Failed to initialize default rules for new database: {e}");
58            } else {
59                log::info!("Initialized new database with comprehensive default rewrite rules");
60            }
61        }
62
63        Ok(storage)
64    }
65
66    /// Initialize default rewrite rules for a new database
67    fn initialize_default_rules(&mut self) -> Result<(), FileStorageError> {
68        let default_rules = load_comprehensive_default_rules();
69        let rules_state = RewriteRulesState {
70            rewrite_rules: default_rules,
71        };
72
73        self.db
74            .set("rewrite_rules_state", &rules_state)
75            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
76
77        // Force a database dump to ensure the changes are persisted immediately
78        self.db
79            .dump()
80            .map_err(|e| FileStorageError::DatabaseError(e.to_string()))?;
81
82        Ok(())
83    }
84}
85
86#[async_trait]
87impl StateStorage for FileStorage {
88    type Error = FileStorageError;
89
90    async fn save_timestamp_state(&mut self, state: &TimestampState) -> Result<(), Self::Error> {
91        self.db
92            .set("timestamp_state", state)
93            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
94        Ok(())
95    }
96
97    async fn load_timestamp_state(&self) -> Result<TimestampState, Self::Error> {
98        Ok(self.db.get("timestamp_state").unwrap_or_default())
99    }
100
101    async fn save_rewrite_rules_state(
102        &mut self,
103        state: &RewriteRulesState,
104    ) -> Result<(), Self::Error> {
105        self.db
106            .set("rewrite_rules_state", state)
107            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
108
109        // Force a database dump to ensure the changes are persisted immediately
110        self.db
111            .dump()
112            .map_err(|e| FileStorageError::DatabaseError(e.to_string()))?;
113
114        Ok(())
115    }
116
117    async fn load_rewrite_rules_state(&self) -> Result<RewriteRulesState, Self::Error> {
118        Ok(self.db.get("rewrite_rules_state").unwrap_or_default())
119    }
120
121    async fn save_pending_edits_state(
122        &mut self,
123        state: &PendingEditsState,
124    ) -> Result<(), Self::Error> {
125        self.db
126            .set("pending_edits_state", state)
127            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
128
129        // Force a database dump to ensure the changes are persisted immediately
130        self.db
131            .dump()
132            .map_err(|e| FileStorageError::DatabaseError(e.to_string()))?;
133
134        Ok(())
135    }
136
137    async fn load_pending_edits_state(&self) -> Result<PendingEditsState, Self::Error> {
138        Ok(self.db.get("pending_edits_state").unwrap_or_default())
139    }
140
141    async fn save_pending_rewrite_rules_state(
142        &mut self,
143        state: &PendingRewriteRulesState,
144    ) -> Result<(), Self::Error> {
145        self.db
146            .set("pending_rewrite_rules_state", state)
147            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
148
149        // Force a database dump to ensure the changes are persisted immediately
150        self.db
151            .dump()
152            .map_err(|e| FileStorageError::DatabaseError(e.to_string()))?;
153
154        Ok(())
155    }
156
157    async fn load_pending_rewrite_rules_state(
158        &self,
159    ) -> Result<PendingRewriteRulesState, Self::Error> {
160        Ok(self
161            .db
162            .get("pending_rewrite_rules_state")
163            .unwrap_or_default())
164    }
165
166    async fn save_settings_state(&mut self, state: &SettingsState) -> Result<(), Self::Error> {
167        self.db
168            .set("settings_state", state)
169            .map_err(|e| FileStorageError::SerializationError(e.to_string()))?;
170        Ok(())
171    }
172
173    async fn load_settings_state(&self) -> Result<SettingsState, Self::Error> {
174        Ok(self.db.get("settings_state").unwrap_or_default())
175    }
176}
177
178// PickleDb is not Send + Sync by default, but since we're using it in a controlled manner
179// with proper synchronization via Arc<Mutex<FileStorage>>, we can safely implement Send + Sync
180unsafe impl Send for FileStorage {}
181unsafe impl Sync for FileStorage {}