use std::path::PathBuf;
use tracing::debug;
use crate::Res;
use crate::flow;
use crate::io::remote::Remote;
use crate::io::storage::Storage;
use crate::lineage::PackageLineage;
use crate::manifest::Manifest;
use crate::paths::DomainPaths;
use crate::paths::copy_cached_to_installed;
use quilt_uri::ManifestUri;
use quilt_uri::Namespace;
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct Applied {
pub installed: Vec<PathBuf>,
pub uninstalled: Vec<PathBuf>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn apply_latest_update(
lineage: PackageLineage,
manifest: &mut Manifest,
paths: &DomainPaths,
storage: &(impl Storage + Sync),
remote: &impl Remote,
working_dir: PathBuf,
namespace: Namespace,
latest: ManifestUri,
touched: &[PathBuf],
) -> Res<(PackageLineage, Applied)> {
let to_uninstall: Vec<PathBuf> = touched
.iter()
.filter(|p| lineage.paths.contains_key(*p))
.cloned()
.collect();
debug!("⏳ Uninstalling {} touched paths", to_uninstall.len());
let mut lineage =
flow::uninstall_paths(lineage, working_dir.clone(), storage, &to_uninstall).await?;
debug!("⏳ Advancing lineage to latest {}", latest.hash);
lineage.remote_mut()?.hash.clone_from(&latest.hash);
lineage.base_hash.clone_from(&latest.hash);
lineage.latest_hash.clone_from(&latest.hash);
debug!("⏳ Caching + installing latest manifest as the new base");
flow::cache_remote_manifest(paths, storage, remote, &latest).await?;
copy_cached_to_installed(
paths,
storage,
&ManifestUri {
namespace: namespace.clone(),
..latest.clone()
},
)
.await?;
*manifest =
Manifest::from_path(storage, &paths.installed_manifest(&namespace, &latest.hash)).await?;
lineage.remote_uri = Some(latest);
lineage
.paths
.retain(|path, _| manifest.contains_record(path));
let to_install: Vec<PathBuf> = touched
.iter()
.filter(|p| manifest.contains_record(p))
.cloned()
.collect();
debug!(
"⏳ Reinstalling {} touched paths present in latest",
to_install.len()
);
let install_refs: Vec<&PathBuf> = to_install.iter().collect();
let lineage = flow::install_paths(
lineage,
manifest,
paths,
working_dir,
namespace,
storage,
remote,
&install_refs,
)
.await?;
Ok((
lineage,
Applied {
installed: to_install,
uninstalled: to_uninstall,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use std::collections::BTreeMap;
use crate::io::remote::mocks::MockRemote;
use crate::io::storage::mocks::MockStorage;
use crate::lineage::PathState;
use quilt_uri::S3Uri;
#[test(tokio::test)]
async fn empty_touch_set_advances_hashes() -> Res {
let manifest_uri = ManifestUri {
bucket: "b".to_string(),
namespace: ("f", "a").into(),
hash: "OLD".to_string(),
origin: None,
};
let paths = DomainPaths::default();
let storage = MockStorage::default();
paths
.scaffold_for_caching(&storage, &manifest_uri.bucket)
.await?;
let lineage = PackageLineage {
remote_uri: Some(manifest_uri.clone()),
base_hash: "OLD".to_string(),
latest_hash: "NEW".to_string(),
..PackageLineage::default()
};
let new_hash = "deadbeef";
let remote = MockRemote::default();
remote
.put_object(
None,
&S3Uri::try_from(format!("s3://b/.quilt/packages/{new_hash}").as_str())?,
r#"{"version": "v0"}"#.as_bytes().to_vec(),
)
.await?;
let latest = ManifestUri {
hash: new_hash.to_string(),
..manifest_uri
};
let mut manifest = Manifest::default();
let result = apply_latest_update(
lineage,
&mut manifest,
&paths,
&storage,
&remote,
PathBuf::default(),
Namespace::default(),
latest,
&[],
)
.await?;
let (lineage, applied) = result;
assert_eq!(lineage.base_hash, new_hash);
assert_eq!(lineage.latest_hash, new_hash);
assert_eq!(lineage.remote()?.hash, new_hash);
assert_eq!(applied, Applied::default());
Ok(())
}
#[test(tokio::test)]
async fn prunes_lineage_path_absent_from_latest_manifest() -> Res {
let manifest_uri = ManifestUri {
bucket: "b".to_string(),
namespace: ("f", "a").into(),
hash: "OLD".to_string(),
origin: None,
};
let paths = DomainPaths::default();
let storage = MockStorage::default();
paths
.scaffold_for_caching(&storage, &manifest_uri.bucket)
.await?;
let stale = PathBuf::from("both-removed.txt");
let lineage = PackageLineage {
remote_uri: Some(manifest_uri.clone()),
base_hash: "OLD".to_string(),
latest_hash: "NEW".to_string(),
paths: BTreeMap::from([(stale.clone(), PathState::default())]),
..PackageLineage::default()
};
let new_hash = "deadbeef";
let remote = MockRemote::default();
remote
.put_object(
None,
&S3Uri::try_from(format!("s3://b/.quilt/packages/{new_hash}").as_str())?,
r#"{"version": "v0"}"#.as_bytes().to_vec(),
)
.await?;
let latest = ManifestUri {
hash: new_hash.to_string(),
..manifest_uri
};
let mut manifest = Manifest::default();
let result = apply_latest_update(
lineage,
&mut manifest,
&paths,
&storage,
&remote,
PathBuf::default(),
Namespace::default(),
latest,
&[],
)
.await?;
assert!(
!result.0.paths.contains_key(&stale),
"stale both-removed path must be pruned from lineage"
);
assert!(
result.1.uninstalled.is_empty(),
"pruning a stale row is not an uninstall, and must not be reported as one"
);
Ok(())
}
}