summaryrefslogtreecommitdiff
path: root/makima/src/daemon/temp.rs
blob: 42d4a28f4d58060b826225ea6dd5ab4d5787ed84 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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"));
    }
}