use std::io::ErrorKind;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
use std::{fs, io, path};
use serde::{Deserialize, Serialize};
use symlink::{remove_symlink_auto, symlink_auto};
static ROOT_DIR: LazyLock<AbsPath> = LazyLock::new(|| AbsPath::_new(PathBuf::from("/")));
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct AbsPath {
path_buf: PathBuf,
}
impl AbsPath {
pub fn new() -> AbsPath {
ROOT_DIR.clone()
}
pub fn from_path(path: &Path) -> io::Result<Self> {
if path.is_absolute() {
Ok(AbsPath::_new(path.to_path_buf()))
} else {
Ok(AbsPath::_new(path::absolute(path)?))
}
}
pub fn from_owned_path(path: PathBuf) -> io::Result<Self> {
if path.is_absolute() {
Ok(AbsPath::_new(path))
} else {
Ok(AbsPath::_new(path::absolute(path)?))
}
}
pub fn relative_to(&self, other: &AbsPath) -> PathBuf {
if other.eq(self) {
return PathBuf::from_str(".").expect("infallible");
}
if other.eq(&ROOT_DIR) {
return self._relative_to_root().to_path_buf();
}
if self.starts_with(other) {
return self.strip_prefix(other).unwrap().to_path_buf();
}
let mut relative_path = PathBuf::new();
let mut current: &Path = other.as_ref();
let mut parent = other.parent();
while let Some(path) = parent {
relative_path.push("..");
current = path;
if self.starts_with(current) {
break;
}
parent = path.parent();
}
if current.eq(self.as_ref()) {
relative_path
} else {
relative_path.join(self.strip_prefix(current).unwrap())
}
}
pub fn relative_to_cwd(&self) -> io::Result<PathBuf> {
let cwd = std::env::current_dir()?;
Ok(self.relative_to(&AbsPath::_new(cwd)))
}
pub fn parent_or_root(&self) -> AbsPath {
match self.parent() {
Some(parent) => AbsPath::_new(parent.to_path_buf()),
None => ROOT_DIR.clone(),
}
}
fn _new(path_buf: PathBuf) -> Self {
AbsPath { path_buf }
}
fn _relative_to_root(&self) -> &Path {
self.strip_prefix("/").expect("path is absolute")
}
}
impl Default for AbsPath {
fn default() -> Self {
AbsPath::new()
}
}
impl Deref for AbsPath {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.path_buf
}
}
impl AsRef<Path> for AbsPath {
fn as_ref(&self) -> &Path {
&self.path_buf
}
}
impl FromStr for AbsPath {
type Err = io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
AbsPath::from_path(&PathBuf::from_str(s).expect("infallible"))
}
}
impl From<AbsPath> for PathBuf {
fn from(value: AbsPath) -> Self {
value.path_buf
}
}
pub trait JoinOnRoot {
fn join_on_root(&self, root: &AbsPath) -> AbsPath;
}
impl JoinOnRoot for Path {
fn join_on_root(&self, root: &AbsPath) -> AbsPath {
AbsPath::_new(path::absolute(root.join(self.strip_prefix("/").unwrap_or(self))).unwrap())
}
}
impl JoinOnRoot for AbsPath {
fn join_on_root(&self, root: &AbsPath) -> AbsPath {
AbsPath::_new(path::absolute(root.join(self._relative_to_root())).unwrap())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct Symlink {
pub source: AbsPath,
pub destination: AbsPath,
}
impl Symlink {
pub fn new(source: AbsPath, destination: AbsPath) -> Self {
Symlink {
source,
destination,
}
}
pub fn exists(&self) -> bool {
matches!(self.status(), SymlinkStatus::Exists)
}
pub fn is_broken(&self) -> bool {
matches!(self.status(), SymlinkStatus::Broken)
}
pub fn status(&self) -> SymlinkStatus {
if self.destination.is_symlink() {
return if let Ok(source) = self.destination.read_link()
&& source.eq(&self.source.path_buf)
&& source.exists()
{
SymlinkStatus::Exists
} else {
SymlinkStatus::Broken
};
}
SymlinkStatus::NoSymlink
}
pub fn create(&self) -> io::Result<()> {
if !self.source.exists() {
Err(io::Error::new(
ErrorKind::NotFound,
format!("Source does not exist: {}", self.source.display()),
))
} else if self.exists() {
Ok(())
} else {
if let Some(parent) = self.destination.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)?;
}
symlink_auto(&self.source, &self.destination)?;
Ok(())
}
}
pub fn remove(&self) -> io::Result<()> {
match self.status() {
SymlinkStatus::NoSymlink => {}
_ => remove_symlink_auto(&self.destination)?,
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymlinkStatus {
Exists,
Broken,
NoSymlink,
}
pub fn expand_to_path(s: &str) -> io::Result<PathBuf> {
Ok(PathBuf::from_str(
shellexpand::full(s)
.map_err(|err| io::Error::new(ErrorKind::NotFound, err))?
.to_string()
.as_str(),
)
.expect("infallible"))
}
pub fn move_to_backup(path: &AbsPath) -> io::Result<()> {
let mut i: u8 = 0;
let mut backup_path = path.with_added_extension(format!("bkp-{i}"));
while backup_path.exists() && i < 100 {
i += 1;
backup_path = path.with_added_extension(format!("bkp-{i}"));
}
if i >= 50 {
return Err(io::Error::other(format!(
"Too many backups for {}",
path.display()
)));
}
if path.is_symlink() {
let source = path.read_link()?;
symlink::remove_symlink_auto(path)?;
symlink::symlink_auto(&source, backup_path)?;
} else {
fs::rename(path, backup_path)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs::File;
use std::io::Write;
use anyhow;
use tempfile::tempdir;
use super::*;
#[test]
fn relative_to() {
let a = AbsPath::from_str("/hello").unwrap();
let b = AbsPath::from_str("/hello/kitty").unwrap();
let c = AbsPath::from_str("/hello/kitty/cat").unwrap();
let d = AbsPath::from_str("/hello/world").unwrap();
let e = AbsPath::from_str("/goodbye/eri").unwrap();
assert_eq!(a.relative_to(&a), PathBuf::from_str(".").unwrap());
assert_eq!(c.relative_to(&c), PathBuf::from_str(".").unwrap());
assert_eq!(b.relative_to(&a), PathBuf::from_str("kitty").unwrap());
assert_eq!(c.relative_to(&a), PathBuf::from_str("kitty/cat").unwrap());
assert_eq!(a.relative_to(&b), PathBuf::from_str("..").unwrap());
assert_eq!(a.relative_to(&c), PathBuf::from_str("../..").unwrap());
assert_eq!(
c.relative_to(&d),
PathBuf::from_str("../kitty/cat").unwrap()
);
assert_eq!(d.relative_to(&c), PathBuf::from_str("../../world").unwrap());
assert_eq!(
c.relative_to(&e),
PathBuf::from_str("../../hello/kitty/cat").unwrap()
);
assert_eq!(
e.relative_to(&c),
PathBuf::from_str("../../../goodbye/eri").unwrap()
);
}
#[test]
fn symlink_file() -> anyhow::Result<()> {
let tmp = tempdir()?;
let source = AbsPath::from_path(&tmp.path().join("s"))?;
let destination = AbsPath::from_path(&tmp.path().join("t"))?;
let symlink = Symlink::new(source.clone(), destination.clone());
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::NoSymlink);
let mut file = File::create(&source)?;
file.write_all(b"Trans rights!")?;
file.sync_all()?;
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::NoSymlink);
symlink.create()?;
assert!(destination.is_symlink());
assert_eq!(destination.read_link()?, source.to_path_buf());
assert_eq!(fs::read_to_string(&destination)?, "Trans rights!");
assert!(symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::Exists);
fs::remove_file(&source)?;
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::Broken);
tmp.close()?;
Ok(())
}
#[test]
fn symlink_dir() -> anyhow::Result<()> {
let tmp = tempdir()?;
let source = AbsPath::from_path(&tmp.path().join("s"))?;
let destination = AbsPath::from_path(&tmp.path().join("t"))?;
let symlink = Symlink::new(source.clone(), destination.clone());
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::NoSymlink);
fs::create_dir(&source)?;
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::NoSymlink);
symlink.create()?;
assert!(destination.is_symlink());
assert_eq!(destination.read_link()?, source.to_path_buf());
assert!(symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::Exists);
fs::remove_dir(&source)?;
assert!(!symlink.exists());
assert_eq!(symlink.status(), SymlinkStatus::Broken);
tmp.close()?;
Ok(())
}
#[test]
fn backup_file() -> anyhow::Result<()> {
let tmp = tempdir()?;
let orig = AbsPath::from_owned_path(tmp.path().join("orig_file"))?;
let mut file = File::create(&orig)?;
file.write_all(b"This memorial dedicated to those Who perished on the climb")?;
file.sync_all()?;
assert!(orig.exists());
move_to_backup(&orig)?;
assert!(!orig.exists());
assert!(orig.with_added_extension("bkp-0").exists());
assert_eq!(
fs::read_to_string(orig.with_added_extension("bkp-0"))?,
"This memorial dedicated to those Who perished on the climb"
);
fs::remove_file(orig.with_added_extension("bkp-0"))?;
let mut file = File::create(&orig)?;
file.write_all(b"This memorial dedicated to those Who perished on the climb")?;
file.sync_all()?;
for i in 0..3 {
File::create(orig.with_added_extension(format!("bkp-{i}")))?;
}
move_to_backup(&orig)?;
assert!(!orig.exists());
assert!(orig.with_added_extension("bkp-3").exists());
assert_eq!(
fs::read_to_string(orig.with_added_extension("bkp-3"))?,
"This memorial dedicated to those Who perished on the climb"
);
tmp.close()?;
Ok(())
}
#[test]
fn backup_dir() -> anyhow::Result<()> {
let tmp = tempdir()?;
let orig = AbsPath::from_owned_path(tmp.path().join("orig_dir"))?;
fs::create_dir(&orig)?;
assert!(orig.exists());
move_to_backup(&orig)?;
assert!(!orig.exists());
assert!(orig.with_added_extension("bkp-0").exists());
fs::remove_dir(orig.with_added_extension("bkp-0"))?;
fs::create_dir(&orig)?;
for i in 0..3 {
fs::create_dir(orig.with_added_extension(format!("bkp-{i}")))?;
}
move_to_backup(&orig)?;
assert!(!orig.exists());
assert!(orig.with_added_extension("bkp-3").exists());
tmp.close()?;
Ok(())
}
#[test]
fn expand_path() -> anyhow::Result<()> {
unsafe {
std::env::set_var("SYMP_TEST_VAR", "/tmp");
}
assert_eq!(
expand_to_path("$SYMP_TEST_VAR/howdy")?,
PathBuf::from_str("/tmp/howdy")?
);
assert_eq!(
expand_to_path("~/blåhaj")?,
PathBuf::from_str(
format!("{}/blåhaj", std::env::home_dir().unwrap().display()).as_str()
)?
);
Ok(())
}
}