use std::fs::{self, DirBuilder, File};
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(test)]
use super::fixtures::{Event, Hooks};
use super::types::Error;
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
pub(super) struct Stage {
path: PathBuf,
pub published: bool,
}
impl Stage {
pub(super) fn create(
destination: &Path,
#[cfg(test)] hooks: &mut Hooks,
) -> Result<Self, Error> {
let parent = destination
.parent()
.ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?;
for _ in 0..64 {
let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
let path = parent.join(format!(".haem-stage-{}-{sequence}", std::process::id()));
match DirBuilder::new().mode(0o700).create(&path) {
Ok(()) => {
#[cfg(test)]
hooks.events.push(Event::Stage(path.clone()));
return Ok(Self {
path,
published: false,
});
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(Error::new("bundle_write_failed", &error)),
}
}
Err(Error::new(
"bundle_write_failed",
"all staging names are occupied",
))
}
pub(super) fn path(&self) -> &Path {
&self.path
}
pub(super) fn publish(
&mut self,
destination: &Path,
#[cfg(test)] hooks: &mut Hooks,
) -> Result<(), Error> {
#[cfg(test)]
hooks
.before_publish(destination)
.map_err(|error| Error::new("bundle_publish_failed", &error))?;
fs::rename(&self.path, destination)
.map_err(|error| Error::new("bundle_publish_failed", &error))?;
self.published = true;
#[cfg(test)]
hooks.events.push(Event::Publish(destination.to_owned()));
sync_path(
destination
.parent()
.ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?,
#[cfg(test)]
hooks,
)
}
pub(super) fn cleanup(&self, #[cfg(test)] hooks: &mut Hooks) -> Result<(), Error> {
if self.published {
return Ok(());
}
#[cfg(test)]
hooks
.before_cleanup()
.map_err(|error| Error::new("bundle_cleanup_failed", &error))?;
fs::remove_dir_all(&self.path)
.map_err(|error| Error::new("bundle_cleanup_failed", &error))?;
sync_path(
self.path
.parent()
.ok_or_else(|| Error::new("bundle_path_invalid", "staging parent missing"))?,
#[cfg(test)]
hooks,
)
.map_err(|error| Error::new("bundle_cleanup_failed", &error))
}
}
pub(super) fn sync_path(path: &Path, #[cfg(test)] hooks: &mut Hooks) -> Result<(), Error> {
let file = File::open(path).map_err(|error| Error::new("bundle_sync_failed", &error))?;
sync_file(
&file,
path,
#[cfg(test)]
hooks,
)
}
pub(super) fn sync_file(
file: &File,
path: &Path,
#[cfg(test)] hooks: &mut Hooks,
) -> Result<(), Error> {
#[cfg(test)]
hooks
.before_sync()
.map_err(|error| Error::new("bundle_sync_failed", &error))?;
file.sync_all().map_err(|error| {
Error::new(
"bundle_sync_failed",
&format!("{}: {error}", path.display()),
)
})?;
#[cfg(test)]
hooks.events.push(Event::Sync(path.to_owned()));
Ok(())
}
pub(super) fn stage_label(path: &Path) -> String {
path.to_str().map_or_else(
|| format!("[lossy] {}", path.to_string_lossy()),
str::to_owned,
)
}
pub(super) fn stale_stages(destination: &Path) -> Result<Vec<String>, Error> {
let parent = destination
.parent()
.ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?;
let mut stages = Vec::new();
let result: Result<(), Error> = (|| {
for entry in
fs::read_dir(parent).map_err(|error| Error::new("bundle_read_failed", &error))?
{
let entry = entry.map_err(|error| Error::new("bundle_read_failed", &error))?;
if entry
.file_name()
.as_encoded_bytes()
.starts_with(b".haem-stage-")
{
let path = entry.path();
stages.push(stage_label(&path));
}
}
Ok(())
})();
stages.sort_unstable();
if let Err(error) = result {
return Err(error.with_stale_stages(stages));
}
Ok(stages)
}