diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md index 7332de8..7d26f3e 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -9,7 +9,9 @@ labels: enhancement ## Description -Describe the feature. +Analyze artifact size and disk usage. + +아티팩트 크기와 디스크 사용량을 분석. ## Expected Behavior @@ -17,4 +19,28 @@ Describe how it should work. ## Additional Notes -Optional. +Optional. (Closes #) + +## Checklist + +### Required + +- [ ] `cargo check` passes +- [ ] `cargo fmt --check` passes +- [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes +- [ ] `cargo test` passes + +### Functional Validation + +- [ ] I verified the behavior locally +- [ ] I added or updated tests when necessary + +### Documentation + +- [ ] README or docs were updated if needed +- [ ] New configuration or behavior is documented + +### Safety + +- [ ] Cleanup behavior was reviewed for safety +- [ ] No destructive behavior was introduced without confirmation diff --git a/Cargo.lock b/Cargo.lock index 0eb02ad..35301a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -58,18 +67,53 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -116,6 +160,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "dustfril-cli" version = "0.1.0" @@ -128,6 +178,7 @@ dependencies = [ name = "dustfril-core" version = "0.1.0" dependencies = [ + "chrono", "tempfile", ] @@ -153,12 +204,42 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -193,6 +274,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -223,6 +328,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -253,6 +370,15 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -265,6 +391,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "prettyplease" version = "0.2.37" @@ -312,6 +444,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "semver" version = "1.0.28" @@ -360,6 +498,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "strsim" version = "0.11.1" @@ -426,6 +576,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -460,12 +655,65 @@ dependencies = [ "semver", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/apps/dustfril-cli/src/commands/analyze.rs b/apps/dustfril-cli/src/commands/analyze.rs index ef60fcc..9f43c88 100644 --- a/apps/dustfril-cli/src/commands/analyze.rs +++ b/apps/dustfril-cli/src/commands/analyze.rs @@ -1,3 +1,93 @@ +use std::path::Path; + +use dustfril_core::{analyzer, detector}; + +use dustfril_core::models::{AnalysisResult, CleanupRecommendation}; + +fn print_summary(analysis: &AnalysisResult) { + let mut keep = 0; + let mut review = 0; + let mut safe_to_clean = 0; + let mut review_size = 0_u64; + let mut safe_size = 0_u64; + + for artifact in &analysis.artifacts { + match artifact.recommendation { + CleanupRecommendation::Keep => { + keep += 1; + } + + CleanupRecommendation::Review => { + review += 1; + review_size += artifact.size_bytes; + } + + CleanupRecommendation::SafeToClean => { + safe_to_clean += 1; + safe_size += artifact.size_bytes; + } + } + } + + println!("----------------------------------------\n"); + + println!("DustFril Analysis Summary\n"); + + println!("Artifacts: {}", analysis.artifacts.len()); + + println!( + "Total Size: {}\n", + analyzer::format_size(analysis.total_size_bytes) + ); + + println!("Keep: {}", keep); + println!( + "Review: {}, Size: {}", + review, + analyzer::format_size(review_size) + ); + println!( + "Safe To Clean: {}, Size: {}", + safe_to_clean, + analyzer::format_size(safe_size) + ); + + println!("\n----------------------------------------\n"); +} + pub fn execute() { - println!("Analyze command is not implemented yet."); + let scan_result = detector::scan(Path::new(".")); + + let analysis_result = analyzer::analyze(scan_result); + + if analysis_result.artifacts.is_empty() { + println!("No Rust artifacts found."); + return; + } + + println!("Found {} artifact(s)\n", analysis_result.artifacts.len()); + + for artifact in &analysis_result.artifacts { + println!("[{}]", artifact.artifact.artifact_type); + + println!(" Path: {}", artifact.artifact.path.display()); + + println!(" Size: {}", analyzer::format_size(artifact.size_bytes)); + + println!( + " Modified: {}", + analyzer::format_modified(artifact.last_modified) + ); + + let age_display = artifact + .age_days + .map(|d| format!("{d} days")) + .unwrap_or_else(|| "Unknown".to_string()); + + println!(" Age: {}", age_display); + + println!(" Recommendation: {}\n", artifact.recommendation); + } + + print_summary(&analysis_result); } diff --git a/crates/dustfril-core/Cargo.toml b/crates/dustfril-core/Cargo.toml index 9164324..07b1d36 100644 --- a/crates/dustfril-core/Cargo.toml +++ b/crates/dustfril-core/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4" [dev-dependencies] tempfile = "3.0" \ No newline at end of file diff --git a/crates/dustfril-core/src/analyzer/age.rs b/crates/dustfril-core/src/analyzer/age.rs new file mode 100644 index 0000000..62e72ad --- /dev/null +++ b/crates/dustfril-core/src/analyzer/age.rs @@ -0,0 +1,11 @@ +use std::time::SystemTime; + +pub fn calculate_age_days(modified: Option) -> Option { + let modified = modified?; + + let now = SystemTime::now(); + + let duration = now.duration_since(modified).ok()?; + + Some(duration.as_secs() / 86_400) +} diff --git a/crates/dustfril-core/src/analyzer/analyze.rs b/crates/dustfril-core/src/analyzer/analyze.rs new file mode 100644 index 0000000..56d7496 --- /dev/null +++ b/crates/dustfril-core/src/analyzer/analyze.rs @@ -0,0 +1,33 @@ +use crate::{ + analyzer::{ + calculate_age_days, calculate_directory_size, find_latest_modified, recommend_cleanup, + }, + models::{AnalysisResult, ArtifactAnalysis, ScanResult}, +}; + +pub fn analyze(scan_result: ScanResult) -> AnalysisResult { + let mut result = AnalysisResult::default(); + + for artifact in scan_result.artifacts { + let size_bytes = calculate_directory_size(&artifact.path); + let last_modified = find_latest_modified(&artifact.path); + let age_days = calculate_age_days(last_modified); + let recommendation = recommend_cleanup(age_days); + + result.total_size_bytes += size_bytes; + + result.artifacts.push(ArtifactAnalysis { + artifact, + size_bytes, + last_modified, + age_days, + recommendation, + }); + } + + result + .artifacts + .sort_by_key(|b| std::cmp::Reverse(b.size_bytes)); + + result +} diff --git a/crates/dustfril-core/src/analyzer/format.rs b/crates/dustfril-core/src/analyzer/format.rs new file mode 100644 index 0000000..ba40a73 --- /dev/null +++ b/crates/dustfril-core/src/analyzer/format.rs @@ -0,0 +1,33 @@ +pub fn format_size(bytes: u64) -> String { + const KB: f64 = 1024.0; + const MB: f64 = KB * 1024.0; + const GB: f64 = MB * 1024.0; + const TB: f64 = GB * 1024.0; + + let bytes = bytes as f64; + + if bytes >= TB { + format!("{:.2} TB", bytes / TB) + } else if bytes >= GB { + format!("{:.2} GB", bytes / GB) + } else if bytes >= MB { + format!("{:.2} MB", bytes / MB) + } else if bytes >= KB { + format!("{:.2} KB", bytes / KB) + } else { + format!("{:.0} B", bytes) + } +} + +use chrono::{DateTime, Local}; +use std::time::SystemTime; + +pub fn format_modified(modified: Option) -> String { + let Some(modified) = modified else { + return "Unknown".to_string(); + }; + + let datetime: DateTime = modified.into(); + + datetime.format("%Y-%m-%d %H:%M:%S").to_string() +} diff --git a/crates/dustfril-core/src/analyzer/metadata.rs b/crates/dustfril-core/src/analyzer/metadata.rs new file mode 100644 index 0000000..1838a18 --- /dev/null +++ b/crates/dustfril-core/src/analyzer/metadata.rs @@ -0,0 +1,35 @@ +use std::{fs, path::Path, time::SystemTime}; + +pub fn find_latest_modified(path: &Path) -> Option { + let mut latest = fs::metadata(path).ok()?.modified().ok(); + + let Ok(entries) = fs::read_dir(path) else { + return latest; + }; + + for entry in entries.flatten() { + let path = entry.path(); + + let metadata = entry.metadata().ok(); + + let current = match metadata { + Some(metadata) if metadata.is_dir() => find_latest_modified(&path), + + Some(metadata) => metadata.modified().ok(), + + None => None, + }; + + if let Some(current) = current { + match latest { + Some(existing) if current <= existing => {} + + _ => { + latest = Some(current); + } + } + } + } + + latest +} diff --git a/crates/dustfril-core/src/analyzer/mod.rs b/crates/dustfril-core/src/analyzer/mod.rs index c048a99..6b88675 100644 --- a/crates/dustfril-core/src/analyzer/mod.rs +++ b/crates/dustfril-core/src/analyzer/mod.rs @@ -1 +1,17 @@ //! Analyzer module. +mod age; +mod analyze; +mod format; +mod metadata; +mod recommendation; +mod size; + +#[cfg(test)] +mod tests; + +pub use age::calculate_age_days; +pub use analyze::analyze; +pub use format::{format_modified, format_size}; +pub use metadata::find_latest_modified; +pub use recommendation::recommend_cleanup; +pub use size::calculate_directory_size; diff --git a/crates/dustfril-core/src/analyzer/recommendation.rs b/crates/dustfril-core/src/analyzer/recommendation.rs new file mode 100644 index 0000000..42ff2e4 --- /dev/null +++ b/crates/dustfril-core/src/analyzer/recommendation.rs @@ -0,0 +1,38 @@ +use crate::models::CleanupRecommendation; + +pub fn recommend_cleanup(age_days: Option) -> CleanupRecommendation { + let Some(days) = age_days else { + return CleanupRecommendation::Review; + }; + + match days { + 0..=30 => CleanupRecommendation::Keep, + + 31..=90 => CleanupRecommendation::Review, + + _ => CleanupRecommendation::SafeToClean, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn keep_when_recent() { + assert_eq!(recommend_cleanup(Some(10)), CleanupRecommendation::Keep); + } + + #[test] + fn review_when_middle_age() { + assert_eq!(recommend_cleanup(Some(60)), CleanupRecommendation::Review); + } + + #[test] + fn safe_to_clean_when_old() { + assert_eq!( + recommend_cleanup(Some(180)), + CleanupRecommendation::SafeToClean + ); + } +} diff --git a/crates/dustfril-core/src/analyzer/size.rs b/crates/dustfril-core/src/analyzer/size.rs new file mode 100644 index 0000000..c01b0fe --- /dev/null +++ b/crates/dustfril-core/src/analyzer/size.rs @@ -0,0 +1,23 @@ +use std::{fs, path::Path}; + +pub fn calculate_directory_size(path: &Path) -> u64 { + let mut total_size = 0; + + let Ok(entries) = fs::read_dir(path) else { + return 0; + }; + + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + + if metadata.is_file() { + total_size += metadata.len(); + } else if metadata.is_dir() { + total_size += calculate_directory_size(&entry.path()); + } + } + + total_size +} diff --git a/crates/dustfril-core/src/analyzer/tests.rs b/crates/dustfril-core/src/analyzer/tests.rs new file mode 100644 index 0000000..d71fa6d --- /dev/null +++ b/crates/dustfril-core/src/analyzer/tests.rs @@ -0,0 +1,123 @@ +use std::fs; +use tempfile::TempDir; + +use crate::analyzer::{ + calculate_age_days, calculate_directory_size, find_latest_modified, format_size, +}; +use crate::{ + analyzer::analyze, + models::{ArtifactLocation, ArtifactType, ScanResult}, +}; + +#[test] +fn analyze_empty_scan_result() { + let result = analyze(ScanResult::default()); + + assert_eq!(result.total_size_bytes, 0); + + assert!(result.artifacts.is_empty()); +} + +#[test] +fn calculate_directory_size_returns_total_size() { + let temp_dir = TempDir::new().unwrap(); + + fs::write(temp_dir.path().join("a.txt"), vec![0_u8; 100]).unwrap(); + + fs::write(temp_dir.path().join("b.txt"), vec![0_u8; 200]).unwrap(); + + let size = calculate_directory_size(temp_dir.path()); + + assert_eq!(size, 300); +} + +#[test] +fn analyze_returns_total_size() { + let temp_dir = TempDir::new().unwrap(); + + fs::write(temp_dir.path().join("a.txt"), vec![0_u8; 100]).unwrap(); + + let artifact = ArtifactLocation { + path: temp_dir.path().to_path_buf(), + artifact_type: ArtifactType::Target, + }; + + let scan_result = ScanResult { + artifacts: vec![artifact], + }; + + let result = analyze(scan_result); + + assert_eq!(result.total_size_bytes, 100); + + assert_eq!(result.artifacts.len(), 1); +} + +#[test] +fn find_latest_modified_returns_some() { + let temp_dir = TempDir::new().unwrap(); + + let modified = find_latest_modified(temp_dir.path()); + + assert!(modified.is_some()); +} + +#[test] +fn format_size_bytes() { + assert_eq!(format_size(512), "512 B"); +} + +#[test] +fn format_size_kilobytes() { + assert_eq!(format_size(2048), "2.00 KB"); +} + +#[test] +fn format_size_megabytes() { + assert_eq!(format_size(1024 * 1024), "1.00 MB"); +} + +#[test] +fn format_size_gigabytes() { + assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB"); +} + +#[test] +fn analyze_sorts_by_size_descending() { + let small = TempDir::new().unwrap(); + let large = TempDir::new().unwrap(); + + fs::write(small.path().join("small.bin"), vec![0_u8; 100]).unwrap(); + + fs::write(large.path().join("large.bin"), vec![0_u8; 200]).unwrap(); + + let scan_result = ScanResult { + artifacts: vec![ + ArtifactLocation { + path: small.path().to_path_buf(), + artifact_type: ArtifactType::Target, + }, + ArtifactLocation { + path: large.path().to_path_buf(), + artifact_type: ArtifactType::CargoRegistry, + }, + ], + }; + + let result = analyze(scan_result); + + assert_eq!(result.artifacts[0].size_bytes, 200); + + assert_eq!(result.artifacts[1].size_bytes, 100); +} + +use std::time::{Duration, SystemTime}; + +#[test] +fn calculate_age_days_returns_correct_days() { + let modified = SystemTime::now() - Duration::from_secs(10 * 86_400); + + let age = calculate_age_days(Some(modified)); + + assert_eq!(age, Some(10),); +} diff --git a/crates/dustfril-core/src/models/analysis_result.rs b/crates/dustfril-core/src/models/analysis_result.rs new file mode 100644 index 0000000..88ad86d --- /dev/null +++ b/crates/dustfril-core/src/models/analysis_result.rs @@ -0,0 +1,8 @@ +use super::ArtifactAnalysis; + +/// Total Analysis Results +#[derive(Debug, Default)] +pub struct AnalysisResult { + pub artifacts: Vec, + pub total_size_bytes: u64, +} diff --git a/crates/dustfril-core/src/models/artifact_analysis.rs b/crates/dustfril-core/src/models/artifact_analysis.rs new file mode 100644 index 0000000..80da2a5 --- /dev/null +++ b/crates/dustfril-core/src/models/artifact_analysis.rs @@ -0,0 +1,14 @@ +use crate::models::{ArtifactLocation, CleanupRecommendation}; +use std::time::SystemTime; + +/// Analyzed artifact information +#[derive(Debug)] +pub struct ArtifactAnalysis { + pub artifact: ArtifactLocation, + pub size_bytes: u64, + // permission denied, broken symlink, network filesystem failed to detect + pub last_modified: Option, + pub age_days: Option, + + pub recommendation: CleanupRecommendation, +} diff --git a/crates/dustfril-core/src/models/cleanup_recommendation.rs b/crates/dustfril-core/src/models/cleanup_recommendation.rs new file mode 100644 index 0000000..d62291a --- /dev/null +++ b/crates/dustfril-core/src/models/cleanup_recommendation.rs @@ -0,0 +1,26 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CleanupRecommendation { + Keep, + Review, + SafeToClean, +} + +impl fmt::Display for CleanupRecommendation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CleanupRecommendation::Keep => { + write!(f, "Keep") + } + + CleanupRecommendation::Review => { + write!(f, "Review") + } + + CleanupRecommendation::SafeToClean => { + write!(f, "Safe To Clean") + } + } + } +} diff --git a/crates/dustfril-core/src/models/mod.rs b/crates/dustfril-core/src/models/mod.rs index 7de9a2b..b8597fc 100644 --- a/crates/dustfril-core/src/models/mod.rs +++ b/crates/dustfril-core/src/models/mod.rs @@ -3,6 +3,16 @@ mod artifact_location; mod artifact_type; mod scan_result; +mod analysis_result; +mod artifact_analysis; + +mod cleanup_recommendation; + pub use artifact_location::*; pub use artifact_type::*; pub use scan_result::*; + +pub use analysis_result::*; +pub use artifact_analysis::*; + +pub use cleanup_recommendation::*;