use std::{
collections::BTreeMap,
env::current_dir,
path::{Path, PathBuf},
};
use bytes::Bytes;
use miette::{Context, IntoDiagnostic, miette};
use tokio::fs;
use walkdir::WalkDir;
use crate::{
manifest::{MANIFEST_FILE, Manifest, PackageManifest, PackagesManifest},
package::{Package, PackageName},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageStore {
root: PathBuf,
}
impl PackageStore {
pub const PROTO_PATH: &'static str = "proto";
pub const PROTO_VENDOR_PATH: &'static str = "proto/vendor";
fn new(root: PathBuf) -> Self {
Self { root }
}
pub async fn current() -> miette::Result<Self> {
Self::open(¤t_dir().into_diagnostic()?).await
}
pub fn proto_path(&self) -> PathBuf {
self.root.join(Self::PROTO_PATH)
}
pub fn proto_vendor_path(&self) -> PathBuf {
self.root.join(Self::PROTO_VENDOR_PATH)
}
fn populated_path(&self, manifest: &PackageManifest) -> PathBuf {
self.proto_vendor_path().join(manifest.name.to_string())
}
pub async fn open(path: impl AsRef<Path>) -> miette::Result<Self> {
let store = PackageStore::new(path.as_ref().to_path_buf());
let create = |dir: PathBuf| async move {
fs::create_dir_all(&dir)
.await
.into_diagnostic()
.wrap_err(miette!("failed to create {} directory", dir.display()))
};
create(store.proto_path()).await?;
create(store.proto_vendor_path()).await?;
Ok(store)
}
pub async fn clear(&self) -> miette::Result<()> {
let path = self.proto_vendor_path();
match fs::remove_dir_all(&path).await {
Ok(()) => {}
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {}
Err(_) => return Err(miette!("failed to clear {path:?} directory",)),
}
fs::create_dir(&path)
.await
.map_err(|_| miette!("failed to reinitialize {path:?} directory after cleaning"))
}
pub async fn unpack(&self, package: &Package) -> miette::Result<()> {
let pkg_dir = self.locate(package.name());
package.unpack(&pkg_dir).await?;
tracing::debug!(
"unpacked {}@{} into {}",
package.name(),
package.version(),
pkg_dir.display()
);
Ok(())
}
pub async fn uninstall(&self, package: &PackageName) -> miette::Result<()> {
let pkg_dir = self.proto_vendor_path().join(&**package);
fs::remove_dir_all(&pkg_dir)
.await
.into_diagnostic()
.wrap_err(miette!("failed to uninstall package {package}"))
}
pub async fn resolve(&self, package: &PackageName) -> miette::Result<PackagesManifest> {
let manifest_path = self.locate(package).join(MANIFEST_FILE);
let manifest = Manifest::require_package_manifest(&manifest_path)
.await
.wrap_err({
miette!(
"the package store is corrupted: `{}` is not present",
manifest_path.display()
)
})?;
Ok(manifest)
}
#[cfg(feature = "validation")]
pub async fn validate(
&self,
manifest: &PackageManifest,
) -> miette::Result<crate::validation::Violations> {
let root_path = self.proto_vendor_path();
let source_files = self.populated_files(manifest).await;
let mut parser = crate::validation::Validator::new(&root_path, manifest);
for file in &source_files {
parser.input(file);
}
parser.validate()
}
pub async fn release(
&self,
manifest: &PackagesManifest,
preserve_mtime: bool,
) -> miette::Result<Package> {
let pkg_path = self.proto_path();
let mut entries = BTreeMap::new();
for entry in self.collect(&pkg_path, false).await {
let path = entry.strip_prefix(&pkg_path).into_diagnostic()?;
let contents = tokio::fs::read(&entry).await.unwrap();
entries.insert(
path.into(),
Entry {
contents: contents.into(),
metadata: tokio::fs::metadata(&entry).await.ok(),
},
);
}
let package = Package::create(manifest.clone(), entries, preserve_mtime)?;
tracing::info!("packaged {}@{}", package.name(), package.version());
Ok(package)
}
pub fn locate(&self, package: &PackageName) -> PathBuf {
self.proto_vendor_path().join(&**package)
}
pub async fn collect(&self, path: &Path, vendored: bool) -> Vec<PathBuf> {
let mut paths: Vec<_> = WalkDir::new(path)
.into_iter()
.filter_map(Result::ok)
.map(|entry| entry.into_path())
.filter(|path| {
if vendored {
true
} else {
!path.starts_with(self.proto_vendor_path())
}
})
.filter(|path| {
let ext = path.extension().map(|s| s.to_str());
matches!(ext, Some(Some("proto")))
})
.collect();
paths.sort();
paths
}
pub async fn populate(&self, manifest: &PackageManifest) -> miette::Result<()> {
let source_path = self.proto_path();
let target_dir = self.proto_vendor_path().join(manifest.name.to_string());
if tokio::fs::try_exists(&target_dir)
.await
.into_diagnostic()
.wrap_err(format!(
"failed to check whether directory {} still exists",
target_dir.to_str().unwrap()
))?
{
tokio::fs::remove_dir_all(&target_dir)
.await
.into_diagnostic()
.wrap_err(format!(
"failed to remove directory {} and its contents.",
target_dir.to_str().unwrap()
))?;
}
for entry in self.collect(&source_path, false).await {
let file_name = entry.strip_prefix(&source_path).into_diagnostic()?;
let target_path = target_dir.join(file_name);
tokio::fs::create_dir_all(target_path.parent().unwrap())
.await
.into_diagnostic()
.wrap_err(format!(
"Failed to create directory {} and its parents.",
target_path.parent().unwrap().to_str().unwrap()
))?;
tokio::fs::copy(entry, target_path)
.await
.into_diagnostic()?;
}
Ok(())
}
pub async fn populated_files(&self, manifest: &PackageManifest) -> Vec<PathBuf> {
self.collect(&self.populated_path(manifest), true).await
}
}
pub struct Entry {
pub contents: Bytes,
pub metadata: Option<std::fs::Metadata>,
}
#[test]
fn can_get_proto_path() {
assert_eq!(
PackageStore::new("/tmp".into()).proto_path(),
PathBuf::from("/tmp/proto")
);
assert_eq!(
PackageStore::new("/tmp".into()).proto_vendor_path(),
PathBuf::from("/tmp/proto/vendor")
);
}