use std::{
collections::HashMap,
path::{Path, PathBuf},
result::Result as StdResult,
};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use thiserror::Error as ErrorTrait;
pub mod local;
#[cfg(feature = "s3")]
pub mod s3;
mod tests;
#[derive(Debug, ErrorTrait)]
pub enum SyncError {
#[error("Not enough metadata to tell if file `{}` has changed", filename.display())]
NoMetadata { filename: PathBuf },
#[error("Errors occurred while comparing files. No changes have been written:\n{}", errors.iter().map(SyncError::to_string).collect::<Vec<String>>().join("\n"))]
ErrorComparing { errors: Vec<SyncError> },
#[error(transparent)]
FileSourceError(#[from] Box<dyn std::error::Error + Send>),
}
impl SyncError {
fn boxed<E: std::error::Error + Send + 'static>(error: E) -> Self {
SyncError::FileSourceError(Box::new(error))
}
}
pub type Result<T> = StdResult<T, SyncError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileEntry {
pub path: PathBuf,
pub modified: Option<DateTime<Utc>>,
pub size: Option<u64>,
pub md5_hash: Option<u128>,
}
impl FileEntry {
pub fn is_changed_from(&self, other: &FileEntry) -> Result<bool> {
let size_different = match (self.size, other.size) {
(Some(a), Some(b)) => Some(a != b),
_ => None,
};
let hash_different = match (self.md5_hash, other.md5_hash) {
(Some(a), Some(b)) => Some(a != b),
_ => None,
};
let date_later = match (self.modified, other.modified) {
(Some(a), Some(b)) => Some(a > b),
_ => None,
};
Ok(match (size_different, hash_different, date_later) {
(Some(false), Some(false), _) => false,
(_, _, Some(true)) => true,
(_, _, Some(false)) => false,
(Some(x), Some(y), None) => x || y,
(Some(x), None, None) => x,
(None, Some(x), None) => x,
(None, None, None) => {
return Err(SyncError::NoMetadata {
filename: self.path.clone(),
});
}
})
}
}
#[async_trait]
pub trait FileSource {
type Error: std::error::Error + Send + 'static;
async fn list_files(&mut self) -> StdResult<Vec<FileEntry>, Self::Error>;
async fn read_file<P: AsRef<Path> + Send>(
&mut self,
path: P,
) -> StdResult<Vec<u8>, Self::Error>;
async fn write_file<P: AsRef<Path> + Send>(
&mut self,
path: P,
bytes: &[u8],
) -> StdResult<(), Self::Error>;
async fn set_modified<P: AsRef<Path> + Send>(
&mut self,
path: P,
modified: Option<DateTime<Utc>>,
) -> StdResult<bool, Self::Error>;
}
pub async fn sync_one_way<A, B>(from: &mut A, to: &mut B) -> Result<Vec<PathBuf>>
where
A: FileSource,
B: FileSource,
{
let destination_files = to
.list_files()
.await
.map_err(SyncError::boxed)?
.into_iter()
.map(|entry| (entry.path.clone(), entry))
.collect::<HashMap<_, _>>();
let source_files = from.list_files().await.map_err(SyncError::boxed)?;
struct Write {
path: PathBuf,
src_modified: Option<DateTime<Utc>>,
dst_modified: Option<DateTime<Utc>>,
}
let mut to_write: Vec<Write> = vec![];
let mut errors: Vec<SyncError> = vec![];
for source_file in &source_files {
let path = &source_file.path;
let matching = destination_files.get(path);
match matching {
Some(dest_file) => match source_file.is_changed_from(dest_file) {
Ok(true) => to_write.push(Write {
path: path.to_owned(),
src_modified: source_file.modified,
dst_modified: dest_file.modified,
}),
Ok(false) => (),
Err(err) => errors.push(err),
},
None => to_write.push(Write {
path: path.to_owned(),
src_modified: source_file.modified,
dst_modified: None,
}),
}
}
if !errors.is_empty() {
return Err(SyncError::ErrorComparing { errors });
}
for write in &to_write {
let path = &write.path;
let bytes = from.read_file(path).await.map_err(SyncError::boxed)?;
to.write_file(path, &bytes)
.await
.map_err(SyncError::boxed)?;
let dest_file_modified_time_updated = to
.set_modified(path, write.src_modified)
.await
.map_err(SyncError::boxed)?;
if !dest_file_modified_time_updated {
from.set_modified(path, write.dst_modified)
.await
.map_err(SyncError::boxed)?;
}
}
Ok(to_write.into_iter().map(|write| write.path).collect())
}