use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use tracing::log;
#[cfg(test)]
use tempfile::TempDir;
use crate::io::storage::Storage;
use crate::io::storage::StorageExt;
use crate::paths;
use crate::uri::Namespace;
use crate::Error;
use crate::Res;
mod status;
pub use status::Change;
pub use status::ChangeSet;
pub use status::InstalledPackageStatus;
pub use status::UpstreamState;
mod package;
pub use package::CommitState;
pub use package::LineagePaths;
pub use package::PackageLineage;
pub use package::PathState;
mod home;
pub use home::Home;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct DomainLineage {
#[serde(default = "BTreeMap::new")]
pub packages: BTreeMap<Namespace, PackageLineage>,
#[serde(default)]
pub home: Home,
}
impl DomainLineage {
pub fn new(home_dir: impl AsRef<Path>) -> Self {
DomainLineage {
packages: BTreeMap::new(),
home: Home::from(home_dir),
}
}
pub fn namespaces(&self) -> Vec<Namespace> {
let mut namespaces: Vec<Namespace> = self.packages.keys().cloned().collect();
namespaces.sort();
namespaces
}
#[cfg(test)]
pub fn from_temp_dir() -> Res<(Self, tempfile::TempDir)> {
let temp_dir = TempDir::new()?;
Ok((DomainLineage::new(temp_dir.path()), temp_dir))
}
}
impl AsRef<PathBuf> for DomainLineage {
fn as_ref(&self) -> &PathBuf {
self.home.as_ref()
}
}
impl DomainLineage {
pub fn from_slice(input: &[u8]) -> Res<Self> {
let result: Result<Self, serde_json::Error> = serde_json::from_slice(input);
match result {
Ok(lineage) => {
if lineage.as_ref().as_os_str().is_empty() {
return Err(Error::LineageMissingHome);
}
Ok(lineage)
}
Err(err) => {
log::error!("Failed to parse DomainLineage from `{input:?}`");
Err(Error::LineageParse(err))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DomainLineageIo {
path: PathBuf,
}
impl DomainLineageIo {
pub fn new(path: PathBuf) -> Self {
DomainLineageIo { path }
}
pub async fn read(&self, storage: &(impl Storage + Sync)) -> Res<DomainLineage> {
match storage.read_bytes(&self.path).await {
Ok(bytes) => DomainLineage::from_slice(&bytes),
Err(_) if !storage.exists(&self.path).await => Err(Error::LineageMissing),
Err(e) => Err(e),
}
}
pub async fn read_package_lineage(
&self,
storage: &(impl Storage + Sync),
namespace: &Namespace,
) -> Res<(PathBuf, PackageLineage)> {
let domain_lineage = self.read(storage).await?;
match domain_lineage.packages.get(namespace) {
Some(package_lineage) => {
let package_home = paths::package_home(&domain_lineage.home, namespace);
Ok((package_home, package_lineage.clone()))
}
None => Err(Error::PackageNotInstalled(namespace.clone())),
}
}
pub async fn write_package_lineage(
&self,
storage: &(impl Storage + Sync),
namespace: &Namespace,
package_lineage: PackageLineage,
) -> Res<PackageLineage> {
let mut domain_lineage = self.read(storage).await?;
domain_lineage
.packages
.insert(namespace.clone(), package_lineage.clone());
self.write(storage, domain_lineage).await?;
Ok(package_lineage)
}
pub async fn set_home(
&self,
storage: &(impl Storage + Sync),
home: impl AsRef<Path>,
) -> Res<DomainLineage> {
match storage.read_bytes(&self.path).await {
Ok(bytes) => {
let mut lineage = DomainLineage::from_slice(&bytes)?;
lineage.home = home.into();
self.write(storage, lineage).await
}
Err(_) if !storage.exists(&self.path).await => {
self.write(storage, DomainLineage::new(home)).await
}
Err(e) => Err(e),
}
}
pub async fn write(
&self,
storage: &(impl Storage + Sync),
lineage: DomainLineage,
) -> Res<DomainLineage> {
let contents = serde_json::to_string_pretty(&lineage)?;
storage
.write_byte_stream(self.path.clone(), contents.into_bytes().into())
.await?;
Ok(lineage)
}
pub fn create_package_lineage(&self, namespace: Namespace) -> PackageLineageIo {
PackageLineageIo::new(self.clone(), namespace)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageLineageIo {
domain_lineage: DomainLineageIo,
namespace: Namespace,
}
impl PackageLineageIo {
pub fn new(domain_lineage: DomainLineageIo, namespace: Namespace) -> Self {
PackageLineageIo {
domain_lineage,
namespace,
}
}
pub async fn read(&self, storage: &(impl Storage + Sync)) -> Res<(PathBuf, PackageLineage)> {
self.domain_lineage
.read_package_lineage(storage, &self.namespace)
.await
}
pub async fn package_home(&self, storage: &(impl Storage + Sync)) -> Res<PathBuf> {
Ok(self
.domain_home(storage)
.await?
.join(self.namespace.to_string()))
}
pub async fn domain_home(&self, storage: &(impl Storage + Sync)) -> Res<Home> {
let domain_lineage = self.domain_lineage.read(storage).await?;
Ok(domain_lineage.home)
}
pub async fn write(
&self,
storage: &(impl Storage + Sync),
lineage: PackageLineage,
) -> Res<PackageLineage> {
self.domain_lineage
.write_package_lineage(storage, &self.namespace, lineage)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use aws_sdk_s3::primitives::ByteStream;
use aws_smithy_types::base64;
use crate::checksum::Sha256ChunkedHash;
use crate::io::storage::mocks::MockStorage;
use crate::uri::ManifestUri;
#[test]
fn test_syntax_error() {
assert_eq!(
DomainLineage::from_slice(b"err").unwrap_err().to_string(),
"Failed to parse lineage file: expected value at line 1 column 1".to_string()
);
}
#[test]
fn test_wrong_key() {
assert!(DomainLineage::from_slice(br#"{"notkey": 123}"#).is_err());
}
#[test]
fn test_wrong_value() {
assert!(DomainLineage::from_slice(br#"{"packages": 123}"#)
.unwrap_err()
.to_string()
.starts_with("Failed to parse lineage file: invalid type:"));
}
#[test]
fn test_missing_working_directory() {
assert_eq!(
DomainLineage::from_slice(br###"{"packages":{}}"###)
.unwrap_err()
.to_string(),
"Domain lineage missing Home directory".to_string()
);
}
#[test]
fn test_with_working_directory() -> Res {
let lineage =
DomainLineage::from_slice(br###"{"packages":{},"home":"/tmp/working_dir"}"###).unwrap();
assert_eq!(lineage.as_ref(), &PathBuf::from("/tmp/working_dir"));
Ok(())
}
#[test]
fn test_domain_lineage_from_temp_dir() -> Res {
let (lineage, temp_dir) = DomainLineage::from_temp_dir()?;
assert_eq!(lineage.as_ref(), &temp_dir.path().to_path_buf());
assert!(lineage.packages.is_empty());
Ok(())
}
#[test]
fn test_namespaces() -> Res {
let mut lineage = DomainLineage::new("/tmp/home");
assert!(lineage.namespaces().is_empty());
lineage
.packages
.insert(Namespace::from(("foo", "bar")), PackageLineage::default());
lineage
.packages
.insert(Namespace::from(("abc", "xyz")), PackageLineage::default());
lineage.packages.insert(
Namespace::from(("test", "package")),
PackageLineage::default(),
);
let namespaces = lineage.namespaces();
assert_eq!(namespaces.len(), 3);
assert_eq!(namespaces[0], Namespace::from(("abc", "xyz")));
assert_eq!(namespaces[1], Namespace::from(("foo", "bar")));
assert_eq!(namespaces[2], Namespace::from(("test", "package")));
Ok(())
}
#[test(tokio::test)]
async fn test_domain_lineage_from_file() -> Res {
let storage = MockStorage::default();
let file_path = PathBuf::from("foo");
storage
.write_byte_stream(
&file_path,
ByteStream::from_static(br###"{"packages":{},"home":"/home/directory"}"###),
)
.await?;
let lineage = DomainLineageIo::new(file_path).read(&storage).await?;
assert_eq!(lineage, DomainLineage::new("/home/directory"));
Ok(())
}
#[test(tokio::test)]
async fn test_domain_lineage_from_nothing() -> Res {
let storage = MockStorage::default();
let lineage = DomainLineageIo::new(PathBuf::from("does-not-exist"))
.read(&storage)
.await
.unwrap_err();
assert!(matches!(lineage, Error::LineageMissing));
Ok(())
}
#[test(tokio::test)]
async fn test_domain_lineage_write() -> Res {
let storage = MockStorage::default();
let file_path = PathBuf::from("foo");
assert!(!storage.exists(&file_path).await);
let bytes = "0123456789abcdef".as_bytes();
let working_dir = PathBuf::from("/tmp/working_dir");
DomainLineageIo::new(file_path.clone())
.write(
&storage,
DomainLineage {
home: Home::new(working_dir),
packages: BTreeMap::from([(
("foo", "bar").into(),
PackageLineage {
commit: None,
remote: ManifestUri {
bucket: "bucket".to_string(),
namespace: ("foo", "bar").into(),
hash: "abcdef".to_string(),
origin: None,
},
base_hash: "abcdef".to_string(),
latest_hash: "abcdef".to_string(),
paths: BTreeMap::from([(
PathBuf::from("foo"),
PathState {
timestamp: chrono::DateTime::from_timestamp_millis(
1737031820534,
)
.unwrap(),
hash: Sha256ChunkedHash::from_async_read(
bytes,
bytes.len() as u64,
)
.await?
.into(),
},
)]),
},
)]),
},
)
.await?;
assert!(storage.exists(&file_path).await);
let file_contents = storage.read_bytes(&file_path).await?;
let lineage = DomainLineage::from_slice(&file_contents)?;
assert_eq!(lineage.as_ref(), &PathBuf::from("/tmp/working_dir"));
let multihash_from_lineage = lineage
.packages
.get(&(("foo".to_string(), "bar".to_string()).into()))
.unwrap()
.paths
.get(&PathBuf::from("foo"))
.unwrap()
.hash;
let hash_from_lineage = base64::encode(multihash_from_lineage.digest());
assert_eq!(
hash_from_lineage,
"Xb1PbjJeWof4zD7zuHc9PI7sLiz/Ykj4gphlaZEt3xA="
);
Ok(())
}
#[test(tokio::test)]
async fn test_read_package_lineage() -> Res {
let storage = MockStorage::default();
let file_path = PathBuf::from("lineage.json");
let namespace = Namespace::from(("foo", "bar"));
let package_lineage = PackageLineage {
commit: None,
remote: ManifestUri {
bucket: "bucket".to_string(),
namespace: namespace.clone(),
hash: "abcdef".to_string(),
origin: None,
},
base_hash: "abcdef".to_string(),
latest_hash: "abcdef".to_string(),
paths: BTreeMap::new(),
};
let lineage = DomainLineage {
home: Home::from("/home/user/quilt"),
packages: BTreeMap::from([(namespace.clone(), package_lineage.clone())]),
};
let lineage_io = DomainLineageIo::new(file_path.clone());
lineage_io.write(&storage, lineage).await?;
let (package_home, read_package_lineage) = lineage_io
.read_package_lineage(&storage, &namespace)
.await?;
assert_eq!(package_home, PathBuf::from("/home/user/quilt/foo/bar"));
assert_eq!(read_package_lineage, package_lineage);
let non_existent = Namespace::from(("does", "notexist"));
let result = lineage_io
.read_package_lineage(&storage, &non_existent)
.await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"The given package is not installed: does/notexist"
);
Ok(())
}
#[test(tokio::test)]
async fn test_write_package_lineage() -> Res {
let storage = MockStorage::default();
let file_path = PathBuf::from("lineage.json");
let lineage_io = DomainLineageIo::new(file_path.clone());
let initial_lineage = DomainLineage {
home: Home::from("/home/user/quilt"),
packages: BTreeMap::new(),
};
lineage_io.write(&storage, initial_lineage).await?;
let namespace = Namespace::from(("foo", "bar"));
let package_lineage = PackageLineage {
commit: None,
remote: ManifestUri {
bucket: "bucket".to_string(),
namespace: namespace.clone(),
hash: "abcdef".to_string(),
origin: None,
},
base_hash: "abcdef".to_string(),
latest_hash: "abcdef".to_string(),
paths: BTreeMap::new(),
};
let written_lineage = lineage_io
.write_package_lineage(&storage, &namespace, package_lineage.clone())
.await?;
assert_eq!(written_lineage, package_lineage);
let domain_lineage = lineage_io.read(&storage).await?;
assert_eq!(domain_lineage.packages.len(), 1);
assert!(domain_lineage.packages.contains_key(&namespace));
assert_eq!(
domain_lineage.packages.get(&namespace).unwrap(),
&package_lineage
);
let updated_package_lineage = PackageLineage {
commit: Some(CommitState {
timestamp: chrono::Utc::now(),
hash: "".to_string(),
prev_hashes: Vec::new(),
}),
..package_lineage.clone()
};
lineage_io
.write_package_lineage(&storage, &namespace, updated_package_lineage.clone())
.await?;
let updated_domain_lineage = lineage_io.read(&storage).await?;
assert_eq!(updated_domain_lineage.packages.len(), 1);
assert!(updated_domain_lineage.packages.contains_key(&namespace));
assert_eq!(
updated_domain_lineage.packages.get(&namespace).unwrap(),
&updated_package_lineage
);
Ok(())
}
#[test(tokio::test)]
async fn test_domain_lineage_create_package_lineage() -> Res {
let namespace = ("foo", "bar");
let domain_lineage = DomainLineageIo::default();
let lineage = domain_lineage.create_package_lineage(namespace.into());
assert_eq!(
lineage,
PackageLineageIo {
namespace: namespace.into(),
domain_lineage,
}
);
Ok(())
}
}