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!(
133 "Preconditions validated, total {} operations",
134 total_operations
135 );
136 Ok(())
137 }
138
139 async fn execute_patch_pipeline<F>(
141 &mut self,
142 patch_info: &PatchPackageInfo,
143 operations: &PatchOperations,
144 progress_callback: &F,
145 ) -> Result<(), PatchExecutorError>
146 where
147 F: Fn(f64) + Send + Sync,
148 {
149 info!("Downloading patch package...");
151 let patch_path = self.patch_processor.download_patch(patch_info).await?;
152 progress_callback(0.25);
153
154 info!("Verifying patch integrity...");
156 self.patch_processor
157 .verify_patch_integrity(&patch_path, patch_info)
158 .await?;
159 progress_callback(0.35);
160
161 info!("Extracting patch package...");
163 let extracted_path = self.patch_processor.extract_patch(&patch_path).await?;
164 progress_callback(0.45);
165
166 info!("Verifying patch file structure...");
168 self.validate_patch_structure(&extracted_path, operations)
169 .await?;
170 progress_callback(0.5);
171
172 info!("Applying patch operations...");
174 self.apply_patch_operations(&extracted_path, operations, progress_callback)
175 .await?;
176
177 Ok(())
178 }
179
180 async fn validate_patch_structure(
182 &self,
183 extracted_path: &Path,
184 operations: &PatchOperations,
185 ) -> Result<(), PatchExecutorError> {
186 let mut required_files = Vec::new();
188
189 if let Some(replace) = &operations.replace {
191 for file in &replace.files {
192 required_files.push(file.clone());
193 }
194 for dir in &replace.directories {
196 let dir_path = extracted_path.join(dir);
197 if !dir_path.exists() || !dir_path.is_dir() {
198 return Err(PatchExecutorError::verification_failed(format!(
199 "Required directory missing in patch: {dir}"
200 )));
201 }
202 }
203 }
204
205 self.patch_processor
207 .validate_extracted_structure(&required_files)
208 .await?;
209
210 debug!("Patch file structure verified");
211 Ok(())
212 }
213
214 async fn apply_patch_operations<F>(
216 &mut self,
217 extracted_path: &Path,
218 operations: &PatchOperations,
219 progress_callback: &F,
220 ) -> Result<(), PatchExecutorError>
221 where
222 F: Fn(f64) + Send + Sync,
223 {
224 self.file_executor.set_patch_source(extracted_path)?;
226
227 let total_operations = operations.total_operations();
229
230 let mut completed_operations = 0;
231
232 let base_progress = 0.5; let operations_progress_range = 0.5; if let Some(replace) = &operations.replace {
237 if !replace.files.is_empty() {
239 info!("Replacing {} files", &replace.files.len());
240 self.file_executor.replace_files(&replace.files).await?;
241 completed_operations += replace.files.len();
242 let progress = base_progress
243 + (completed_operations as f64 / total_operations as f64)
244 * operations_progress_range;
245 progress_callback(progress);
246 }
247
248 if !replace.directories.is_empty() {
250 info!("Replacing {} directories", &replace.directories.len());
251 self.file_executor
252 .replace_directories(&replace.directories)
253 .await?;
254 completed_operations += replace.directories.len();
255 let progress = base_progress
256 + (completed_operations as f64 / total_operations as f64)
257 * operations_progress_range;
258 progress_callback(progress);
259 }
260 }
261
262 if let Some(delete) = &operations.delete {
264 if !delete.files.is_empty() {
266 info!("Deleting {} items", &delete.files.len());
267 self.file_executor.delete_items(&delete.files).await?;
268 completed_operations += &delete.files.len();
269 let progress = base_progress
270 + (completed_operations as f64 / total_operations as f64)
271 * operations_progress_range;
272 progress_callback(progress);
273 }
274 if !delete.directories.is_empty() {
276 info!("Deleting {} directories", &delete.directories.len());
277 self.file_executor.delete_items(&delete.directories).await?;
278 completed_operations += &delete.directories.len();
279 let progress = base_progress
280 + (completed_operations as f64 / total_operations as f64)
281 * operations_progress_range;
282 progress_callback(progress);
283 }
284 }
285
286 info!("Patch operations applied");
287 Ok(())
288 }
289
290 pub async fn rollback(&mut self) -> Result<(), PatchExecutorError> {
292 if !self.backup_enabled {
293 return Err(PatchExecutorError::BackupNotEnabled);
294 }
295
296 warn!("Starting rollback of patch operations...");
297 self.file_executor.rollback().await?;
298 info!("Patch rollback completed");
299 Ok(())
300 }
301
302 pub fn work_dir(&self) -> &Path {
304 &self.work_dir
305 }
306
307 pub fn is_backup_enabled(&self) -> bool {
309 self.backup_enabled
310 }
311
312 pub fn get_operation_summary(&self, operations: &PatchOperations) -> String {
314 let mut replace_file_count = 0;
315 let mut replace_dir_count = 0;
316 let mut delete_file_count = 0;
317 let mut delete_dir_count = 0;
318 if let Some(replace) = &operations.replace {
319 replace_file_count = replace.files.len();
320 replace_dir_count = replace.directories.len();
321 }
322 if let Some(delete) = &operations.delete {
323 delete_file_count = delete.files.len();
324 delete_dir_count = delete.directories.len();
325 }
326 let total = operations.total_operations();
327 format!(
328 "Patch operation summary: {} total operations (file replacements: {}, directory replacements: {}, file deletions: {}, directory deletions: {})",
329 total, replace_file_count, replace_dir_count, delete_file_count, delete_dir_count
330 )
331 }
332
333 pub fn temp_dir(&self) -> &Path {
335 self.patch_processor.temp_dir()
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::api_types::ReplaceOperations;
343 use tempfile::TempDir;
344
345 #[tokio::test]
346 async fn test_patch_executor_creation() {
347 let temp_dir = TempDir::new().unwrap();
348 let executor = PatchExecutor::new(temp_dir.path().to_owned());
349 assert!(executor.is_ok());
350 }
351
352 #[tokio::test]
353 async fn test_enable_backup() {
354 let temp_dir = TempDir::new().unwrap();
355 let mut executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
356
357 assert!(!executor.is_backup_enabled());
358 let result = executor.enable_backup();
359 assert!(result.is_ok());
360 assert!(executor.is_backup_enabled());
361 }
362
363 #[tokio::test]
364 async fn test_validate_preconditions() {
365 let temp_dir = TempDir::new().unwrap();
366 let executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
367
368 let valid_operations = PatchOperations {
370 replace: Some(ReplaceOperations {
371 files: vec!["test.txt".to_string()],
372 directories: vec!["test_dir".to_string()],
373 }),
374 delete: Some(ReplaceOperations {
375 files: vec!["test.txt".to_string()],
376 directories: vec!["test_dir".to_string()],
377 }),
378 };
379
380 let result = executor.validate_preconditions(&valid_operations);
381 assert!(result.is_ok());
382
383 let empty_operations = PatchOperations {
385 replace: Some(ReplaceOperations {
386 files: vec![],
387 directories: vec![],
388 }),
389 delete: Some(ReplaceOperations {
390 files: vec![],
391 directories: vec![],
392 }),
393 };
394
395 let result = executor.validate_preconditions(&empty_operations);
396 assert!(result.is_err());
397 }
398
399 #[tokio::test]
400 async fn test_operation_summary() {
401 let temp_dir = TempDir::new().unwrap();
402 let executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
403
404 let operations = PatchOperations {
405 replace: Some(ReplaceOperations {
406 files: vec!["file1.txt".to_string(), "file2.txt".to_string()],
407 directories: vec!["dir1".to_string()],
408 }),
409 delete: Some(ReplaceOperations {
410 files: vec!["old_file.txt".to_string()],
411 directories: vec![],
412 }),
413 };
414
415 let summary = executor.get_operation_summary(&operations);
416 assert!(summary.contains("4 total operations"));
417 assert!(summary.contains("file replacements: 2"));
418 assert!(summary.contains("directory replacements: 1"));
419 assert!(summary.contains("deletions: 1"));
420 }
421
422 #[tokio::test]
423 async fn test_rollback_without_backup() {
424 let temp_dir = TempDir::new().unwrap();
425 let mut executor = PatchExecutor::new(temp_dir.path().to_owned()).unwrap();
426
427 let result = executor.rollback().await;
429 assert!(result.is_err());
430 assert!(matches!(
431 result.unwrap_err(),
432 PatchExecutorError::BackupNotEnabled
433 ));
434 }
435}