use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use super::path::CacheLocation;
static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ArtifactReadError {
Missing,
Unreadable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct PublishError;
pub(super) fn read_artifact(
path: &Path,
maximum_bytes: u64,
) -> Result<Vec<u8>, ArtifactReadError> {
let metadata = fs::metadata(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
return ArtifactReadError::Missing;
}
return ArtifactReadError::Unreadable;
})?;
if !metadata.is_file() || metadata.len() > maximum_bytes {
return Err(ArtifactReadError::Unreadable);
}
return fs::read(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
return ArtifactReadError::Missing;
}
return ArtifactReadError::Unreadable;
})
}
#[cfg(unix)]
fn make_directory_private(path: &Path) -> Result<(), PublishError> {
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(path, fs::Permissions::from_mode(0o700))
.map_err(|_| return PublishError)
}
#[cfg(not(unix))]
fn make_directory_private(_path: &Path) -> Result<(), PublishError> {
return Ok(())
}
#[cfg(unix)]
fn make_file_private(path: &Path) -> Result<(), PublishError> {
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(path, fs::Permissions::from_mode(0o600))
.map_err(|_| return PublishError)
}
#[cfg(not(unix))]
fn make_file_private(_path: &Path) -> Result<(), PublishError> {
return Ok(())
}
fn prepare_directories(location: &CacheLocation) -> Result<(), PublishError> {
for directory in &location.protected_directories {
fs::create_dir_all(directory).map_err(|_| return PublishError)?;
make_directory_private(directory)?;
}
return Ok(())
}
fn temporary_path(artifact_path: &Path) -> Result<PathBuf, PublishError> {
let parent = artifact_path.parent().ok_or(PublishError)?;
let sequence = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
return Ok(parent.join(format!(
".rules.bin.{}.{}.tmp",
std::process::id(),
sequence,
)))
}
pub(super) fn publish_artifact(
location: &CacheLocation,
bytes: &[u8],
) -> Result<(), PublishError> {
prepare_directories(location)?;
let temporary = temporary_path(&location.artifact_path)?;
let publication = publish_from_temporary(&temporary, &location.artifact_path, bytes);
if publication.is_err() {
let _ = fs::remove_file(&temporary);
}
return publication
}
fn publish_from_temporary(
temporary: &Path,
artifact_path: &Path,
bytes: &[u8],
) -> Result<(), PublishError> {
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.open(temporary)
.map_err(|_| return PublishError)?;
make_file_private(temporary)?;
file.write_all(bytes).map_err(|_| return PublishError)?;
file.sync_all().map_err(|_| return PublishError)?;
drop(file);
fs::rename(temporary, artifact_path).map_err(|_| return PublishError)?;
sync_parent_best_effort(artifact_path);
return Ok(())
}
fn sync_parent_best_effort(artifact_path: &Path) {
let Some(parent) = artifact_path.parent() else {
return;
};
let Ok(directory) = File::open(parent) else {
return;
};
let _ = directory.sync_all();
}
#[cfg(test)]
#[path = "publish_tests.rs"]
mod tests;