client_core/patch_executor/
mod.rs1pub mod error;
10pub mod file_operations;
11pub mod patch_processor;
12
13pub use error::PatchExecutorError;
15pub use file_operations::FileOperationExecutor;
16pub use patch_processor::PatchProcessor;
17
18use crate::api_types::{PatchOperations, PatchPackageInfo};
19use std::path::{Path, PathBuf};
20use tracing::{debug, error, info, warn};
21
22pub struct PatchExecutor {
26 work_dir: PathBuf,
28 file_executor: FileOperationExecutor,
30 patch_processor: PatchProcessor,
32 backup_enabled: bool,
34}
35
36impl PatchExecutor {
37 pub fn new(work_dir: PathBuf) -> Result<Self, PatchExecutorError> {
39 let file_executor = FileOperationExecutor::new(work_dir.clone())?;
40 let patch_processor = PatchProcessor::new()?;
41
42 Ok(Self {
43 work_dir,
44 file_executor,
45 patch_processor,
46 backup_enabled: false,
47 })
48 }
49
50 pub fn enable_backup(&mut self) -> Result<(), PatchExecutorError> {
52 self.file_executor.enable_backup()?;
53 self.backup_enabled = true;
54 info!("Patch execution backup mode enabled");
55 Ok(())
56 }
57
58 pub async fn apply_patch<F>(
65 &mut self,
66 patch_info: &PatchPackageInfo,
67 operations: &PatchOperations,
68 progress_callback: F,
69 ) -> Result<(), PatchExecutorError>
70 where
71 F: Fn(f64) + Send + Sync,
72 {
73 info!("Starting to apply incremental patch...");
74 progress_callback(0.0);
75
76 self.validate_preconditions(operations)?;
78 progress_callback(0.05);
79
80 match self
82 .execute_patch_pipeline(patch_info, operations, &progress_callback)
83 .await
84 {
85 Ok(_) => {
86 progress_callback(1.0);
87 info!("Incremental patch applied successfully");
88 Ok(())
89 }
90 Err(e) => {
91 error!("Patch application failed: {}", e);
92
93 if e.requires_rollback() && self.backup_enabled {
95 warn!("Starting automatic rollback...");
96 if let Err(rollback_err) = self.rollback().await {
97 error!("Rollback failed: {}", rollback_err);
98 return Err(PatchExecutorError::rollback_failed(format!(
99 "Original error: {e}, rollback error: {rollback_err}"
100 )));
101 }
102 info!("Automatic rollback completed");
103 }
104
105 Err(e)
106 }
107 }
108 }
109
110 fn validate_preconditions(
112 &self,
113 operations: &PatchOperations,
114 ) -> Result<(), PatchExecutorError> {
115 debug!("Validating patch application preconditions");
116
117 if !self.work_dir.exists() {
119 return Err(PatchExecutorError::path_error(format!(
120 "Working directory does not exist: {:?}",
121 self.work_dir
122 )));
123 }
124
125 let total_operations = operations.total_operations();
127
128 if total_operations == 0 {
129 return Err(PatchExecutorError::custom("Patch operations are empty"));
130 }
131
132 debug!("Preconditions validated, total {} operations", total_operations);
133 Ok(())
134 }
135
136 async fn execute_patch_pipeline<F>(
138 &mut self,
139 patch_info: &PatchPackageInfo,
140 operations: &PatchOperations,
141 progress_callback: &F,
142 ) -> Result<(), PatchExecutorError>
143 where
144 F: Fn(f64) + Send + Sync,
145 {
146 info!("Downloading patch package...");
148 let patch_path = self.patch_processor.download_patch(patch_info).await?;
149 progress_callback(0.25);
150
151 info!("Verifying patch integrity...");
153 self.patch_processor
154 .verify_patch_integrity(&patch_path, patch_info)
155 .await?;
156 progress_callback(0.35);
157
158 info!("Extracting patch package...");
160 let extracted_path = self.patch_processor.extract_patch(&patch_path).await?;
161 progress_callback(0.45);
162
163 info!("Verifying patch file structure...");
165 self.validate_patch_structure(&extracted_path, operations)
166 .await?;
167 progress_callback(0.5);
168
169 info!("Applying patch operations...");
171 self.apply_patch_operations(&extracted_path, operations, progress_callback)
172 .await?;
173
174 Ok(())
175 }
176
177 async fn validate_patch_structure(
179 &self,
180 extracted_path: &Path,
181 operations: &PatchOperations,
182 ) -> Result<(), PatchExecutorError> {
183 let mut required_files = Vec::new();
185
186 if let Some(replace) = &operations.replace {
188 for file in &replace.files {
189 required_files.push(file.clone());
190 }
191 for dir in &replace.directories {
193 let dir_path = extracted_path.join(dir);
194 if !dir_path.exists() || !dir_path.is_dir() {
195 return Err(PatchExecutorError::verification_failed(format!(
196 "Required directory missing in patch: {dir}"
197 )));
198 }
199 }
200 }
201
202 self.patch_processor
204 .validate_extracted_structure(&required_files)
205 .await?;
206
207 debug!("Patch file structure verified");
208 Ok(())
209 }
210
211 async fn apply_patch_operations<F>(
213 &mut self,
214 extracted_path: &Path,
215 operations: &PatchOperations,
216 progress_callback: &F,
217 ) -> Result<(), PatchExecutorError>
218 where
219 F: Fn(f64) + Send + Sync,
220 {
221 self.file_executor.set_patch_source(extracted_path)?;
223
224 let total_operations = operations.total_operations();
226
227 let mut completed_operations = 0;
228
229 let base_progress = 0.5; let operations_progress_range = 0.5; if let Some(replace) = &operations.replace {
234 if !replace.files.is_empty() {
236 info!("Replacing {} files", &replace.files.len());
237 self.file_executor.replace_files(&replace.files).await?;
238 completed_operations += replace.files.len();
239 let progress = base_progress
240 + (completed_operations as f64 / total_operations as f64)
241 * operations_progress_range;
242 progress_callback(progress);
243 }
244
245 if !replace.directories.is_empty() {
247 info!("Replacing {} directories", &replace.directories.len());
248 self.file_executor
249 .replace_directories(&replace.directories)
250 .await?;
251 completed_operations += replace.directories.len();
252 let progress = base_progress
253 + (completed_operations as f64 / total_operations as f64)
254 * operations_progress_range;
255 progress_callback(progress);
256 }
257 }
258
259 if let Some(delete) = &operations.delete {
261 if !delete.files.is_empty() {
263 info!("Deleting {} items", &delete.files.len());
264 self.file_executor.delete_items(&delete.files).await?;
265 completed_operations += &delete.files.len();
266 let progress = base_progress
267 + (completed_operations as f64 / total_operations as f64)
268 * operations_progress_range;
269 progress_callback(progress);
270 }
271 if !delete.directories.is_empty() {
273 info!("Deleting {} directories", &delete.directories.len());
274 self.file_executor.delete_items(&delete.directories).await?;
275 completed_operations += &delete.directories.len();
276 let progress = base_progress
277 + (completed_operations as f64 / total_operations as f64)
278 * operations_progress_range;
279 progress_callback(progress);
280 }
281 }
282
283 info!("Patch operations applied");
284 Ok(())
285 }
286
287 pub async fn rollback(&mut self) -> Result<(), PatchExecutorError> {
289 if !self.backup_enabled {
290 return Err(PatchExecutorError::BackupNotEnabled);
291 }
292
293 warn!("Starting rollback of patch operations...");
294 self.file_executor.rollback().await?;
295 info!("Patch rollback completed");
296 Ok(())
297 }
298
299 pub fn work_dir(&self) -> &Path {
301 &self.work_dir
302 }
303
304 pub fn is_backup_enabled(&self) -> bool {
306 self.backup_enabled
307 }
308
309 pub fn get_operation_summary(&self, operations: &PatchOperations) -> String {
311 let mut replace_file_count = 0;
312 let mut replace_dir_count = 0;
313 let mut delete_file_count = 0;
314 let mut delete_dir_count = 0;
315 if let Some(replace) = &operations.replace {
316 replace_file_count = replace.files.len();
317 replace_dir_count = replace.directories.len();
318 }
319 if let Some(delete) = &operations.delete {
320 delete_file_count = delete.files.len();
321 delete_dir_count = delete.directories.len();
322 }
323 let total = operations.total_operations();
324 format!(
325 "Patch operation summary: {} total operations (file replacements: {}, directory replacements: {}, file deletions: {}, directory deletions: {})",
326 total, replace_file_count, replace_dir_count, delete_file_count, delete_dir_count
327 )
328 }
329
330 pub fn temp_dir(&self) -> &Path {
332 self.patch_processor.temp_dir()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::api_types::ReplaceOperations;
340 use tempfile::TempDir;
341
342 #[tokio::test]
343 async fn test_patch_executor_creation() {
344 let temp_dir = TempDir::new().unwrap();
345 let executor = PatchExecutor::new(temp_dir.path().to_owned());
346 assert!(executor.is_ok());
347 }
348
349 #[tokio::test]
350 async fn test_enable_backup() {
351 let temp_dir = TempDir::new().unwrap();
352 let mut executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
353
354 assert!(!executor.is_backup_enabled());
355 let result = executor.enable_backup();
356 assert!(result.is_ok());
357 assert!(executor.is_backup_enabled());
358 }
359
360 #[tokio::test]
361 async fn test_validate_preconditions() {
362 let temp_dir = TempDir::new().unwrap();
363 let executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
364
365 let valid_operations = PatchOperations {
367 replace: Some(ReplaceOperations {
368 files: vec!["test.txt".to_string()],
369 directories: vec!["test_dir".to_string()],
370 }),
371 delete: Some(ReplaceOperations {
372 files: vec!["test.txt".to_string()],
373 directories: vec!["test_dir".to_string()],
374 }),
375 };
376
377 let result = executor.validate_preconditions(&valid_operations);
378 assert!(result.is_ok());
379
380 let empty_operations = PatchOperations {
382 replace: Some(ReplaceOperations {
383 files: vec![],
384 directories: vec![],
385 }),
386 delete: Some(ReplaceOperations {
387 files: vec![],
388 directories: vec![],
389 }),
390 };
391
392 let result = executor.validate_preconditions(&empty_operations);
393 assert!(result.is_err());
394 }
395
396 #[tokio::test]
397 async fn test_operation_summary() {
398 let temp_dir = TempDir::new().unwrap();
399 let executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
400
401 let operations = PatchOperations {
402 replace: Some(ReplaceOperations {
403 files: vec!["file1.txt".to_string(), "file2.txt".to_string()],
404 directories: vec!["dir1".to_string()],
405 }),
406 delete: Some(ReplaceOperations {
407 files: vec!["old_file.txt".to_string()],
408 directories: vec![],
409 }),
410 };
411
412 let summary = executor.get_operation_summary(&operations);
413 assert!(summary.contains("4 total operations"));
414 assert!(summary.contains("file replacements: 2"));
415 assert!(summary.contains("directory replacements: 1"));
416 assert!(summary.contains("deletions: 1"));
417 }
418
419 #[tokio::test]
420 async fn test_rollback_without_backup() {
421 let temp_dir = TempDir::new().unwrap();
422 let mut executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
423
424 let result = executor.rollback().await;
426 assert!(result.is_err());
427 assert!(matches!(
428 result.unwrap_err(),
429 PatchExecutorError::BackupNotEnabled
430 ));
431 }
432}