use std::collections::BTreeMap;
use std::collections::HashSet;
use std::collections::hash_map::RandomState;
use std::path::Path;
use std::path::PathBuf;
use tokio_stream::StreamExt;
use tracing::debug;
use tracing::info;
use url::Url;
use crate::Error;
use crate::InstallPathError;
use crate::Res;
use crate::checksum::refresh_hash;
use crate::error::PackageOpError;
use crate::io::manifest::RowsStream;
use crate::io::manifest::build_manifest_from_rows_stream;
use crate::io::remote::Remote;
use crate::io::storage::Storage;
use crate::lineage::LineagePaths;
use crate::lineage::PackageLineage;
use crate::lineage::PathState;
use crate::manifest::Manifest;
use crate::manifest::ManifestRow;
use crate::paths::DomainPaths;
use quilt_uri::Host;
use quilt_uri::Namespace;
use quilt_uri::S3Uri;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InstallPathsReport {
pub paths: LineagePaths,
pub skipped: Vec<PathBuf>,
}
async fn cache_immutable_object(
storage: &impl Storage,
remote: &impl Remote,
host: Option<&Host>,
object_dest: &PathBuf,
row: &ManifestRow,
run_staging: &Path,
) -> Res<bool> {
let uri: S3Uri = row.physical_key.parse()?;
let stream = remote.get_object_stream(host, &uri).await?;
let fetched = run_staging.join(uuid::Uuid::new_v4().to_string());
storage.write_byte_stream(&fetched, stream.body).await?;
if refresh_hash(storage, &fetched, row.clone())
.await?
.is_some()
{
debug!(
"❌ Fetched bytes for {} do not match its row",
row.logical_key.display()
);
storage.remove_file(&fetched).await?;
return Ok(false);
}
storage.rename(&fetched, object_dest).await?;
Ok(true)
}
async fn stage_object(
storage: &impl Storage,
immutable_source: &Path,
run_staging: &Path,
) -> Res<PathBuf> {
let staged = run_staging.join(uuid::Uuid::new_v4().to_string());
storage.copy(&immutable_source, &staged).await?;
Ok(staged)
}
async fn commit_staged(
storage: &impl Storage,
staged: &Path,
mutable_target: &Path,
) -> Res<chrono::DateTime<chrono::Utc>> {
if let Some(parent) = mutable_target.parent() {
storage.create_dir_all(parent).await?;
}
storage.rename(staged, &mutable_target).await?;
storage.modified_timestamp(&mutable_target).await
}
async fn place_where_absent(
storage: &(impl Storage + Sync),
staged: &[(PathBuf, PathBuf, ManifestRow)],
lineage: &mut PackageLineage,
) -> Res {
let mut existing = Vec::new();
for (_, working_dest, row) in staged {
if storage.exists(working_dest).await {
existing.push(row.logical_key.clone());
}
}
if !existing.is_empty() {
debug!("❌ Local files appeared while the paths were being staged");
return Err(Error::InstallPath(InstallPathError::LocalFileExists(
existing,
)));
}
for (staged_path, working_dest, row) in staged {
let last_modified = commit_staged(storage, staged_path, working_dest).await?;
lineage.paths.insert(
row.logical_key.clone(),
PathState {
timestamp: last_modified,
hash: row.hash.clone().into(),
},
);
debug!("✔️ Swapped in {}", working_dest.display());
}
Ok(())
}
async fn swap_staged_into_place(
storage: &(impl Storage + Sync),
staged: &[(PathBuf, PathBuf, ManifestRow)],
protect: &Protect<'_>,
lineage: &mut PackageLineage,
) -> Res {
if let Protect::Absent = protect {
return place_where_absent(storage, staged, lineage).await;
}
for (staged_path, working_dest, row) in staged {
if let Protect::BaseContent(expected) = protect
&& let Some(base_row) = expected.get(&row.logical_key)
{
let unchanged = matches!(
refresh_hash(storage, working_dest, (*base_row).clone()).await,
Ok(None)
);
if !unchanged {
debug!(
"❌ {} changed while the revision was being staged",
row.logical_key.display()
);
return Err(Error::PackageOp(PackageOpError::PullConflict(vec![
row.logical_key.clone(),
])));
}
}
let last_modified = commit_staged(storage, staged_path, working_dest).await?;
lineage.paths.insert(
row.logical_key.clone(),
PathState {
timestamp: last_modified,
hash: row.hash.clone().into(),
},
);
debug!("✔️ Swapped in {}", working_dest.display());
}
Ok(())
}
async fn stream_remote_with_installed_rows(
remote_manifest: &Manifest,
local_entries: BTreeMap<PathBuf, ManifestRow>,
) -> impl RowsStream {
remote_manifest
.records_stream()
.await
.map(move |rows_result| {
rows_result.map(|rows| {
rows.into_iter()
.map(|row_result| {
row_result.map(|row| match local_entries.get(&row.logical_key) {
Some(local_row) => local_row.clone(),
None => row,
})
})
.collect()
})
})
}
pub(crate) enum Protect<'a> {
BaseContent(&'a BTreeMap<PathBuf, ManifestRow>),
Nothing,
Absent,
}
pub(crate) enum OnMismatch {
Refuse,
Skip,
}
fn refuse_already_installed(lineage: &PackageLineage, entries_paths: &[&PathBuf]) -> Res {
debug!("🔍 Checking for already installed paths");
if !lineage
.paths
.keys()
.collect::<HashSet<&PathBuf, RandomState>>()
.is_disjoint(&entries_paths.iter().copied().collect::<HashSet<_>>())
{
debug!("❌ Found paths that are already installed");
return Err(Error::InstallPath(InstallPathError::AlreadyInstalled));
}
Ok(())
}
async fn refuse_existing_working_files(
storage: &impl Storage,
working_dir: &Path,
entries_paths: &[&PathBuf],
) -> Res {
debug!("🔍 Checking for local files at the requested paths");
let mut existing = Vec::new();
for path in entries_paths {
if storage.exists(working_dir.join(path)).await {
existing.push((*path).clone());
}
}
if !existing.is_empty() {
debug!("❌ Found local files at requested paths");
return Err(Error::InstallPath(InstallPathError::LocalFileExists(
existing,
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn install_paths(
lineage: PackageLineage,
manifest: &mut Manifest,
paths: &DomainPaths,
working_dir: PathBuf,
namespace: Namespace,
storage: &(impl Storage + Sync),
remote: &impl Remote,
entries_paths: &[&PathBuf],
) -> Res<(PackageLineage, Vec<PathBuf>)> {
refuse_already_installed(&lineage, entries_paths)?;
refuse_existing_working_files(storage, &working_dir, entries_paths).await?;
install_paths_over(
lineage,
manifest,
paths,
working_dir,
namespace,
storage,
remote,
entries_paths,
&Protect::Absent,
OnMismatch::Skip,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn install_paths_over(
mut lineage: PackageLineage,
manifest: &mut Manifest,
paths: &DomainPaths,
working_dir: PathBuf, namespace: Namespace,
storage: &(impl Storage + Sync),
remote: &impl Remote,
entries_paths: &[&PathBuf],
protect: &Protect<'_>,
on_mismatch: OnMismatch,
) -> Res<(PackageLineage, Vec<PathBuf>)> {
if entries_paths.is_empty() {
info!("No paths to install");
return Ok((lineage, Vec::new()));
}
let remote_uri = lineage.remote()?.clone();
info!(
"⏳ Installing {} paths for package {}",
entries_paths.len(),
namespace
);
let mut entries = BTreeMap::new();
let mut skipped = Vec::new();
let run_staging = paths.staging_dir().join(uuid::Uuid::new_v4().to_string());
storage.create_dir_all(&run_staging).await?;
let staging: Res<Vec<(PathBuf, PathBuf, ManifestRow)>> = async {
let mut staged: Vec<(PathBuf, PathBuf, ManifestRow)> = Vec::new();
for path in entries_paths {
let row = manifest.get_record(path).ok_or_else(|| {
Error::InstallPath(InstallPathError::NotInRevision((*path).clone()))
})?;
let object_dest = paths.object(row.hash.digest());
if storage.exists(&object_dest).await {
debug!("✔️ Object already in cache: {}", object_dest.display());
} else if cache_immutable_object(
storage,
remote,
remote_uri.origin.as_ref(),
&object_dest,
row,
&run_staging,
)
.await?
{
debug!("✔️ Cached object: {}", object_dest.display());
} else {
match on_mismatch {
OnMismatch::Refuse => {
return Err(Error::InstallPath(InstallPathError::ContentMismatch(
row.logical_key.clone(),
)));
}
OnMismatch::Skip => {
skipped.push(row.logical_key.clone());
continue;
}
}
}
let place = Url::from_file_path(&object_dest)
.map_err(|()| Error::InstallPath(InstallPathError::Install(object_dest.clone())))?
.to_string();
debug!(
"✔️ Path {} converted to a `place` {}",
object_dest.display(),
place
);
entries.insert(row.logical_key.clone(), row.clone());
let staged_path = stage_object(storage, &object_dest, &run_staging).await?;
staged.push((staged_path, working_dir.join(&row.logical_key), row.clone()));
debug!("✔️ Staged {}", row.logical_key.display());
}
Ok(staged)
}
.await;
let staged = match staging {
Ok(staged) => staged,
Err(err) => {
let _ = storage.remove_dir_all(&run_staging).await;
return Err(err);
}
};
debug!("⏳ Swapping {} staged files into place", staged.len());
let swapped = swap_staged_into_place(storage, &staged, protect, &mut lineage).await;
let _ = storage.remove_dir_all(&run_staging).await;
swapped?;
debug!("⏳ Building manifest with installed rows");
let stream = stream_remote_with_installed_rows(manifest, entries).await;
let dest_dir = paths.installed_manifests_dir(&namespace);
build_manifest_from_rows_stream(storage, dest_dir, manifest.header.clone(), stream).await?;
info!(
"✔️ Installed {} paths, skipped {}",
entries_paths.len() - skipped.len(),
skipped.len()
);
Ok((lineage, skipped))
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use aws_sdk_s3::primitives::ByteStream;
use std::path::PathBuf;
use std::str::FromStr;
use crate::fixtures;
use crate::io::remote::mocks::MockRemote;
use crate::io::storage::StorageExt;
use crate::io::storage::mocks::MockStorage;
use crate::lineage::Home;
use crate::paths;
use quilt_uri::ManifestUri;
fn sha256_of(body: &[u8]) -> multihash::Multihash<256> {
use sha2::Digest;
multihash::Multihash::wrap(0x12, &sha2::Sha256::digest(body)).unwrap()
}
#[test(tokio::test)]
async fn test_installing_one_cached_path() -> Res {
let (home, _temp_dir1) = Home::from_temp_dir()?;
let (domain_paths, _temp_dir2) = &DomainPaths::from_temp_dir()?;
let namespace = Namespace::from(("foo", "bar"));
let package_home = paths::package_home(&home, &namespace);
let storage = MockStorage::default();
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let single_object_path = PathBuf::from("less-then-8mb.txt");
let entries_paths = vec![&single_object_path];
let mut manifest = fixtures::manifest_with_objects_all_sizes::manifest().await?;
let hash: multihash::Multihash<256> = manifest
.get_record(&single_object_path)
.unwrap()
.hash
.clone()
.into();
let object_path = domain_paths.object(hash.digest());
let absolute_path = home.join(object_path);
storage
.write_byte_stream(absolute_path, ByteStream::default())
.await?;
assert!(lineage.paths.is_empty());
let remote = MockRemote::default();
let (lineage, _) = install_paths(
lineage,
&mut manifest,
domain_paths,
package_home.clone(),
namespace,
&storage,
&remote,
&entries_paths,
)
.await?;
assert!(lineage.paths.contains_key(&single_object_path));
assert!(storage.exists(&package_home.join(single_object_path)).await);
Ok(())
}
#[test(tokio::test)]
async fn test_installing_one_uncached_path() -> Res {
let (home, _temp_dir1) = Home::from_temp_dir()?;
let (domain_paths, _temp_dir2) = &DomainPaths::from_temp_dir()?;
let namespace = Namespace::from(("foo", "bar"));
let package_home = paths::package_home(&home, &namespace);
let remote = MockRemote::default();
let storage = MockStorage::default();
let single_object_path = PathBuf::from("a/a");
let entries_paths = vec![&single_object_path];
domain_paths
.scaffold_for_installing(&storage, &home, &namespace)
.await?;
let remote_file_url = "s3://any/valid-url.md".to_string();
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let remote_object_uri = S3Uri::from_str(&remote_file_url)?;
remote
.put_object(None, &remote_object_uri, Vec::new())
.await?;
let hash = sha256_of(b"");
let mut manifest = Manifest::default();
manifest
.insert_record(ManifestRow {
logical_key: single_object_path.clone(),
hash: hash.try_into()?,
physical_key: remote_file_url,
..ManifestRow::default()
})
.await?;
assert!(lineage.paths.is_empty());
let (lineage, _) = install_paths(
lineage,
&mut manifest,
domain_paths,
package_home.clone(),
namespace,
&storage,
&remote,
&entries_paths,
)
.await?;
assert!(lineage.paths.contains_key(&single_object_path));
assert!(storage.exists(&package_home.join(single_object_path)).await);
let object_path = domain_paths.object(hash.digest());
assert!(storage.exists(object_path).await);
Ok(())
}
#[test(tokio::test)]
async fn test_installing_multiple_paths() -> Res {
let (home, _temp_dir1) = Home::from_temp_dir()?;
let (domain_paths, _temp_dir2) = &DomainPaths::from_temp_dir()?;
let namespace = Namespace::from(("foo", "bar"));
let package_home = paths::package_home(&home, &namespace);
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let row_1 = ManifestRow {
logical_key: PathBuf::from("a"),
physical_key: "file:///ignored".to_string(),
hash: multihash::Multihash::wrap(0x12, b"one")?.try_into()?,
..ManifestRow::default()
};
let row_2 = ManifestRow {
logical_key: PathBuf::from("b/b"),
physical_key: "s3://bucket/foo/bar".to_string(),
hash: sha256_of(b"two").try_into()?,
..ManifestRow::default()
};
let row_3 = ManifestRow {
logical_key: PathBuf::from("c/c/c"),
physical_key: "file:///ignored".to_string(),
hash: multihash::Multihash::wrap(0x12, b"three")?.try_into()?,
..ManifestRow::default()
};
let row_4 = ManifestRow {
logical_key: PathBuf::from("d/d/d/d"),
physical_key: "s3://bucket/foo/baz".to_string(),
hash: sha256_of(b"four").try_into()?,
..ManifestRow::default()
};
let mut manifest = Manifest::default();
manifest.insert_record(row_1.clone()).await?;
manifest.insert_record(row_2.clone()).await?;
manifest.insert_record(row_3.clone()).await?;
manifest.insert_record(row_4.clone()).await?;
let storage = MockStorage::default();
let object_path_1 = home.join(domain_paths.object(row_1.hash.digest()));
storage
.write_byte_stream(object_path_1, ByteStream::default())
.await?;
let object_path_3 = home.join(domain_paths.object(row_3.hash.digest()));
storage
.write_byte_stream(object_path_3, ByteStream::default())
.await?;
let remote = MockRemote::default();
let remote_object_uri_2 = S3Uri::from_str(&row_2.physical_key)?;
remote
.put_object(None, &remote_object_uri_2, b"two".to_vec())
.await?;
let remote_object_uri_4 = S3Uri::from_str(&row_4.physical_key)?;
remote
.put_object(None, &remote_object_uri_4, b"four".to_vec())
.await?;
let entries_paths = vec![
&row_1.logical_key,
&row_2.logical_key,
&row_3.logical_key,
&row_4.logical_key,
];
assert!(lineage.paths.is_empty());
let (lineage, _) = install_paths(
lineage,
&mut manifest,
domain_paths,
package_home.clone(),
namespace,
&storage,
&remote,
&entries_paths,
)
.await?;
assert!(lineage.paths.contains_key(&row_1.logical_key));
assert!(lineage.paths.contains_key(&row_2.logical_key));
assert!(lineage.paths.contains_key(&row_3.logical_key));
assert!(lineage.paths.contains_key(&row_4.logical_key));
assert!(storage.exists(&package_home.join(&row_1.logical_key)).await);
assert!(storage.exists(&package_home.join(&row_2.logical_key)).await);
assert!(storage.exists(&package_home.join(&row_3.logical_key)).await);
assert!(storage.exists(&package_home.join(&row_4.logical_key)).await);
Ok(())
}
#[test(tokio::test)]
async fn test_installing_path_that_doesnt_exists_in_manifest() -> Res {
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let remote = MockRemote::default();
let storage = MockStorage::default();
let not_existed = PathBuf::from("z/z");
let entries_paths = vec![¬_existed];
let mut manifest = fixtures::manifest_with_objects_all_sizes::manifest().await?;
assert!(lineage.paths.is_empty());
let lineage = install_paths(
lineage,
&mut manifest,
&DomainPaths::default(),
PathBuf::new(),
Namespace::default(),
&storage,
&remote,
&entries_paths,
)
.await;
assert_eq!(
lineage.unwrap_err().to_string(),
"z/z is not in the package's current revision"
);
Ok(())
}
#[test(tokio::test)]
async fn test_installing_more_than_1024_paths() -> Res {
let (home, _temp_dir1) = Home::from_temp_dir()?;
let (domain_paths, _temp_dir2) = &DomainPaths::from_temp_dir()?;
let namespace = Namespace::from(("foo", "bar"));
let package_home = paths::package_home(&home, &namespace);
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let storage = MockStorage::default();
let remote = MockRemote::default();
let mut manifest = Manifest::default();
let mut entries_paths = Vec::new();
let mut path_refs = Vec::new();
for i in 0..2048 {
let path = PathBuf::from(format!("path_{i}.txt"));
let place = format!("s3://bucket/path_{i}.txt");
let body = format!("body_{i}").into_bytes();
let hash = sha256_of(&body);
let row = ManifestRow {
logical_key: path.clone(),
physical_key: place.clone(),
hash: hash.try_into()?,
..ManifestRow::default()
};
manifest.insert_record(row).await?;
entries_paths.push(path);
let remote_uri = S3Uri::from_str(&place)?;
remote.put_object(None, &remote_uri, body).await?;
}
for path in &entries_paths {
path_refs.push(path);
}
domain_paths
.scaffold_for_installing(&storage, &home, &namespace)
.await?;
assert!(lineage.paths.is_empty());
let (lineage, _) = install_paths(
lineage,
&mut manifest,
domain_paths,
package_home.clone(),
namespace,
&storage,
&remote,
&path_refs,
)
.await?;
assert_eq!(lineage.paths.len(), 2048);
for path in &entries_paths {
assert!(lineage.paths.contains_key(path));
}
for path in &entries_paths {
assert!(storage.exists(&package_home.join(path)).await);
}
Ok(())
}
#[test(tokio::test)]
async fn an_untracked_local_file_at_a_requested_path_is_refused() -> Res {
let (home, _temp_dir1) = Home::from_temp_dir()?;
let (domain_paths, _temp_dir2) = &DomainPaths::from_temp_dir()?;
let namespace = Namespace::from(("foo", "bar"));
let package_home = paths::package_home(&home, &namespace);
let remote = MockRemote::default();
let storage = MockStorage::default();
let requested = PathBuf::from("a/a");
let entries_paths = vec![&requested];
domain_paths
.scaffold_for_installing(&storage, &home, &namespace)
.await?;
let remote_file_url = "s3://any/valid-url.md".to_string();
remote
.put_object(
None,
&S3Uri::from_str(&remote_file_url)?,
b"remote bytes".to_vec(),
)
.await?;
let hash: multihash::Multihash<256> = multihash::Multihash::wrap(0x12, b"anything")?;
let mut manifest = Manifest::default();
manifest
.insert_record(ManifestRow {
logical_key: requested.clone(),
hash: hash.try_into()?,
physical_key: remote_file_url,
..ManifestRow::default()
})
.await?;
let users_file = package_home.join(&requested);
storage
.write_byte_stream(&users_file, ByteStream::from_static(b"users new file"))
.await?;
let lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
let result = install_paths(
lineage,
&mut manifest,
domain_paths,
package_home.clone(),
namespace,
&storage,
&remote,
&entries_paths,
)
.await;
assert!(matches!(
result,
Err(Error::InstallPath(InstallPathError::LocalFileExists(ref p))) if p == &vec![requested.clone()]
));
assert_eq!(storage.read_bytes(&users_file).await?, b"users new file");
assert!(!storage.exists(domain_paths.object(hash.digest())).await);
Ok(())
}
#[test(tokio::test)]
async fn a_tracked_path_is_refused() -> Res {
let (domain_paths, _temp_dir) = &DomainPaths::from_temp_dir()?;
let storage = MockStorage::default();
let tracked = PathBuf::from("a/a");
let mut lineage = PackageLineage {
remote_uri: Some(ManifestUri::default()),
..PackageLineage::default()
};
lineage.paths.insert(tracked.clone(), PathState::default());
let result = install_paths(
lineage,
&mut Manifest::default(),
domain_paths,
PathBuf::from("work"),
Namespace::from(("foo", "bar")),
&storage,
&MockRemote::default(),
&[&tracked],
)
.await;
assert!(matches!(
result,
Err(Error::InstallPath(InstallPathError::AlreadyInstalled))
));
Ok(())
}
fn row(key: &str) -> Res<ManifestRow> {
Ok(ManifestRow {
logical_key: PathBuf::from(key),
hash: multihash::Multihash::wrap(0x12, b"anything")?.try_into()?,
..ManifestRow::default()
})
}
async fn two_staged(storage: &MockStorage) -> Res<Vec<(PathBuf, PathBuf, ManifestRow)>> {
let staged = vec![
(
PathBuf::from("staging/1"),
PathBuf::from("work/a"),
row("a")?,
),
(
PathBuf::from("staging/2"),
PathBuf::from("work/sub/b"),
row("sub/b")?,
),
];
for (staged_path, _, _) in &staged {
storage
.write_byte_stream(staged_path, ByteStream::from_static(b"remote bytes"))
.await?;
}
Ok(staged)
}
async fn users_file_at_second(storage: &MockStorage) -> Res<PathBuf> {
let users_file = PathBuf::from("work/sub/b");
storage
.write_byte_stream(&users_file, ByteStream::from_static(b"users new file"))
.await?;
Ok(users_file)
}
#[test(tokio::test)]
async fn every_file_is_placed_where_nothing_is() -> Res {
let storage = MockStorage::default();
let staged = two_staged(&storage).await?;
let mut lineage = PackageLineage::default();
swap_staged_into_place(&storage, &staged, &Protect::Absent, &mut lineage).await?;
assert_eq!(storage.read_bytes("work/a").await?, b"remote bytes");
assert_eq!(storage.read_bytes("work/sub/b").await?, b"remote bytes");
assert_eq!(
lineage.paths.keys().collect::<Vec<_>>(),
[&PathBuf::from("a"), &PathBuf::from("sub/b")]
);
Ok(())
}
#[test(tokio::test)]
async fn a_local_file_that_appeared_after_the_download_refuses_before_any_rename() -> Res {
let storage = MockStorage::default();
let staged = two_staged(&storage).await?;
let users_file = users_file_at_second(&storage).await?;
let mut lineage = PackageLineage::default();
let result =
swap_staged_into_place(&storage, &staged, &Protect::Absent, &mut lineage).await;
assert!(matches!(
result,
Err(Error::InstallPath(InstallPathError::LocalFileExists(ref p))) if p == &vec![PathBuf::from("sub/b")]
));
assert_eq!(storage.read_bytes(&users_file).await?, b"users new file");
assert!(!storage.exists("work/a").await, "nothing is renamed");
assert!(lineage.paths.is_empty());
Ok(())
}
}