use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::cachefile;
const FILE: &str = "project-links.toml";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalLink {
pub file_server: String,
pub file_project: String,
pub origin: String,
pub server_id: String,
pub project: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<PathBuf>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Links {
#[serde(default)]
link: Vec<LocalLink>,
}
fn parse(dir: &Path, text: &str) -> anyhow::Result<Links> {
toml::from_str(text)
.with_context(|| format!("reading {}: fix or delete it", dir.join(FILE).display()))
}
pub fn load_all(dir: &Path) -> anyhow::Result<Vec<LocalLink>> {
match std::fs::read_to_string(dir.join(FILE)) {
Ok(text) => Ok(parse(dir, &text)?.link),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e).with_context(|| format!("reading {}", dir.join(FILE).display())),
}
}
pub fn find(
dir: &Path,
file_server: &str,
file_project: &str,
) -> anyhow::Result<Option<LocalLink>> {
Ok(load_all(dir)?
.into_iter()
.find(|l| l.file_server == file_server && l.file_project == file_project))
}
fn change<T>(dir: &Path, f: impl FnOnce(&mut Vec<LocalLink>) -> T) -> anyhow::Result<T> {
let mut out = None;
let mut failed = None;
cachefile::rewrite(dir, FILE, |text| match parse(dir, text) {
Ok(mut links) => {
out = Some(f(&mut links.link));
toml::to_string(&links).unwrap_or_else(|_| text.to_string())
}
Err(e) => {
failed = Some(e);
text.to_string()
}
})?;
match (out, failed) {
(Some(v), _) => Ok(v),
(None, Some(e)) => Err(e),
(None, None) => unreachable!("the change runs once"),
}
}
pub fn save(dir: &Path, link: LocalLink) -> anyhow::Result<()> {
change(dir, |all| {
all.retain(|l| !(l.file_server == link.file_server && l.file_project == link.file_project));
all.push(link);
})
}
pub fn remove(dir: &Path, file_server: &str, file_project: &str) -> anyhow::Result<bool> {
if find(dir, file_server, file_project)?.is_none() {
return Ok(false);
}
change(dir, |all| {
let before = all.len();
all.retain(|l| !(l.file_server == file_server && l.file_project == file_project));
all.len() < before
})
}
pub fn remove_shadowed(
dir: &Path,
server_id: &str,
file_project: &str,
root: &Path,
) -> anyhow::Result<Vec<LocalLink>> {
let shadowed = |l: &LocalLink| {
l.server_id == server_id
&& l.file_project == file_project
&& l.file_server != server_id
&& l.root.as_deref() == Some(root)
};
if !load_all(dir)?.iter().any(shadowed) {
return Ok(Vec::new());
}
change(dir, |all| {
let (gone, kept) = std::mem::take(all).into_iter().partition(shadowed);
*all = kept;
gone
})
}
#[cfg(test)]
mod tests {
use super::*;
fn link(file_server: &str, project: &str) -> LocalLink {
LocalLink {
file_server: file_server.into(),
file_project: "proj".into(),
origin: "https://mine.example.com".into(),
server_id: "1".repeat(32),
project: project.into(),
root: Some(PathBuf::from("/r")),
}
}
#[test]
fn links_are_saved_found_replaced_and_removed() {
let d = tempfile::tempdir().unwrap();
assert_eq!(find(d.path(), "a", "proj").unwrap(), None);
save(d.path(), link("a", "proj")).unwrap();
save(d.path(), link("b", "proj")).unwrap();
save(d.path(), link("a", "mine")).unwrap();
assert_eq!(
find(d.path(), "a", "proj").unwrap().unwrap().project,
"mine"
);
assert_eq!(load_all(d.path()).unwrap().len(), 2);
assert!(remove(d.path(), "a", "proj").unwrap());
assert!(!remove(d.path(), "a", "proj").unwrap());
assert_eq!(load_all(d.path()).unwrap().len(), 1);
}
#[test]
fn shadowed_links_are_dropped() {
let d = tempfile::tempdir().unwrap();
save(d.path(), link("a", "mine")).unwrap();
let other = remove_shadowed(d.path(), &"1".repeat(32), "proj", Path::new("/s")).unwrap();
assert!(other.is_empty(), "another folder's link is kept");
let gone = remove_shadowed(d.path(), &"1".repeat(32), "proj", Path::new("/r")).unwrap();
assert_eq!(gone.len(), 1);
assert!(load_all(d.path()).unwrap().is_empty());
}
#[test]
fn a_broken_links_file_names_itself() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("project-links.toml"), "[[link]\n").unwrap();
let err = format!("{:#}", load_all(d.path()).unwrap_err());
assert!(
err.contains("project-links.toml") && err.contains("fix or delete it"),
"{err}"
);
let err = format!("{:#}", save(d.path(), link("a", "proj")).unwrap_err());
assert!(err.contains("fix or delete it"), "{err}");
assert_eq!(
std::fs::read_to_string(d.path().join("project-links.toml")).unwrap(),
"[[link]\n",
"a broken file is left as it is"
);
}
}