summaryrefslogblamecommitdiff
path: root/makima/src/daemon/temp.rs
blob: 42d4a28f4d58060b826225ea6dd5ab4d5787ed84 (plain) (tree)























































































































































































































                                                                                                      
                         






                                                              
//! 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 crate::daemon::*;

    #[test]
    fn test_temp_manager_default_dir() {
        let manager = TempManager::new();
        assert!(manager.temp_dir().ends_with(".makima/temp"));
    }
}