use anyhow::Context;
#[cfg(feature = "fs")]
use slog::Logger;
use std::sync::Arc;
use thiserror::Error;
use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
#[cfg(feature = "fs")]
use crate::feedback::FeedbackSender;
#[cfg(feature = "fs")]
use crate::snapshot_downloader::SnapshotDownloader;
use crate::{MithrilResult, Snapshot, SnapshotListItem};
#[derive(Error, Debug)]
pub enum SnapshotClientError {
#[error("Could not find a working download location for the snapshot digest '{digest}', tried location: {{'{locations}'}}.")]
NoWorkingLocation {
digest: String,
locations: String,
},
}
pub struct SnapshotClient {
aggregator_client: Arc<dyn AggregatorClient>,
#[cfg(feature = "fs")]
snapshot_downloader: Arc<dyn SnapshotDownloader>,
#[cfg(feature = "fs")]
feedback_sender: FeedbackSender,
#[cfg(feature = "fs")]
logger: Logger,
}
impl SnapshotClient {
pub fn new(
aggregator_client: Arc<dyn AggregatorClient>,
#[cfg(feature = "fs")] snapshot_downloader: Arc<dyn SnapshotDownloader>,
#[cfg(feature = "fs")] feedback_sender: FeedbackSender,
#[cfg(feature = "fs")] logger: Logger,
) -> Self {
Self {
aggregator_client,
#[cfg(feature = "fs")]
snapshot_downloader,
#[cfg(feature = "fs")]
feedback_sender,
#[cfg(feature = "fs")]
logger,
}
}
pub async fn list(&self) -> MithrilResult<Vec<SnapshotListItem>> {
let response = self
.aggregator_client
.get_content(AggregatorRequest::ListSnapshots)
.await
.with_context(|| "Snapshot Client can not get the artifact list")?;
let items = serde_json::from_str::<Vec<SnapshotListItem>>(&response)
.with_context(|| "Snapshot Client can not deserialize artifact list")?;
Ok(items)
}
pub async fn get(&self, digest: &str) -> MithrilResult<Option<Snapshot>> {
match self
.aggregator_client
.get_content(AggregatorRequest::GetSnapshot {
digest: digest.to_string(),
})
.await
{
Ok(content) => {
let snapshot: Snapshot = serde_json::from_str(&content)
.with_context(|| "Snapshot Client can not deserialize artifact")?;
Ok(Some(snapshot))
}
Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
cfg_fs! {
pub async fn download_unpack(
&self,
snapshot: &Snapshot,
target_dir: &std::path::Path,
) -> MithrilResult<()> {
use crate::feedback::MithrilEvent;
for location in snapshot.locations.as_slice() {
if self.snapshot_downloader.probe(location).await.is_ok() {
let download_id = MithrilEvent::new_snapshot_download_id();
self.feedback_sender
.send_event(MithrilEvent::SnapshotDownloadStarted {
digest: snapshot.digest.clone(),
download_id: download_id.clone(),
size: snapshot.size,
})
.await;
return match self
.snapshot_downloader
.download_unpack(
location,
target_dir,
snapshot.compression_algorithm.unwrap_or_default(),
&download_id,
snapshot.size,
)
.await
{
Ok(()) => {
self.feedback_sender
.send_event(MithrilEvent::SnapshotDownloadCompleted { download_id })
.await;
Ok(())
}
Err(e) => {
slog::warn!(
self.logger,
"Failed downloading snapshot from '{location}' Error: {e}."
);
Err(e)
}
};
}
}
let locations = snapshot.locations.join(", ");
Err(SnapshotClientError::NoWorkingLocation {
digest: snapshot.digest.clone(),
locations,
}
.into())
}
}
}
#[cfg(all(test, feature = "fs"))]
mod tests_download {
use crate::{
aggregator_client::MockAggregatorHTTPClient,
feedback::{MithrilEvent, StackFeedbackReceiver},
snapshot_downloader::MockHttpSnapshotDownloader,
test_utils,
};
use std::path::Path;
use super::*;
#[tokio::test]
async fn download_unpack_send_feedbacks() {
let mut snapshot_downloader = MockHttpSnapshotDownloader::new();
snapshot_downloader.expect_probe().returning(|_| Ok(()));
snapshot_downloader
.expect_download_unpack()
.returning(|_, _, _, _, _| Ok(()));
let feedback_receiver = Arc::new(StackFeedbackReceiver::new());
let client = SnapshotClient::new(
Arc::new(MockAggregatorHTTPClient::new()),
Arc::new(snapshot_downloader),
FeedbackSender::new(&[feedback_receiver.clone()]),
test_utils::test_logger(),
);
let snapshot = Snapshot::dummy();
client
.download_unpack(&snapshot, Path::new(""))
.await
.expect("download should succeed");
let actual = feedback_receiver.stacked_events();
let id = actual[0].event_id();
let expected = vec![
MithrilEvent::SnapshotDownloadStarted {
digest: snapshot.digest,
download_id: id.to_string(),
size: snapshot.size,
},
MithrilEvent::SnapshotDownloadCompleted {
download_id: id.to_string(),
},
];
assert_eq!(actual, expected);
}
}