//! 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<PathBuf, std::io::Error> {
// 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<u64, std::io::Error> {
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<u64, std::io::Error> {
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"));
}
}