use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use atelier_sdk_diff::PackageId;
use sha2::{Digest, Sha256};
use crate::engine::FileBlob;
use crate::error::Error;
const PROJECTIONS_DIR: &str = "projections";
static STAGED_SEQUENCE: AtomicU64 = AtomicU64::new(0);
pub(crate) struct ProjectionCache {
dir: PathBuf,
}
impl ProjectionCache {
pub fn new(control: &Path) -> Self {
Self {
dir: control.join(PROJECTIONS_DIR),
}
}
pub fn read(&self, package: PackageId, blob: &FileBlob) -> Option<String> {
let entry = fs::read_to_string(self.entry(package, blob)).ok()?;
let (digest, text) = entry.split_once('\n')?;
if digest != hex_sha256(text) {
return None;
}
Some(text.to_owned())
}
pub fn store(&self, package: PackageId, blob: &FileBlob, text: &str) -> Result<(), Error> {
let entry = self.entry(package, blob);
let parent = entry.parent().ok_or_else(|| {
Error::Engine(format!(
"projection entry {} has no parent",
entry.display()
))
})?;
fs::create_dir_all(parent)?;
let staged = entry.with_extension(format!(
"staged-{}-{}",
std::process::id(),
STAGED_SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
fs::write(&staged, format!("{}\n{text}", hex_sha256(text)))?;
if let Err(error) = fs::rename(&staged, &entry) {
if entry.is_file() {
remove_if_present(&staged)?;
return Ok(());
}
return Err(error.into());
}
Self::sweep_staged(parent, &blob.id)
}
fn sweep_staged(parent: &Path, blob_id: &str) -> Result<(), Error> {
let staged_prefix = format!("{blob_id}.staged-");
for sibling in fs::read_dir(parent)? {
let sibling = sibling?;
if sibling
.file_name()
.to_str()
.is_some_and(|name| name.starts_with(&staged_prefix))
{
remove_if_present(&sibling.path())?;
}
}
Ok(())
}
fn entry(&self, package: PackageId, blob: &FileBlob) -> PathBuf {
self.dir.join(package.to_string()).join(&blob.id)
}
}
pub(crate) fn content_id(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
fn hex_sha256(text: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(text.as_bytes());
format!("{:x}", hasher.finalize())
}
fn remove_if_present(path: &Path) -> Result<(), Error> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
}