summaryrefslogtreecommitdiff
path: root/makima/src/daemon/temp.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/daemon/temp.rs
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/src/daemon/temp.rs')
-rw-r--r--makima/src/daemon/temp.rs224
1 files changed, 224 insertions, 0 deletions
diff --git a/makima/src/daemon/temp.rs b/makima/src/daemon/temp.rs
new file mode 100644
index 0000000..42d4a28
--- /dev/null
+++ b/makima/src/daemon/temp.rs
@@ -0,0 +1,224 @@
+//! 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"));
+ }
+}