use super::{TransferResult, Transport};
use crate::error::Result;
use crate::sync::scanner::FileEntry;
use async_trait::async_trait;
use std::path::Path;
pub struct DualTransport {
source: Box<dyn Transport>,
dest: Box<dyn Transport>,
}
impl DualTransport {
pub fn new(source: Box<dyn Transport>, dest: Box<dyn Transport>) -> Self {
Self { source, dest }
}
}
#[async_trait]
impl Transport for DualTransport {
fn set_scan_options(&mut self, options: crate::sync::scanner::ScanOptions) {
self.source.set_scan_options(options);
}
async fn prepare_for_transfer(&self, file_count: usize) -> Result<()> {
self.source.prepare_for_transfer(file_count).await?;
self.dest.prepare_for_transfer(file_count).await?;
Ok(())
}
async fn scan(&self, path: &Path) -> Result<Vec<FileEntry>> {
self.source.scan(path).await
}
async fn scan_streaming(&self, path: &Path) -> Result<futures::stream::BoxStream<'static, Result<FileEntry>>> {
self.source.scan_streaming(path).await
}
async fn exists(&self, path: &Path) -> Result<bool> {
self.dest.exists(path).await
}
async fn metadata(&self, path: &Path) -> Result<std::fs::Metadata> {
self.dest.metadata(path).await
}
async fn file_info(&self, path: &Path) -> Result<super::FileInfo> {
self.dest.file_info(path).await
}
async fn create_dir_all(&self, path: &Path) -> Result<()> {
self.dest.create_dir_all(path).await
}
async fn create_dirs_batch(&self, paths: &[&Path]) -> Result<()> {
self.dest.create_dirs_batch(paths).await
}
async fn copy_file(&self, source: &Path, dest: &Path) -> Result<TransferResult> {
tracing::debug!("DualTransport: copying {} to {}", source.display(), dest.display());
match self.dest.copy_file(source, dest).await {
Ok(result) => {
tracing::debug!("DualTransport: copy succeeded via destination transport (efficient path)");
return Ok(result);
}
Err(e) => {
tracing::debug!("DualTransport: destination transport copy failed ({}), falling back to read+write", e);
}
}
tracing::debug!("DualTransport: using memory-buffered copy for {} (may be slow for large files)", source.display());
let data = self.source.read_file(source).await?;
let bytes_written = data.len() as u64;
let mtime = self.source.get_mtime(source).await?;
self.dest.write_file(dest, &data, mtime).await?;
Ok(TransferResult::new(bytes_written))
}
async fn sync_file_with_delta(&self, source: &Path, dest: &Path) -> Result<TransferResult> {
if !self.dest.exists(dest).await? {
tracing::debug!("Destination doesn't exist, using full copy");
return self.copy_file(source, dest).await;
}
match self.dest.sync_file_with_delta(source, dest).await {
Ok(result) => {
tracing::debug!("DualTransport: delta sync succeeded via destination transport (likely local→remote)");
Ok(result)
}
Err(e) => {
tracing::debug!("DualTransport: destination transport delta sync failed ({}), trying source transport", e);
match self.source.sync_file_with_delta(source, dest).await {
Ok(result) => {
tracing::debug!("DualTransport: delta sync succeeded via source transport");
Ok(result)
}
Err(e2) => {
tracing::debug!("DualTransport: both transports failed delta sync ({}, {}), falling back to full copy", e, e2);
self.copy_file(source, dest).await
}
}
}
}
}
async fn remove(&self, path: &Path, is_dir: bool) -> Result<()> {
self.dest.remove(path, is_dir).await
}
async fn create_hardlink(&self, source: &Path, dest: &Path) -> Result<()> {
self.dest.create_hardlink(source, dest).await
}
async fn create_symlink(&self, target: &Path, dest: &Path) -> Result<()> {
self.dest.create_symlink(target, dest).await
}
async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
self.dest.read_file(path).await
}
async fn check_disk_space(&self, path: &Path, bytes_needed: u64) -> Result<()> {
self.dest.check_disk_space(path, bytes_needed).await
}
async fn set_xattrs(&self, path: &Path, xattrs: &[(String, Vec<u8>)]) -> Result<()> {
self.dest.set_xattrs(path, xattrs).await
}
async fn set_acls(&self, path: &Path, acls_text: &str) -> Result<()> {
self.dest.set_acls(path, acls_text).await
}
async fn set_bsd_flags(&self, path: &Path, flags: u32) -> Result<()> {
self.dest.set_bsd_flags(path, flags).await
}
async fn compute_checksum(&self, path: &Path, verifier: &crate::integrity::IntegrityVerifier) -> Result<crate::integrity::Checksum> {
if path.exists() {
let path = path.to_path_buf();
let verifier = verifier.clone();
tokio::task::spawn_blocking(move || verifier.compute_file_checksum(&path))
.await
.map_err(|e| crate::error::SyncError::Io(std::io::Error::other(e.to_string())))?
} else {
self.dest.compute_checksum(path, verifier).await
}
}
async fn bulk_copy_files(&self, source_base: &Path, dest_base: &Path, relative_paths: &[&Path]) -> Result<u64> {
self.dest.bulk_copy_files(source_base, dest_base, relative_paths).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::integrity::{ChecksumType, IntegrityVerifier};
use crate::transport::local::LocalTransport;
use tempfile::TempDir;
#[tokio::test]
async fn test_compute_checksum_routes_local_path() {
let temp = TempDir::new().unwrap();
let local_file = temp.path().join("local_file.txt");
std::fs::write(&local_file, b"test content").unwrap();
let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
let source = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dest = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dual = DualTransport::new(source, dest);
let result = dual.compute_checksum(&local_file, &verifier).await;
assert!(result.is_ok(), "Should compute checksum for local file");
}
#[tokio::test]
async fn test_compute_checksum_routes_nonexistent_to_dest() {
let temp = TempDir::new().unwrap();
let dest_dir = temp.path().join("dest");
std::fs::create_dir_all(&dest_dir).unwrap();
let dest_file = dest_dir.join("remote_file.txt");
std::fs::write(&dest_file, b"remote content").unwrap();
let fake_remote_path = std::path::PathBuf::from("/nonexistent/remote/path.txt");
let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
let source = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dest = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dual = DualTransport::new(source, dest);
let result = dual.compute_checksum(&fake_remote_path, &verifier).await;
assert!(result.is_err(), "Should fail for path that doesn't exist anywhere");
}
#[tokio::test]
async fn test_dual_transport_verification_local_paths() {
let temp = TempDir::new().unwrap();
let source_file = temp.path().join("source.txt");
let dest_file = temp.path().join("dest.txt");
let content = b"identical content for verification";
std::fs::write(&source_file, content).unwrap();
std::fs::write(&dest_file, content).unwrap();
let verifier = IntegrityVerifier::new(ChecksumType::Fast, false);
let source = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dest = Box::new(LocalTransport::with_verifier(verifier.clone()));
let dual = DualTransport::new(source, dest);
let source_checksum = dual.compute_checksum(&source_file, &verifier).await.unwrap();
let dest_checksum = dual.compute_checksum(&dest_file, &verifier).await.unwrap();
assert_eq!(source_checksum, dest_checksum, "Checksums should match for identical files");
}
}