use {
crate::{
error::{DebianError, Result},
io::{Compression, ContentDigest, DataResolver, DigestingReader},
repository::{
release::ReleaseFile, ReleaseReader, RepositoryPathVerification,
RepositoryPathVerificationState, RepositoryRootReader, RepositoryWrite,
RepositoryWriter,
},
},
async_trait::async_trait,
futures::{io::BufReader, AsyncRead, AsyncReadExt},
std::{
borrow::Cow,
path::{Path, PathBuf},
pin::Pin,
},
url::Url,
};
#[derive(Clone, Debug)]
pub struct FilesystemRepositoryReader {
root_dir: PathBuf,
}
impl FilesystemRepositoryReader {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
root_dir: path.as_ref().to_path_buf(),
}
}
}
#[async_trait]
impl DataResolver for FilesystemRepositoryReader {
async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
let path = self.root_dir.join(path);
let f = std::fs::File::open(&path)
.map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?;
Ok(Box::pin(futures::io::AllowStdIo::new(f)))
}
}
#[async_trait]
impl RepositoryRootReader for FilesystemRepositoryReader {
fn url(&self) -> Result<Url> {
Url::from_file_path(&self.root_dir)
.map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string()))
}
async fn release_reader_with_distribution_path(
&self,
path: &str,
) -> Result<Box<dyn ReleaseReader>> {
let distribution_path = path.trim_matches('/').to_string();
let inrelease_path = format!("{}/InRelease", distribution_path);
let release_path = format!("{}/Release", distribution_path);
let distribution_dir = self.root_dir.join(&distribution_path);
let release = self
.fetch_inrelease_or_release(&inrelease_path, &release_path)
.await?;
let fetch_compression = Compression::default_preferred_order()
.next()
.expect("iterator should not be empty");
Ok(Box::new(FilesystemReleaseClient {
distribution_dir,
relative_path: distribution_path,
release,
fetch_compression,
}))
}
}
pub struct FilesystemReleaseClient {
distribution_dir: PathBuf,
relative_path: String,
release: ReleaseFile<'static>,
fetch_compression: Compression,
}
#[async_trait]
impl DataResolver for FilesystemReleaseClient {
async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
let path = self.distribution_dir.join(path);
let f = std::fs::File::open(&path)
.map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?;
Ok(Box::pin(BufReader::new(futures::io::AllowStdIo::new(f))))
}
}
#[async_trait]
impl ReleaseReader for FilesystemReleaseClient {
fn url(&self) -> Result<Url> {
Url::from_file_path(&self.distribution_dir)
.map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string()))
}
fn root_relative_path(&self) -> &str {
&self.relative_path
}
fn release_file(&self) -> &ReleaseFile<'static> {
&self.release
}
fn preferred_compression(&self) -> Compression {
self.fetch_compression
}
fn set_preferred_compression(&mut self, compression: Compression) {
self.fetch_compression = compression;
}
}
pub struct FilesystemRepositoryWriter {
root_dir: PathBuf,
}
impl FilesystemRepositoryWriter {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
root_dir: path.as_ref().to_path_buf(),
}
}
}
#[async_trait]
impl RepositoryWriter for FilesystemRepositoryWriter {
async fn verify_path<'path>(
&self,
path: &'path str,
expected_content: Option<(u64, ContentDigest)>,
) -> Result<RepositoryPathVerification<'path>> {
let dest_path = self.root_dir.join(path);
let metadata = match async_std::fs::metadata(&dest_path).await {
Ok(res) => res,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(RepositoryPathVerification {
path,
state: RepositoryPathVerificationState::Missing,
});
}
Err(e) => return Err(DebianError::RepositoryIoPath(path.to_string(), e)),
};
if metadata.is_file() {
if let Some((expected_size, expected_digest)) = expected_content {
if metadata.len() != expected_size as u64 {
Ok(RepositoryPathVerification {
path,
state: RepositoryPathVerificationState::ExistsIntegrityMismatch,
})
} else {
let f = async_std::fs::File::open(&dest_path)
.await
.map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?;
let mut remaining = expected_size;
let mut reader = DigestingReader::new(f);
let mut buf = [0u8; 16384];
loop {
let size = reader
.read(&mut buf[..])
.await
.map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?
as u64;
if size >= remaining || size == 0 {
break;
}
remaining -= size;
}
let digest = reader.finish().1;
Ok(RepositoryPathVerification {
path,
state: if digest.matches_digest(&expected_digest) {
RepositoryPathVerificationState::ExistsIntegrityVerified
} else {
RepositoryPathVerificationState::ExistsIntegrityMismatch
},
})
}
} else {
Ok(RepositoryPathVerification {
path,
state: RepositoryPathVerificationState::ExistsNoIntegrityCheck,
})
}
} else {
Ok(RepositoryPathVerification {
path,
state: RepositoryPathVerificationState::Missing,
})
}
}
async fn write_path<'path, 'reader>(
&self,
path: Cow<'path, str>,
reader: Pin<Box<dyn AsyncRead + Send + 'reader>>,
) -> Result<RepositoryWrite<'path>> {
let dest_path = self.root_dir.join(path.as_ref());
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| DebianError::RepositoryIoPath(format!("{}", parent.display()), e))?;
}
let fh = std::fs::File::create(&dest_path)
.map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?;
let mut writer = futures::io::AllowStdIo::new(fh);
let bytes_written = futures::io::copy(reader, &mut writer)
.await
.map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?;
Ok(RepositoryWrite {
path,
bytes_written,
})
}
}