1use std::{
2 collections::BTreeMap,
3 path::Path,
4 sync::{Arc, Mutex},
5 time::Duration,
6};
7
8use rusqlite::{Connection, params};
9
10use crate::{
11 Result, SyncwebError,
12 bandwidth_stats::{BandwidthStats, FolderStats, PeerStats},
13};
14
15#[derive(Clone, Debug)]
16pub struct StatsDatabase {
17 connection: Arc<Mutex<Connection>>,
18}
19
20impl StatsDatabase {
21 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
27 let db_path = path.as_ref();
28 if let Some(parent) = db_path.parent() {
29 std::fs::create_dir_all(parent)
30 .map_err(|error| SyncwebError::operation("failed to create stats db directory", error))?;
31 }
32 let connection = Connection::open(db_path)
33 .map_err(|error| SyncwebError::operation("failed to open stats database", error))?;
34 let db = Self {
35 connection: Arc::new(Mutex::new(connection)),
36 };
37 db.init_schema()?;
38 Ok(db)
39 }
40
41 fn init_schema(&self) -> Result<()> {
42 let connection = self
43 .connection
44 .lock()
45 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
46 connection
47 .busy_timeout(std::time::Duration::from_secs(5))
48 .map_err(|error| SyncwebError::operation("failed to configure stats database", error))?;
49 connection
50 .pragma_update(None, "journal_mode", "WAL")
51 .map_err(|error| SyncwebError::operation("failed to set WAL mode", error))?;
52 connection
53 .pragma_update(None, "foreign_keys", "ON")
54 .map_err(|error| SyncwebError::operation("failed to enable foreign keys", error))?;
55 connection
56 .execute_batch(
57 "CREATE TABLE IF NOT EXISTS schema_version (
58 key TEXT PRIMARY KEY,
59 value TEXT NOT NULL
60 );
61 CREATE TABLE IF NOT EXISTS bandwidth_events (
62 id INTEGER PRIMARY KEY AUTOINCREMENT,
63 timestamp INTEGER NOT NULL,
64 direction TEXT NOT NULL CHECK(direction IN ('upload','download')),
65 bytes INTEGER NOT NULL CHECK(bytes > 0),
66 files INTEGER NOT NULL DEFAULT 1,
67 folder_namespace TEXT,
68 peer TEXT
69 );
70 CREATE INDEX IF NOT EXISTS idx_bw_ts ON bandwidth_events(timestamp);
71 CREATE INDEX IF NOT EXISTS idx_bw_folder ON bandwidth_events(folder_namespace);
72 CREATE INDEX IF NOT EXISTS idx_bw_peer ON bandwidth_events(peer);
73 CREATE TABLE IF NOT EXISTS bandwidth_period (
74 period_start INTEGER PRIMARY KEY,
75 period_end INTEGER,
76 total_upload INTEGER NOT NULL DEFAULT 0,
77 total_download INTEGER NOT NULL DEFAULT 0,
78 closed INTEGER NOT NULL DEFAULT 0
79 );
80 CREATE TABLE IF NOT EXISTS bandwidth_folder (
81 period_start INTEGER NOT NULL REFERENCES bandwidth_period(period_start),
82 folder_namespace TEXT NOT NULL,
83 upload INTEGER NOT NULL DEFAULT 0,
84 download INTEGER NOT NULL DEFAULT 0,
85 files_transferred INTEGER NOT NULL DEFAULT 0,
86 PRIMARY KEY(period_start, folder_namespace)
87 );
88 CREATE TABLE IF NOT EXISTS bandwidth_peer (
89 period_start INTEGER NOT NULL REFERENCES bandwidth_period(period_start),
90 peer TEXT NOT NULL,
91 upload INTEGER NOT NULL DEFAULT 0,
92 download INTEGER NOT NULL DEFAULT 0,
93 connection_count INTEGER NOT NULL DEFAULT 0,
94 PRIMARY KEY(period_start, peer)
95 );
96 CREATE TABLE IF NOT EXISTS daemon_log (
97 id INTEGER PRIMARY KEY AUTOINCREMENT,
98 timestamp INTEGER NOT NULL,
99 level TEXT NOT NULL CHECK(level IN ('trace','debug','info','warn','error')),
100 module TEXT,
101 message TEXT NOT NULL
102 );
103 CREATE INDEX IF NOT EXISTS idx_log_ts ON daemon_log(timestamp);
104 CREATE INDEX IF NOT EXISTS idx_log_level ON daemon_log(level);
105 CREATE TABLE IF NOT EXISTS network_events (
106 id INTEGER PRIMARY KEY AUTOINCREMENT,
107 timestamp INTEGER NOT NULL,
108 network_id TEXT NOT NULL,
109 event_type TEXT NOT NULL CHECK(event_type IN (
110 'peer_joined','peer_left','sync_started','sync_finished',
111 'relay_connected','relay_disconnected','relay_failed',
112 'topic_subscribed','topic_unsubscribed',
113 'member_added','member_removed','folder_added','folder_removed',
114 'ticket_created','ticket_accepted','kicked'
115 )),
116 peer TEXT,
117 details TEXT,
118 metadata_json TEXT
119 );
120 CREATE INDEX IF NOT EXISTS idx_network_events_ts ON network_events(timestamp);
121 CREATE INDEX IF NOT EXISTS idx_network_events_network ON network_events(network_id);
122 CREATE INDEX IF NOT EXISTS idx_network_events_type ON network_events(event_type);
123 CREATE TABLE IF NOT EXISTS network_sync_sessions (
124 id INTEGER PRIMARY KEY AUTOINCREMENT,
125 network_id TEXT NOT NULL,
126 folder_namespace TEXT NOT NULL,
127 started_at INTEGER NOT NULL,
128 finished_at INTEGER,
129 files_transferred INTEGER NOT NULL DEFAULT 0,
130 bytes_transferred INTEGER NOT NULL DEFAULT 0,
131 errors INTEGER NOT NULL DEFAULT 0,
132 status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running','completed','failed','cancelled'))
133 );
134 CREATE INDEX IF NOT EXISTS idx_network_sessions_network ON network_sync_sessions(network_id);
135 CREATE TABLE IF NOT EXISTS relay_health (
136 relay_url TEXT NOT NULL,
137 checked_at INTEGER NOT NULL,
138 connected INTEGER NOT NULL,
139 latency_ms INTEGER,
140 error_message TEXT,
141 PRIMARY KEY(relay_url, checked_at)
142 );",
143 )
144 .map_err(|error| SyncwebError::operation("failed to initialize stats database schema", error))?;
145 let _ = connection.execute_batch("ALTER TABLE bandwidth_events ADD COLUMN network_id TEXT");
146 let _ = connection.execute_batch(
147 "CREATE VIEW IF NOT EXISTS network_bandwidth_summary AS
148 SELECT network_id,
149 MIN(timestamp) AS period_start,
150 MAX(timestamp) AS period_end,
151 SUM(CASE WHEN direction='upload' THEN bytes ELSE 0 END) AS total_upload,
152 SUM(CASE WHEN direction='download' THEN bytes ELSE 0 END) AS total_download,
153 COUNT(DISTINCT peer) AS active_peers
154 FROM bandwidth_events
155 WHERE network_id IS NOT NULL
156 GROUP BY network_id",
157 );
158 connection
159 .execute(
160 "INSERT INTO schema_version(key, value) VALUES ('version', '1')
161 ON CONFLICT(key) DO NOTHING",
162 [],
163 )
164 .map_err(|error| SyncwebError::operation("failed to seed schema version", error))?;
165 drop(connection);
166 Ok(())
167 }
168
169 pub fn record_upload(
175 &self,
176 bytes: u64,
177 files: u64,
178 folder: Option<&str>,
179 peer: Option<&str>,
180 network_id: Option<&str>,
181 ) -> Result<()> {
182 self.record_transfer("upload", bytes, files, folder, peer, network_id)
183 }
184
185 pub fn record_download(
191 &self,
192 bytes: u64,
193 files: u64,
194 folder: Option<&str>,
195 peer: Option<&str>,
196 network_id: Option<&str>,
197 ) -> Result<()> {
198 self.record_transfer("download", bytes, files, folder, peer, network_id)
199 }
200
201 pub fn record_transfer(
207 &self,
208 direction: &str,
209 bytes: u64,
210 files: u64,
211 folder: Option<&str>,
212 peer: Option<&str>,
213 network_id: Option<&str>,
214 ) -> Result<()> {
215 let connection = self
216 .connection
217 .lock()
218 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
219 let now = current_timestamp();
220 connection
221 .execute(
222 "INSERT INTO bandwidth_events(timestamp, direction, bytes, files, folder_namespace, peer, network_id)
223 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
224 params![
225 now,
226 direction,
227 bytes.cast_signed(),
228 files.cast_signed(),
229 folder,
230 peer,
231 network_id
232 ],
233 )
234 .map_err(|error| SyncwebError::operation("failed to record transfer event", error))?;
235 drop(connection);
236 Ok(())
237 }
238
239 pub fn record_connection(&self, peer: &str) -> Result<()> {
245 let connection = self
246 .connection
247 .lock()
248 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
249 let now = current_timestamp();
250 let period = find_or_create_period(&connection, now)
251 .map_err(|error| SyncwebError::operation("failed to find or create bandwidth period", error))?;
252 connection
253 .execute(
254 "INSERT INTO bandwidth_peer(period_start, peer, upload, download, connection_count)
255 VALUES (?1, ?2, 0, 0, 1)
256 ON CONFLICT(period_start, peer) DO UPDATE SET
257 connection_count = connection_count + 1",
258 params![period, peer],
259 )
260 .map_err(|error| SyncwebError::operation("failed to record connection", error))?;
261 drop(connection);
262 Ok(())
263 }
264
265 pub fn current_stats(&self) -> Result<BandwidthStats> {
271 let connection = self
272 .connection
273 .lock()
274 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
275 let now = current_timestamp();
276 let period_start = find_or_create_period(&connection, now)
277 .map_err(|error| SyncwebError::operation("failed to find or create bandwidth period", error))?;
278
279 let (total_upload, total_download): (i64, i64) = connection
280 .query_row(
281 "SELECT COALESCE(SUM(CASE WHEN direction='upload' THEN bytes ELSE 0 END), 0),
282 COALESCE(SUM(CASE WHEN direction='download' THEN bytes ELSE 0 END), 0)
283 FROM bandwidth_events WHERE timestamp >= ?1",
284 params![period_start],
285 |row| Ok((row.get(0)?, row.get(1)?)),
286 )
287 .map_err(|error| SyncwebError::operation("failed to query bandwidth totals", error))?;
288
289 let mut per_folder = BTreeMap::new();
290 let mut folder_stmt = connection
291 .prepare(
292 "SELECT folder_namespace, upload, download, files_transferred
293 FROM bandwidth_folder WHERE period_start = ?1",
294 )
295 .map_err(|error| SyncwebError::operation("failed to prepare folder stats query", error))?;
296 let folder_rows = folder_stmt
297 .query_map(params![period_start], |row| {
298 Ok((
299 row.get::<_, String>(0)?,
300 row.get::<_, i64>(1)?,
301 row.get::<_, i64>(2)?,
302 row.get::<_, i64>(3)?,
303 ))
304 })
305 .map_err(|error| SyncwebError::operation("failed to query folder stats", error))?
306 .collect::<std::result::Result<Vec<_>, _>>()
307 .map_err(|error| SyncwebError::operation("failed to read folder stats", error))?;
308 for (ns, upload, download, files) in folder_rows {
309 per_folder.insert(
310 ns,
311 FolderStats {
312 upload: upload.cast_unsigned(),
313 download: download.cast_unsigned(),
314 files_transferred: files.cast_unsigned(),
315 },
316 );
317 }
318
319 let mut per_peer = BTreeMap::new();
320 let mut peer_stmt = connection
321 .prepare(
322 "SELECT peer, upload, download, connection_count
323 FROM bandwidth_peer WHERE period_start = ?1",
324 )
325 .map_err(|error| SyncwebError::operation("failed to prepare peer stats query", error))?;
326 let peer_rows = peer_stmt
327 .query_map(params![period_start], |row| {
328 Ok((
329 row.get::<_, String>(0)?,
330 row.get::<_, i64>(1)?,
331 row.get::<_, i64>(2)?,
332 row.get::<_, i64>(3)?,
333 ))
334 })
335 .map_err(|error| SyncwebError::operation("failed to query peer stats", error))?
336 .collect::<std::result::Result<Vec<_>, _>>()
337 .map_err(|error| SyncwebError::operation("failed to read peer stats", error))?;
338 for (peer, upload, download, conns) in peer_rows {
339 per_peer.insert(
340 peer,
341 PeerStats {
342 upload: upload.cast_unsigned(),
343 download: download.cast_unsigned(),
344 connection_count: conns.cast_unsigned(),
345 },
346 );
347 }
348
349 drop(folder_stmt);
350 drop(peer_stmt);
351 drop(connection);
352 Ok(BandwidthStats {
353 total_upload: total_upload.cast_unsigned(),
354 total_download: total_download.cast_unsigned(),
355 per_folder,
356 per_peer,
357 period_start: period_start.cast_unsigned(),
358 })
359 }
360
361 pub fn reset_bandwidth(&self) -> Result<()> {
368 let connection = self
369 .connection
370 .lock()
371 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
372 connection
373 .execute("DELETE FROM bandwidth_events", [])
374 .map_err(|error| SyncwebError::operation("failed to clear bandwidth events", error))?;
375 connection
376 .execute("DELETE FROM bandwidth_folder", [])
377 .map_err(|error| SyncwebError::operation("failed to clear folder bandwidth", error))?;
378 connection
379 .execute("DELETE FROM bandwidth_peer", [])
380 .map_err(|error| SyncwebError::operation("failed to clear peer bandwidth", error))?;
381 connection
382 .execute("DELETE FROM bandwidth_period", [])
383 .map_err(|error| SyncwebError::operation("failed to clear bandwidth periods", error))?;
384 drop(connection);
385 Ok(())
386 }
387
388 pub fn append_log(&self, level: &str, module: Option<&str>, message: &str) -> Result<()> {
394 let connection = self
395 .connection
396 .lock()
397 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
398 let now = current_timestamp();
399 connection
400 .execute(
401 "INSERT INTO daemon_log(timestamp, level, module, message) VALUES (?1, ?2, ?3, ?4)",
402 params![now, level, module, message],
403 )
404 .map_err(|error| SyncwebError::operation("failed to append log entry", error))?;
405 drop(connection);
406 Ok(())
407 }
408
409 pub fn purge_old_bandwidth(&self, older_than: Duration) -> Result<usize> {
415 let connection = self
416 .connection
417 .lock()
418 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
419 let cutoff = current_timestamp().saturating_sub(older_than.as_secs().cast_signed());
420 let deleted = connection
421 .execute("DELETE FROM bandwidth_events WHERE timestamp < ?1", params![cutoff])
422 .map_err(|error| SyncwebError::operation("failed to purge old bandwidth events", error))?;
423 drop(connection);
424 Ok(deleted)
425 }
426
427 pub fn record_network_event(
433 &self,
434 network_id: &str,
435 event_type: &str,
436 peer: Option<&str>,
437 details: Option<&str>,
438 metadata_json: Option<&str>,
439 ) -> Result<()> {
440 let connection = self
441 .connection
442 .lock()
443 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
444 let now = current_timestamp();
445 connection
446 .execute(
447 "INSERT INTO network_events(timestamp, network_id, event_type, peer, details, metadata_json)
448 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
449 params![now, network_id, event_type, peer, details, metadata_json],
450 )
451 .map_err(|error| SyncwebError::operation("failed to record network event", error))?;
452 drop(connection);
453 Ok(())
454 }
455
456 pub fn record_sync_session_start(&self, network_id: &str, folder_namespace: &str) -> Result<i64> {
462 let connection = self
463 .connection
464 .lock()
465 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
466 let now = current_timestamp();
467 connection
468 .execute(
469 "INSERT INTO network_sync_sessions(network_id, folder_namespace, started_at)
470 VALUES (?1, ?2, ?3)",
471 params![network_id, folder_namespace, now],
472 )
473 .map_err(|error| SyncwebError::operation("failed to record sync session start", error))?;
474 let session_id = connection.last_insert_rowid();
475 drop(connection);
476 Ok(session_id)
477 }
478
479 pub fn record_sync_session_finish(
485 &self,
486 session_id: i64,
487 files: u64,
488 bytes: u64,
489 errors: u64,
490 status: &str,
491 ) -> Result<()> {
492 let connection = self
493 .connection
494 .lock()
495 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
496 let now = current_timestamp();
497 connection
498 .execute(
499 "UPDATE network_sync_sessions SET finished_at = ?1, files_transferred = ?2,
500 bytes_transferred = ?3, errors = ?4, status = ?5 WHERE id = ?6",
501 params![
502 now,
503 files.cast_signed(),
504 bytes.cast_signed(),
505 errors.cast_signed(),
506 status,
507 session_id
508 ],
509 )
510 .map_err(|error| SyncwebError::operation("failed to record sync session finish", error))?;
511 drop(connection);
512 Ok(())
513 }
514
515 pub fn record_relay_check(
521 &self,
522 relay_url: &str,
523 connected: bool,
524 latency_ms: Option<i64>,
525 error_message: Option<&str>,
526 ) -> Result<()> {
527 let connection = self
528 .connection
529 .lock()
530 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
531 let now = current_timestamp();
532 connection
533 .execute(
534 "INSERT INTO relay_health(relay_url, checked_at, connected, latency_ms, error_message)
535 VALUES (?1, ?2, ?3, ?4, ?5)",
536 params![relay_url, now, i64::from(connected), latency_ms, error_message],
537 )
538 .map_err(|error| SyncwebError::operation("failed to record relay check", error))?;
539 drop(connection);
540 Ok(())
541 }
542
543 pub fn recent_network_events(&self, network_id: &str, limit: usize) -> Result<Vec<NetworkEventRecord>> {
549 let connection = self
550 .connection
551 .lock()
552 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
553 let mut stmt = connection
554 .prepare(
555 "SELECT id, timestamp, network_id, event_type, peer, details, metadata_json
556 FROM network_events WHERE network_id = ?1
557 ORDER BY timestamp DESC LIMIT ?2",
558 )
559 .map_err(|error| SyncwebError::operation("failed to prepare network events query", error))?;
560 let records = stmt
561 .query_map(params![network_id, limit.cast_signed()], |row| {
562 Ok(NetworkEventRecord {
563 id: row.get(0)?,
564 timestamp: row.get(1)?,
565 network_id: row.get(2)?,
566 event_type: row.get(3)?,
567 peer: row.get(4)?,
568 details: row.get(5)?,
569 metadata_json: row.get(6)?,
570 })
571 })
572 .map_err(|error| SyncwebError::operation("failed to query network events", error))?
573 .collect::<std::result::Result<Vec<_>, _>>()
574 .map_err(|error| SyncwebError::operation("failed to read network event rows", error))?;
575 drop(stmt);
576 drop(connection);
577 Ok(records)
578 }
579
580 pub fn recent_sync_sessions(&self, network_id: &str, limit: usize) -> Result<Vec<SyncSessionRecord>> {
586 let connection = self
587 .connection
588 .lock()
589 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
590 let mut stmt = connection
591 .prepare(
592 "SELECT id, network_id, folder_namespace, started_at, finished_at,
593 files_transferred, bytes_transferred, errors, status
594 FROM network_sync_sessions WHERE network_id = ?1
595 ORDER BY started_at DESC LIMIT ?2",
596 )
597 .map_err(|error| SyncwebError::operation("failed to prepare sync sessions query", error))?;
598 let records = stmt
599 .query_map(params![network_id, limit.cast_signed()], |row| {
600 Ok(SyncSessionRecord {
601 id: row.get(0)?,
602 network_id: row.get(1)?,
603 folder_namespace: row.get(2)?,
604 started_at: row.get(3)?,
605 finished_at: row.get(4)?,
606 files_transferred: row.get::<_, i64>(5)?.cast_unsigned(),
607 bytes_transferred: row.get::<_, i64>(6)?.cast_unsigned(),
608 errors: row.get::<_, i64>(7)?.cast_unsigned(),
609 status: row.get(8)?,
610 })
611 })
612 .map_err(|error| SyncwebError::operation("failed to query sync sessions", error))?
613 .collect::<std::result::Result<Vec<_>, _>>()
614 .map_err(|error| SyncwebError::operation("failed to read sync session rows", error))?;
615 drop(stmt);
616 drop(connection);
617 Ok(records)
618 }
619
620 pub fn relay_uptime(&self, relay_url: &str, window_secs: i64) -> Result<f64> {
626 let connection = self
627 .connection
628 .lock()
629 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
630 let cutoff = current_timestamp().saturating_sub(window_secs);
631 let (total, connected): (i64, i64) = connection
632 .query_row(
633 "SELECT COUNT(*), SUM(connected) FROM relay_health
634 WHERE relay_url = ?1 AND checked_at >= ?2",
635 params![relay_url, cutoff],
636 |row| Ok((row.get(0)?, row.get::<_, Option<i64>>(1)?.unwrap_or(0))),
637 )
638 .map_err(|error| SyncwebError::operation("failed to query relay uptime", error))?;
639 drop(connection);
640 Ok(if total > 0 {
641 f64::from(i32::try_from(connected).unwrap_or(0))
642 .mul_add(f64::from(i32::try_from(total).unwrap_or(1)).recip(), 0.0)
643 } else {
644 1.0
645 })
646 }
647
648 pub fn vacuum(&self) -> Result<()> {
654 let connection = self
655 .connection
656 .lock()
657 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
658 connection
659 .execute_batch("VACUUM")
660 .map_err(|error| SyncwebError::operation("vacuum failed", error))?;
661 drop(connection);
662 Ok(())
663 }
664
665 pub fn size_on_disk(&self) -> Result<u64> {
671 let connection = self
672 .connection
673 .lock()
674 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
675 let db_path: String = connection
676 .query_row("PRAGMA database_list", [], |row| row.get(2))
677 .map_err(|error| SyncwebError::operation("failed to query database path", error))?;
678 drop(connection);
679 std::fs::metadata(&db_path)
680 .map(|meta| meta.len())
681 .map_err(|error| SyncwebError::operation("failed to get database file size", error))
682 }
683
684 pub fn freelist_count(&self) -> Result<i64> {
690 let connection = self
691 .connection
692 .lock()
693 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
694 let count: i64 = connection
695 .pragma_query_value(None, "freelist_count", |row| row.get(0))
696 .map_err(|error| SyncwebError::operation("failed to query freelist count", error))?;
697 drop(connection);
698 Ok(count)
699 }
700
701 pub fn check_integrity(&self) -> Result<Vec<String>> {
707 let connection = self
708 .connection
709 .lock()
710 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
711 let mut stmt = connection
712 .prepare("PRAGMA integrity_check")
713 .map_err(|error| SyncwebError::operation("failed to prepare integrity check", error))?;
714 let errors: Vec<String> = stmt
715 .query_map([], |row| row.get::<_, String>(0))
716 .map_err(|error| SyncwebError::operation("failed to run integrity check", error))?
717 .filter_map(std::result::Result::ok)
718 .filter(|s| s != "ok")
719 .collect();
720 drop(stmt);
721 drop(connection);
722 Ok(errors)
723 }
724
725 pub fn purge_old_logs(&self, older_than: Duration) -> Result<usize> {
731 let connection = self
732 .connection
733 .lock()
734 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
735 let cutoff = current_timestamp().saturating_sub(older_than.as_secs().cast_signed());
736 let deleted = connection
737 .execute("DELETE FROM daemon_log WHERE timestamp < ?1", params![cutoff])
738 .map_err(|error| SyncwebError::operation("failed to purge old log entries", error))?;
739 drop(connection);
740 Ok(deleted)
741 }
742}
743
744#[derive(Clone, Debug)]
746#[non_exhaustive]
747pub struct NetworkEventRecord {
748 pub id: i64,
749 pub timestamp: i64,
750 pub network_id: String,
751 pub event_type: String,
752 pub peer: Option<String>,
753 pub details: Option<String>,
754 pub metadata_json: Option<String>,
755}
756
757#[derive(Clone, Debug)]
759#[non_exhaustive]
760pub struct SyncSessionRecord {
761 pub id: i64,
762 pub network_id: String,
763 pub folder_namespace: String,
764 pub started_at: i64,
765 pub finished_at: Option<i64>,
766 pub files_transferred: u64,
767 pub bytes_transferred: u64,
768 pub errors: u64,
769 pub status: String,
770}
771
772fn current_timestamp() -> i64 {
773 std::time::SystemTime::now()
774 .duration_since(std::time::UNIX_EPOCH)
775 .map_or(0, |d| d.as_secs().cast_signed())
776}
777
778impl crate::storage::Vacuumable for StatsDatabase {
779 fn vacuum(&self) -> Result<()> {
780 self.vacuum()
781 }
782
783 fn freelist_count(&self) -> Result<i64> {
784 self.freelist_count()
785 }
786}
787
788fn find_or_create_period(connection: &Connection, now: i64) -> std::result::Result<i64, rusqlite::Error> {
789 let period_start = now.div_euclid(3600).saturating_mul(3600);
790 connection.execute(
791 "INSERT OR IGNORE INTO bandwidth_period(period_start, total_upload, total_download, closed)
792 VALUES (?1, 0, 0, 0)",
793 params![period_start],
794 )?;
795 Ok(period_start)
796}