use crate::IntegrityFailure;
use crate::Result;
use futures::StreamExt;
use indexmap::IndexSet;
use sha2::{Digest, Sha256};
use sos_backend::BackendTarget;
use sos_core::{commit::CommitHash, ExternalFile};
use sos_vfs as vfs;
use std::{path::PathBuf, sync::Arc};
use tokio::sync::{
mpsc::{self, Receiver, Sender},
watch, Mutex, Semaphore,
};
use tokio_util::io::ReaderStream;
#[derive(Debug)]
pub enum FileIntegrityEvent {
Begin(usize),
Failure(ExternalFile, IntegrityFailure),
OpenFile(ExternalFile, u64),
ReadFile(ExternalFile, usize),
CloseFile(ExternalFile),
Complete,
}
pub async fn file_integrity(
target: &BackendTarget,
external_files: IndexSet<ExternalFile>,
concurrency: usize,
) -> Result<(Receiver<FileIntegrityEvent>, watch::Sender<bool>)> {
let paths = target.paths();
let (mut event_tx, event_rx) = mpsc::channel::<FileIntegrityEvent>(64);
let (cancel_tx, mut cancel_rx) = watch::channel(false);
notify_listeners(
&mut event_tx,
FileIntegrityEvent::Begin(external_files.len()),
)
.await;
let paths: Vec<_> = external_files
.into_iter()
.map(|file| (file, paths.into_file_path(&file)))
.collect();
let num_files = paths.len();
let semaphore = Arc::new(Semaphore::new(concurrency));
let cancel = cancel_tx.clone();
tokio::task::spawn(async move {
let mut stream = futures::stream::iter(paths);
let completed = Arc::new(Mutex::new(0));
loop {
tokio::select! {
biased;
_ = cancel_rx.changed() => {
break;
}
Some((file, path)) = stream.next() => {
let semaphore = semaphore.clone();
let cancel_tx = cancel.clone();
let mut cancel_rx = cancel_rx.clone();
let mut event_tx = event_tx.clone();
let completed = completed.clone();
tokio::task::spawn(async move {
let _permit = semaphore.acquire().await;
check_file(file, path, &mut event_tx, &mut cancel_rx).await?;
let mut writer = completed.lock().await;
*writer += 1;
if *writer == num_files {
if let Err(error) = cancel_tx.send(true) {
tracing::error!(error = ?error);
}
}
Ok::<_, crate::Error>(())
});
}
}
}
notify_listeners(&mut event_tx, FileIntegrityEvent::Complete).await;
Ok::<_, crate::Error>(())
});
Ok((event_rx, cancel_tx))
}
async fn check_file(
file: ExternalFile,
path: PathBuf,
tx: &mut Sender<FileIntegrityEvent>,
cancel_rx: &mut watch::Receiver<bool>,
) -> Result<()> {
if vfs::try_exists(&path).await? {
let metadata = vfs::metadata(&path).await?;
notify_listeners(
tx,
FileIntegrityEvent::OpenFile(file, metadata.len()),
)
.await;
match compare_file(&file, path, tx, cancel_rx).await {
Ok(result) => {
if let Some(failure) = result {
notify_listeners(
tx,
FileIntegrityEvent::Failure(file, failure),
)
.await;
}
notify_listeners(tx, FileIntegrityEvent::CloseFile(file))
.await;
}
Err(e) => {
notify_listeners(
tx,
FileIntegrityEvent::Failure(
file,
IntegrityFailure::Error(e),
),
)
.await;
}
}
} else {
notify_listeners(
tx,
FileIntegrityEvent::Failure(
file,
IntegrityFailure::MissingFile(file),
),
)
.await;
}
Ok(())
}
async fn compare_file(
external_file: &ExternalFile,
path: PathBuf,
tx: &mut Sender<FileIntegrityEvent>,
cancel_rx: &mut watch::Receiver<bool>,
) -> Result<Option<IntegrityFailure>> {
let mut hasher = Sha256::new();
let file = vfs::File::open(&path).await?;
let metadata = vfs::metadata(&path).await?;
let bytes_total = metadata.len();
let mut bytes_read = 0;
let mut reader_stream = ReaderStream::new(file);
loop {
tokio::select! {
biased;
_ = cancel_rx.changed() => {
break;
}
chunk = reader_stream.next() => {
if let Some(chunk) = chunk {
let chunk = chunk?;
hasher.update(&chunk);
bytes_read += chunk.len();
notify_listeners(
tx,
FileIntegrityEvent::ReadFile(*external_file, chunk.len()),
)
.await;
} else {
break;
}
}
}
}
let digest = hasher.finalize();
let is_completed = bytes_read as u64 == bytes_total;
if is_completed && digest.as_slice() != external_file.file_name().as_ref()
{
let slice: [u8; 32] = digest.as_slice().try_into()?;
Ok(Some(IntegrityFailure::CorruptedFile {
external_file: *external_file,
expected: external_file.file_name().into(),
actual: CommitHash(slice),
}))
} else {
Ok(None)
}
}
async fn notify_listeners(
tx: &mut Sender<FileIntegrityEvent>,
event: FileIntegrityEvent,
) {
if let Err(error) = tx.send(event).await {
tracing::warn!(error = ?error.0, "file_integrity::send");
}
}