heldar_kernel/services/
storage.rs1use std::ffi::CString;
5use std::os::unix::ffi::OsStrExt;
6use std::path::{Path, PathBuf};
7
8use chrono::{DateTime, Duration, Utc};
9use serde::Serialize;
10use sqlx::SqlitePool;
11
12use crate::config::Config;
13
14#[derive(Debug, Clone, Copy, Serialize, Default)]
15pub struct DiskStats {
16 pub total_bytes: u64,
17 pub free_bytes: u64,
18 pub used_bytes: u64,
19 pub used_percent: f64,
20}
21
22pub fn disk_stats(path: &Path) -> Option<DiskStats> {
24 let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
25 let stat = unsafe {
28 let mut stat: libc::statvfs = std::mem::zeroed();
29 if libc::statvfs(c_path.as_ptr(), &mut stat) != 0 {
30 return None;
31 }
32 stat
33 };
34 let block = stat.f_frsize as u64;
35 let total = stat.f_blocks as u64 * block;
36 let free = stat.f_bavail as u64 * block;
38 let free_total = stat.f_bfree as u64 * block;
41 let used = total.saturating_sub(free_total);
42 let used_percent = if total > 0 {
43 used as f64 / total as f64 * 100.0
44 } else {
45 0.0
46 };
47 Some(DiskStats {
48 total_bytes: total,
49 free_bytes: free,
50 used_bytes: used,
51 used_percent,
52 })
53}
54
55pub async fn disk_stats_async(path: PathBuf) -> Option<DiskStats> {
57 tokio::task::spawn_blocking(move || disk_stats(&path))
58 .await
59 .ok()
60 .flatten()
61}
62
63#[derive(Debug, Clone, Serialize)]
64pub struct StorageReport {
65 pub disk: Option<DiskStats>,
66 pub recordings_bytes: i64,
67 pub segment_count: i64,
68 pub oldest_segment: Option<DateTime<Utc>>,
69 pub newest_segment: Option<DateTime<Utc>>,
70 pub write_rate_bytes_per_day: i64,
72 pub projected_days_remaining: Option<f64>,
74}
75
76pub async fn storage_report(pool: &SqlitePool, cfg: &Config) -> sqlx::Result<StorageReport> {
78 let recordings_bytes: i64 =
79 sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments")
80 .fetch_one(pool)
81 .await?;
82 let segment_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM segments")
83 .fetch_one(pool)
84 .await?;
85 let oldest_segment: Option<DateTime<Utc>> =
86 sqlx::query_scalar("SELECT MIN(start_time) FROM segments")
87 .fetch_one(pool)
88 .await?;
89 let newest_segment: Option<DateTime<Utc>> =
90 sqlx::query_scalar("SELECT MAX(end_time) FROM segments")
91 .fetch_one(pool)
92 .await?;
93
94 let since = Utc::now() - Duration::hours(24);
97 let last_day_bytes: i64 =
98 sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments WHERE end_time >= ?")
99 .bind(since)
100 .fetch_one(pool)
101 .await?;
102
103 let disk = disk_stats_async(cfg.recordings_dir.clone()).await;
104 let projected_days_remaining = match (disk, last_day_bytes) {
105 (Some(d), rate) if rate > 0 => Some(d.free_bytes as f64 / rate as f64),
106 _ => None,
107 };
108
109 Ok(StorageReport {
110 disk,
111 recordings_bytes,
112 segment_count,
113 oldest_segment,
114 newest_segment,
115 write_rate_bytes_per_day: last_day_bytes,
116 projected_days_remaining,
117 })
118}