use crate::config::Config;
use crate::core::retry::{retry_with_backoff, RetryConfig};
use crate::error::{Result, RustDriveSyncError};
use crate::google_drive::auth::DriveAuthenticator;
use crate::google_drive::client::DriveClient;
use crate::google_drive::DriveStorageBackend;
use crate::storage::{StorageBackend, UploadOptions};
use crate::sync::scanner::{FileScanner, LocalFile};
use crate::sync::state::{FileState, SyncStateManager};
use crate::sync::tracker::{ChangeTracker, ChangeType, FileChange};
use crate::watcher::{FileEvent, FileWatcher, WatcherConfig};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncMode {
Once,
Watch,
}
#[derive(Debug, Clone, Default)]
pub struct SyncStats {
pub files_scanned: usize,
pub files_uploaded: usize,
pub files_updated: usize,
pub files_failed: usize,
pub bytes_uploaded: u64,
pub duration_secs: f64,
}
impl SyncStats {
pub fn record_upload(&mut self, size: u64, is_new: bool) {
if is_new {
self.files_uploaded += 1;
} else {
self.files_updated += 1;
}
self.bytes_uploaded += size;
}
pub fn record_failure(&mut self) {
self.files_failed += 1;
}
pub fn summary(&self) -> String {
format!(
"Escaneados: {}, Novos: {}, Atualizados: {}, Falhas: {}, Bytes: {}, Tempo: {:.2}s",
self.files_scanned,
self.files_uploaded,
self.files_updated,
self.files_failed,
self.bytes_uploaded,
self.duration_secs
)
}
}
#[derive(Debug, Clone)]
pub struct SyncResult {
pub stats: SyncStats,
pub success: bool,
pub error_message: Option<String>,
}
pub struct SyncEngine<S: StorageBackend> {
config: Config,
storage_backend: Arc<S>,
file_scanner: FileScanner,
state_manager: Arc<Mutex<SyncStateManager>>,
mode: SyncMode,
dry_run: bool,
shutdown: Arc<AtomicBool>,
retry_config: RetryConfig,
upload_semaphore: Arc<Semaphore>,
folder_cache: Arc<Mutex<std::collections::HashMap<String, String>>>,
}
impl SyncEngine<DriveStorageBackend> {
pub async fn new(config: Config, mode: SyncMode, dry_run: bool) -> Result<Self> {
info!("Inicializando engine de sincronização com Google Drive");
let auth = DriveAuthenticator::new(
&config.google_drive.credentials_file,
&config.google_drive.token_file,
config.google_drive.scopes.clone(),
)
.await?;
let drive_client = DriveClient::new(auth).await?;
let storage_backend = DriveStorageBackend::new(drive_client);
let folder_name = config
.google_drive
.target_folder_name
.clone()
.unwrap_or_else(|| "RustDriveSync".to_string());
let drive_folder = storage_backend.ensure_folder(&folder_name, None).await?;
info!(
"Pasta do Drive: {} (ID: {})",
drive_folder.name, drive_folder.id
);
Self::with_backend(config, Arc::new(storage_backend), drive_folder.id, mode, dry_run)
}
}
impl<S: StorageBackend + 'static> SyncEngine<S> {
pub fn with_backend(
config: Config,
storage_backend: Arc<S>,
target_folder_id: String,
mode: SyncMode,
dry_run: bool,
) -> Result<Self> {
let max_file_size = if config.sync.max_file_size_mb > 0 {
Some(config.sync.max_file_size_mb * 1024 * 1024)
} else {
None
};
let file_scanner = FileScanner::new()
.with_ignore_patterns(config.source.ignore_patterns.clone())
.with_max_file_size(max_file_size.unwrap_or(u64::MAX));
let state_file_path = config.state.state_file.clone();
let state_manager = SyncStateManager::load_or_create(
state_file_path,
config.source.path.display().to_string(),
target_folder_id,
)?;
let retry_config = RetryConfig {
max_attempts: config.retry.max_attempts,
initial_delay_secs: config.retry.initial_delay_seconds,
backoff_multiplier: config.retry.backoff_multiplier,
max_delay_secs: config.retry.max_delay_seconds,
};
info!("Retry configurado: {:?}", retry_config);
let max_concurrent = config.sync.max_concurrent_uploads;
info!(
"Uploads concorrentes configurados: {} simultâneos",
max_concurrent
);
Ok(Self {
config,
storage_backend,
file_scanner,
state_manager: Arc::new(Mutex::new(state_manager)),
mode,
dry_run,
shutdown: Arc::new(AtomicBool::new(false)),
retry_config,
upload_semaphore: Arc::new(Semaphore::new(max_concurrent)),
folder_cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
})
}
pub fn shutdown_handle(&self) -> Arc<AtomicBool> {
Arc::clone(&self.shutdown)
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
info!("Shutdown solicitado");
}
fn should_shutdown(&self) -> bool {
self.shutdown.load(Ordering::Relaxed)
}
pub async fn sync_once(&mut self) -> Result<SyncResult> {
let start_time = std::time::Instant::now();
let mut stats = SyncStats::default();
info!("Iniciando sincronização única");
info!("Escaneando diretório: {}", self.config.source.path.display());
let local_files = self
.file_scanner
.scan_directory(&self.config.source.path)?;
stats.files_scanned = local_files.len();
info!("Encontrados {} arquivos", local_files.len());
let state_guard = self.state_manager.lock().await;
let changes = ChangeTracker::detect_changes(&local_files, state_guard.state());
let sync_needed = ChangeTracker::filter_sync_needed(changes);
drop(state_guard);
info!("Arquivos a sincronizar: {}", sync_needed.len());
let upload_stats = self.sync_files_parallel(sync_needed).await;
stats.files_uploaded = upload_stats.files_uploaded;
stats.files_updated = upload_stats.files_updated;
stats.files_failed = upload_stats.files_failed;
stats.bytes_uploaded = upload_stats.bytes_uploaded;
if stats.files_failed == 0 {
let mut state_guard = self.state_manager.lock().await;
state_guard.mark_full_sync_and_save()?;
}
stats.duration_secs = start_time.elapsed().as_secs_f64();
info!("Sincronização concluída: {}", stats.summary());
Ok(SyncResult {
success: stats.files_failed == 0,
stats,
error_message: None,
})
}
pub async fn sync_watch(&mut self) -> Result<SyncResult> {
let start_time = std::time::Instant::now();
info!("🔍 Iniciando modo watch - monitoramento contínuo de arquivos");
info!("Executando sincronização inicial...");
let initial_result = self.sync_once().await?;
let mut stats = initial_result.stats;
info!(
"Sincronização inicial concluída: {}",
stats.summary()
);
let watcher_config = WatcherConfig::default()
.with_ignore_patterns(self.config.source.ignore_patterns.clone())
.with_debounce(Duration::from_secs(2));
let watcher = FileWatcher::new(&self.config.source.path, watcher_config)?;
info!(
"👁️ Monitorando mudanças em: {}",
self.config.source.path.display()
);
info!("Pressione Ctrl+C para parar...");
loop {
if self.should_shutdown() {
info!("Shutdown detectado, encerrando modo watch");
break;
}
match self.wait_for_event_with_timeout(&watcher, Duration::from_secs(1)) {
Some(event) => {
if event.needs_sync() {
info!(
"📝 Mudança detectada: {:?} - {}",
event.event_type,
event.path.display()
);
match self.process_watch_event(event, &mut stats).await {
Ok(_) => {
info!("✅ Arquivo sincronizado com sucesso");
}
Err(e) => {
error!("❌ Erro ao sincronizar arquivo: {}", e);
stats.record_failure();
}
}
} else {
debug!("Evento ignorado: {:?}", event.event_type);
}
}
None => {
continue;
}
}
}
stats.duration_secs = start_time.elapsed().as_secs_f64();
info!("Modo watch encerrado: {}", stats.summary());
Ok(SyncResult {
success: stats.files_failed == 0,
stats,
error_message: None,
})
}
fn wait_for_event_with_timeout(
&self,
watcher: &FileWatcher,
timeout: Duration,
) -> Option<FileEvent> {
let start = std::time::Instant::now();
loop {
if start.elapsed() >= timeout {
return None;
}
if let Some(event) = watcher.try_next_event() {
return Some(event);
}
std::thread::sleep(Duration::from_millis(50));
}
}
async fn sync_files_parallel(&self, changes: Vec<FileChange>) -> SyncStats {
if changes.is_empty() {
return SyncStats::default();
}
let total_files = changes.len();
info!(
"🚀 Iniciando sincronização paralela de {} arquivos",
total_files
);
let uploaded_count = Arc::new(AtomicUsize::new(0));
let updated_count = Arc::new(AtomicUsize::new(0));
let failed_count = Arc::new(AtomicUsize::new(0));
let bytes_uploaded = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for change in changes {
let storage = Arc::clone(&self.storage_backend);
let state_manager = Arc::clone(&self.state_manager);
let retry_config = self.retry_config.clone();
let semaphore = Arc::clone(&self.upload_semaphore);
let dry_run = self.dry_run;
let preserve_structure = self.config.sync.preserve_folder_structure;
let folder_cache = Arc::clone(&self.folder_cache);
let uploaded = Arc::clone(&uploaded_count);
let updated = Arc::clone(&updated_count);
let failed = Arc::clone(&failed_count);
let bytes = Arc::clone(&bytes_uploaded);
let task = tokio::spawn(async move {
let _permit = semaphore.acquire().await.expect("Semaphore closed");
match Self::sync_single_file(
&change,
&storage,
&state_manager,
retry_config,
dry_run,
preserve_structure,
&folder_cache,
)
.await
{
Ok(size) => {
let is_new = change.change_type == ChangeType::New;
if is_new {
uploaded.fetch_add(1, Ordering::Relaxed);
} else {
updated.fetch_add(1, Ordering::Relaxed);
}
bytes.fetch_add(size as usize, Ordering::Relaxed);
debug!("✅ Sincronizado: {}", change.relative_path);
}
Err(e) => {
failed.fetch_add(1, Ordering::Relaxed);
error!("❌ Falha ao sincronizar {}: {}", change.relative_path, e);
}
}
});
tasks.push(task);
}
info!("⏳ Aguardando conclusão de {} uploads paralelos...", total_files);
let results = futures::future::join_all(tasks).await;
let task_failures = results.iter().filter(|r| r.is_err()).count();
if task_failures > 0 {
warn!("{} tasks falharam com panic", task_failures);
}
let stats = SyncStats {
files_scanned: 0, files_uploaded: uploaded_count.load(Ordering::Relaxed),
files_updated: updated_count.load(Ordering::Relaxed),
files_failed: failed_count.load(Ordering::Relaxed) + task_failures,
bytes_uploaded: bytes_uploaded.load(Ordering::Relaxed) as u64,
duration_secs: 0.0, };
info!(
"✅ Sincronização paralela concluída: {} novos, {} atualizados, {} falhas",
stats.files_uploaded, stats.files_updated, stats.files_failed
);
stats
}
async fn sync_single_file<B: StorageBackend + 'static>(
change: &FileChange,
storage: &Arc<B>,
state_manager: &Arc<Mutex<SyncStateManager>>,
retry_config: RetryConfig,
dry_run: bool,
preserve_structure: bool,
folder_cache: &Arc<Mutex<std::collections::HashMap<String, String>>>,
) -> Result<u64> {
let local_file = change
.local_file
.as_ref()
.ok_or_else(|| RustDriveSyncError::DriveApiError {
message: "Arquivo local não disponível".to_string(),
})?;
debug!(
"Sincronizando: {} ({} bytes)",
change.relative_path, local_file.size
);
if dry_run {
debug!("[DRY-RUN] Pulando upload real de {}", change.relative_path);
return Ok(local_file.size);
}
let root_folder_id = {
let guard = state_manager.lock().await;
guard.state().drive_folder_id.clone()
};
let target_folder_id = if preserve_structure {
use std::path::Path;
let rel_path = Path::new(&local_file.relative_path);
if let Some(parent) = rel_path.parent() {
let parent_str = parent.display().to_string();
if parent_str.is_empty() || parent_str == "." {
root_folder_id.clone()
} else {
{
let cache = folder_cache.lock().await;
if let Some(cached_id) = cache.get(&parent_str) {
debug!("📂 Usando pasta em cache: {} -> {}", parent_str, cached_id);
cached_id.clone()
} else {
drop(cache);
debug!("📂 Criando hierarquia de pastas: {}", parent_str);
let folder_info = storage
.ensure_folder_path(&parent_str, root_folder_id.clone())
.await?;
let mut cache = folder_cache.lock().await;
cache.insert(parent_str.clone(), folder_info.id.clone());
debug!("📂 Pasta criada e cacheada: {} -> {}", parent_str, folder_info.id);
folder_info.id
}
}
}
} else {
root_folder_id.clone()
}
} else {
root_folder_id
};
let upload_options = UploadOptions::with_parent(target_folder_id);
let file_path = local_file.path.clone();
let relative_path = change.relative_path.clone();
let upload_result = retry_with_backoff(
retry_config,
|| {
let storage_ref = Arc::clone(storage);
let path = file_path.clone();
let opts = upload_options.clone();
async move { storage_ref.upload_file(&path, opts).await }
},
&format!("upload_{}", relative_path),
)
.await?;
let file_state = FileState {
relative_path: local_file.relative_path.display().to_string(),
drive_file_id: upload_result.file_id.clone(),
size: upload_result.size,
modified: local_file.modified,
md5_hash: upload_result
.md5_checksum
.clone()
.unwrap_or_else(|| "unknown".to_string()),
last_synced: chrono::Utc::now().timestamp(),
};
let mut guard = state_manager.lock().await;
guard.update_and_save(file_state)?;
debug!(
"Upload concluído: {} -> {}",
relative_path, upload_result.file_id
);
Ok(local_file.size)
}
async fn process_watch_event(
&mut self,
event: FileEvent,
stats: &mut SyncStats,
) -> Result<()> {
if !event.path.exists() {
debug!("Arquivo não existe mais, ignorando: {}", event.path.display());
return Ok(());
}
if event.path.is_dir() {
debug!("Ignorando diretório: {}", event.path.display());
return Ok(());
}
let local_file = LocalFile::from_path(
event.path.clone(),
&self.config.source.path,
)?;
if let Some(max_size) = self.file_scanner.config().max_file_size {
if local_file.size > max_size {
warn!(
"Arquivo muito grande ({}), ignorando: {}",
local_file.size,
event.path.display()
);
return Ok(());
}
}
let relative_path = local_file.relative_path.display().to_string();
let state_guard = self.state_manager.lock().await;
let is_new = !state_guard.state().is_synced(&relative_path);
let change = if is_new {
drop(state_guard);
FileChange::new(local_file)
} else {
let previous_state = state_guard
.state()
.get_file(&relative_path)
.cloned()
.ok_or_else(|| RustDriveSyncError::StateError {
message: format!(
"Arquivo não encontrado no estado: {}",
relative_path
),
})?;
drop(state_guard);
FileChange::modified(local_file, previous_state)
};
match Self::sync_single_file(
&change,
&self.storage_backend,
&self.state_manager,
self.retry_config.clone(),
self.dry_run,
self.config.sync.preserve_folder_structure,
&self.folder_cache,
)
.await
{
Ok(size) => {
stats.record_upload(size, change.change_type == ChangeType::New);
}
Err(e) => {
error!("Erro ao sincronizar: {}", e);
stats.record_failure();
return Err(e);
}
}
Ok(())
}
pub async fn run(&mut self) -> Result<SyncResult> {
match self.mode {
SyncMode::Once => self.sync_once().await,
SyncMode::Watch => self.sync_watch().await,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_stats() {
let mut stats = SyncStats::default();
assert_eq!(stats.files_uploaded, 0);
stats.record_upload(1024, true);
assert_eq!(stats.files_uploaded, 1);
assert_eq!(stats.bytes_uploaded, 1024);
stats.record_upload(2048, false);
assert_eq!(stats.files_updated, 1);
assert_eq!(stats.bytes_uploaded, 3072);
stats.record_failure();
assert_eq!(stats.files_failed, 1);
}
#[test]
fn test_sync_mode() {
assert_eq!(SyncMode::Once, SyncMode::Once);
assert_ne!(SyncMode::Once, SyncMode::Watch);
}
#[test]
fn test_retry_config_from_app_config() {
let retry = crate::config::schema::RetryConfig::default();
assert_eq!(retry.max_attempts, 3);
}
}