1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Artifact {
17 pub path: PathBuf,
19 pub kind: ArtifactKind,
21 pub size: u64,
23 pub file_count: u64,
25 pub age: Option<Duration>,
27 pub metadata: ArtifactMetadata,
29}
30
31impl Artifact {
32 pub fn new(path: PathBuf, kind: ArtifactKind) -> Self {
34 Self {
35 path,
36 kind,
37 size: 0,
38 file_count: 0,
39 age: None,
40 metadata: ArtifactMetadata::default(),
41 }
42 }
43
44 pub fn name(&self) -> &str {
46 self.path
47 .file_name()
48 .and_then(|n| n.to_str())
49 .unwrap_or("unknown")
50 }
51
52 pub fn is_safe_to_clean(&self) -> bool {
54 match self.kind.default_safety() {
55 ArtifactSafety::AlwaysSafe => true,
56 ArtifactSafety::SafeIfGitClean => true, ArtifactSafety::SafeWithLockfile => self.metadata.lockfile.is_some(),
58 ArtifactSafety::RequiresConfirmation => false,
59 ArtifactSafety::NeverAuto => false,
60 }
61 }
62
63 pub fn size_display(&self) -> String {
65 humansize::format_size(self.size, humansize::BINARY)
66 }
67}
68
69impl std::fmt::Display for Artifact {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(
72 f,
73 "{} ({}) - {}",
74 self.name(),
75 self.kind.description(),
76 self.size_display()
77 )
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[non_exhaustive]
84pub enum ArtifactKind {
85 Dependencies,
87 BuildOutput,
89 Cache,
91 VirtualEnv,
93 IdeArtifacts,
95 TestOutput,
97 Logs,
99 Temporary,
101 LockFile,
103 Docker,
105 PackageManagerCache,
107 Bytecode,
109 DocsBuild,
111 Custom(u32),
113}
114
115impl ArtifactKind {
116 pub fn default_safety(&self) -> ArtifactSafety {
118 match self {
119 Self::Cache | Self::Logs | Self::Temporary | Self::Bytecode => ArtifactSafety::AlwaysSafe,
120 Self::BuildOutput | Self::TestOutput | Self::DocsBuild => ArtifactSafety::SafeIfGitClean,
121 Self::Dependencies | Self::PackageManagerCache => ArtifactSafety::SafeWithLockfile,
122 Self::VirtualEnv => ArtifactSafety::RequiresConfirmation,
123 Self::IdeArtifacts => ArtifactSafety::RequiresConfirmation,
124 Self::Docker => ArtifactSafety::RequiresConfirmation,
125 Self::LockFile => ArtifactSafety::NeverAuto,
126 Self::Custom(_) => ArtifactSafety::RequiresConfirmation,
127 }
128 }
129
130 pub fn description(&self) -> &'static str {
132 match self {
133 Self::Dependencies => "dependencies",
134 Self::BuildOutput => "build output",
135 Self::Cache => "cache",
136 Self::VirtualEnv => "virtual environment",
137 Self::IdeArtifacts => "IDE artifacts",
138 Self::TestOutput => "test output",
139 Self::Logs => "logs",
140 Self::Temporary => "temporary files",
141 Self::LockFile => "lock file",
142 Self::Docker => "Docker artifacts",
143 Self::PackageManagerCache => "package cache",
144 Self::Bytecode => "bytecode",
145 Self::DocsBuild => "documentation build",
146 Self::Custom(_) => "custom",
147 }
148 }
149
150 pub fn icon(&self) -> &'static str {
152 match self {
153 Self::Dependencies => "๐ฆ",
154 Self::BuildOutput => "๐จ",
155 Self::Cache => "๐พ",
156 Self::VirtualEnv => "๐",
157 Self::IdeArtifacts => "๐ป",
158 Self::TestOutput => "๐งช",
159 Self::Logs => "๐",
160 Self::Temporary => "๐๏ธ",
161 Self::LockFile => "๐",
162 Self::Docker => "๐ณ",
163 Self::PackageManagerCache => "๐ฅ",
164 Self::Bytecode => "โ๏ธ",
165 Self::DocsBuild => "๐",
166 Self::Custom(_) => "๐",
167 }
168 }
169
170 pub fn all() -> &'static [ArtifactKind] {
172 &[
173 Self::Dependencies,
174 Self::BuildOutput,
175 Self::Cache,
176 Self::VirtualEnv,
177 Self::IdeArtifacts,
178 Self::TestOutput,
179 Self::Logs,
180 Self::Temporary,
181 Self::Docker,
182 Self::PackageManagerCache,
183 Self::Bytecode,
184 Self::DocsBuild,
185 ]
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum ArtifactSafety {
192 AlwaysSafe,
194 SafeIfGitClean,
196 SafeWithLockfile,
198 RequiresConfirmation,
200 NeverAuto,
202}
203
204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct ArtifactMetadata {
207 pub restorable: bool,
209 pub restore_command: Option<String>,
211 pub lockfile: Option<PathBuf>,
213 pub restore_time_estimate: Option<u32>,
215 #[serde(default)]
217 pub extra: HashMap<String, String>,
218}
219
220impl ArtifactMetadata {
221 pub fn restorable(command: impl Into<String>) -> Self {
223 Self {
224 restorable: true,
225 restore_command: Some(command.into()),
226 ..Default::default()
227 }
228 }
229
230 pub fn with_lockfile(mut self, lockfile: PathBuf) -> Self {
232 self.lockfile = Some(lockfile);
233 self
234 }
235
236 pub fn with_restore_time(mut self, seconds: u32) -> Self {
238 self.restore_time_estimate = Some(seconds);
239 self
240 }
241}
242
243#[derive(Debug, Clone, Default)]
245pub struct ArtifactStats {
246 pub total_size: u64,
248 pub total_files: u64,
250 pub total_artifacts: usize,
252 pub by_kind: HashMap<ArtifactKind, KindStats>,
254}
255
256impl ArtifactStats {
257 pub fn add(&mut self, artifact: &Artifact) {
259 self.total_size += artifact.size;
260 self.total_files += artifact.file_count;
261 self.total_artifacts += 1;
262
263 let entry = self.by_kind.entry(artifact.kind).or_default();
264 entry.count += 1;
265 entry.total_size += artifact.size;
266 entry.file_count += artifact.file_count;
267 }
268
269 pub fn largest_kind(&self) -> Option<(ArtifactKind, u64)> {
271 self.by_kind
272 .iter()
273 .max_by_key(|(_, stats)| stats.total_size)
274 .map(|(kind, stats)| (*kind, stats.total_size))
275 }
276}
277
278#[derive(Debug, Clone, Default)]
280pub struct KindStats {
281 pub count: usize,
283 pub total_size: u64,
285 pub file_count: u64,
287}
288
289#[derive(Debug, Clone)]
291pub struct CleanResult {
292 pub artifact: Artifact,
294 pub success: bool,
296 pub error: Option<String>,
298 pub bytes_freed: u64,
300 pub trashed: bool,
302}
303
304impl CleanResult {
305 pub fn success(artifact: Artifact, trashed: bool) -> Self {
307 let bytes_freed = artifact.size;
308 Self {
309 artifact,
310 success: true,
311 error: None,
312 bytes_freed,
313 trashed,
314 }
315 }
316
317 pub fn failure(artifact: Artifact, error: impl Into<String>) -> Self {
319 Self {
320 artifact,
321 success: false,
322 error: Some(error.into()),
323 bytes_freed: 0,
324 trashed: false,
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_artifact_kind_safety() {
335 assert_eq!(
336 ArtifactKind::Cache.default_safety(),
337 ArtifactSafety::AlwaysSafe
338 );
339 assert_eq!(
340 ArtifactKind::Dependencies.default_safety(),
341 ArtifactSafety::SafeWithLockfile
342 );
343 assert_eq!(
344 ArtifactKind::LockFile.default_safety(),
345 ArtifactSafety::NeverAuto
346 );
347 }
348
349 #[test]
350 fn test_artifact_stats() {
351 let mut stats = ArtifactStats::default();
352
353 let artifact1 = Artifact {
354 path: PathBuf::from("/test/node_modules"),
355 kind: ArtifactKind::Dependencies,
356 size: 1000,
357 file_count: 100,
358 age: None,
359 metadata: ArtifactMetadata::default(),
360 };
361
362 let artifact2 = Artifact {
363 path: PathBuf::from("/test/.cache"),
364 kind: ArtifactKind::Cache,
365 size: 500,
366 file_count: 50,
367 age: None,
368 metadata: ArtifactMetadata::default(),
369 };
370
371 stats.add(&artifact1);
372 stats.add(&artifact2);
373
374 assert_eq!(stats.total_size, 1500);
375 assert_eq!(stats.total_files, 150);
376 assert_eq!(stats.total_artifacts, 2);
377 }
378}