//! Managed temporary directory for tasks without repositories. //! //! Tasks that don't have a repository URL and aren't subtasks (which inherit //! from parent) use a managed temp directory in ~/.makima/temp/. The directory //! is automatically cleaned up when it exceeds a size limit. use std::path::PathBuf; use tokio::fs; use uuid::Uuid; /// Maximum size of the temp directory before cleanup (5GB). const MAX_TEMP_SIZE_BYTES: u64 = 5 * 1024 * 1024 * 1024; /// Manages temporary directories for tasks without repositories. pub struct TempManager { /// Base directory for temp task directories (~/.makima/temp/). temp_dir: PathBuf, } impl TempManager { /// Create a new TempManager. pub fn new() -> Self { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); Self { temp_dir: home.join(".makima").join("temp"), } } /// Create a new TempManager with a custom base directory. #[allow(dead_code)] pub fn with_base_dir(base_dir: PathBuf) -> Self { Self { temp_dir: base_dir } } /// Get the base temp directory path. pub fn temp_dir(&self) -> &PathBuf { &self.temp_dir } /// Create a temp directory for a task. /// /// This creates a directory at ~/.makima/temp/task-{id}/ and triggers /// cleanup if the total size exceeds the limit. pub async fn create_task_dir(&self, task_id: Uuid) -> Result { // Ensure base directory exists fs::create_dir_all(&self.temp_dir).await?; // Check size and cleanup if needed if let Err(e) = self.cleanup_if_needed().await { tracing::warn!("Temp directory cleanup failed: {}", e); // Continue anyway, cleanup is best-effort } // Create task-specific directory let task_dir = self.temp_dir.join(format!("task-{}", task_id)); fs::create_dir_all(&task_dir).await?; tracing::info!( task_id = %task_id, path = %task_dir.display(), "Created temp directory for task" ); Ok(task_dir) } /// Calculate total size of temp directory recursively. async fn get_total_size(&self) -> Result { if !self.temp_dir.exists() { return Ok(0); } let mut total = 0u64; let mut stack = vec![self.temp_dir.clone()]; while let Some(dir) = stack.pop() { let mut entries = match fs::read_dir(&dir).await { Ok(e) => e, Err(_) => continue, }; while let Ok(Some(entry)) = entries.next_entry().await { let metadata = match entry.metadata().await { Ok(m) => m, Err(_) => continue, }; if metadata.is_dir() { stack.push(entry.path()); } else { total += metadata.len(); } } } Ok(total) } /// Remove oldest directories if total size exceeds limit. async fn cleanup_if_needed(&self) -> Result<(), std::io::Error> { let size = self.get_total_size().await?; if size <= MAX_TEMP_SIZE_BYTES { return Ok(()); } tracing::info!( current_size_mb = size / 1024 / 1024, limit_mb = MAX_TEMP_SIZE_BYTES / 1024 / 1024, "Temp directory exceeds size limit, starting cleanup" ); // Get all task dirs with modification times let mut dirs: Vec<(PathBuf, std::time::SystemTime, u64)> = vec![]; let mut entries = fs::read_dir(&self.temp_dir).await?; while let Ok(Some(entry)) = entries.next_entry().await { let path = entry.path(); if !path.is_dir() { continue; } let metadata = match entry.metadata().await { Ok(m) => m, Err(_) => continue, }; let modified = metadata.modified().unwrap_or(std::time::UNIX_EPOCH); let dir_size = self.get_dir_size(&path).await.unwrap_or(0); dirs.push((path, modified, dir_size)); } // Sort by oldest first dirs.sort_by(|a, b| a.1.cmp(&b.1)); // Remove oldest until under limit let mut current_size = size; for (path, _, dir_size) in dirs { if current_size <= MAX_TEMP_SIZE_BYTES { break; } tracing::info!( path = %path.display(), size_mb = dir_size / 1024 / 1024, "Removing old temp directory" ); if let Err(e) = fs::remove_dir_all(&path).await { tracing::warn!(path = %path.display(), error = %e, "Failed to remove temp directory"); continue; } current_size = current_size.saturating_sub(dir_size); } tracing::info!( new_size_mb = current_size / 1024 / 1024, "Temp directory cleanup complete" ); Ok(()) } /// Calculate size of a directory recursively. async fn get_dir_size(&self, path: &PathBuf) -> Result { let mut total = 0u64; let mut stack = vec![path.clone()]; while let Some(dir) = stack.pop() { let mut entries = match fs::read_dir(&dir).await { Ok(e) => e, Err(_) => continue, }; while let Ok(Some(entry)) = entries.next_entry().await { let metadata = match entry.metadata().await { Ok(m) => m, Err(_) => continue, }; if metadata.is_dir() { stack.push(entry.path()); } else { total += metadata.len(); } } } Ok(total) } /// Remove a specific task's temp directory. #[allow(dead_code)] pub async fn remove_task_dir(&self, task_id: Uuid) -> Result<(), std::io::Error> { let task_dir = self.temp_dir.join(format!("task-{}", task_id)); if task_dir.exists() { fs::remove_dir_all(&task_dir).await?; tracing::info!( task_id = %task_id, path = %task_dir.display(), "Removed temp directory for task" ); } Ok(()) } } impl Default for TempManager { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_temp_manager_default_dir() { let manager = TempManager::new(); assert!(manager.temp_dir().ends_with(".makima/temp")); } }