1use std::str::FromStr;
2use std::sync::{Arc, Mutex};
3
4use chrono::{DateTime, Utc};
5use rusqlite::{params, Connection};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::error::{MnemeError, Result};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum TransportType {
15 Http,
16 Ssh,
17 File,
18}
19
20impl std::fmt::Display for TransportType {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 let s = match self {
23 TransportType::Http => "http",
24 TransportType::Ssh => "ssh",
25 TransportType::File => "file",
26 };
27 write!(f, "{}", s)
28 }
29}
30
31impl std::str::FromStr for TransportType {
32 type Err = MnemeError;
33
34 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
35 match s.to_lowercase().as_str() {
36 "http" => Ok(TransportType::Http),
37 "ssh" => Ok(TransportType::Ssh),
38 "file" => Ok(TransportType::File),
39 other => Err(MnemeError::UnsupportedTransport(other.to_string())),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Peer {
47 pub id: Uuid,
49 pub name: String,
51 pub transport: TransportType,
53 pub address: String,
55 pub project: String,
57 pub last_sync: Option<DateTime<Utc>>,
59 pub last_status: Option<String>,
61 pub auto_sync: bool,
63 pub created_at: DateTime<Utc>,
65}
66
67pub struct PeerStore {
69 conn: Arc<Mutex<Connection>>,
70}
71
72impl PeerStore {
73 pub fn new(conn: Arc<Mutex<Connection>>) -> Self {
75 Self { conn }
76 }
77
78 pub fn add(&self, peer: &Peer) -> Result<()> {
80 let conn = self
81 .conn
82 .lock()
83 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
84 conn.execute(
85 "INSERT INTO sync_peers (id, name, transport, address, project, last_sync, last_status, auto_sync, created_at)
86 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
87 ON CONFLICT(id) DO UPDATE SET
88 name = excluded.name,
89 transport = excluded.transport,
90 address = excluded.address,
91 project = excluded.project,
92 auto_sync = excluded.auto_sync",
93 params![
94 peer.id.to_string(),
95 &peer.name,
96 peer.transport.to_string(),
97 &peer.address,
98 &peer.project,
99 peer.last_sync.map(|d| d.to_rfc3339()),
100 peer.last_status.as_deref(),
101 peer.auto_sync as i32,
102 peer.created_at.to_rfc3339(),
103 ],
104 )?;
105 tracing::info!("added peer: {} ({})", peer.id, peer.name);
106 Ok(())
107 }
108
109 pub fn remove(&self, id: Uuid) -> Result<()> {
111 let conn = self
112 .conn
113 .lock()
114 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
115 let rows = conn.execute(
116 "DELETE FROM sync_peers WHERE id = ?1",
117 params![id.to_string()],
118 )?;
119 if rows == 0 {
120 return Err(MnemeError::PeerNotFound(id));
121 }
122 tracing::info!("removed peer: {}", id);
123 Ok(())
124 }
125
126 pub fn list(&self, project: &str) -> Result<Vec<Peer>> {
128 let conn = self
129 .conn
130 .lock()
131 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
132 let mut stmt = conn.prepare(
133 "SELECT id, name, transport, address, project, last_sync, last_status, auto_sync, created_at
134 FROM sync_peers WHERE project = ?1 ORDER BY created_at DESC",
135 )?;
136 let rows = stmt.query_map(params![project], |row| {
137 Ok(Peer {
138 id: Uuid::parse_str(&row.get::<_, String>(0)?).map_err(|e| {
139 rusqlite::Error::FromSqlConversionFailure(
140 0,
141 rusqlite::types::Type::Text,
142 Box::new(e),
143 )
144 })?,
145 name: row.get(1)?,
146 transport: TransportType::from_str(&row.get::<_, String>(2)?).map_err(|e| {
147 rusqlite::Error::FromSqlConversionFailure(
148 2,
149 rusqlite::types::Type::Text,
150 Box::new(e),
151 )
152 })?,
153 address: row.get(3)?,
154 project: row.get(4)?,
155 last_sync: row
156 .get::<_, Option<String>>(5)?
157 .and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
158 .map(|d| d.with_timezone(&Utc)),
159 last_status: row.get(6)?,
160 auto_sync: row.get::<_, i32>(7)? != 0,
161 created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(8)?)
162 .map_err(|e| {
163 rusqlite::Error::FromSqlConversionFailure(
164 8,
165 rusqlite::types::Type::Text,
166 Box::new(e),
167 )
168 })?
169 .with_timezone(&Utc),
170 })
171 })?;
172
173 let mut peers = Vec::new();
174 for row in rows {
175 peers.push(row?);
176 }
177 Ok(peers)
178 }
179
180 pub fn get(&self, id: Uuid) -> Result<Option<Peer>> {
182 let conn = self
183 .conn
184 .lock()
185 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
186 let mut stmt = conn.prepare(
187 "SELECT id, name, transport, address, project, last_sync, last_status, auto_sync, created_at
188 FROM sync_peers WHERE id = ?1",
189 )?;
190 let result = stmt.query_row(params![id.to_string()], |row| {
191 Ok(Peer {
192 id: Uuid::parse_str(&row.get::<_, String>(0)?).map_err(|e| {
193 rusqlite::Error::FromSqlConversionFailure(
194 0,
195 rusqlite::types::Type::Text,
196 Box::new(e),
197 )
198 })?,
199 name: row.get(1)?,
200 transport: TransportType::from_str(&row.get::<_, String>(2)?).map_err(|e| {
201 rusqlite::Error::FromSqlConversionFailure(
202 2,
203 rusqlite::types::Type::Text,
204 Box::new(e),
205 )
206 })?,
207 address: row.get(3)?,
208 project: row.get(4)?,
209 last_sync: row
210 .get::<_, Option<String>>(5)?
211 .and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
212 .map(|d| d.with_timezone(&Utc)),
213 last_status: row.get(6)?,
214 auto_sync: row.get::<_, i32>(7)? != 0,
215 created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(8)?)
216 .map_err(|e| {
217 rusqlite::Error::FromSqlConversionFailure(
218 8,
219 rusqlite::types::Type::Text,
220 Box::new(e),
221 )
222 })?
223 .with_timezone(&Utc),
224 })
225 });
226
227 match result {
228 Ok(peer) => Ok(Some(peer)),
229 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
230 Err(e) => Err(e.into()),
231 }
232 }
233
234 pub fn update_status(&self, id: Uuid, status: &str) -> Result<()> {
236 let conn = self
237 .conn
238 .lock()
239 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
240 let rows = conn.execute(
241 "UPDATE sync_peers SET last_sync = ?1, last_status = ?2 WHERE id = ?3",
242 params![Utc::now().to_rfc3339(), status, id.to_string()],
243 )?;
244 if rows == 0 {
245 return Err(MnemeError::PeerNotFound(id));
246 }
247 tracing::info!("updated peer status: {} -> {}", id, status);
248 Ok(())
249 }
250
251 pub fn record_sync(
253 &self,
254 result: &crate::sync::protocol::SyncResult,
255 project: &str,
256 ) -> Result<()> {
257 let conn = self
258 .conn
259 .lock()
260 .map_err(|_| MnemeError::Config("mutex poisoned".into()))?;
261 let id = Uuid::new_v4();
262 let direction = match result.direction {
263 crate::sync::protocol::SyncDirection::Push => "push",
264 crate::sync::protocol::SyncDirection::Pull => "pull",
265 crate::sync::protocol::SyncDirection::Bidirectional => "bidirectional",
266 };
267 let status = match result.status {
268 crate::sync::protocol::SyncStatus::Ok => "ok",
269 crate::sync::protocol::SyncStatus::Error => "error",
270 crate::sync::protocol::SyncStatus::Partial => "partial",
271 };
272 conn.execute(
273 "INSERT INTO sync_log (id, peer_id, direction, project, memories_sent, memories_received, conflicts_resolved, duration_ms, status, error, started_at, finished_at)
274 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
275 params![
276 id.to_string(),
277 result.peer_name,
278 direction,
279 project,
280 result.memories_sent as i32,
281 result.memories_received as i32,
282 result.conflicts_resolved as i32,
283 result.duration_ms as i32,
284 status,
285 result.error.as_deref(),
286 Utc::now().to_rfc3339(),
287 Utc::now().to_rfc3339(),
288 ],
289 )?;
290 Ok(())
291 }
292}