1use crate::*;
2use anyhow::Result;
3use rusqlite::{Connection, params};
4use serde::{Deserialize, Serialize};
5use std::{
6 path::Path,
7 sync::{Arc, Mutex},
8};
9
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct History {
13 pub content: String,
14 pub timestamp: i64,
15 pub file: String,
16 pub uuid: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct QueueItem {
21 pub local_file: String,
22 pub remote_file: String,
23 pub uuid: String,
24}
25
26#[derive(Debug)]
27pub struct Database {
28 conn: Arc<Mutex<Connection>>,
29}
30
31impl Database {
32 pub fn new<P: AsRef<Path>>(db_path: P) -> Result<Self> {
33 if let Some(parent) = db_path.as_ref().parent() {
35 std::fs::create_dir_all(parent)?;
36 }
37
38 let conn = Connection::open(db_path)?;
39 let db = Database {
40 conn: Arc::new(Mutex::new(conn)),
41 };
42 db.init_schema()?;
43 Ok(db)
44 }
45
46
47 fn init_schema(&self) -> Result<()> {
48 let conn = self.conn.lock().unwrap();
49
50 conn.execute(
51 "CREATE TABLE IF NOT EXISTS history (
52 id INTEGER PRIMARY KEY AUTOINCREMENT,
53 content TEXT NOT NULL,
54 timestamp INTEGER NOT NULL,
55 file TEXT NOT NULL,
56 uuid TEXT NOT NULL UNIQUE
57 )",
58 [],
59 )?;
60
61 conn.execute(
62 "CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp)",
63 [],
64 )?;
65
66 conn.execute(
67 "CREATE TABLE IF NOT EXISTS queue (
68 id INTEGER PRIMARY KEY AUTOINCREMENT,
69 local_file TEXT NOT NULL,
70 remote_file TEXT NOT NULL,
71 uuid TEXT NOT NULL UNIQUE
72 )",
73 [],
74 )?;
75
76 Ok(())
77 }
78
79
80 pub fn add_to_queue(&self, item: &QueueItem) -> Result<()> {
81 let conn = self.conn.lock().unwrap();
82 conn.execute(
83 "INSERT OR IGNORE INTO queue (local_file, remote_file, uuid) VALUES (?1, ?2, ?3)",
84 params![&item.local_file, &item.remote_file, &item.uuid],
85 )?;
86 Ok(())
87 }
88
89
90 pub fn get_queue(&self) -> Result<Vec<QueueItem>> {
91 let conn = self.conn.lock().unwrap();
92 let mut stmt = conn.prepare("SELECT local_file, remote_file, uuid FROM queue")?;
93 let items = stmt
94 .query_map([], |row| {
95 Ok(QueueItem {
96 local_file: row.get(0)?,
97 remote_file: row.get(1)?,
98 uuid: row.get(2)?,
99 })
100 })?
101 .collect::<Result<Vec<_>, _>>()?;
102 Ok(items)
103 }
104
105
106 pub fn remove_from_queue(&self, uuid: &str) -> Result<()> {
107 let conn = self.conn.lock().unwrap();
108 conn.execute("DELETE FROM queue WHERE uuid = ?1", params![uuid])?;
109 Ok(())
110 }
111
112
113 pub fn add_history(&self, history: &History) -> Result<()> {
114 let conn = self.conn.lock().unwrap();
115 conn.execute(
116 "INSERT OR IGNORE INTO history (content, timestamp, file, uuid) VALUES (?1, ?2, ?3, ?4)",
117 params![&history.content, &history.timestamp, &history.file, &history.uuid],
118 )?;
119 Ok(())
120 }
121
122
123 pub fn get_history(&self, limit: Option<usize>) -> Result<Vec<History>> {
124 let conn = self.conn.lock().unwrap();
125 let query = if let Some(lim) = limit {
126 format!(
127 "SELECT content, timestamp, file, uuid FROM history ORDER BY timestamp DESC LIMIT {}",
128 lim
129 )
130 } else {
131 "SELECT content, timestamp, file, uuid FROM history ORDER BY timestamp DESC"
132 .to_string()
133 };
134
135 let mut stmt = conn.prepare(&query)?;
136 let items = stmt
137 .query_map([], |row| {
138 Ok(History {
139 content: row.get(0)?,
140 timestamp: row.get(1)?,
141 file: row.get(2)?,
142 uuid: row.get(3)?,
143 })
144 })?
145 .collect::<Result<Vec<_>, _>>()?;
146 Ok(items)
147 }
148
149
150 pub fn dump_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
151 let conn = self.conn.lock().unwrap();
152 let mut backup_conn = Connection::open(&path)?;
153
154 let backup = rusqlite::backup::Backup::new(&conn, &mut backup_conn)?;
155 backup.run_to_completion(5, std::time::Duration::from_millis(250), None)?;
156
157 info!("Database dumped to: {:?}", path.as_ref());
158 Ok(())
159 }
160}