use crate::config::DiskConfig;
use crate::errors::ResourceError;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::fs;
use tracing::{debug, info, warn};
#[allow(dead_code)] async fn ensure_dir(path: &Path) -> Result<(), ResourceError> {
fs::create_dir_all(path).await.map_err(|e| {
ResourceError::DiskExhausted(format!(
"Failed to create directory {}: {}",
path.display(),
e
))
})
}
pub struct DiskManager {
#[allow(dead_code)]
config: DiskConfig,
checkpoints_path: PathBuf,
#[allow(dead_code)] logs_path: PathBuf,
models_path: PathBuf,
models_size_cache: std::sync::Mutex<Option<(std::time::SystemTime, u64)>>,
}
#[derive(Debug, Clone, Default)]
pub struct DiskUsage {
pub used: u64,
pub total: u64,
pub available: u64,
pub percent: f32,
}
#[derive(Debug, Clone)]
pub struct StorageEstimate {
pub checkpoints: u64,
pub logs: u64,
pub models: u64,
pub buffer: u64,
}
impl StorageEstimate {
pub fn total(&self) -> u64 {
self.checkpoints + self.logs + self.models + self.buffer
}
}
impl DiskManager {
pub async fn new(config: &DiskConfig) -> Result<Self, ResourceError> {
let checkpoints_path = std::env::var("SELFWARE_CHECKPOINT_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./checkpoints"));
let logs_path = std::env::var("SELFWARE_LOG_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./logs"));
let models_path = std::env::var("SELFWARE_MODEL_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./models"));
info!(
checkpoints = %checkpoints_path.display(),
logs = %logs_path.display(),
models = %models_path.display(),
"Disk manager initialized"
);
Ok(Self::with_paths(
config,
checkpoints_path,
logs_path,
models_path,
))
}
fn with_paths(
config: &DiskConfig,
checkpoints_path: PathBuf,
logs_path: PathBuf,
models_path: PathBuf,
) -> Self {
Self {
config: config.clone(),
checkpoints_path,
logs_path,
models_path,
models_size_cache: std::sync::Mutex::new(None),
}
}
pub async fn get_usage(&self) -> Result<DiskUsage, ResourceError> {
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
for disk in disks.list() {
if self.checkpoints_path.starts_with(disk.mount_point()) {
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
return Ok(DiskUsage {
used,
total,
available,
percent: if total > 0 {
used as f32 / total as f32
} else {
0.0
},
});
}
}
let current = std::env::current_dir().map_err(|e| {
ResourceError::DiskExhausted(format!("Failed to get current directory: {}", e))
})?;
for disk in disks.list() {
if current.starts_with(disk.mount_point()) {
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
return Ok(DiskUsage {
used,
total,
available,
percent: if total > 0 {
used as f32 / total as f32
} else {
0.0
},
});
}
}
Err(ResourceError::DiskExhausted(
"Could not determine disk usage".to_string(),
))
}
#[allow(dead_code)]
async fn perform_maintenance(&self) -> Result<(), ResourceError> {
debug!("Starting disk maintenance");
let usage = self.get_usage().await?;
if usage.percent > self.config.max_usage_percent {
warn!(percent = usage.percent, "Disk usage high, cleaning up");
self.cleanup_old_files().await?;
}
self.compress_old_checkpoints().await?;
self.cleanup_orphaned_files().await?;
debug!("Disk maintenance completed");
Ok(())
}
#[allow(dead_code)] async fn cleanup_old_files(&self) -> Result<u64, ResourceError> {
let mut deleted = 0u64;
if let Ok(mut entries) = fs::read_dir(&self.logs_path).await {
let cutoff = std::time::SystemTime::now() - Duration::from_secs(7 * 24 * 3600);
while let Ok(Some(entry)) = entries.next_entry().await {
if let Ok(metadata) = entry.metadata().await {
if let Ok(modified) = metadata.modified() {
if modified < cutoff {
if let Err(e) = fs::remove_file(entry.path()).await {
debug!(path = %entry.path().display(), error = %e, "Failed to delete old log");
} else {
deleted += 1;
}
}
}
}
}
}
info!(deleted_files = deleted, "Old files cleaned up");
Ok(deleted)
}
#[allow(dead_code)] async fn compress_old_checkpoints(&self) -> Result<u64, ResourceError> {
let mut compressed = 0u64;
let compress_after =
Duration::from_secs(self.config.compress_after_days as u64 * 24 * 3600);
let cutoff = std::time::SystemTime::now() - compress_after;
if let Ok(mut entries) = fs::read_dir(&self.checkpoints_path).await {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path.extension().map(|e| e == "zst").unwrap_or(false) {
continue;
}
if let Ok(metadata) = entry.metadata().await {
if let Ok(modified) = metadata.modified() {
if modified < cutoff {
if let Err(e) = self.compress_file(&path).await {
debug!(path = %path.display(), error = %e, "Failed to compress checkpoint");
} else {
compressed += 1;
}
}
}
}
}
}
info!(compressed_files = compressed, "Old checkpoints compressed");
Ok(compressed)
}
#[allow(dead_code)] async fn compress_file(&self, path: &PathBuf) -> Result<(), ResourceError> {
let data = fs::read(path)
.await
.map_err(|e| ResourceError::DiskExhausted(format!("Failed to read file: {}", e)))?;
let compressed = zstd::encode_all(&data[..], 6)
.map_err(|e| ResourceError::DiskExhausted(format!("Failed to compress: {}", e)))?;
let mut new_path = path.clone();
new_path.set_extension("chk.zst");
if let Some(parent) = new_path.parent() {
ensure_dir(parent).await?;
}
fs::write(&new_path, &compressed).await.map_err(|e| {
ResourceError::DiskExhausted(format!("Failed to write compressed file: {}", e))
})?;
fs::remove_file(path).await.map_err(|e| {
ResourceError::DiskExhausted(format!("Failed to remove original file: {}", e))
})?;
Ok(())
}
#[allow(dead_code)] async fn cleanup_orphaned_files(&self) -> Result<u64, ResourceError> {
Ok(0)
}
pub fn estimate_storage_needs(&self, days: u32) -> StorageEstimate {
let daily_checkpoint_size = 500 * 1024 * 1024u64; let daily_log_size = 100 * 1024 * 1024u64;
StorageEstimate {
checkpoints: daily_checkpoint_size * days as u64,
logs: daily_log_size * days as u64,
models: self.get_models_size(),
buffer: daily_checkpoint_size * 2, }
}
fn get_models_size(&self) -> u64 {
const CACHE_TTL: Duration = Duration::from_secs(60);
const FALLBACK_SIZE: u64 = 10_000_000_000;
if let Ok(cache) = self.models_size_cache.lock() {
if let Some((cached_at, cached_size)) = *cache {
if cached_at.elapsed().unwrap_or(CACHE_TTL * 2) < CACHE_TTL {
debug!(size = cached_size, "Returning cached models directory size");
return cached_size;
}
}
}
let size = self.calculate_dir_size(&self.models_path);
let size = if size == 0 && !self.models_path.exists() {
warn!(
path = %self.models_path.display(),
"Models directory does not exist, returning 10GB fallback estimate"
);
FALLBACK_SIZE
} else {
debug!(
size,
path = %self.models_path.display(),
"Calculated models directory size"
);
size
};
if let Ok(mut cache) = self.models_size_cache.lock() {
*cache = Some((std::time::SystemTime::now(), size));
}
size
}
fn calculate_dir_size(&self, path: &PathBuf) -> u64 {
let mut total = 0u64;
let entries = match std::fs::read_dir(path) {
Ok(e) => e,
Err(_) => return 0,
};
for entry in entries.flatten() {
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_symlink() {
continue;
}
if file_type.is_file() {
if let Ok(metadata) = entry.metadata() {
total += metadata.len();
}
} else if file_type.is_dir() {
total += self.calculate_dir_size(&entry.path());
}
}
total
}
pub async fn available_space(&self) -> Result<u64, ResourceError> {
let usage = self.get_usage().await?;
Ok(usage.available)
}
}
#[cfg(test)]
#[path = "../../tests/unit/resource/disk/disk_test.rs"]
mod tests;