darkwing/server/services/
local_file_services.rsuse async_trait::async_trait;
use darkwing_diff::{apply, diff, Patch, DEFAULT_ALGO};
use mockall::automock;
use std::sync::Arc;
use tokio::io::AsyncReadExt;
use crate::server::error::AppResult;
pub type DynLocalFileService = Arc<dyn LocalFileServiceTrait + Send + Sync>;
#[automock]
#[async_trait]
pub trait LocalFileServiceTrait {
async fn calc_hash_from_path(&self, path: String) -> AppResult<String>;
async fn calc_hash(&self, mut file: tokio::fs::File) -> AppResult<String>;
async fn calc_diff(&self, first: String, second: String) -> AppResult<Patch>;
async fn apply_patch(
&self,
base: String,
delta: Patch,
applied_path: String,
) -> AppResult<()>;
}
#[derive(Clone)]
pub struct LocalFileService {}
impl LocalFileService {
pub fn new() -> Self {
Self {}
}
fn calc_hash_from_bytes(&self, data: &[u8]) -> String {
let hash = md5::compute(data);
hex::encode(hash.0)
}
}
#[async_trait]
impl LocalFileServiceTrait for LocalFileService {
async fn calc_hash_from_path(&self, path: String) -> AppResult<String> {
let file = tokio::fs::File::open(path).await?;
self.calc_hash(file).await
}
async fn calc_hash(&self, mut file: tokio::fs::File) -> AppResult<String> {
let mut data = Vec::new();
file.read_to_end(&mut data).await?;
let hash = self.calc_hash_from_bytes(&data);
Ok(hash)
}
async fn calc_diff(&self, first: String, second: String) -> AppResult<Patch> {
let first = std::fs::read(first)?;
let second = std::fs::read(second)?;
diff(&first, &second, DEFAULT_ALGO.0, DEFAULT_ALGO.1).map_err(|e| e.into())
}
async fn apply_patch(
&self,
base: String,
delta: Patch,
applied_path: String,
) -> AppResult<()> {
let first = std::fs::read(base)?;
let applied = apply(&first, &delta)?;
std::fs::write(applied_path, applied)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
use tokio::fs::File;
#[tokio::test]
async fn test_calc_hash() -> AppResult<()> {
let mut temp_file = NamedTempFile::new()?;
let test_content = b"Hello, world!";
temp_file.write_all(test_content)?;
temp_file.flush()?;
let file = File::open(temp_file.path()).await?;
let service = LocalFileService::new();
let hash = service.calc_hash(file).await?;
let expected_hash = "6cd3556deb0da54bca060b4c39479839";
assert_eq!(hash, expected_hash);
Ok(())
}
#[tokio::test]
async fn test_calc_hash_empty_file() -> AppResult<()> {
let temp_file = NamedTempFile::new()?;
let file = File::open(temp_file.path()).await?;
let service = LocalFileService::new();
let hash = service.calc_hash(file).await?;
let expected_hash = "d41d8cd98f00b204e9800998ecf8427e";
assert_eq!(hash, expected_hash);
Ok(())
}
#[tokio::test]
async fn test_calc_hash_with_special_characters() -> AppResult<()> {
let mut temp_file = NamedTempFile::new()?;
let test_content = "Hello, 世界! Special chars: !@#$%^&*()".as_bytes();
temp_file.write_all(test_content)?;
temp_file.flush()?;
let service = LocalFileService::new();
let hash = service
.calc_hash(File::open(temp_file.path()).await?)
.await?;
assert_eq!(hash.len(), 32);
assert!(hex::decode(&hash).is_ok());
Ok(())
}
}