use crate::error::HbackupError;
use crate::{Result, constants::CONFIG_NAME, sysexits};
use hbackup::job::{BackupModel, CompressFormat, Job, Level};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io, process};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(crate) struct Application {
pub version: String,
pub jobs: Vec<Job>,
}
impl Default for Application {
fn default() -> Self {
Self {
version: "1.0".to_string(),
jobs: vec![],
}
}
}
impl Application {
pub(crate) fn new() -> Self {
Self {
version: "1.0".to_string(),
jobs: vec![],
}
}
pub(crate) fn load_config() -> Self {
if config_file_exists() {
read_config_file()
} else {
Self::new()
}
}
pub(crate) fn add_job(
&mut self,
source: PathBuf,
target: PathBuf,
compression: Option<CompressFormat>,
level: Option<Level>,
ignore: Option<Vec<String>>,
model: Option<BackupModel>,
) -> Result<()> {
let id = self
.jobs
.iter()
.map(|job| job.id)
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or(HbackupError::TooManyJobs(u32::MAX))?;
self.jobs.push(Job {
id,
source,
target,
compression,
level,
ignore,
model,
});
Ok(())
}
pub(crate) fn reset_jobs(&mut self) {
self.jobs = vec![];
}
pub(crate) fn write(&self) -> Result<()> {
write_config(self)?;
Ok(())
}
pub(crate) fn get_jobs() -> Vec<Job> {
Self::load_config().jobs
}
pub(crate) fn list_by_ids(ids: Vec<u32>) -> Vec<Job> {
Self::get_jobs()
.into_iter()
.filter(|job| ids.contains(&job.id))
.collect()
}
pub(crate) fn list_by_gte(id: u32) -> Vec<Job> {
Self::get_jobs()
.into_iter()
.filter(|job| job.id >= id)
.collect()
}
pub(crate) fn list_by_lte(id: u32) -> Vec<Job> {
Self::get_jobs()
.into_iter()
.filter(|job| job.id <= id)
.collect()
}
pub(crate) fn remove_job(&mut self, id: u32) -> Option<()> {
if let Some(index) = self.jobs.iter().position(|j| j.id == id) {
self.jobs.remove(index);
Some(())
} else {
None
}
}
}
pub(crate) fn config_file() -> PathBuf {
config_dir().join(CONFIG_NAME)
}
#[cfg(not(target_os = "macos"))]
fn config_dir() -> PathBuf {
use crate::constants::PKG_NAME;
let config_dir = dirs::config_dir().unwrap_or_else(|| {
eprintln!("Couldn't get the home directory!!!");
process::exit(sysexits::EX_UNAVAILABLE);
});
config_dir.join(PKG_NAME)
}
#[cfg(target_os = "macos")]
fn config_dir() -> PathBuf {
use crate::constants::PKG_NAME;
let home_dir = dirs::home_dir().unwrap_or_else(|| {
eprintln!("Couldn't get the home directory!!!");
process::exit(sysexits::EX_UNAVAILABLE);
});
home_dir.join(".config").join(PKG_NAME)
}
fn config_file_exists() -> bool {
config_file().exists()
}
pub(crate) fn write_config(data: &Application) -> Result<()> {
let file_path = config_file();
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::File::create(file_path)?;
let mut writer = io::BufWriter::new(file);
let toml_str = toml::to_string_pretty(&data)?;
writer.write_all(toml_str.as_bytes())?;
writer.flush()?;
Ok(())
}
fn read_config_file() -> Application {
let file_path = config_file();
let toml_str = fs::read_to_string(&file_path).unwrap_or_else(|e| {
eprintln!("Error reading config file: {e}");
std::process::exit(1);
});
toml::from_str(&toml_str).unwrap_or_else(|e| {
eprintln!("Error parsing config file: {e}");
std::process::exit(1);
})
}
pub(crate) fn init_config() {
let config_file = config_file();
if !config_file.exists() {
let app = Application::new();
let parent = config_file.parent().unwrap_or_else(|| Path::new(""));
fs::create_dir_all(parent).unwrap();
let file = fs::File::create(config_file).unwrap();
let mut writer = io::BufWriter::new(file);
let toml_str = toml::to_string_pretty(&app).unwrap();
writer.write_all(toml_str.as_bytes()).unwrap();
writer.flush().unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_config_file() {
let file = config_dir().join("hbackup").join("config.toml");
assert_eq!(config_file(), file);
}
#[test]
fn test_application_new() {
let app = Application::new();
assert_eq!(app.version, "1.0");
assert!(app.jobs.is_empty());
}
#[test]
fn test_application_add_job() -> Result<()> {
let mut app = Application::new();
let source = PathBuf::from("/test/source");
let target = PathBuf::from("/test/target");
app.add_job(
source.clone(),
target.clone(),
Some(CompressFormat::Gzip),
Some(Level::Default),
None,
None,
)?;
assert_eq!(app.jobs.len(), 1);
assert_eq!(app.jobs[0].id, 1);
assert_eq!(app.jobs[0].source, source);
assert_eq!(app.jobs[0].target, target);
assert!(matches!(
app.jobs[0].compression,
Some(CompressFormat::Gzip)
));
assert!(matches!(app.jobs[0].level, Some(Level::Default)));
Ok(())
}
#[test]
fn test_application_add_multiple_jobs() -> Result<()> {
let mut app = Application::new();
app.add_job(
PathBuf::from("/test/source1"),
PathBuf::from("/test/target1"),
Some(CompressFormat::Zip),
Some(Level::Fastest),
None,
None,
)?;
app.add_job(
PathBuf::from("/test/source2"),
PathBuf::from("/test/target2"),
Some(CompressFormat::Zstd),
Some(Level::Best),
Some(vec!["*.log".to_string()]),
None,
)?;
assert_eq!(app.jobs.len(), 2);
assert_eq!(app.jobs[0].id, 1);
assert_eq!(app.jobs[1].id, 2);
assert_ne!(app.jobs[0].id, app.jobs[1].id);
Ok(())
}
#[test]
fn test_application_remove_job() -> Result<()> {
let mut app = Application::new();
app.add_job(
PathBuf::from("/test/source1"),
PathBuf::from("/test/target1"),
None,
None,
None,
None,
)?;
app.add_job(
PathBuf::from("/test/source2"),
PathBuf::from("/test/target2"),
None,
None,
None,
None,
)?;
assert_eq!(app.jobs.len(), 2);
let result = app.remove_job(1);
assert!(result.is_some());
assert_eq!(app.jobs.len(), 1);
assert_eq!(app.jobs[0].id, 2);
let result = app.remove_job(999);
assert!(result.is_none());
assert_eq!(app.jobs.len(), 1);
Ok(())
}
#[test]
fn test_application_reset_jobs() -> Result<()> {
let mut app = Application::new();
app.add_job(
PathBuf::from("/test/source1"),
PathBuf::from("/test/target1"),
None,
None,
None,
None,
)?;
app.add_job(
PathBuf::from("/test/source2"),
PathBuf::from("/test/target2"),
None,
None,
None,
None,
)?;
assert_eq!(app.jobs.len(), 2);
app.reset_jobs();
assert!(app.jobs.is_empty());
Ok(())
}
#[test]
fn test_application_serialization() -> Result<()> {
let mut app = Application::new();
app.add_job(
PathBuf::from("/test/source"),
PathBuf::from("/test/target"),
Some(CompressFormat::Gzip),
Some(Level::Default),
Some(vec!["*.log".to_string()]),
None,
)?;
let toml_str = toml::to_string(&app).expect("Failed to serialize to TOML");
assert!(toml_str.contains("version = \"1.0\""));
assert!(toml_str.contains("id = 1"));
assert!(toml_str.contains("Gzip"));
let deserialized: Application =
toml::from_str(&toml_str).expect("Failed to deserialize from TOML");
assert_eq!(deserialized.version, app.version);
assert_eq!(deserialized.jobs.len(), app.jobs.len());
assert_eq!(deserialized.jobs[0].id, app.jobs[0].id);
assert_eq!(deserialized.jobs[0].source, app.jobs[0].source);
assert_eq!(deserialized.jobs[0].target, app.jobs[0].target);
Ok(())
}
#[test]
fn test_application_default() {
let app = Application::default();
assert_eq!(app.version, "1.0");
assert!(app.jobs.is_empty());
}
fn config_dir() -> PathBuf {
if cfg!(target_os = "macos") {
let home = env::var("HOME").unwrap();
PathBuf::from(home).join(".config")
} else {
dirs::config_dir().unwrap()
}
}
}