use std::{
fmt::Display,
fs::{create_dir_all, File},
io::Read,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::{
backup::{Backup, Id},
config::Config,
error::{Error, Result},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathInfo {
is_file: bool,
rel_location: String,
location_root: LocationRoot,
last_modified: Option<Id>,
children: Vec<PathInfo>,
}
impl PathInfo {
pub fn from_path(config: &Config, path: &str) -> Result<Self> {
let locations = Self::parse_location(path, config)?;
Self::handle_dir(config, &locations.0, &locations.1)
}
fn handle_dir(
config: &Config,
rel_location: &str,
location_root: &LocationRoot,
) -> Result<Self> {
info!("Handling {rel_location}");
let path = Self::get_abs_path(&location_root.to_string(), rel_location);
Ok(if path.is_dir() {
let mut last_modified = Some(String::new());
let mut last_modified_timestamp = 0;
let mut children: Vec<PathInfo> = Vec::new();
let paths = std::fs::read_dir(path).unwrap();
for path in paths {
let pathstr = path.unwrap().path().to_string_lossy().to_string();
let root = format!("{location_root}/");
let Some(rl) = pathstr.split_once(&root) else {
panic!("HUH");
};
let handle = Self::handle_dir(config, rl.1, location_root)?;
if let Some(lm) = handle.last_modified.clone() {
if last_modified.is_some() {
let ts = Backup::from_index(config, &lm)?.timestamp;
if ts > last_modified_timestamp {
last_modified_timestamp = ts;
last_modified = Some(lm);
};
}
} else {
last_modified = None;
};
children.push(handle);
}
Self {
is_file: false,
rel_location: rel_location.to_string(),
location_root: location_root.clone(),
last_modified,
children,
}
} else {
Self::from_file(config, rel_location, location_root)?
})
}
fn from_file(
config: &Config,
rel_location: &str,
location_root: &LocationRoot,
) -> Result<Self> {
let last_modified = Self::compare_to_last_modified(config, location_root, rel_location)?;
info!("From file {rel_location} ({last_modified:?})");
Ok(Self {
rel_location: rel_location.to_string(),
location_root: location_root.clone(),
last_modified,
is_file: true,
children: Vec::new(),
})
}
pub fn compare_to_last_modified(
config: &Config,
location_root: &LocationRoot,
rel_location: &str,
) -> Result<Option<String>> {
let Some(last_backup) = Backup::get_last(config)? else {
// First Backup
return Ok(None);
};
let files = last_backup.files.clone();
let last_file_opt = Self::find_last_modified(files, rel_location, location_root);
let Some(last_file) = last_file_opt else {
// File didn't exist last Backup
return Ok(None);
};
let modified_backup = if let Some(modified_backup_id) = &last_file.last_modified {
Backup::from_index(config, modified_backup_id)?
} else {
last_backup
};
let old_path = modified_backup.get_absolute_file_location(config, &last_file.rel_location);
let new_path = format!("{location_root}/{rel_location}");
let mut old = File::open(old_path)?;
let mut new = File::open(new_path)?;
let old_len = old.metadata()?.len();
let new_len = new.metadata()?.len();
if old_len != new_len {
return Ok(None);
}
let mut old_content = String::new();
old.read_to_string(&mut old_content)?;
let mut new_content = String::new();
new.read_to_string(&mut new_content)?;
if old_content != new_content {
return Ok(None);
}
Ok(Some(modified_backup.id.clone()))
}
pub fn find_last_modified(
files: Vec<Self>,
rel_location: &str,
location_root: &LocationRoot,
) -> Option<PathInfo> {
for path in files {
if path.is_file {
if path.rel_location == rel_location && path.location_root == *location_root {
return Some(path);
};
} else {
let is_modified =
PathInfo::find_last_modified(path.children, rel_location, location_root);
if is_modified.is_some() {
return is_modified;
};
}
}
None
}
pub fn get_absolute_path(&self) -> PathBuf {
Self::get_abs_path(&self.location_root.to_string(), &self.rel_location)
}
pub fn save(&self, backup_root: &str) -> Result<()> {
if self.last_modified.is_some() {
return Ok(());
}
info!("Save File {:?}", self.rel_location);
if self.is_file {
let new_path = format!("{}/{}", backup_root, self.rel_location);
let np = Path::new(&new_path);
if let Some(parent) = np.parent() {
create_dir_all(parent)?;
}
std::fs::copy(self.get_absolute_path(), new_path)?;
} else {
for child in &self.children {
child.save(backup_root)?;
}
};
Ok(())
}
pub fn restore(&self, config: &Config, backup_root: &str) -> Result<()> {
if self.is_file {
info!(?self.rel_location, "Restore File");
let backup_path = if let Some(last_modified) = self.last_modified.clone() {
let backup = Backup::from_index(config, &last_modified)?;
&backup.get_location(config).get_absolute_dir(config)
} else {
backup_root
};
let backup_loc = format!("{}/{}", backup_path, self.rel_location);
let system_loc = self.get_absolute_path();
debug!(?backup_loc, ?system_loc, "copy");
if let Some(parents) = system_loc.parent() {
create_dir_all(parents)?;
}
std::fs::copy(backup_loc, system_loc)?;
} else {
for path in &self.children {
path.restore(config, backup_root)?;
}
}
Ok(())
}
fn get_abs_path(location_root: &str, rel_location: &str) -> PathBuf {
let path = format!("{location_root}/{rel_location}");
PathBuf::from(path)
}
fn parse_location(value: &str, config: &Config) -> Result<(String, LocationRoot)> {
let Some(split) = value.split_once('/') else {
return Err(Error::InvalidDirectory(value.to_string()));
};
if split.0.starts_with('~') {
return Ok((split.1.to_string(), LocationRoot::User));
};
Ok((
split.1.to_string(),
LocationRoot::from_op_str(split.0, config)?,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum LocationRoot {
User,
Custom(String),
SystemConfig,
UserConfig,
Root,
}
impl Display for LocationRoot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LocationRoot::User => write!(f, "{}", dirs::home_dir().unwrap().to_string_lossy()),
LocationRoot::Custom(loc) => write!(f, "{loc}"),
LocationRoot::SystemConfig => write!(f, "/etc"),
LocationRoot::UserConfig => {
write!(f, "{}", dirs::config_local_dir().unwrap().to_string_lossy())
}
LocationRoot::Root => write!(f, "/"),
}
}
}
impl LocationRoot {
fn from_op_str(value: &str, config: &Config) -> Result<Self> {
let split_str = value.split_once(':');
let Some(split_op) = split_str else {
return Err(Error::NoIndex);
};
match split_op.0 {
"u" => Ok(Self::User),
"s" => Ok(Self::SystemConfig),
"d" => Ok(Self::UserConfig),
"r" => Ok(Self::Root),
"c" => Ok(Self::Custom(
config
.custom_directories
.get(split_op.1)
.ok_or_else(|| Error::CustomDirectory(split_op.1.to_string()))?
.to_string(),
)),
_ => Err(Error::InvalidIndex(split_op.0.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use std::{
fs::{create_dir_all, remove_dir_all, File},
io::Write,
};
use crate::{
backup::Backup,
config::Config,
error::{Error, Result},
};
use super::LocationRoot;
use super::PathInfo;
#[test]
fn from_op_str() -> Result<()> {
let mut config = Config::default();
config
.custom_directories
.insert("test".to_string(), "/usr/local/test".to_string());
let mut values_ok: Vec<(&str, LocationRoot)> = Vec::new();
values_ok.push(("u:test", LocationRoot::User));
values_ok.push(("s:", LocationRoot::SystemConfig));
values_ok.push(("r:", LocationRoot::Root));
values_ok.push((
"c:test",
LocationRoot::Custom("/usr/local/test".to_string()),
));
for value in values_ok {
println!("Testing {value:?}");
assert_eq!(LocationRoot::from_op_str(value.0, &config)?, value.1);
println!("\x1B[FTesting {value:?} ✓");
}
let mut values_err: Vec<(&str, String)> = Vec::new();
values_err.push((
"c:rest",
Error::CustomDirectory("rest".to_string()).to_string(),
));
values_err.push(("t:test/", Error::InvalidIndex("t".to_string()).to_string()));
values_err.push((
"test:test/usr",
Error::InvalidIndex("test".to_string()).to_string(),
));
values_err.push(("/usr/local/test", Error::NoIndex.to_string()));
values_err.push(("c/usr/local/test", Error::NoIndex.to_string()));
for value in values_err {
println!("Testing {value:?}");
assert_eq!(
LocationRoot::from_op_str(value.0, &config)
.err()
.unwrap()
.to_string(),
value.1
);
println!("\x1B[FTesting {value:?} ✓");
}
Ok(())
}
#[test]
fn parse_location() -> Result<()> {
let mut config = Config::default();
config
.custom_directories
.insert("test".to_string(), "/usr/local/test".to_string());
let mut values_ok: Vec<(&str, (String, LocationRoot))> = Vec::new();
values_ok.push((
"~/.config/nvim",
(".config/nvim".to_string(), LocationRoot::User),
));
values_ok.push((
"u:test/.config/nvim",
(".config/nvim".to_string(), LocationRoot::User),
));
values_ok.push((
"r:/.config/nvim",
(".config/nvim".to_string(), LocationRoot::Root),
));
values_ok.push((
"r:/.config/nvim",
(".config/nvim".to_string(), LocationRoot::Root),
));
values_ok.push((
"s:/.config/nvim",
(".config/nvim".to_string(), LocationRoot::SystemConfig),
));
values_ok.push((
"c:test/.config/nvim",
(
".config/nvim".to_string(),
LocationRoot::Custom("/usr/local/test".to_string()),
),
));
for value in values_ok {
print!("Testing {value:?}");
assert_eq!(PathInfo::parse_location(&value.0, &config)?, value.1);
println!("\x1B[FTesting {value:?} ✓");
}
Ok(())
}
#[test]
fn compare_to_last_modified() -> color_eyre::Result<()> {
let cwd = std::env::current_dir()?;
let test_dir = format!("{}/backup-test-dir", cwd.display());
let mut config = Config::default();
config.root = "./backup-test".to_string();
config
.directories
.push(format!("r:{test_dir}"));
create_dir_all("./backup-test-dir")?;
let mut f = File::create("./backup-test-dir/size.txt")?;
f.write_all("unmodified".as_bytes())?;
let mut f = File::create("./backup-test-dir/content.txt")?;
f.write_all("unmodified".as_bytes())?;
let mut f = File::create("./backup-test-dir/nothing.txt")?;
f.write_all("unmodified".as_bytes())?;
let backup = Backup::create(&config, None)?;
backup.save(&config)?;
let mut f = File::create("./backup-test-dir/size.txt")?;
f.write_all("modified".as_bytes())?;
let mut f = File::create("./backup-test-dir/content.txt")?;
f.write_all("unmodefied".as_bytes())?;
let pi = PathInfo::from_path(&config, format!("r:{test_dir}").as_str())?;
let nothing_full = format!("{test_dir}/nothing.txt");
let nothing = ¬hing_full[1..nothing_full.len()];
let last_backup = Backup::get_last(&config)?.unwrap();
for file in pi.children {
println!("test rel: {}", file.rel_location);
println!("nothing: {}", nothing);
let res = if file.rel_location == nothing {
Some(last_backup.id.clone())
} else {
None
};
// println!("Testing {file:?}");
assert_eq!(
PathInfo::compare_to_last_modified(
&config,
&file.location_root,
&file.rel_location
)?,
res
);
// println!("\x1B[FTesting {file:?} ✓");
}
remove_dir_all("./backup-test-dir")?;
remove_dir_all("./backup-test")?;
Ok(())
}
}