1use std::{collections::BTreeMap, path::Path};
7
8use rusqlite::{Connection, OptionalExtension, params};
9
10use crate::{
11 Error, Result,
12 plan::{Base, Local, RelPath, Remote},
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Node {
18 pub id: String,
19 pub parent: Option<String>,
20 pub name: String,
21 pub directory: bool,
22 pub blob: Option<String>,
23 pub size: u64,
24 pub role: Option<String>,
25}
26
27#[derive(Debug)]
28pub struct State {
29 db: Connection,
30}
31
32impl From<rusqlite::Error> for Error {
33 fn from(error: rusqlite::Error) -> Self {
34 Error::State(error.to_string())
35 }
36}
37
38impl State {
39 pub fn open(path: &Path) -> Result<State> {
40 let db = Connection::open(path)?;
41 db.execute_batch(
42 "PRAGMA journal_mode = WAL;
43 CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
44 CREATE TABLE IF NOT EXISTS remote (
45 id TEXT PRIMARY KEY, parent TEXT, name TEXT NOT NULL, directory INTEGER NOT NULL, blob TEXT,
46 size INTEGER NOT NULL, role TEXT);
47 CREATE TABLE IF NOT EXISTS base (
48 path TEXT PRIMARY KEY, id TEXT NOT NULL, directory INTEGER NOT NULL, blob TEXT, size INTEGER NOT NULL,
49 local_directory INTEGER NOT NULL, local_size INTEGER NOT NULL, local_modified TEXT NOT NULL);",
50 )?;
51 Ok(State { db })
52 }
53
54 pub fn meta(&self, key: &str) -> Result<Option<String>> {
55 Ok(self.db.query_row("SELECT value FROM meta WHERE key = ?1", [key], |row| row.get(0)).optional()?)
56 }
57
58 pub fn set_meta(&self, key: &str, value: &str) -> Result<()> {
59 self.db.execute("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)", [key, value])?;
60 Ok(())
61 }
62
63 pub fn remote(&self) -> Result<BTreeMap<String, Node>> {
64 let mut query = self.db.prepare("SELECT id, parent, name, directory, blob, size, role FROM remote")?;
65 let rows = query.query_map([], |row| {
66 Ok(Node {
67 id: row.get(0)?,
68 parent: row.get(1)?,
69 name: row.get(2)?,
70 directory: row.get(3)?,
71 blob: row.get(4)?,
72 size: row.get::<_, i64>(5)? as u64,
73 role: row.get(6)?,
74 })
75 })?;
76 let mut out = BTreeMap::new();
77 for node in rows {
78 let node = node?;
79 out.insert(node.id.clone(), node);
80 }
81 Ok(out)
82 }
83
84 pub fn set_remote(&mut self, nodes: &BTreeMap<String, Node>, state: &str) -> Result<()> {
86 let tx = self.db.transaction()?;
87 tx.execute("DELETE FROM remote", [])?;
88 {
89 let mut insert = tx.prepare(
90 "INSERT INTO remote (id, parent, name, directory, blob, size, role) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
91 )?;
92 for node in nodes.values() {
93 insert.execute(params![
94 node.id,
95 node.parent,
96 node.name,
97 node.directory,
98 node.blob,
99 node.size as i64,
100 node.role
101 ])?;
102 }
103 }
104 tx.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('state', ?1)", [state])?;
105 tx.commit()?;
106 Ok(())
107 }
108
109 pub fn base(&self) -> Result<BTreeMap<RelPath, Base>> {
110 let mut query = self
111 .db
112 .prepare("SELECT path, id, directory, blob, size, local_directory, local_size, local_modified FROM base")?;
113 let rows = query.query_map([], |row| {
114 let modified: String = row.get(7)?;
115 Ok((
116 row.get::<_, String>(0)?,
117 Base {
118 remote: Remote {
119 id: row.get(1)?,
120 directory: row.get(2)?,
121 blob: row.get(3)?,
122 size: row.get::<_, i64>(4)? as u64,
123 },
124 local: Local {
125 directory: row.get(5)?,
126 size: row.get::<_, i64>(6)? as u64,
127 modified: modified.parse().unwrap_or_default(),
128 },
129 },
130 ))
131 })?;
132 let mut out = BTreeMap::new();
133 for row in rows {
134 let (path, base) = row?;
135 out.insert(path, base);
136 }
137 Ok(out)
138 }
139
140 pub fn set_base(&mut self, base: &BTreeMap<RelPath, Base>) -> Result<()> {
142 let tx = self.db.transaction()?;
143 tx.execute("DELETE FROM base", [])?;
144 {
145 let mut insert = tx.prepare(
146 "INSERT INTO base (path, id, directory, blob, size, local_directory, local_size, local_modified)
147 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
148 )?;
149 for (path, base) in base {
150 insert.execute(params![
151 path,
152 base.remote.id,
153 base.remote.directory,
154 base.remote.blob,
155 base.remote.size as i64,
156 base.local.directory,
157 base.local.size as i64,
158 base.local.modified.to_string()
159 ])?;
160 }
161 }
162 tx.commit()?;
163 Ok(())
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn what_is_stored_comes_back() {
173 let dir = std::env::temp_dir().join(format!("kernoid-sync-state-{}", std::process::id()));
174 std::fs::create_dir_all(&dir).unwrap();
175 let path = dir.join("state.db");
176 let mut state = State::open(&path).unwrap();
177 let node = Node {
178 id: "n1".into(),
179 parent: None,
180 name: "a.txt".into(),
181 directory: false,
182 blob: Some("b1".into()),
183 size: 3,
184 role: None,
185 };
186 state.set_remote(&BTreeMap::from([("n1".to_owned(), node.clone())]), "7").unwrap();
187 let base = Base {
188 remote: Remote { id: "n1".into(), directory: false, blob: Some("b1".into()), size: 3 },
189 local: Local { directory: false, size: 3, modified: 1_761_000_000_123_456_789 },
190 };
191 state.set_base(&BTreeMap::from([("a.txt".to_owned(), base.clone())])).unwrap();
192 drop(state);
193 let state = State::open(&path).unwrap();
194 assert_eq!(state.meta("state").unwrap().as_deref(), Some("7"));
195 assert_eq!(state.remote().unwrap()["n1"], node);
196 assert_eq!(state.base().unwrap()["a.txt"], base);
197 std::fs::remove_dir_all(&dir).unwrap();
198 }
199}