use std::{
fmt::{Debug, Formatter},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use digest::generic_array::GenericArray;
use fs4::fs_std::FileExt;
use rattler_conda_types::package::{IndexJson, PathsJson};
use rattler_digest::Sha256Hash;
use crate::package_cache::PackageCacheLayerError;
pub struct CacheMetadata {
pub(super) revision: u64,
pub(super) sha256: Option<Sha256Hash>,
pub(super) path: PathBuf,
pub(super) index_json: Option<IndexJson>,
pub(super) paths_json: Option<PathsJson>,
}
impl Debug for CacheMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CacheMetadata")
.field("path", &self.path)
.field("revision", &self.revision)
.field("sha256", &self.sha256)
.finish()
}
}
impl CacheMetadata {
pub fn path(&self) -> &Path {
&self.path
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn index_json(&self) -> Option<&IndexJson> {
self.index_json.as_ref()
}
pub fn paths_json(&self) -> Option<&PathsJson> {
self.paths_json.as_ref()
}
}
pub struct CacheGlobalLock {
file: std::fs::File,
}
impl Debug for CacheGlobalLock {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CacheGlobalLock").finish()
}
}
impl Drop for CacheGlobalLock {
fn drop(&mut self) {
let _ = fs4::fs_std::FileExt::unlock(&self.file);
}
}
impl CacheGlobalLock {
pub async fn acquire(path: &Path) -> Result<Self, PackageCacheLayerError> {
let lock_file_path = path.to_path_buf();
let acquire_lock_fut = simple_spawn_blocking::tokio::run_blocking_task(move || {
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.read(true)
.open(&lock_file_path)
.map_err(|e| {
PackageCacheLayerError::LockError(
format!(
"failed to open global cache lock for writing: '{}'",
lock_file_path.display()
),
e,
)
})?;
file.lock_exclusive().map_err(move |e| {
PackageCacheLayerError::LockError(
format!(
"failed to acquire write lock on global cache lock file: '{}'",
lock_file_path.display()
),
e,
)
})?;
Ok(CacheGlobalLock { file })
});
tokio::select!(
lock = acquire_lock_fut => lock,
_ = warn_timeout_future(
"Blocking waiting for global file lock on package cache".to_string()
) => unreachable!("warn_timeout_future should never finish")
)
}
}
pub struct CacheMetadataFile {
file: Arc<std::fs::File>,
}
impl CacheMetadataFile {
pub async fn acquire(path: &Path) -> Result<Self, PackageCacheLayerError> {
let lock_file_path = path.to_path_buf();
simple_spawn_blocking::tokio::run_blocking_task(move || {
let file = std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock_file_path)
.map_err(|e| {
PackageCacheLayerError::LockError(
format!(
"failed to open cache metadata file: '{}'",
lock_file_path.display()
),
e,
)
})?;
Ok(CacheMetadataFile {
file: Arc::new(file),
})
})
.await
}
}
impl CacheMetadataFile {
pub async fn write_revision_and_sha(
&mut self,
revision: u64,
sha256: Option<&Sha256Hash>,
) -> Result<(), PackageCacheLayerError> {
let file = self.file.clone();
let sha256 = sha256.cloned();
simple_spawn_blocking::tokio::run_blocking_task(move || {
(&*file).rewind().map_err(|e| {
PackageCacheLayerError::LockError(
"failed to rewind cache lock for reading revision".to_string(),
e,
)
})?;
let revision_bytes = revision.to_be_bytes();
(&*file).write_all(&revision_bytes).map_err(|e| {
PackageCacheLayerError::LockError(
"failed to write revision from cache lock".to_string(),
e,
)
})?;
let sha_bytes = if let Some(sha) = sha256 {
let len = sha.len();
let sha = &sha[..];
(&*file).write_all(sha).map_err(|e| {
PackageCacheLayerError::LockError(
"failed to write sha256 from cache lock".to_string(),
e,
)
})?;
len
} else {
0
};
(&*file).flush().map_err(|e| {
PackageCacheLayerError::LockError(
"failed to flush cache lock after writing revision".to_string(),
e,
)
})?;
let file_length = revision_bytes.len() + sha_bytes;
file.set_len(file_length as u64).map_err(|e| {
PackageCacheLayerError::LockError(
"failed to truncate cache lock after writing revision".to_string(),
e,
)
})?;
Ok(())
})
.await
}
pub fn read_revision(&mut self) -> Result<u64, PackageCacheLayerError> {
(&*self.file).rewind().map_err(|e| {
PackageCacheLayerError::LockError(
"failed to rewind cache lock for reading revision".to_string(),
e,
)
})?;
let mut buf = [0; 8];
match (&*self.file).read_exact(&mut buf) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(0);
}
Err(e) => {
return Err(PackageCacheLayerError::LockError(
"failed to read revision from cache lock".to_string(),
e,
));
}
}
Ok(u64::from_be_bytes(buf))
}
pub fn read_sha256(&mut self) -> Result<Option<Sha256Hash>, PackageCacheLayerError> {
const SHA256_LEN: usize = 32;
const REVISION_LEN: u64 = 8;
(&*self.file).rewind().map_err(|e| {
PackageCacheLayerError::LockError(
"failed to rewind cache lock for reading sha256".to_string(),
e,
)
})?;
let mut buf = [0; SHA256_LEN];
let _ = (&*self.file)
.seek(SeekFrom::Start(REVISION_LEN))
.map_err(|e| {
PackageCacheLayerError::LockError(
"failed to seek to sha256 in cache lock".to_string(),
e,
)
})?;
match (&*self.file).read_exact(&mut buf) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
Err(e) => {
return Err(PackageCacheLayerError::LockError(
"failed to read sha256 from cache lock".to_string(),
e,
));
}
}
Ok(Some(GenericArray::clone_from_slice(&buf)))
}
}
async fn warn_timeout_future(message: String) {
loop {
tokio::time::sleep(Duration::from_secs(30)).await;
tracing::warn!("{}", &message);
}
}
#[cfg(test)]
mod tests {
use rattler_digest::{parse_digest_from_hex, Sha256};
use super::CacheMetadataFile;
#[tokio::test]
async fn cache_metadata_serialize_deserialize() {
let temp_dir = tempfile::tempdir().unwrap();
let metadata_file = temp_dir.path().join("foo.lock");
let mut metadata = CacheMetadataFile::acquire(&metadata_file).await.unwrap();
let sha = parse_digest_from_hex::<Sha256>(
"4dd9893f1eee45e1579d1a4f5533ef67a84b5e4b7515de7ed0db1dd47adc6bc8",
);
metadata
.write_revision_and_sha(1, sha.as_ref())
.await
.unwrap();
let revision = metadata.read_revision().unwrap();
assert_eq!(revision, 1);
let read_sha = metadata.read_sha256().unwrap();
assert_eq!(sha, read_sha);
}
}