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 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 append_log(&self, level: &str, module: Option<&str>, message: &str) -> Result<()> {
367 let connection = self
368 .connection
369 .lock()
370 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
371 let now = current_timestamp();
372 connection
373 .execute(
374 "INSERT INTO daemon_log(timestamp, level, module, message) VALUES (?1, ?2, ?3, ?4)",
375 params![now, level, module, message],
376 )
377 .map_err(|error| SyncwebError::operation("failed to append log entry", error))?;
378 drop(connection);
379 Ok(())
380 }
381
382 pub fn purge_old_bandwidth(&self, older_than: Duration) -> Result<usize> {
388 let connection = self
389 .connection
390 .lock()
391 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
392 let cutoff = current_timestamp().saturating_sub(older_than.as_secs().cast_signed());
393 let deleted = connection
394 .execute("DELETE FROM bandwidth_events WHERE timestamp < ?1", params![cutoff])
395 .map_err(|error| SyncwebError::operation("failed to purge old bandwidth events", error))?;
396 drop(connection);
397 Ok(deleted)
398 }
399
400 pub fn record_network_event(
406 &self,
407 network_id: &str,
408 event_type: &str,
409 peer: Option<&str>,
410 details: Option<&str>,
411 metadata_json: Option<&str>,
412 ) -> Result<()> {
413 let connection = self
414 .connection
415 .lock()
416 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
417 let now = current_timestamp();
418 connection
419 .execute(
420 "INSERT INTO network_events(timestamp, network_id, event_type, peer, details, metadata_json)
421 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
422 params![now, network_id, event_type, peer, details, metadata_json],
423 )
424 .map_err(|error| SyncwebError::operation("failed to record network event", error))?;
425 drop(connection);
426 Ok(())
427 }
428
429 pub fn record_sync_session_start(&self, network_id: &str, folder_namespace: &str) -> Result<i64> {
435 let connection = self
436 .connection
437 .lock()
438 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
439 let now = current_timestamp();
440 connection
441 .execute(
442 "INSERT INTO network_sync_sessions(network_id, folder_namespace, started_at)
443 VALUES (?1, ?2, ?3)",
444 params![network_id, folder_namespace, now],
445 )
446 .map_err(|error| SyncwebError::operation("failed to record sync session start", error))?;
447 let session_id = connection.last_insert_rowid();
448 drop(connection);
449 Ok(session_id)
450 }
451
452 pub fn record_sync_session_finish(
458 &self,
459 session_id: i64,
460 files: u64,
461 bytes: u64,
462 errors: u64,
463 status: &str,
464 ) -> Result<()> {
465 let connection = self
466 .connection
467 .lock()
468 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
469 let now = current_timestamp();
470 connection
471 .execute(
472 "UPDATE network_sync_sessions SET finished_at = ?1, files_transferred = ?2,
473 bytes_transferred = ?3, errors = ?4, status = ?5 WHERE id = ?6",
474 params![
475 now,
476 files.cast_signed(),
477 bytes.cast_signed(),
478 errors.cast_signed(),
479 status,
480 session_id
481 ],
482 )
483 .map_err(|error| SyncwebError::operation("failed to record sync session finish", error))?;
484 drop(connection);
485 Ok(())
486 }
487
488 pub fn record_relay_check(
494 &self,
495 relay_url: &str,
496 connected: bool,
497 latency_ms: Option<i64>,
498 error_message: Option<&str>,
499 ) -> Result<()> {
500 let connection = self
501 .connection
502 .lock()
503 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
504 let now = current_timestamp();
505 connection
506 .execute(
507 "INSERT INTO relay_health(relay_url, checked_at, connected, latency_ms, error_message)
508 VALUES (?1, ?2, ?3, ?4, ?5)",
509 params![relay_url, now, i64::from(connected), latency_ms, error_message],
510 )
511 .map_err(|error| SyncwebError::operation("failed to record relay check", error))?;
512 drop(connection);
513 Ok(())
514 }
515
516 pub fn recent_network_events(&self, network_id: &str, limit: usize) -> Result<Vec<NetworkEventRecord>> {
522 let connection = self
523 .connection
524 .lock()
525 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
526 let mut stmt = connection
527 .prepare(
528 "SELECT id, timestamp, network_id, event_type, peer, details, metadata_json
529 FROM network_events WHERE network_id = ?1
530 ORDER BY timestamp DESC LIMIT ?2",
531 )
532 .map_err(|error| SyncwebError::operation("failed to prepare network events query", error))?;
533 let records = stmt
534 .query_map(params![network_id, limit.cast_signed()], |row| {
535 Ok(NetworkEventRecord {
536 id: row.get(0)?,
537 timestamp: row.get(1)?,
538 network_id: row.get(2)?,
539 event_type: row.get(3)?,
540 peer: row.get(4)?,
541 details: row.get(5)?,
542 metadata_json: row.get(6)?,
543 })
544 })
545 .map_err(|error| SyncwebError::operation("failed to query network events", error))?
546 .collect::<std::result::Result<Vec<_>, _>>()
547 .map_err(|error| SyncwebError::operation("failed to read network event rows", error))?;
548 drop(stmt);
549 drop(connection);
550 Ok(records)
551 }
552
553 pub fn recent_sync_sessions(&self, network_id: &str, limit: usize) -> Result<Vec<SyncSessionRecord>> {
559 let connection = self
560 .connection
561 .lock()
562 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
563 let mut stmt = connection
564 .prepare(
565 "SELECT id, network_id, folder_namespace, started_at, finished_at,
566 files_transferred, bytes_transferred, errors, status
567 FROM network_sync_sessions WHERE network_id = ?1
568 ORDER BY started_at DESC LIMIT ?2",
569 )
570 .map_err(|error| SyncwebError::operation("failed to prepare sync sessions query", error))?;
571 let records = stmt
572 .query_map(params![network_id, limit.cast_signed()], |row| {
573 Ok(SyncSessionRecord {
574 id: row.get(0)?,
575 network_id: row.get(1)?,
576 folder_namespace: row.get(2)?,
577 started_at: row.get(3)?,
578 finished_at: row.get(4)?,
579 files_transferred: row.get::<_, i64>(5)?.cast_unsigned(),
580 bytes_transferred: row.get::<_, i64>(6)?.cast_unsigned(),
581 errors: row.get::<_, i64>(7)?.cast_unsigned(),
582 status: row.get(8)?,
583 })
584 })
585 .map_err(|error| SyncwebError::operation("failed to query sync sessions", error))?
586 .collect::<std::result::Result<Vec<_>, _>>()
587 .map_err(|error| SyncwebError::operation("failed to read sync session rows", error))?;
588 drop(stmt);
589 drop(connection);
590 Ok(records)
591 }
592
593 pub fn relay_uptime(&self, relay_url: &str, window_secs: i64) -> Result<f64> {
599 let connection = self
600 .connection
601 .lock()
602 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
603 let cutoff = current_timestamp().saturating_sub(window_secs);
604 let (total, connected): (i64, i64) = connection
605 .query_row(
606 "SELECT COUNT(*), SUM(connected) FROM relay_health
607 WHERE relay_url = ?1 AND checked_at >= ?2",
608 params![relay_url, cutoff],
609 |row| Ok((row.get(0)?, row.get::<_, Option<i64>>(1)?.unwrap_or(0))),
610 )
611 .map_err(|error| SyncwebError::operation("failed to query relay uptime", error))?;
612 drop(connection);
613 Ok(if total > 0 {
614 f64::from(i32::try_from(connected).unwrap_or(0))
615 .mul_add(f64::from(i32::try_from(total).unwrap_or(1)).recip(), 0.0)
616 } else {
617 1.0
618 })
619 }
620
621 pub fn vacuum(&self) -> Result<()> {
627 let connection = self
628 .connection
629 .lock()
630 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
631 connection
632 .execute_batch("VACUUM")
633 .map_err(|error| SyncwebError::operation("vacuum failed", error))?;
634 drop(connection);
635 Ok(())
636 }
637
638 pub fn size_on_disk(&self) -> Result<u64> {
644 let connection = self
645 .connection
646 .lock()
647 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
648 let db_path: String = connection
649 .query_row("PRAGMA database_list", [], |row| row.get(2))
650 .map_err(|error| SyncwebError::operation("failed to query database path", error))?;
651 drop(connection);
652 std::fs::metadata(&db_path)
653 .map(|meta| meta.len())
654 .map_err(|error| SyncwebError::operation("failed to get database file size", error))
655 }
656
657 pub fn freelist_count(&self) -> Result<i64> {
663 let connection = self
664 .connection
665 .lock()
666 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
667 let count: i64 = connection
668 .pragma_query_value(None, "freelist_count", |row| row.get(0))
669 .map_err(|error| SyncwebError::operation("failed to query freelist count", error))?;
670 drop(connection);
671 Ok(count)
672 }
673
674 pub fn check_integrity(&self) -> Result<Vec<String>> {
680 let connection = self
681 .connection
682 .lock()
683 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
684 let mut stmt = connection
685 .prepare("PRAGMA integrity_check")
686 .map_err(|error| SyncwebError::operation("failed to prepare integrity check", error))?;
687 let errors: Vec<String> = stmt
688 .query_map([], |row| row.get::<_, String>(0))
689 .map_err(|error| SyncwebError::operation("failed to run integrity check", error))?
690 .filter_map(std::result::Result::ok)
691 .filter(|s| s != "ok")
692 .collect();
693 drop(stmt);
694 drop(connection);
695 Ok(errors)
696 }
697
698 pub fn purge_old_logs(&self, older_than: Duration) -> Result<usize> {
704 let connection = self
705 .connection
706 .lock()
707 .map_err(|error| SyncwebError::operation("stats database mutex is poisoned", error))?;
708 let cutoff = current_timestamp().saturating_sub(older_than.as_secs().cast_signed());
709 let deleted = connection
710 .execute("DELETE FROM daemon_log WHERE timestamp < ?1", params![cutoff])
711 .map_err(|error| SyncwebError::operation("failed to purge old log entries", error))?;
712 drop(connection);
713 Ok(deleted)
714 }
715}
716
717#[derive(Clone, Debug)]
719#[non_exhaustive]
720pub struct NetworkEventRecord {
721 pub id: i64,
722 pub timestamp: i64,
723 pub network_id: String,
724 pub event_type: String,
725 pub peer: Option<String>,
726 pub details: Option<String>,
727 pub metadata_json: Option<String>,
728}
729
730#[derive(Clone, Debug)]
732#[non_exhaustive]
733pub struct SyncSessionRecord {
734 pub id: i64,
735 pub network_id: String,
736 pub folder_namespace: String,
737 pub started_at: i64,
738 pub finished_at: Option<i64>,
739 pub files_transferred: u64,
740 pub bytes_transferred: u64,
741 pub errors: u64,
742 pub status: String,
743}
744
745fn current_timestamp() -> i64 {
746 std::time::SystemTime::now()
747 .duration_since(std::time::UNIX_EPOCH)
748 .map_or(0, |d| d.as_secs().cast_signed())
749}
750
751impl crate::storage::Vacuumable for StatsDatabase {
752 fn vacuum(&self) -> Result<()> {
753 self.vacuum()
754 }
755
756 fn freelist_count(&self) -> Result<i64> {
757 self.freelist_count()
758 }
759}
760
761fn find_or_create_period(connection: &Connection, now: i64) -> std::result::Result<i64, rusqlite::Error> {
762 let period_start = now.div_euclid(3600).saturating_mul(3600);
763 connection.execute(
764 "INSERT OR IGNORE INTO bandwidth_period(period_start, total_upload, total_download, closed)
765 VALUES (?1, 0, 0, 0)",
766 params![period_start],
767 )?;
768 Ok(period_start)
769}