use std::fs;
use std::io;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use crate::error::Error;
fn tmp_path(current: &Path) -> PathBuf {
let mut name = current.file_name().unwrap_or_default().to_os_string();
name.push(".tmp");
current.with_file_name(name)
}
pub fn point_at(current: &Path, release: &Path) -> Result<(), Error> {
let tmp = tmp_path(current);
symlink(release, &tmp).map_err(|source| Error::Io {
path: tmp.clone(),
source,
})?;
fs::rename(&tmp, current).map_err(|source| Error::Io {
path: current.to_owned(),
source,
})
}
pub fn resolve(current: &Path) -> Result<Option<PathBuf>, Error> {
match fs::read_link(current) {
Ok(target) => Ok(Some(target)),
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(Error::Io {
path: current.to_owned(),
source,
}),
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_leftover_tmp_path_refuses_the_swap() {
let dir = tempfile::tempdir().expect("tempdir");
let release = dir.path().join("releases/abc");
std::fs::create_dir_all(&release).expect("release");
let current = dir.path().join("current");
std::fs::write(dir.path().join("current.tmp"), b"in the way").expect("blocker");
let err = point_at(¤t, &release).expect_err("an occupied tmp path must refuse");
assert!(
matches!(&err, Error::Io { path, .. } if path.ends_with("current.tmp")),
"the refusal must name the path in the way, got: {err}"
);
assert!(
!current.exists(),
"a refused swap must leave `current` exactly as it found it"
);
}
use super::*;
use tempfile::tempdir;
#[test]
fn the_swap_never_leaves_current_dangling() {
let root = tempdir().unwrap();
let (a, b) = (root.path().join("a"), root.path().join("b"));
std::fs::create_dir_all(&a).unwrap();
std::fs::create_dir_all(&b).unwrap();
let current = root.path().join("current");
point_at(¤t, &a).expect("first");
assert_eq!(resolve(¤t).unwrap().as_deref(), Some(a.as_path()));
point_at(¤t, &b).expect("swap over an existing link");
assert_eq!(resolve(¤t).unwrap().as_deref(), Some(b.as_path()));
let tmp = root.path().join("current.tmp");
assert!(
!tmp.exists(),
"a successful swap must not leave current.tmp behind"
);
}
#[test]
fn resolve_of_a_missing_current_is_none() {
let root = tempdir().unwrap();
let current = root.path().join("current");
assert_eq!(resolve(¤t).unwrap(), None);
}
#[test]
fn resolve_of_a_plain_file_is_an_error() {
let root = tempdir().unwrap();
let current = root.path().join("current");
std::fs::write(¤t, b"not a symlink").unwrap();
let err = resolve(¤t).expect_err("a plain file is not a symlink to read");
assert!(matches!(err, Error::Io { .. }));
}
#[test]
fn point_at_works_the_first_time_current_does_not_exist() {
let root = tempdir().unwrap();
let release = root.path().join("release");
std::fs::create_dir_all(&release).unwrap();
let current = root.path().join("current");
point_at(¤t, &release).expect("first deploy, no prior current");
assert_eq!(
resolve(¤t).unwrap().as_deref(),
Some(release.as_path())
);
}
}