# 20 - Delta Sync Implementation Delta sync transfers only the modified parts of a file instead of the whole thing. Based on the rsync algorithm, it can save 90-99% bandwidth in common scenarios. **Status**: pending implementation **Priority**: medium **Estimated savings**: 10-100x less data transfer ## Contents 1. [Problem Statement](#problem-statement) 2. [How It Works](#how-it-works) 3. [Key Algorithms](#key-algorithms) 4. [Proposed Architecture](#proposed-architecture) 5. [Data Structures](#data-structures) 6. [API Endpoints](#api-endpoints) 7. [Step-by-Step Implementation](#step-by-step-implementation) 8. [Integration with Existing System](#integration-with-existing-system) 9. [Use Cases and Effectiveness](#use-cases-and-effectiveness) 10. [Performance Considerations](#performance-considerations) 11. [Testing](#testing) 12. [Required Dependencies](#required-dependencies) --- ## Key Benefits | Metric | Without Delta Sync | With Delta Sync | |---------|----------------|----------------| | Edit 1 line in 100MB | 100MB transferred | ~1KB transferred | | Sync time (slow connection) | 4+ minutes | <1 second | | Bandwidth consumption | 100% | 0.1-10% | --- ## Problem Statement ### Current scenario (no delta sync) ``` User has document.docx (50MB) on OxiCloud │ ▼ Downloads full file (50MB) ──────────────────────► 50MB ↓ │ ▼ Edits one word │ ▼ Uploads full file again (50MB) ──────────────────► 50MB ↑ │ ▼ TOTAL: 100MB transferred to change one word ``` ### Target scenario (with delta sync) ``` User has document.docx (50MB) on OxiCloud │ ▼ Downloads full file (50MB) ──────────────────────► 50MB ↓ (first time) │ ▼ Edits one word │ ▼ Uploads ONLY modified blocks ───────────────────► ~50KB ↑ │ ▼ TOTAL: 50.05MB (99.9% savings on upload) ``` --- ## How It Works ### Block (chunk) concept The file gets divided into fixed-size blocks (typically 4KB-64KB): ``` Original file (server): ┌────────┬────────┬────────┬────────┬────────┐ │ Bloque │ Bloque │ Bloque │ Bloque │ Bloque │ │ 0 │ 1 │ 2 │ 3 │ 4 │ │ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │ │ │ │ │ │ │ │ weak:A │ weak:B │ weak:C │ weak:D │ weak:E │ │ sha:X1 │ sha:X2 │ sha:X3 │ sha:X4 │ sha:X5 │ └────────┴────────┴────────┴────────┴────────┘ Modified file (client): ┌────────┬────────┬────────┬────────┬────────┐ │ Bloque │ Bloque │ Bloque │ Bloque │ Bloque │ │ 0 │ 1 │ 2 │ 3 │ 4 │ │ 4KB │ 4KB │ 4KB │ 4KB │ 4KB │ │ │ │ │ │ │ │ weak:A │ weak:B │ weak:F │ weak:D │ weak:E │ ← Block 2 changed │ sha:X1 │ sha:X2 │ sha:Y3 │ sha:X4 │ sha:X5 │ └────────┴────────┴───▲────┴────────┴────────┘ │ ONLY THIS ONE GETS TRANSFERRED ``` ### Sync flow ``` ┌─────────────────────────────────────────────────────────────────────┐ │ DELTA SYNC FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ CLIENT SERVER │ │ │ │ 1. Has modified 1. Has original file │ │ file + block index │ │ │ │ 2. Requests signatures ─────────────► │ │ GET /files/{id}/signatures │ │ │ │ ◄────────────── 3. Returns signature list │ │ [(weak, strong), ...] │ │ │ │ 4. Compares local blocks │ │ against server │ │ signatures │ │ │ │ 5. Generates delta ──────────────────► │ │ POST /files/{id}/delta │ │ [References + New data] │ │ │ │ 6. Reconstructs file │ │ by applying delta │ │ │ │ ◄────────────── 7. Confirms update │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Key Algorithms ### 1. Rolling Checksum (modified Adler-32) The rolling checksum computes the hash of a sliding window in O(1): ```rust /// Rolling checksum para búsqueda rápida de bloques coincidentes /// Similar al usado por rsync (Adler-32 modificado) pub struct RollingChecksum { a: u32, // Suma simple de bytes b: u32, // Suma ponderada window_size: usize, buffer: VecDeque, } impl RollingChecksum { pub fn new(window_size: usize) -> Self { Self { a: 0, b: 0, window_size, buffer: VecDeque::with_capacity(window_size), } } /// Añadir un byte y calcular nuevo checksum /// Complejidad: O(1) pub fn roll(&mut self, new_byte: u8) -> u32 { if self.buffer.len() >= self.window_size { // Remover byte antiguo let old_byte = self.buffer.pop_front().unwrap() as u32; self.a = self.a.wrapping_sub(old_byte).wrapping_add(new_byte as u32); self.b = self.b.wrapping_sub(old_byte * self.window_size as u32) .wrapping_add(self.a); } else { // Ventana no llena todavía self.a = self.a.wrapping_add(new_byte as u32); self.b = self.b.wrapping_add(self.a); } self.buffer.push_back(new_byte); self.checksum() } /// Calcular checksum actual pub fn checksum(&self) -> u32 { (self.b << 16) | (self.a & 0xFFFF) } /// Reset para nuevo archivo pub fn reset(&mut self) { self.a = 0; self.b = 0; self.buffer.clear(); } } ``` ### 2. Block Signature Each block carries two signatures: - **Weak checksum** (32-bit) -- fast O(1) lookup - **Strong hash** (SHA-256) -- definitive verification ```rust /// Firma de un bloque para identificación #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlockSignature { /// Índice del bloque en el archivo pub index: u32, /// Offset en bytes desde el inicio del archivo pub offset: u64, /// Tamaño del bloque (puede ser menor para el último) pub size: u32, /// Rolling checksum (32-bit) - búsqueda rápida pub weak_checksum: u32, /// SHA-256 hash (256-bit) - verificación definitiva pub strong_hash: [u8; 32], } /// Genera firmas para todos los bloques de un archivo pub fn generate_signatures(data: &[u8], block_size: usize) -> Vec { let mut signatures = Vec::new(); let mut offset = 0u64; let mut index = 0u32; for chunk in data.chunks(block_size) { // Weak checksum (rolling) let weak = adler32_checksum(chunk); // Strong hash (SHA-256) let mut hasher = Sha256::new(); hasher.update(chunk); let strong: [u8; 32] = hasher.finalize().into(); signatures.push(BlockSignature { index, offset, size: chunk.len() as u32, weak_checksum: weak, strong_hash: strong, }); offset += chunk.len() as u64; index += 1; } signatures } fn adler32_checksum(data: &[u8]) -> u32 { let mut a: u32 = 1; let mut b: u32 = 0; for &byte in data { a = (a + byte as u32) % 65521; b = (b + a) % 65521; } (b << 16) | a } ``` ### 3. Delta Generation ```rust /// Instrucción de delta #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DeltaInstruction { /// Copiar bloque existente del archivo original Copy { /// Índice del bloque en el archivo original block_index: u32, }, /// Insertar datos literales nuevos Literal { /// Datos nuevos a insertar data: Vec, }, } /// Delta completo para actualizar un archivo #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileDelta { /// ID del archivo base pub base_file_id: String, /// Hash del archivo base (para verificación) pub base_file_hash: String, /// Nuevo tamaño del archivo pub new_size: u64, /// Instrucciones de delta pub instructions: Vec, /// Hash del archivo resultante (para verificación) pub result_hash: String, } /// Genera delta comparando archivo local con firmas remotas pub fn generate_delta( local_data: &[u8], remote_signatures: &[BlockSignature], block_size: usize, ) -> FileDelta { // Crear índice de weak checksums para búsqueda O(1) let mut weak_index: HashMap> = HashMap::new(); for sig in remote_signatures { weak_index.entry(sig.weak_checksum) .or_default() .push(sig); } let mut instructions = Vec::new(); let mut rolling = RollingChecksum::new(block_size); let mut pos = 0; let mut literal_buffer = Vec::new(); while pos < local_data.len() { // Calcular rolling checksum de la ventana actual let end = (pos + block_size).min(local_data.len()); let window = &local_data[pos..end]; let weak = if window.len() == block_size { rolling.reset(); for &b in window { rolling.roll(b); } rolling.checksum() } else { adler32_checksum(window) }; // Buscar coincidencia let mut found_match = false; if let Some(candidates) = weak_index.get(&weak) { // Verificar con strong hash let mut hasher = Sha256::new(); hasher.update(window); let strong: [u8; 32] = hasher.finalize().into(); for sig in candidates { if sig.strong_hash == strong && sig.size as usize == window.len() { // ¡Coincidencia encontrada! // Flush literal buffer si hay datos pendientes if !literal_buffer.is_empty() { instructions.push(DeltaInstruction::Literal { data: std::mem::take(&mut literal_buffer), }); } // Añadir instrucción de copia instructions.push(DeltaInstruction::Copy { block_index: sig.index, }); pos += window.len(); found_match = true; break; } } } if !found_match { // No hay coincidencia, añadir byte a literal buffer literal_buffer.push(local_data[pos]); pos += 1; } } // Flush remaining literal buffer if !literal_buffer.is_empty() { instructions.push(DeltaInstruction::Literal { data: literal_buffer, }); } // Calcular hash del resultado let mut hasher = Sha256::new(); hasher.update(local_data); let result_hash = hex::encode(hasher.finalize()); FileDelta { base_file_id: String::new(), // Se llena al enviar base_file_hash: String::new(), // Se llena al enviar new_size: local_data.len() as u64, instructions, result_hash, } } ``` ### 4. Delta Application ```rust /// Aplica delta a un archivo base para obtener el nuevo archivo pub fn apply_delta( base_data: &[u8], signatures: &[BlockSignature], delta: &FileDelta, block_size: usize, ) -> Result, DeltaSyncError> { let mut result = Vec::with_capacity(delta.new_size as usize); for instruction in &delta.instructions { match instruction { DeltaInstruction::Copy { block_index } => { // Copiar bloque del archivo base let sig = signatures.get(*block_index as usize) .ok_or(DeltaSyncError::InvalidBlockIndex(*block_index))?; let start = sig.offset as usize; let end = start + sig.size as usize; if end > base_data.len() { return Err(DeltaSyncError::InvalidBlockRange); } result.extend_from_slice(&base_data[start..end]); } DeltaInstruction::Literal { data } => { // Insertar datos literales result.extend_from_slice(data); } } } // Verificar hash del resultado let mut hasher = Sha256::new(); hasher.update(&result); let actual_hash = hex::encode(hasher.finalize()); if actual_hash != delta.result_hash { return Err(DeltaSyncError::HashMismatch { expected: delta.result_hash.clone(), actual: actual_hash, }); } Ok(result) } ``` --- ## Proposed Architecture ### File structure ``` src/ ├── infrastructure/ │ └── services/ │ ├── mod.rs # Añadir: pub mod delta_sync_service; │ └── delta_sync_service.rs # NUEVO: Servicio principal │ ├── interfaces/ │ └── api/ │ └── handlers/ │ ├── mod.rs # Añadir: pub mod delta_sync_handler; │ └── delta_sync_handler.rs # NUEVO: Endpoints API │ └── common/ └── di.rs # Añadir: delta_sync_service a CoreServices ``` ### Main service (delta_sync_service.rs) ```rust //! Delta Sync Service - Sincronización eficiente por diferencias //! //! Implementa algoritmo similar a rsync para transferir solo //! las partes modificadas de los archivos. use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs; use tokio::sync::RwLock; use sha2::{Sha256, Digest}; use serde::{Deserialize, Serialize}; /// Tamaño de bloque por defecto (16KB - buen balance) pub const DEFAULT_BLOCK_SIZE: usize = 16 * 1024; /// Tamaño mínimo de archivo para usar delta sync pub const MIN_DELTA_SYNC_SIZE: u64 = 64 * 1024; // 64KB /// Errores del servicio Delta Sync #[derive(Debug, thiserror::Error)] pub enum DeltaSyncError { #[error("Archivo no encontrado: {0}")] FileNotFound(String), #[error("Firmas no encontradas para archivo: {0}")] SignaturesNotFound(String), #[error("Índice de bloque inválido: {0}")] InvalidBlockIndex(u32), #[error("Rango de bloque inválido")] InvalidBlockRange, #[error("Hash no coincide: esperado {expected}, actual {actual}")] HashMismatch { expected: String, actual: String }, #[error("Error de I/O: {0}")] IoError(#[from] std::io::Error), #[error("Error de serialización: {0}")] SerializationError(String), } /// Servicio de Delta Sync pub struct DeltaSyncService { /// Directorio para almacenar índices de firmas signatures_dir: PathBuf, /// Cache en memoria de firmas recientes signature_cache: Arc>>>, /// Tamaño de bloque configurado block_size: usize, /// Máximo de entradas en cache max_cache_entries: usize, } impl DeltaSyncService { pub fn new(storage_root: &Path) -> Self { Self { signatures_dir: storage_root.join(".delta_signatures"), signature_cache: Arc::new(RwLock::new(HashMap::new())), block_size: DEFAULT_BLOCK_SIZE, max_cache_entries: 1000, } } pub fn with_block_size(mut self, block_size: usize) -> Self { self.block_size = block_size; self } /// Inicializar servicio (crear directorios) pub async fn initialize(&self) -> std::io::Result<()> { fs::create_dir_all(&self.signatures_dir).await?; tracing::info!("Delta Sync service initialized with block size: {}KB", self.block_size / 1024); Ok(()) } /// Generar y almacenar firmas para un archivo pub async fn index_file( &self, file_id: &str, file_path: &Path ) -> Result, DeltaSyncError> { let data = fs::read(file_path).await?; // No indexar archivos pequeños if data.len() < MIN_DELTA_SYNC_SIZE as usize { return Ok(Vec::new()); } let signatures = generate_signatures(&data, self.block_size); // Guardar en disco let sig_path = self.signature_path(file_id); let sig_json = serde_json::to_vec(&signatures) .map_err(|e| DeltaSyncError::SerializationError(e.to_string()))?; fs::write(&sig_path, sig_json).await?; // Actualizar cache { let mut cache = self.signature_cache.write().await; if cache.len() >= self.max_cache_entries { // LRU simple: eliminar primera entrada if let Some(key) = cache.keys().next().cloned() { cache.remove(&key); } } cache.insert(file_id.to_string(), signatures.clone()); } tracing::debug!("Indexed file {} with {} blocks", file_id, signatures.len()); Ok(signatures) } /// Obtener firmas de un archivo pub async fn get_signatures( &self, file_id: &str ) -> Result, DeltaSyncError> { // Buscar en cache primero { let cache = self.signature_cache.read().await; if let Some(sigs) = cache.get(file_id) { return Ok(sigs.clone()); } } // Cargar de disco let sig_path = self.signature_path(file_id); if !sig_path.exists() { return Err(DeltaSyncError::SignaturesNotFound(file_id.to_string())); } let sig_json = fs::read(&sig_path).await?; let signatures: Vec = serde_json::from_slice(&sig_json) .map_err(|e| DeltaSyncError::SerializationError(e.to_string()))?; // Actualizar cache { let mut cache = self.signature_cache.write().await; cache.insert(file_id.to_string(), signatures.clone()); } Ok(signatures) } /// Aplicar delta a un archivo pub async fn apply_delta( &self, file_id: &str, base_path: &Path, delta: &FileDelta, ) -> Result, DeltaSyncError> { let base_data = fs::read(base_path).await?; let signatures = self.get_signatures(file_id).await?; apply_delta(&base_data, &signatures, delta, self.block_size) } /// Eliminar firmas de un archivo (cuando se borra) pub async fn remove_signatures(&self, file_id: &str) -> Result<(), DeltaSyncError> { // Eliminar de cache { let mut cache = self.signature_cache.write().await; cache.remove(file_id); } // Eliminar de disco let sig_path = self.signature_path(file_id); if sig_path.exists() { fs::remove_file(&sig_path).await?; } Ok(()) } /// Estadísticas del servicio pub async fn get_stats(&self) -> DeltaSyncStats { let cache = self.signature_cache.read().await; DeltaSyncStats { cached_files: cache.len() as u64, block_size: self.block_size, } } fn signature_path(&self, file_id: &str) -> PathBuf { // Usar primeros 2 chars del ID para subdirectorio let prefix = &file_id[..2.min(file_id.len())]; self.signatures_dir.join(prefix).join(format!("{}.sig", file_id)) } } #[derive(Debug, Clone, Serialize)] pub struct DeltaSyncStats { pub cached_files: u64, pub block_size: usize, } ``` --- ## API Endpoints ### Handler (delta_sync_handler.rs) ```rust use axum::{ extract::{Path, State, Json}, http::StatusCode, response::IntoResponse, }; use crate::common::di::AppState; use crate::infrastructure::services::delta_sync_service::*; pub struct DeltaSyncHandler; impl DeltaSyncHandler { /// GET /api/files/{id}/signatures /// /// Obtiene las firmas de bloques de un archivo para calcular delta pub async fn get_signatures( State(state): State, Path(file_id): Path, ) -> impl IntoResponse { let delta_service = &state.core.delta_sync_service; match delta_service.get_signatures(&file_id).await { Ok(signatures) => { Json(SignaturesResponse { file_id, block_size: delta_service.block_size, block_count: signatures.len() as u32, signatures, }).into_response() } Err(DeltaSyncError::SignaturesNotFound(_)) => { // Archivo no indexado - cliente debe hacer upload completo (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Signatures not found", "hint": "File not indexed for delta sync, use full upload" }))).into_response() } Err(e) => { (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response() } } } /// POST /api/files/{id}/delta /// /// Aplica un delta para actualizar un archivo pub async fn apply_delta( State(state): State, Path(file_id): Path, Json(delta): Json, ) -> impl IntoResponse { let delta_service = &state.core.delta_sync_service; let file_service = &state.applications.file_service; // Obtener path del archivo actual let file = match file_service.get_file(&file_id).await { Ok(f) => f, Err(_) => { return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "File not found" }))).into_response(); } }; // Aplicar delta let file_path = state.core.path_service.resolve_path(file.path()); match delta_service.apply_delta(&file_id, &file_path, &delta).await { Ok(new_data) => { // Guardar nuevo contenido if let Err(e) = tokio::fs::write(&file_path, &new_data).await { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("Failed to write file: {}", e) }))).into_response(); } // Re-indexar archivo if let Err(e) = delta_service.index_file(&file_id, &file_path).await { tracing::warn!("Failed to re-index file after delta: {}", e); } // Calcular estadísticas let delta_size: usize = delta.instructions.iter() .filter_map(|i| match i { DeltaInstruction::Literal { data } => Some(data.len()), _ => None, }) .sum(); Json(DeltaApplyResponse { success: true, new_size: new_data.len() as u64, delta_size: delta_size as u64, savings_percent: if new_data.len() > 0 { ((1.0 - (delta_size as f64 / new_data.len() as f64)) * 100.0) as u32 } else { 0 }, }).into_response() } Err(e) => { (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": e.to_string() }))).into_response() } } } /// POST /api/files/{id}/index /// /// Fuerza la indexación de un archivo para delta sync pub async fn index_file( State(state): State, Path(file_id): Path, ) -> impl IntoResponse { let delta_service = &state.core.delta_sync_service; let file_service = &state.applications.file_service; // Obtener path del archivo let file = match file_service.get_file(&file_id).await { Ok(f) => f, Err(_) => { return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "File not found" }))).into_response(); } }; let file_path = state.core.path_service.resolve_path(file.path()); match delta_service.index_file(&file_id, &file_path).await { Ok(signatures) => { Json(serde_json::json!({ "success": true, "blocks_indexed": signatures.len() })).into_response() } Err(e) => { (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response() } } } /// GET /api/delta/stats /// /// Estadísticas del servicio delta sync pub async fn get_stats( State(state): State, ) -> impl IntoResponse { let delta_service = &state.core.delta_sync_service; Json(delta_service.get_stats().await) } } #[derive(Serialize)] struct SignaturesResponse { file_id: String, block_size: usize, block_count: u32, signatures: Vec, } #[derive(Serialize)] struct DeltaApplyResponse { success: bool, new_size: u64, delta_size: u64, savings_percent: u32, } ``` ### Routes to add in routes.rs ```rust // Delta Sync routes let delta_sync_router = Router::new() .route("/files/:id/signatures", get(DeltaSyncHandler::get_signatures)) .route("/files/:id/delta", post(DeltaSyncHandler::apply_delta)) .route("/files/:id/index", post(DeltaSyncHandler::index_file)) .route("/delta/stats", get(DeltaSyncHandler::get_stats)) .with_state(app_state.clone()); // Añadir a router principal router = router.nest("/api", delta_sync_router); ``` --- ## Integration with Existing System ### 1. Auto-index on upload In `file_handler.rs`, after a successful upload: ```rust // Después de guardar el archivo... // Indexar para delta sync (archivos >64KB) if total_size >= 64 * 1024 { let delta_service = &state.core.delta_sync_service; if let Err(e) = delta_service.index_file(&file.id, &file_path).await { tracing::warn!("Failed to index file for delta sync: {}", e); // No es error fatal, el archivo se subió correctamente } } ``` ### 2. Clean up signatures on delete In `file_handler.rs`, when deleting a file: ```rust // Limpiar firmas de delta sync let delta_service = &state.core.delta_sync_service; if let Err(e) = delta_service.remove_signatures(&id).await { tracing::warn!("Failed to remove delta signatures: {}", e); } ``` ### 3. Integration with Dedup Service Delta sync and dedup are complementary: ``` ┌──────────────────────────────────────────────────────────────┐ │ UPLOAD WITH DELTA + DEDUP │ ├──────────────────────────────────────────────────────────────┤ │ │ │ 1. Client has modified file.txt │ │ │ │ 2. GET /files/{id}/signatures │ │ → Server returns block signatures │ │ │ │ 3. Client computes delta locally │ │ → Only 3 of 100 blocks changed │ │ │ │ 4. POST /files/{id}/delta │ │ → Sends only the 3 new blocks │ │ │ │ 5. Server applies delta │ │ → Reconstructs complete file │ │ │ │ 6. Server runs dedup on resulting file │ │ → If another user has same content, it deduplicates │ │ │ │ RESULT: │ │ ├── Delta sync: 97% less transfer │ │ └── Dedup: 30-50% less storage │ │ │ └──────────────────────────────────────────────────────────────┘ ``` --- ## Use Cases and Effectiveness | File type | Scenario | Without Delta | With Delta | Savings | |-----------------|-----------|-----------|-----------|--------| | `.txt` / `.md` | Edit paragraph | 1MB | ~4KB | **99.6%** | | `.json` / `.xml` | Change value | 500KB | ~1KB | **99.8%** | | `.rs` / `.js` | Modify function | 100KB | ~2KB | **98%** | | `.docx` | Edit page | 5MB | ~100KB | **98%** | | `.xlsx` | Change cells | 2MB | ~50KB | **97.5%** | | `.pdf` | Edit text | 10MB | ~2MB | **80%** | | `.psd` | Edit layer | 100MB | ~5MB | **95%** | | `.zip` | Add file | 50MB | ~5MB | **90%** | | `.mp4` | Re-encode | 500MB | 450MB | **10%** | | `.jpg` | Edit image | 5MB | 4MB | **20%** | For highly compressed or re-encoded files, delta sync is less effective. --- ## Performance Considerations ### Optimal block size | Size | Pros | Cons | Best for | |--------|------|------|------------| | 4KB | More granular, better savings | More signature overhead | Small files | | 16KB | Good balance | - | **General use** | | 64KB | Less overhead | Less granular | Large files | | 256KB | Minimal overhead | Little savings | Very large files | ### Memory ```rust // Estimación de memoria por archivo indexado // // BlockSignature size ≈ 48 bytes (4 + 8 + 4 + 4 + 32 - con padding) // // Archivo 100MB con bloques de 16KB: // - 100MB / 16KB = 6,400 bloques // - 6,400 × 48 bytes = ~300KB de firmas // // Cache de 1000 archivos ≈ 300MB máximo ``` ### CPU ```rust // Operaciones costosas: // // 1. generate_signatures(): O(n) donde n = tamaño archivo // - SHA-256: ~500MB/s en CPU moderna // - Adler32: ~2GB/s // // 2. generate_delta(): O(n × m) peor caso, O(n) típico // - n = tamaño archivo nuevo // - m = número de bloques originales // - HashMap lookup: O(1) promedio // // 3. apply_delta(): O(n) donde n = tamaño resultado // - Mayormente copias de memoria ``` --- ## Testing ### Unit tests ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_rolling_checksum() { let mut rc = RollingChecksum::new(4); // Alimentar bytes for b in b"test" { rc.roll(*b); } let checksum1 = rc.checksum(); // Rolling: quitar 't', añadir 'X' rc.roll(b'X'); let checksum2 = rc.checksum(); // Checksums deben ser diferentes assert_ne!(checksum1, checksum2); } #[test] fn test_generate_signatures() { let data = b"Hello, World! This is a test file for delta sync."; let sigs = generate_signatures(data, 16); assert_eq!(sigs.len(), 4); // 50 bytes / 16 = 3.125 → 4 bloques assert_eq!(sigs[0].offset, 0); assert_eq!(sigs[1].offset, 16); } #[test] fn test_delta_identical_files() { let data = b"Hello, World!"; let sigs = generate_signatures(data, 8); let delta = generate_delta(data, &sigs, 8); // Solo instrucciones Copy, sin Literal for instr in &delta.instructions { assert!(matches!(instr, DeltaInstruction::Copy { .. })); } } #[test] fn test_delta_small_change() { let original = b"Hello, World! This is original."; let modified = b"Hello, World! This is MODIFIED."; let sigs = generate_signatures(original, 8); let delta = generate_delta(modified, &sigs, 8); // Debería haber algunas instrucciones Copy y algunas Literal let copies = delta.instructions.iter() .filter(|i| matches!(i, DeltaInstruction::Copy { .. })) .count(); let literals = delta.instructions.iter() .filter(|i| matches!(i, DeltaInstruction::Literal { .. })) .count(); assert!(copies > 0, "Should reuse some blocks"); assert!(literals > 0, "Should have some new data"); } #[test] fn test_apply_delta_roundtrip() { let original = b"The quick brown fox jumps over the lazy dog."; let modified = b"The quick brown cat jumps over the lazy dog."; let sigs = generate_signatures(original, 8); let delta = generate_delta(modified, &sigs, 8); let reconstructed = apply_delta(original, &sigs, &delta, 8).unwrap(); assert_eq!(reconstructed, modified); } } ``` ### Integration tests ```rust #[tokio::test] async fn test_delta_sync_service_workflow() { let temp_dir = tempfile::tempdir().unwrap(); let service = DeltaSyncService::new(temp_dir.path()); service.initialize().await.unwrap(); // Crear archivo original let file_path = temp_dir.path().join("test.txt"); tokio::fs::write(&file_path, b"Original content here").await.unwrap(); // Indexar let sigs = service.index_file("file123", &file_path).await.unwrap(); assert!(!sigs.is_empty()); // Recuperar firmas let retrieved = service.get_signatures("file123").await.unwrap(); assert_eq!(sigs.len(), retrieved.len()); // Simular modificación y delta let modified = b"Modified content here!"; let delta = generate_delta(modified, &sigs, service.block_size); // Aplicar delta let result = service.apply_delta("file123", &file_path, &delta).await.unwrap(); assert_eq!(result, modified); } ``` --- ## Required Dependencies Add to `Cargo.toml`: ```toml [dependencies] # Ya existentes - verificar versiones sha2 = "0.10" hex = "0.4" # Nuevas dependencias para delta sync thiserror = "1.0" # Para errores tipados (probablemente ya existe) ``` --- ## Implementation Checklist - [ ] Create `delta_sync_service.rs` with basic structures - [ ] Implement **RollingChecksum** - [ ] Implement **generate_signatures()** - [ ] Implement **generate_delta()** - [ ] Implement **apply_delta()** - [ ] Create handler and API endpoints - [ ] Integrate into DI (**CoreServices**) - [ ] Add routes in `routes.rs` - [ ] Integrate with upload (automatic indexing) - [ ] Integrate with delete (signature cleanup) - [ ] Unit tests - [ ] Integration tests - [ ] Document API endpoints - [ ] Metrics and logging --- ## References - [rsync algorithm](https://rsync.samba.org/tech_report/) - [Rolling hash - Wikipedia](https://en.wikipedia.org/wiki/Rolling_hash) - [Adler-32 checksum](https://en.wikipedia.org/wiki/Adler-32) - [librsync](https://github.com/librsync/librsync)