Skip to main content

post_archiver/manager/
mod.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use cached::TimedSizedCache;
6use rusqlite::{Connection, Transaction};
7
8use crate::error::{Error, Result};
9use crate::utils::{DATABASE_NAME, VERSION};
10
11pub mod binded;
12pub use binded::*;
13
14pub mod author;
15pub use author::{AuthorThumb, AuthorUpdated, UpdateAuthor};
16
17pub mod collection;
18pub use collection::{CollectionThumb, UpdateCollection};
19
20pub mod file_meta;
21pub use file_meta::{UpdateFileMeta, WritableFileMeta};
22
23pub mod platform;
24pub use platform::UpdatePlatform;
25
26pub mod post;
27pub use post::{PostUpdated, UpdatePost};
28
29pub mod tag;
30pub use tag::UpdateTag;
31
32/// Core manager type for post archive operations with SQLite backend
33///
34/// Provides database connection management and access to entity operations
35/// through the [`Binded`] type via [`bind()`](PostArchiverManager::bind).
36///
37/// # Examples
38/// ```no_run
39/// use post_archiver::manager::PostArchiverManager;
40///
41/// let manager = PostArchiverManager::open_or_create("./data").unwrap();
42/// ```
43#[derive(Debug)]
44pub struct PostArchiverManager<C = Connection> {
45    pub caches: Arc<Mutex<ManagerCaches>>,
46    pub path: PathBuf,
47    conn: C,
48}
49
50impl PostArchiverManager {
51    /// Creates a new archive at the specified path
52    ///
53    /// # Panics
54    /// Panics if the path already contains a database file.
55    ///
56    /// # Examples
57    /// ```no_run
58    /// use post_archiver::manager::PostArchiverManager;
59    ///
60    /// let manager = PostArchiverManager::create("./new_archive").unwrap();
61    /// ```
62    pub fn create<P>(path: P) -> Result<Self>
63    where
64        P: AsRef<Path>,
65    {
66        let path = path.as_ref().to_path_buf();
67        let db_path = path.join(DATABASE_NAME);
68
69        if db_path.exists() {
70            return Err(Error::DatabaseAlreadyExists);
71        }
72
73        let conn = Connection::open(&db_path)?;
74
75        // run the template sql
76        conn.execute_batch(include_str!("../utils/template.sql"))?;
77
78        // push current version
79        conn.execute(
80            "INSERT INTO post_archiver_meta (version) VALUES (?)",
81            [VERSION],
82        )?;
83
84        Ok(Self {
85            conn,
86            path,
87            caches: Default::default(),
88        })
89    }
90
91    /// Opens an existing archive at the specified path
92    ///
93    /// # Returns
94    /// - `Ok(Some(manager))` if archive exists and version is compatible
95    /// - `Ok(None)` if archive doesn't exist
96    /// - `Err(_)` on database errors
97    ///
98    /// # Examples
99    /// ```no_run
100    /// use post_archiver::manager::PostArchiverManager;
101    ///
102    /// if let Some(manager) = PostArchiverManager::open("./archive").unwrap() {
103    ///     println!("Archive opened successfully");
104    /// }
105    /// ```
106    pub fn open<P>(path: P) -> Result<Option<Self>>
107    where
108        P: AsRef<Path>,
109    {
110        let manager = Self::open_uncheck(path)?;
111
112        // check version
113        if let Some(manager) = &manager {
114            let version: String = manager
115                .conn()
116                .query_row("SELECT version FROM post_archiver_meta", [], |row| {
117                    row.get(0)
118                })
119                .unwrap_or("unknown".to_string());
120
121            let get_compatible_version =
122                |version: &str| version.splitn(3, ".").collect::<Vec<_>>()[0..2].join(".");
123
124            let match_version = match version.as_str() {
125                "unknown" => "unknown".to_string(),
126                version => get_compatible_version(version),
127            };
128            let expect_version = get_compatible_version(VERSION);
129
130            if match_version != expect_version {
131                return Err(Error::VersionMismatch {
132                    current: version,
133                    expected: VERSION.to_string(),
134                });
135            }
136        }
137
138        Ok(manager)
139    }
140
141    /// Opens an existing archive at the specified path without checking version.
142    ///
143    /// # Returns
144    /// - `Ok(Some(manager))` if archive exists
145    /// - `Ok(None)` if archive doesn't exist
146    /// - `Err(_)` on database errors
147    pub fn open_uncheck<P>(path: P) -> Result<Option<Self>>
148    where
149        P: AsRef<Path>,
150    {
151        let path = path.as_ref().to_path_buf();
152        let db_path = path.join(DATABASE_NAME);
153
154        if !db_path.exists() {
155            return Ok(None);
156        }
157
158        let conn = Connection::open(db_path)?;
159
160        Ok(Some(Self {
161            conn,
162            path,
163            caches: Default::default(),
164        }))
165    }
166
167    /// Opens an existing archive or creates a new one if it doesn't exist
168    ///
169    /// # Examples
170    /// ```no_run
171    /// use post_archiver::manager::PostArchiverManager;
172    ///
173    /// let manager = PostArchiverManager::open_or_create("./archive").unwrap();
174    /// ```
175    pub fn open_or_create<P>(path: P) -> Result<Self>
176    where
177        P: AsRef<Path>,
178    {
179        Self::open(&path)
180            .transpose()
181            .unwrap_or_else(|| Self::create(&path))
182    }
183
184    /// Creates an in-memory database
185    ///
186    /// # Examples
187    /// ```
188    /// use post_archiver::manager::PostArchiverManager;
189    ///
190    /// let manager = PostArchiverManager::open_in_memory().unwrap();
191    /// ```
192    pub fn open_in_memory() -> Result<Self> {
193        let path = std::env::temp_dir();
194
195        let conn = Connection::open_in_memory()?;
196
197        // run the template sql
198        conn.execute_batch(include_str!("../utils/template.sql"))?;
199
200        // push current version
201        conn.execute(
202            "INSERT INTO post_archiver_meta (version) VALUES (?)",
203            [VERSION],
204        )?;
205
206        Ok(Self {
207            conn,
208            path,
209            caches: Default::default(),
210        })
211    }
212
213    /// Starts a new transaction
214    ///
215    /// # Examples
216    /// ```no_run
217    /// use post_archiver::manager::PostArchiverManager;
218    ///
219    /// let mut manager = PostArchiverManager::open_in_memory().unwrap();
220    /// let mut tx = manager.transaction().unwrap();
221    /// // ... perform operations
222    /// tx.commit().unwrap();
223    /// ```
224    pub fn transaction(&mut self) -> Result<PostArchiverManager<Transaction<'_>>> {
225        Ok(PostArchiverManager {
226            path: self.path.clone(),
227            caches: self.caches.clone(),
228            conn: self.conn.transaction()?,
229        })
230    }
231}
232
233impl PostArchiverManager<Transaction<'_>> {
234    /// Commits the transaction
235    pub fn commit(self) -> Result<()> {
236        Ok(self.conn.commit()?)
237    }
238}
239
240impl<C> PostArchiverManager<C>
241where
242    C: PostArchiverConnection,
243{
244    /// Returns a reference to the underlying database connection
245    pub fn conn(&self) -> &Connection {
246        self.conn.connection()
247    }
248
249    /// Bind an entity ID to get a [`Binded`] context for update/delete/relation operations.
250    ///
251    /// The type parameter is inferred from the ID argument — no turbofish needed.
252    ///
253    /// # Examples
254    /// ```no_run
255    /// # use post_archiver::manager::PostArchiverManager;
256    /// # use post_archiver::PostId;
257    /// # fn example(manager: &PostArchiverManager, post_id: PostId) {
258    /// let binded = manager.bind(post_id);
259    /// // binded is Binded<'_, PostId>
260    /// # }
261    /// ```
262    pub fn bind<Id: BindableId>(&self, id: Id) -> Binded<'_, Id, C> {
263        Binded::new(self, id)
264    }
265}
266
267/// Trait for types that can provide a database connection
268pub trait PostArchiverConnection {
269    fn connection(&self) -> &Connection;
270}
271
272impl PostArchiverConnection for Connection {
273    fn connection(&self) -> &Connection {
274        self
275    }
276}
277
278impl PostArchiverConnection for Transaction<'_> {
279    fn connection(&self) -> &Connection {
280        self
281    }
282}
283
284#[derive(Debug)]
285pub struct ManagerCaches {
286    pub counts: TimedSizedCache<(String, String), u64>,
287}
288
289impl Default for ManagerCaches {
290    fn default() -> Self {
291        Self {
292            counts: TimedSizedCache::with_size_and_lifespan(128, Duration::from_mins(30)),
293        }
294    }
295}