use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Duration, Utc};
use serde::Serialize;
use sqlx::SqlitePool;
use crate::config::Config;
#[derive(Debug, Clone, Copy, Serialize, Default)]
pub struct DiskStats {
pub total_bytes: u64,
pub free_bytes: u64,
pub used_bytes: u64,
pub used_percent: f64,
}
pub fn disk_stats(path: &Path) -> Option<DiskStats> {
let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
let stat = unsafe {
let mut stat: libc::statvfs = std::mem::zeroed();
if libc::statvfs(c_path.as_ptr(), &mut stat) != 0 {
return None;
}
stat
};
let block = stat.f_frsize as u64;
let total = stat.f_blocks as u64 * block;
let free = stat.f_bavail as u64 * block;
let free_total = stat.f_bfree as u64 * block;
let used = total.saturating_sub(free_total);
let used_percent = if total > 0 {
used as f64 / total as f64 * 100.0
} else {
0.0
};
Some(DiskStats {
total_bytes: total,
free_bytes: free,
used_bytes: used,
used_percent,
})
}
pub async fn disk_stats_async(path: PathBuf) -> Option<DiskStats> {
tokio::task::spawn_blocking(move || disk_stats(&path))
.await
.ok()
.flatten()
}
#[derive(Debug, Clone, Serialize)]
pub struct StorageReport {
pub disk: Option<DiskStats>,
pub recordings_bytes: i64,
pub segment_count: i64,
pub oldest_segment: Option<DateTime<Utc>>,
pub newest_segment: Option<DateTime<Utc>>,
pub write_rate_bytes_per_day: i64,
pub projected_days_remaining: Option<f64>,
}
pub async fn storage_report(pool: &SqlitePool, cfg: &Config) -> sqlx::Result<StorageReport> {
let recordings_bytes: i64 =
sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments")
.fetch_one(pool)
.await?;
let segment_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM segments")
.fetch_one(pool)
.await?;
let oldest_segment: Option<DateTime<Utc>> =
sqlx::query_scalar("SELECT MIN(start_time) FROM segments")
.fetch_one(pool)
.await?;
let newest_segment: Option<DateTime<Utc>> =
sqlx::query_scalar("SELECT MAX(end_time) FROM segments")
.fetch_one(pool)
.await?;
let since = Utc::now() - Duration::hours(24);
let last_day_bytes: i64 =
sqlx::query_scalar("SELECT COALESCE(SUM(size_bytes), 0) FROM segments WHERE end_time >= ?")
.bind(since)
.fetch_one(pool)
.await?;
let disk = disk_stats_async(cfg.recordings_dir.clone()).await;
let projected_days_remaining = match (disk, last_day_bytes) {
(Some(d), rate) if rate > 0 => Some(d.free_bytes as f64 / rate as f64),
_ => None,
};
Ok(StorageReport {
disk,
recordings_bytes,
segment_count,
oldest_segment,
newest_segment,
write_rate_bytes_per_day: last_day_bytes,
projected_days_remaining,
})
}