//! Filesystem abstraction for the reconciler.
//!
//! The reconciler only ever touches disk through [`FileSystem`], so tests can
//! substitute an in-memory fake and assert idempotency without root or a real
//! target. [`RealFs`] is the production implementation.
use std::io;
use std::path::Path;
/// The bits of file metadata the reconciler compares against desired state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileMeta {
/// Permission bits only (mode & 0o7777).
pub mode: u32,
pub uid: u32,
pub gid: u32,
/// Whether the path is a directory (as opposed to a file or other node).
/// Symlinks are reported as non-directories (metadata does not follow them).
pub is_dir: bool,
}
/// Side-effecting filesystem operations, injectable for testing.
pub trait FileSystem {
/// File content, or `None` if the path does not exist. Errors only on real
/// I/O failures (permission, not-a-file), never on plain absence.
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>>;
/// Metadata, or `None` if the path does not exist.
fn metadata(&self, path: &Path) -> io::Result<Option<FileMeta>>;
/// Create parent directories as needed and write `content`, atomically
/// where possible (write-temp-then-rename) so readers never see a partial
/// file.
fn write(&mut self, path: &Path, content: &[u8]) -> io::Result<()>;
/// Set permission bits (mode & 0o7777).
fn set_mode(&mut self, path: &Path, mode: u32) -> io::Result<()>;
/// Change owner/group. `None` leaves that id unchanged.
fn chown(&mut self, path: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()>;
/// Create the directory and any missing parents. Succeeds if it already
/// exists as a directory.
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
/// Remove the directory and its contents recursively.
fn remove_dir(&mut self, path: &Path) -> io::Result<()>;
/// Remove the file. Succeeds even if already gone is the caller's concern;
/// this may return `NotFound`.
fn remove(&mut self, path: &Path) -> io::Result<()>;
/// Resolve a username to a uid.
fn resolve_user(&self, name: &str) -> io::Result<u32>;
/// Resolve a group name to a gid.
fn resolve_group(&self, name: &str) -> io::Result<u32>;
}
/// Production [`FileSystem`] backed by `std::fs`, `std::os::unix`, and `nix`
/// for name resolution.
#[derive(Debug, Default, Clone, Copy)]
pub struct RealFs;
impl FileSystem for RealFs {
fn read(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(b) => Ok(Some(b)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn metadata(&self, path: &Path) -> io::Result<Option<FileMeta>> {
use std::os::unix::fs::MetadataExt;
match std::fs::symlink_metadata(path) {
Ok(m) => Ok(Some(FileMeta {
mode: m.mode() & 0o7777,
uid: m.uid(),
gid: m.gid(),
is_dir: m.file_type().is_dir(),
})),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn write(&mut self, path: &Path, content: &[u8]) -> io::Result<()> {
use std::io::Write;
let dir = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"destination has no parent directory",
)
})?;
std::fs::create_dir_all(dir)?;
// Write to a sibling temp file, fsync, then rename over the target so
// the replacement is atomic on the same filesystem.
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid file name"))?;
let tmp = dir.join(format!(".{file_name}.devendra.tmp"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(content)?;
f.sync_all()?;
}
match std::fs::rename(&tmp, path) {
Ok(()) => Ok(()),
Err(e) => {
let _ = std::fs::remove_file(&tmp);
Err(e)
}
}
}
fn set_mode(&mut self, path: &Path, mode: u32) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
fn chown(&mut self, path: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
std::os::unix::fs::chown(path, uid, gid)
}
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
std::fs::create_dir_all(path)
}
fn remove_dir(&mut self, path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path)
}
fn remove(&mut self, path: &Path) -> io::Result<()> {
std::fs::remove_file(path)
}
fn resolve_user(&self, name: &str) -> io::Result<u32> {
// Accept a bare numeric id as a convenience, else look up the name.
if let Ok(uid) = name.parse::<u32>() {
return Ok(uid);
}
match nix::unistd::User::from_name(name) {
Ok(Some(u)) => Ok(u.uid.as_raw()),
Ok(None) => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no such user: {name}"),
)),
Err(e) => Err(io::Error::other(format!("getpwnam({name}): {e}"))),
}
}
fn resolve_group(&self, name: &str) -> io::Result<u32> {
if let Ok(gid) = name.parse::<u32>() {
return Ok(gid);
}
match nix::unistd::Group::from_name(name) {
Ok(Some(g)) => Ok(g.gid.as_raw()),
Ok(None) => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no such group: {name}"),
)),
Err(e) => Err(io::Error::other(format!("getgrnam({name}): {e}"))),
}
}
}
neiam
/devendra
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
5.9 KiB · text
History
d69022d