post_archiver/manager/
mod.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use dashmap::DashMap;
8use rusqlite::{params, Connection, OptionalExtension, Transaction};
9use serde_json::Value;
10
11use crate::TagId;
12use crate::{
13    utils::{DATABASE_NAME, VERSION},
14    CollectionId, PlatformId,
15};
16
17pub mod author;
18pub mod collection;
19pub mod file_meta;
20pub mod platform;
21pub mod post;
22pub mod tag;
23
24/// Core manager type for post archive operations with SQLite backend
25///
26/// # Examples
27/// ```no_run
28/// use post_archiver::manager::PostArchiverManager;
29///    
30/// let manager = PostArchiverManager::open_or_create("./data").unwrap();
31/// ```
32#[derive(Debug)]
33pub struct PostArchiverManager<T = Connection> {
34    pub path: PathBuf,
35    conn: T,
36    pub(crate) cache: Arc<PostArchiverManagerCache>,
37}
38
39impl PostArchiverManager {
40    /// Creates a new archive at the specified path
41    ///
42    /// # Safety
43    /// The path must not already contain a database file.
44    ///
45    /// # Examples
46    /// ```no_run
47    /// use post_archiver::manager::PostArchiverManager;
48    ///
49    /// let manager = PostArchiverManager::create("./new_archive").unwrap();
50    /// ```
51    pub fn create<P>(path: P) -> Result<Self, rusqlite::Error>
52    where
53        P: AsRef<Path>,
54    {
55        let path = path.as_ref().to_path_buf();
56        let db_path = path.join(DATABASE_NAME);
57
58        if db_path.exists() {
59            panic!("Database already exists");
60        }
61
62        let conn = Connection::open(&db_path)?;
63
64        // run the template sql
65        conn.execute_batch(include_str!("../utils/template.sql"))?;
66
67        // push current version
68        conn.execute(
69            "INSERT INTO post_archiver_meta (version) VALUES (?)",
70            [VERSION],
71        )?;
72
73        let cache = Arc::new(PostArchiverManagerCache::default());
74
75        Ok(Self { conn, path, cache })
76    }
77
78    /// Opens an existing archive at the specified path
79    ///
80    /// # Returns
81    /// - `Ok(Some(manager))` if archive exists and version is compatible
82    /// - `Ok(None)` if archive doesn't exist
83    /// - `Err(_)` on database errors
84    ///
85    /// # Examples
86    /// ```no_run
87    /// use post_archiver::manager::PostArchiverManager;
88    ///
89    /// if let Some(manager) = PostArchiverManager::open("./archive").unwrap() {
90    ///     println!("Archive opened successfully");
91    /// }
92    /// ```
93    pub fn open<P>(path: P) -> Result<Option<Self>, rusqlite::Error>
94    where
95        P: AsRef<Path>,
96    {
97        let manager = Self::open_uncheck(path);
98
99        // check version
100        if let Ok(Some(manager)) = &manager {
101            let version: String = manager
102                .conn()
103                .query_row("SELECT version FROM post_archiver_meta", [], |row| {
104                    row.get(0)
105                })
106                .unwrap_or("unknown".to_string());
107
108            let get_compatible_version =
109                |version: &str| version.splitn(3, ".").collect::<Vec<_>>()[0..2].join(".");
110
111            let match_version = match version.as_str() {
112                "unknown" => "unknown".to_string(),
113                version => get_compatible_version(version),
114            };
115            let expect_version = get_compatible_version(VERSION);
116
117            if match_version != expect_version {
118                panic!(
119                    "Database version mismatch \n + current: {}\n + expected: {}",
120                    version, VERSION
121                )
122            }
123        }
124
125        manager
126    }
127
128    /// Opens an existing archive at the specified path
129    /// Does not check the version of the archive.
130    ///
131    /// # Returns
132    /// - `Ok(Some(manager))` if archive exists and version is compatible
133    /// - `Ok(None)` if archive doesn't exist
134    /// - `Err(_)` on database errors
135    ///
136    /// # Examples
137    /// ```no_run
138    /// use post_archiver::manager::PostArchiverManager;
139    ///
140    /// if let Some(manager) = PostArchiverManager::open_uncheck("./archive").unwrap() {
141    ///     println!("Archive opened successfully");
142    /// }
143    /// ```
144    pub fn open_uncheck<P>(path: P) -> Result<Option<Self>, rusqlite::Error>
145    where
146        P: AsRef<Path>,
147    {
148        let path = path.as_ref().to_path_buf();
149        let db_path = path.join(DATABASE_NAME);
150
151        if !db_path.exists() {
152            return Ok(None);
153        }
154
155        let conn = Connection::open(db_path)?;
156
157        let cache = Arc::new(PostArchiverManagerCache::default());
158        Ok(Some(Self { conn, path, cache }))
159    }
160
161    /// Opens an existing archive or creates a new one if it doesn't exist
162    ///
163    /// # Examples
164    /// ```no_run
165    /// use post_archiver::manager::PostArchiverManager;
166    ///
167    /// let manager = PostArchiverManager::open_or_create("./archive").unwrap();
168    /// ```
169    pub fn open_or_create<P>(path: P) -> Result<Self, rusqlite::Error>
170    where
171        P: AsRef<Path>,
172    {
173        Self::open(&path)
174            .transpose()
175            .unwrap_or_else(|| Self::create(&path))
176    }
177
178    /// Creates an in-memory database
179    ///
180    /// it will generate a temporary path for the archive files
181    ///
182    /// # Examples
183    /// ```
184    /// use post_archiver::manager::PostArchiverManager;
185    ///
186    /// let manager = PostArchiverManager::open_in_memory().unwrap();
187    /// ```
188    pub fn open_in_memory() -> Result<Self, rusqlite::Error> {
189        let path = std::env::temp_dir();
190
191        let conn = Connection::open_in_memory()?;
192
193        // run the template sql
194        conn.execute_batch(include_str!("../utils/template.sql"))?;
195
196        // push current version
197        conn.execute(
198            "INSERT INTO post_archiver_meta (version) VALUES (?)",
199            [VERSION],
200        )?;
201
202        let cache = Arc::new(PostArchiverManagerCache::default());
203
204        Ok(Self { conn, path, cache })
205    }
206
207    /// Starts a new transaction
208    ///
209    /// # Examples
210    /// ```no_run
211    /// use post_archiver::manager::PostArchiverManager;
212    ///
213    /// let mut manager = PostArchiverManager::open_in_memory().unwrap();
214    /// let mut tx = manager.transaction().unwrap();
215    /// // ... perform operations
216    /// tx.commit().unwrap();
217    /// ```
218    pub fn transaction(&mut self) -> Result<PostArchiverManager<Transaction<'_>>, rusqlite::Error> {
219        Ok(PostArchiverManager {
220            path: self.path.clone(),
221            conn: self.conn.transaction()?,
222            cache: self.cache.clone(),
223        })
224    }
225}
226
227impl PostArchiverManager<Transaction<'_>> {
228    /// Commits the transaction
229    pub fn commit(self) -> Result<(), rusqlite::Error> {
230        self.conn.commit()
231    }
232}
233
234impl<T> PostArchiverManager<T>
235where
236    T: PostArchiverConnection,
237{
238    pub fn conn(&self) -> &Connection {
239        self.conn.connection()
240    }
241    /// Returns this archive's feature value by name.
242    ///
243    /// # Errors
244    ///
245    /// Returns `rusqlite::Error` if there was an error accessing the database or if the feature does not exist.
246    ///
247    /// # Examples
248    /// ```no_run
249    /// use post_archiver::manager::PostArchiverManager;
250    ///
251    /// let manager = PostArchiverManager::open_in_memory().unwrap();
252    /// let feature_value = manager.get_feature("example_feature").unwrap();
253    /// println!("Feature value: {}", feature_value);
254    /// ```
255    pub fn get_feature(&self, name: &str) -> Result<i64, rusqlite::Error> {
256        self.conn()
257            .query_row("SELECT value FROM features WHERE name = ?", [name], |row| {
258                row.get(0)
259            })
260            .optional()
261            .transpose()
262            .unwrap_or(Ok(0))
263    }
264    /// Returns this archive's feature value and extra data by name.
265    ///
266    /// # Errors
267    ///
268    /// Returns `rusqlite::Error` if there was an error accessing the database or if the feature does not exist.
269    ///
270    /// # Examples
271    ///
272    /// ```no_run
273    /// use post_archiver::manager::PostArchiverManager;
274    /// let manager = PostArchiverManager::open_in_memory().unwrap();
275    /// let (value, extra) = manager.get_feature_with_extra("example_feature").unwrap();
276    /// println!("Feature value: {}, Extra: {:?}", value, extra);
277    /// ```
278    pub fn get_feature_with_extra(
279        &self,
280        name: &str,
281    ) -> Result<(i64, HashMap<String, Value>), rusqlite::Error> {
282        self.conn()
283            .query_row(
284                "SELECT value, extra FROM features WHERE name = ?",
285                [name],
286                |row| {
287                    let value: i64 = row.get(0)?;
288                    let extra: String = row.get(1)?;
289                    let extra: HashMap<String, Value> =
290                        serde_json::from_str(&extra).unwrap_or_default();
291                    Ok((value, extra))
292                },
293            )
294            .optional()
295            .transpose()
296            .unwrap_or(Ok((0, HashMap::default())))
297    }
298    /// Set feature value by name.
299    ///
300    /// if it exists, its value will be updated.
301    ///
302    /// # Errors
303    ///
304    /// Returns `rusqlite::Error` if there was an error accessing the database.
305    pub fn set_feature(&self, name: &str, value: i64) {
306        self.conn()
307            .execute(
308                "INSERT INTO features (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?",
309                params![name, value, value],
310            )
311            .unwrap();
312    }
313    /// Set feature value and extra data by name.
314    ///
315    /// if it exists, its value and extra data will be updated.
316    ///
317    /// # Errors
318    ///
319    /// Returns `rusqlite::Error` if there was an error accessing the database.
320    pub fn set_feature_with_extra(&self, name: &str, value: i64, extra: HashMap<String, Value>) {
321        let extra = serde_json::to_string(&extra).unwrap();
322        self.conn()
323            .execute(
324                "INSERT OR REPLACE INTO features (name, value, extra) VALUES (?, ?, ?)",
325                params![name, value, extra],
326            )
327            .unwrap();
328    }
329}
330
331#[derive(Debug, Default)]
332pub struct PostArchiverManagerCache {
333    pub tags: DashMap<(String, Option<PlatformId>), TagId>,
334    pub collections: DashMap<String, CollectionId>,
335    pub platforms: DashMap<String, PlatformId>,
336}
337
338/// Trait for types that can provide a database connection
339pub trait PostArchiverConnection {
340    fn connection(&self) -> &Connection;
341}
342
343impl PostArchiverConnection for Connection {
344    fn connection(&self) -> &Connection {
345        self
346    }
347}
348
349impl PostArchiverConnection for Transaction<'_> {
350    fn connection(&self) -> &Connection {
351        self
352    }
353}