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}