use std::{fs, path::Path};
use crate::{
config::{DeployMethod, Entry},
error::{Error, Result},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryState {
Deployed,
Modified,
Missing,
Broken,
Conflict,
}
pub fn check_state(entry: &Entry, repo_root: &Path, default_method: DeployMethod) -> EntryState {
let method = entry.method.unwrap_or(default_method);
let Ok(target) = crate::path::expand_tilde(std::path::Path::new(&entry.target)) else {
return EntryState::Missing;
};
let source = if entry.encrypted && !entry.directory {
repo_root.join(format!("{}.enc", entry.source))
} else {
repo_root.join(&entry.source)
};
if !target.exists() && !crate::fs::is_symlink(&target) {
return EntryState::Missing;
}
match method {
DeployMethod::Symlink if !entry.encrypted => {
if crate::fs::is_symlink(&target) {
match crate::fs::read_link(&target) {
Ok(link_target) => {
if link_target == source {
EntryState::Deployed
} else {
EntryState::Broken
}
}
Err(_) => EntryState::Broken,
}
} else {
EntryState::Conflict
}
}
_ => {
if crate::fs::is_symlink(&target) {
EntryState::Conflict
} else if entry.encrypted {
EntryState::Deployed
} else {
match crate::fs::files_identical(&source, &target) {
Ok(true) => EntryState::Deployed,
Ok(false) => EntryState::Modified,
Err(_) => EntryState::Broken,
}
}
}
}
}
pub fn deploy_entry(
entry: &Entry,
repo_root: &Path,
default_method: DeployMethod,
force: bool,
) -> Result<()> {
let method = entry.method.unwrap_or(default_method);
let target = crate::path::expand_tilde(std::path::Path::new(&entry.target))?;
let source = repo_root.join(&entry.source);
if !source.exists() {
return Err(Error::Deploy {
entry: entry.source.clone(),
message: format!("source `{}` does not exist in repo", source.display()),
});
}
if target.exists() || crate::fs::is_symlink(&target) {
if !force {
let state = check_state(entry, repo_root, default_method);
if state == EntryState::Deployed {
return Ok(()); }
if state == EntryState::Conflict {
return Err(Error::Deploy {
entry: entry.source.clone(),
message: format!(
"unmanaged file exists at `{}` — use --force to overwrite",
target.display()
),
});
}
}
if crate::fs::is_symlink(&target) {
crate::fs::remove_symlink(&target)?;
} else if target.is_dir() {
fs::remove_dir_all(&target).map_err(|e| Error::io(&target, "remove directory", e))?;
} else {
fs::remove_file(&target).map_err(|e| Error::io(&target, "remove file", e))?;
}
}
match method {
DeployMethod::Symlink => {
crate::fs::create_symlink(&source, &target)?;
}
DeployMethod::Copy => {
if entry.directory {
copy_directory(&source, &target)?;
} else {
crate::fs::copy_file(&source, &target)?;
}
}
}
if let Some(perms) = entry.permissions {
crate::fs::set_permissions(&target, perms)?;
}
Ok(())
}
pub fn deploy_encrypted(entry: &Entry, repo_root: &Path, password: &str) -> Result<()> {
let target = crate::path::expand_tilde(std::path::Path::new(&entry.target))?;
let master_key = crate::crypto::vault::unlock_vault(password)?;
if entry.directory {
let source = repo_root.join(&entry.source);
if !source.exists() {
return Err(Error::Deploy {
entry: entry.source.clone(),
message: format!(
"encrypted source directory `{}` not found",
source.display()
),
});
}
deploy_encrypted_directory(&source, &target, &master_key)?;
} else {
let source = repo_root.join(format!("{}.enc", entry.source));
if !source.exists() {
return Err(Error::Deploy {
entry: entry.source.clone(),
message: format!("encrypted source `{}` not found", source.display()),
});
}
let encrypted =
fs::read(&source).map_err(|e| Error::io(&source, "read encrypted file", e))?;
let plaintext = crate::crypto::decrypt_with_key(&encrypted, &master_key)?;
crate::fs::atomic_write(&target, &plaintext)?;
}
if let Some(perms) = entry.permissions {
crate::fs::set_permissions(&target, perms)?;
} else if !entry.directory {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
fs::set_permissions(&target, perms)
.map_err(|e| Error::io(&target, "set permissions", e))?;
}
}
Ok(())
}
fn deploy_encrypted_directory(src: &Path, dst: &Path, key: &[u8; 32]) -> Result<()> {
fs::create_dir_all(dst).map_err(|e| Error::io(dst, "create directory", e))?;
for entry in fs::read_dir(src).map_err(|e| Error::io(src, "read directory", e))? {
let entry = entry.map_err(|e| Error::io(src, "read directory entry", e))?;
let src_path = entry.path();
let file_name = entry.file_name();
if src_path.is_dir() {
let dst_path = dst.join(&file_name);
deploy_encrypted_directory(&src_path, &dst_path, key)?;
} else if src_path.extension().and_then(|e| e.to_str()) == Some("enc") {
let encrypted =
fs::read(&src_path).map_err(|e| Error::io(&src_path, "read encrypted file", e))?;
let plaintext = crate::crypto::decrypt_with_key(&encrypted, key)?;
let dst_name = Path::new(&file_name).with_extension("");
let dst_path = dst.join(dst_name);
crate::fs::atomic_write(&dst_path, &plaintext)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
fs::set_permissions(&dst_path, perms).ok();
}
} else {
let dst_path = dst.join(&file_name);
crate::fs::copy_file(&src_path, &dst_path)?;
}
}
Ok(())
}
fn copy_directory(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst).map_err(|e| Error::io(dst, "create directory", e))?;
for entry in fs::read_dir(src).map_err(|e| Error::io(src, "read directory", e))? {
let entry = entry.map_err(|e| Error::io(src, "read directory entry", e))?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_directory(&src_path, &dst_path)?;
} else {
crate::fs::copy_file(&src_path, &dst_path)?;
}
}
Ok(())
}