use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use directories::BaseDirs;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub scan: ScanConfig,
pub clean: CleanConfig,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ScanConfig {
pub roots: Vec<PathBuf>,
pub exclude: Vec<String>,
pub older_than: Option<String>,
pub min_size: Option<String>,
pub max_depth: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CleanConfig {
pub protect_git_tracked: bool,
pub expensive_caches: bool,
}
impl Default for CleanConfig {
fn default() -> Self {
Self {
protect_git_tracked: true,
expensive_caches: false,
}
}
}
#[must_use]
pub fn config_candidates() -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Ok(current) = env::current_dir() {
candidates.push(current.join("devclean.toml"));
}
if let Some(base) = BaseDirs::new() {
candidates.push(base.config_dir().join("devclean/config.toml"));
}
candidates
}
pub fn load_config(explicit: Option<&Path>) -> Result<Config> {
let selected = if let Some(path) = explicit {
Some(path.to_path_buf())
} else {
config_candidates().into_iter().find(|path| path.is_file())
};
let Some(path) = selected else {
return Ok(Config::default());
};
let content = fs::read_to_string(&path)
.with_context(|| format!("failed to read config {}", path.display()))?;
toml::from_str(&content).with_context(|| format!("invalid config {}", path.display()))
}
pub fn parse_age(value: &str) -> Result<Duration> {
humantime::parse_duration(value).with_context(|| format!("invalid duration `{value}`"))
}
pub fn parse_bytes(value: &str) -> Result<u64> {
parse_size::parse_size(value).with_context(|| format!("invalid byte size `{value}`"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_age_should_accept_days() -> Result<()> {
assert_eq!(parse_age("30d")?, Duration::from_secs(30 * 86_400));
Ok(())
}
#[test]
fn parse_bytes_should_accept_binary_units() -> Result<()> {
assert_eq!(parse_bytes("2GiB")?, 2 * 1024 * 1024 * 1024);
Ok(())
}
}