Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ All notable changes to Diskern are documented here. The format follows

### Fixed

- `diskern scan` now rejects missing and nonexistent roots instead of
reporting a successful empty scan
- The landing page now applies its theme to the document root, preventing
light scrollbars, white overscroll areas, and a white flash before React loads
- The system temp rule no longer reaches user and project `var/tmp` directories
Expand Down
56 changes: 54 additions & 2 deletions crates/diskern-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use diskern_core::{human_bytes, report, rules::RulesDb, scanner, Category, Finding, Verdict};
use std::path::PathBuf;
Expand All @@ -19,6 +19,7 @@ enum Command {
/// Read-only scan: find duplicates, caches, and reclaimable space.
Scan {
/// Directories to scan
#[arg(required = true)]
roots: Vec<PathBuf>,
/// Emit full JSON report instead of a summary
#[arg(long)]
Expand Down Expand Up @@ -197,6 +198,18 @@ fn load_rules(path: Option<&std::path::Path>) -> Result<RulesDb> {
Ok(rules.with_embedded_protected_rules())
}

fn validate_roots(roots: &[PathBuf]) -> Result<()> {
for root in roots {
if !root
.try_exists()
.with_context(|| format!("could not check scan root '{}'", root.display()))?
{
bail!("scan root does not exist: {}", root.display());
}
}
Ok(())
}

fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Expand All @@ -207,6 +220,7 @@ fn main() -> Result<()> {
verdict,
rules,
} => {
validate_roots(&roots)?;
let external_rules = rules.as_deref();
let rules_db = load_rules(external_rules)?;
let opts = scanner::ScanOptions {
Expand Down Expand Up @@ -285,7 +299,45 @@ fn main() -> Result<()> {

#[cfg(test)]
mod tests {
use super::{human_bytes, plural};
use super::{human_bytes, plural, validate_roots, Cli};
use clap::{error::ErrorKind, Parser};
use std::path::PathBuf;

#[test]
fn scan_requires_at_least_one_root() {
let error = match Cli::try_parse_from(["diskern", "scan"]) {
Ok(_) => panic!("scan without roots should be rejected"),
Err(error) => error,
};

assert_eq!(error.kind(), ErrorKind::MissingRequiredArgument);
}

#[test]
fn nonexistent_scan_root_is_rejected_before_scanning() {
let missing = PathBuf::from("diskern-test-root-that-does-not-exist");
let error = validate_roots(std::slice::from_ref(&missing)).unwrap_err();

assert_eq!(
error.to_string(),
format!("scan root does not exist: {}", missing.display())
);
}

#[test]
fn unreadable_scan_root_error_names_the_root() {
let temp = tempfile::tempdir().unwrap();
let file = temp.path().join("file");
std::fs::write(&file, b"not a directory").unwrap();
let invalid_root = file.join("child");

let error = validate_roots(std::slice::from_ref(&invalid_root)).unwrap_err();

assert_eq!(
error.to_string(),
format!("could not check scan root '{}'", invalid_root.display())
);
}

#[test]
fn plural_returns_empty_only_for_singular() {
Expand Down
Loading