use std::collections::HashMap;
use std::fs;
use std::fs::File;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use crate::config::ExistingFilePolicy;
use crate::filesystem::{AbsPath, Symlink, move_to_backup};
use crate::table::LinkData;
#[derive(Debug, Deserialize, Serialize)]
pub struct Lock {
#[serde(default)]
pub links: HashMap<String, Vec<Link>>,
#[serde(skip)]
pub symp_lock_file: AbsPath,
}
impl Lock {
pub fn load(symp_lock_file: &AbsPath) -> Result<Self> {
if !symp_lock_file.exists() {
if let Some(parent) = symp_lock_file.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)?;
}
File::create(symp_lock_file)?;
}
let mut lock: Lock = toml::from_str(fs::read_to_string(symp_lock_file)?.as_str())?;
lock.symp_lock_file = symp_lock_file.clone();
Ok(lock)
}
pub fn save(&self) -> Result<()> {
fs::write(&self.symp_lock_file, toml::to_string(self)?)?;
Ok(())
}
pub fn mark_for_removal<'a>(&mut self, package_names: impl Iterator<Item = &'a String>) {
for package_name in package_names {
if let Some(links) = self.links.get_mut(package_name) {
for link in links.iter_mut() {
link.state = State::MarkedForRemoval;
}
}
}
}
pub fn add<'a>(&mut self, table_links: impl Iterator<Item = &'a LinkData>) {
for link_data in table_links {
self.links
.entry(link_data.package_data.package_name.clone())
.and_modify(|links| {
let mut match_found = false;
for link in links.iter_mut() {
if link.matches_link_data(link_data) {
match_found = true;
link.existing_file_policy = link_data.package_data.existing_file_policy;
link.state = match link.symlink.exists() {
true => State::Synced,
false => State::Added,
};
}
}
if !match_found {
links.push(Link::from_link_data(link_data));
}
})
.or_insert(vec![Link::from_link_data(link_data)]);
}
}
pub fn sync(&mut self) -> Result<()> {
for link in self.links.iter_mut().flat_map(|(_, links)| links) {
if link.symlink.exists() {
continue;
}
if link.symlink.destination.exists()
&& matches!(link.existing_file_policy, ExistingFilePolicy::Error)
{
return Err(anyhow!(
"Cannot create symlink to {} (location not empty)",
link.symlink.destination.display(),
));
}
}
for link in self.links.iter_mut().flat_map(|(_, links)| links) {
if link.state.eq(&State::MarkedForRemoval) {
println!("Removing {:?}", link.symlink);
link.symlink.remove()?;
println!("{}", link.symlink.exists());
}
}
for link in self.links.iter_mut().flat_map(|(_, links)| links) {
if link.state.eq(&State::MarkedForRemoval) {
continue;
}
if link.symlink.exists() {
link.state = State::Synced;
continue;
}
if link.symlink.destination.exists() || link.symlink.is_broken() {
match link.existing_file_policy {
ExistingFilePolicy::Backup => {
if link.symlink.is_broken() {
link.symlink.remove()?;
} else {
move_to_backup(&link.symlink.destination)?;
}
}
ExistingFilePolicy::Replace => {
if link.symlink.destination.is_file() {
fs::remove_file(&link.symlink.destination)?;
} else {
fs::remove_dir_all(&link.symlink.destination)?;
}
}
ExistingFilePolicy::Skip => continue,
ExistingFilePolicy::Error => {}
}
}
link.symlink.create()?;
link.state = State::Synced;
}
for links in self.links.values_mut() {
links.retain(|link| link.state.ne(&State::MarkedForRemoval))
}
for package_name in self.all_package_names() {
if self.links.get(&package_name).unwrap().is_empty() {
self.links.remove_entry(&package_name);
}
}
Ok(())
}
pub fn all_package_names(&self) -> Vec<String> {
self.links.keys().cloned().collect()
}
}
#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct Link {
pub existing_file_policy: ExistingFilePolicy,
#[serde(flatten)]
pub symlink: Symlink,
pub state: State,
}
impl Link {
fn from_link_data(link_data: &LinkData) -> Self {
let symlink = Symlink::new(
link_data.symlink.source.clone(),
link_data.symlink.destination.clone(),
);
let existing_file_policy = link_data.package_data.existing_file_policy;
let state = State::Added;
Link {
existing_file_policy,
symlink,
state,
}
}
fn matches_link_data(&self, link_data: &LinkData) -> bool {
self.symlink.eq(&link_data.symlink)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum State {
Synced,
Added,
MarkedForRemoval,
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow;
use tempfile::tempdir;
const DUMMY_LOCK_FILE_CONTENT: &str = r#"
[[links.apples]]
existing_file_policy = "backup"
source = "/apples/are/great"
destination = "/the/best/fruit"
state = "Synced"
[[links.pineapples]]
existing_file_policy = "error"
source = "/pineapples/are/yummy"
destination = "/can/you/tell/im/hungry"
state = "Added"
[[links.goo]]
existing_file_policy = "replace"
source = "/its/slimy"
destination = "/eeeeeewwwwwww"
state = "MarkedForRemoval"
"#;
#[test]
fn round_trip() -> anyhow::Result<()> {
let tmp = tempdir()?;
let symp_lock_file = AbsPath::from_path(&tmp.path().join("symp.lock"))?;
let mut lock: Lock = toml::from_str(DUMMY_LOCK_FILE_CONTENT)?;
lock.symp_lock_file = symp_lock_file.clone();
lock.save()?;
let lock2 = Lock::load(&symp_lock_file)?;
assert_eq!(lock.links, lock2.links);
assert_eq!(lock.symp_lock_file, lock2.symp_lock_file);
Ok(())
}
#[test]
fn mark_for_removal() -> anyhow::Result<()> {
let mut lock: Lock = toml::from_str(DUMMY_LOCK_FILE_CONTENT)?;
for link in lock.links.get("pineapples").unwrap() {
assert_ne!(link.state, State::MarkedForRemoval);
}
lock.mark_for_removal(["pineapples".to_string()].iter());
for link in lock.links.get("pineapples").unwrap() {
assert_eq!(link.state, State::MarkedForRemoval);
}
Ok(())
}
}