use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use std::cell::RefCell;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use crate::media_dedup::MediaDedupOptions;
#[cfg(feature = "test_mode")]
thread_local! {
static TEST_CONFIG_PATH: RefCell<Option<PathBuf>> = RefCell::new(None);
}
#[cfg(feature = "test_mode")]
pub fn set_test_config_path(path: Option<PathBuf>) {
TEST_CONFIG_PATH.with(|cell| {
*cell.borrow_mut() = path;
});
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DedupConfig {
#[serde(default = "default_algorithm")]
pub algorithm: String,
#[serde(default)]
pub parallel: Option<usize>,
#[serde(default = "default_mode")]
pub mode: String,
#[serde(default = "default_format")]
pub format: String,
#[serde(default)]
pub progress: bool,
#[serde(default = "default_sort_by")]
pub sort_by: String,
#[serde(default = "default_sort_order")]
pub sort_order: String,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub cache_location: Option<PathBuf>,
#[serde(default)]
pub fast_mode: bool,
#[serde(default)]
pub media_dedup: MediaDedupOptions,
}
fn default_algorithm() -> String {
"xxhash".to_string()
}
fn default_mode() -> String {
"newest_modified".to_string()
}
fn default_format() -> String {
"json".to_string()
}
fn default_sort_by() -> String {
"modified".to_string()
}
fn default_sort_order() -> String {
"descending".to_string()
}
impl Default for DedupConfig {
fn default() -> Self {
Self {
algorithm: default_algorithm(),
parallel: None,
mode: default_mode(),
format: default_format(),
progress: false,
sort_by: default_sort_by(),
sort_order: default_sort_order(),
include: Vec::new(),
exclude: Vec::new(),
cache_location: None,
fast_mode: false,
media_dedup: MediaDedupOptions::default(),
}
}
}
impl DedupConfig {
pub fn get_config_path() -> Result<PathBuf> {
#[cfg(feature = "test_mode")]
{
if let Some(path) = TEST_CONFIG_PATH.with(|cell| cell.borrow().clone()) {
log::debug!("Using test config path: {:?}", path);
return Ok(path);
}
}
let path = {
#[cfg(target_family = "unix")]
{
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
log::debug!("Unix config path. Home dir: {:?}", home_dir);
home_dir.join(".deduprc")
}
#[cfg(target_family = "windows")]
{
let config_dir = dirs::config_dir();
log::debug!("Windows config dir: {:?}", config_dir);
if let Some(config_dir) = config_dir {
let target_path = config_dir.join("dedup").join("config.toml");
log::debug!("Windows target config path: {:?}", target_path);
if let Some(parent) = target_path.parent() {
log::debug!(
"Windows config parent dir: {:?}, exists: {}",
parent,
parent.exists()
);
if !parent.exists() {
log::debug!("Creating parent directory for Windows config");
match fs::create_dir_all(parent) {
Ok(_) => {}
Err(e) => {
log::warn!(
"Failed to create Windows config dir {:?}: {}",
parent,
e
);
let home_dir = dirs::home_dir()
.context("Could not determine home directory")?;
log::debug!("Falling back to Windows home dir: {:?}", home_dir);
return Ok(home_dir.join(".deduprc"));
}
}
}
}
target_path
} else {
let home_dir =
dirs::home_dir().context("Could not determine home directory")?;
log::debug!(
"No Windows config dir available, using home: {:?}",
home_dir
);
home_dir.join(".deduprc")
}
}
};
log::debug!("Final config path: {:?}, exists: {}", path, path.exists());
Ok(path)
}
pub fn load() -> Result<Self> {
let config_path = Self::get_config_path()?;
Self::load_from_path(&config_path)
}
pub fn load_from_path(path: &Path) -> Result<Self> {
match fs::read_to_string(path) {
Ok(contents) => {
let config: DedupConfig = toml::from_str(&contents)
.with_context(|| format!("Failed to parse config file: {:?}", path))?;
Ok(config)
}
Err(e) if e.kind() == ErrorKind::NotFound => {
Ok(Self::default())
}
Err(e) => {
Err(e).with_context(|| format!("Failed to read config file: {:?}", path))
}
}
}
pub fn save(&self) -> Result<()> {
let config_path = Self::get_config_path()?;
self.save_to_path(&config_path)
}
pub fn save_to_path(&self, path: &Path) -> Result<()> {
let toml = toml::to_string_pretty(self).context("Failed to serialize config to TOML")?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {:?}", parent))?;
}
fs::write(path, toml)
.with_context(|| format!("Failed to write config file: {:?}", path))?;
Ok(())
}
pub fn create_default_if_not_exists() -> Result<bool> {
let config_path = Self::get_config_path()?;
if config_path.exists() {
return Ok(false); }
let default_config = Self::default();
default_config.save_to_path(&config_path)?;
Ok(true) }
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_default_config() {
let config = DedupConfig::default();
assert_eq!(config.algorithm, "xxhash");
assert_eq!(config.mode, "newest_modified");
assert_eq!(config.format, "json");
assert_eq!(config.sort_by, "modified");
assert_eq!(config.sort_order, "descending");
assert!(config.include.is_empty());
assert!(config.exclude.is_empty());
assert_eq!(config.parallel, None);
assert!(!config.progress);
}
#[test]
fn test_save_and_load_config() -> Result<()> {
let temp_dir = tempdir()?;
let config_path = temp_dir.path().join("test_config.toml");
let mut test_config = DedupConfig::default();
test_config.algorithm = "sha256".to_string();
test_config.parallel = Some(4);
test_config.include = vec!["*.jpg".to_string(), "*.png".to_string()];
test_config.exclude = vec!["*tmp*".to_string()];
test_config.save_to_path(&config_path)?;
let loaded_config = DedupConfig::load_from_path(&config_path)?;
assert_eq!(loaded_config.algorithm, "sha256");
assert_eq!(loaded_config.parallel, Some(4));
assert_eq!(loaded_config.include, vec!["*.jpg", "*.png"]);
assert_eq!(loaded_config.exclude, vec!["*tmp*"]);
Ok(())
}
}