agpm_cli/upgrade/backup.rs
1use anyhow::{Context, Result, bail};
2use std::path::{Path, PathBuf};
3use tokio::fs;
4use tracing::{debug, info, warn};
5
6/// Manages backup and restoration of AGPM binaries during upgrades.
7///
8/// `BackupManager` provides comprehensive backup functionality to protect against
9/// failed upgrades and enable rollback capabilities. It creates backups of the
10/// current binary before upgrades and can restore them if needed.
11///
12/// # Safety Features
13///
14/// - **Automatic Backup Creation**: Creates backups before any binary modification
15/// - **Permission Preservation**: Maintains file permissions and metadata on Unix systems
16/// - **Atomic Operations**: Uses file copy operations for reliability
17/// - **Retry Logic**: Handles Windows file locking issues with automatic retries
18/// - **Rollback Support**: Enables quick restoration from backups
19///
20/// # Backup Strategy
21///
22/// The backup manager creates a copy of the original binary with a `.backup` suffix
23/// in the same directory. This approach:
24/// - Keeps backups close to the original for easy access
25/// - Preserves the same file system and permissions context
26/// - Allows for quick restoration without complex path management
27/// - Works consistently across different installation methods
28///
29/// # Cross-Platform Considerations
30///
31/// ## Unix Systems (Linux, macOS)
32/// - Preserves executable permissions and ownership
33/// - Uses standard file copy operations
34/// - Handles symbolic links appropriately
35///
36/// ## Windows
37/// - Implements retry logic for file locking issues
38/// - Handles executable files that might be in use
39/// - Works with Windows permission models
40///
41/// # Examples
42///
43/// ## Basic Backup and Restore
44/// ```rust,no_run
45/// use agpm_cli::upgrade::backup::BackupManager;
46/// use std::path::PathBuf;
47///
48/// # async fn example() -> anyhow::Result<()> {
49/// let exe_path = PathBuf::from("/usr/local/bin/agpm");
50/// let backup_manager = BackupManager::new(exe_path);
51///
52/// // Create backup before upgrade
53/// backup_manager.create_backup().await?;
54///
55/// // ... perform upgrade ...
56///
57/// // Restore if upgrade failed
58/// let upgrade_failed = false; // Set based on upgrade result
59/// if upgrade_failed {
60/// backup_manager.restore_backup().await?;
61/// } else {
62/// backup_manager.cleanup_backup().await?;
63/// }
64/// # Ok(())
65/// # }
66/// ```
67///
68/// ## Check for Existing Backup
69/// ```rust,no_run
70/// use agpm_cli::upgrade::backup::BackupManager;
71/// use std::path::PathBuf;
72///
73/// let backup_manager = BackupManager::new(PathBuf::from("agpm"));
74///
75/// if backup_manager.backup_exists() {
76/// println!("Backup found at: {}", backup_manager.backup_path().display());
77/// }
78/// ```
79///
80/// # Error Handling
81///
82/// All operations return `Result<T, anyhow::Error>` with detailed error context:
83/// - Permission errors when unable to read/write files
84/// - File system errors during copy operations
85/// - Platform-specific issues (Windows file locking, Unix permissions)
86///
87/// # Implementation Details
88///
89/// - Uses `tokio::fs` for async file operations
90/// - Implements platform-specific permission handling
91/// - Provides detailed logging for debugging and monitoring
92/// - Handles edge cases like missing files and permission issues
93pub struct BackupManager {
94 /// Path to the original binary file.
95 original_path: PathBuf,
96 /// Path where the backup will be stored.
97 backup_path: PathBuf,
98}
99
100impl BackupManager {
101 /// Create a new `BackupManager` for the specified executable.
102 ///
103 /// Automatically determines the backup file path by appending `.backup`
104 /// to the original executable name in the same directory.
105 ///
106 /// # Arguments
107 ///
108 /// * `executable_path` - Full path to the executable binary to manage
109 ///
110 /// # Examples
111 ///
112 /// ```rust,no_run
113 /// use agpm_cli::upgrade::backup::BackupManager;
114 /// use std::path::PathBuf;
115 ///
116 /// // Unix-style path
117 /// let manager = BackupManager::new(PathBuf::from("/usr/local/bin/agpm"));
118 /// // Backup will be at /usr/local/bin/agpm.backup
119 ///
120 /// // Windows-style path
121 /// let manager = BackupManager::new(PathBuf::from(r"C:\Program Files\agpm\agpm.exe"));
122 /// // Backup will be at C:\Program Files\agpm\agpm.exe.backup
123 /// ```
124 pub fn new(executable_path: PathBuf) -> Self {
125 let mut backup_path = executable_path.clone();
126 backup_path.set_file_name(format!(
127 "{}.backup",
128 executable_path.file_name().unwrap_or_default().to_string_lossy()
129 ));
130
131 Self {
132 original_path: executable_path,
133 backup_path,
134 }
135 }
136
137 /// Create a backup of the original binary.
138 ///
139 /// Copies the current binary to the backup location, preserving permissions
140 /// and metadata. If a backup already exists, it will be replaced.
141 ///
142 /// # Process
143 ///
144 /// 1. Validate that the original file exists
145 /// 2. Remove any existing backup file
146 /// 3. Copy the original file to the backup location
147 /// 4. Preserve file permissions on Unix systems
148 ///
149 /// # Returns
150 ///
151 /// - `Ok(())` - Backup created successfully
152 /// - `Err(error)` - Backup creation failed
153 ///
154 /// # Errors
155 ///
156 /// This method can fail if:
157 /// - The original file doesn't exist or is not readable
158 /// - Insufficient permissions to create the backup file
159 /// - File system errors during the copy operation
160 /// - Unable to set permissions on the backup file (Unix)
161 ///
162 /// # Examples
163 ///
164 /// ```rust,no_run
165 /// use agpm_cli::upgrade::backup::BackupManager;
166 /// use std::path::PathBuf;
167 ///
168 /// # async fn example() -> anyhow::Result<()> {
169 /// let manager = BackupManager::new(PathBuf::from("./agpm"));
170 ///
171 /// match manager.create_backup().await {
172 /// Ok(()) => println!("Backup created successfully"),
173 /// Err(e) => eprintln!("Failed to create backup: {}", e),
174 /// }
175 /// # Ok(())
176 /// # }
177 /// ```
178 pub async fn create_backup(&self) -> Result<()> {
179 if !self.original_path.exists() {
180 bail!("Original file does not exist: {:?}", self.original_path);
181 }
182
183 // Remove old backup if it exists
184 if self.backup_path.exists() {
185 debug!("Removing old backup at {:?}", self.backup_path);
186 fs::remove_file(&self.backup_path).await.context("Failed to remove old backup")?;
187 }
188
189 // Copy current binary to backup location
190 info!("Creating backup at {:?}", self.backup_path);
191 fs::copy(&self.original_path, &self.backup_path)
192 .await
193 .context("Failed to create backup")?;
194
195 // Preserve permissions on Unix
196 #[cfg(unix)]
197 {
198 let metadata = fs::metadata(&self.original_path)
199 .await
200 .context("Failed to read original file metadata")?;
201 let permissions = metadata.permissions();
202 fs::set_permissions(&self.backup_path, permissions)
203 .await
204 .context("Failed to set backup permissions")?;
205 }
206
207 info!("Backup created successfully");
208 Ok(())
209 }
210
211 /// Restore the original binary from backup.
212 ///
213 /// Replaces the current binary with the backup copy, effectively rolling
214 /// back to the previous version. This operation includes retry logic for
215 /// Windows systems where the binary might be locked.
216 ///
217 /// # Process
218 ///
219 /// 1. Validate that a backup file exists
220 /// 2. Remove the current (potentially corrupted) binary
221 /// 3. Copy the backup file back to the original location
222 /// 4. Restore file permissions on Unix systems
223 /// 5. Retry up to 3 times on Windows for file locking issues
224 ///
225 /// # Returns
226 ///
227 /// - `Ok(())` - Backup restored successfully
228 /// - `Err(error)` - Restoration failed after all retries
229 ///
230 /// # Errors
231 ///
232 /// This method can fail if:
233 /// - No backup file exists at the expected location
234 /// - Insufficient permissions to replace the original file
235 /// - File locking issues prevent replacement (Windows)
236 /// - File system errors during the copy operation
237 /// - Unable to restore permissions (Unix)
238 ///
239 /// # Platform Behavior
240 ///
241 /// ## Windows
242 /// - Implements retry logic with 1-second delays
243 /// - Handles file locking from running processes
244 /// - Attempts up to 3 times before giving up
245 ///
246 /// ## Unix
247 /// - Preserves executable permissions and ownership
248 /// - Single attempt (usually succeeds immediately)
249 ///
250 /// # Examples
251 ///
252 /// ```rust,no_run
253 /// use agpm_cli::upgrade::backup::BackupManager;
254 /// use std::path::PathBuf;
255 ///
256 /// # async fn example() -> anyhow::Result<()> {
257 /// let manager = BackupManager::new(PathBuf::from("./agpm"));
258 ///
259 /// if manager.backup_exists() {
260 /// match manager.restore_backup().await {
261 /// Ok(()) => println!("Successfully restored from backup"),
262 /// Err(e) => eprintln!("Failed to restore backup: {}", e),
263 /// }
264 /// } else {
265 /// eprintln!("No backup found to restore");
266 /// }
267 /// # Ok(())
268 /// # }
269 /// ```
270 pub async fn restore_backup(&self) -> Result<()> {
271 if !self.backup_path.exists() {
272 bail!("No backup found at {:?}", self.backup_path);
273 }
274
275 warn!("Restoring from backup at {:?}", self.backup_path);
276
277 // On Windows, we might need to retry if the file is in use
278 let mut attempts = 0;
279 const MAX_ATTEMPTS: u32 = 3;
280
281 while attempts < MAX_ATTEMPTS {
282 match self.attempt_restore().await {
283 Ok(()) => {
284 info!("Successfully restored from backup");
285 return Ok(());
286 }
287 Err(e) if attempts < MAX_ATTEMPTS - 1 => {
288 warn!("Restore attempt {} failed: {}. Retrying...", attempts + 1, e);
289 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
290 attempts += 1;
291 }
292 Err(e) => return Err(e),
293 }
294 }
295
296 bail!("Failed to restore backup after {MAX_ATTEMPTS} attempts")
297 }
298
299 /// Attempt a single restoration operation.
300 ///
301 /// This is an internal method used by [`restore_backup()`](Self::restore_backup)
302 /// to handle the actual file operations. It's separated to enable retry logic
303 /// for Windows file locking issues.
304 ///
305 /// # Returns
306 ///
307 /// - `Ok(())` - Single restoration attempt succeeded
308 /// - `Err(error)` - Restoration attempt failed
309 ///
310 /// # Process
311 ///
312 /// 1. Remove the current binary file if it exists
313 /// 2. Copy the backup file to the original location
314 /// 3. Restore file permissions on Unix systems
315 async fn attempt_restore(&self) -> Result<()> {
316 // Remove the potentially corrupted binary
317 if self.original_path.exists() {
318 fs::remove_file(&self.original_path)
319 .await
320 .context("Failed to remove corrupted binary")?;
321 }
322
323 // Copy backup back to original location
324 fs::copy(&self.backup_path, &self.original_path)
325 .await
326 .context("Failed to restore backup")?;
327
328 // Restore permissions on Unix
329 #[cfg(unix)]
330 {
331 let metadata =
332 fs::metadata(&self.backup_path).await.context("Failed to read backup metadata")?;
333 let permissions = metadata.permissions();
334 fs::set_permissions(&self.original_path, permissions)
335 .await
336 .context("Failed to restore permissions")?;
337 }
338
339 Ok(())
340 }
341
342 /// Remove the backup file after a successful upgrade.
343 ///
344 /// Cleans up the backup file once it's no longer needed, typically after
345 /// a successful upgrade has been completed and verified.
346 ///
347 /// # Returns
348 ///
349 /// - `Ok(())` - Backup cleaned up successfully or no backup existed
350 /// - `Err(error)` - Failed to remove the backup file
351 ///
352 /// # Errors
353 ///
354 /// This method can fail if:
355 /// - Insufficient permissions to delete the backup file
356 /// - File system errors during deletion
357 /// - File is locked or in use (rare on most systems)
358 ///
359 /// # Examples
360 ///
361 /// ```rust,no_run
362 /// use agpm_cli::upgrade::backup::BackupManager;
363 /// use std::path::PathBuf;
364 ///
365 /// # async fn example() -> anyhow::Result<()> {
366 /// let manager = BackupManager::new(PathBuf::from("./agpm"));
367 ///
368 /// // After successful upgrade
369 /// manager.cleanup_backup().await?;
370 /// println!("Backup cleaned up");
371 /// # Ok(())
372 /// # }
373 /// ```
374 ///
375 /// # Note
376 ///
377 /// This method silently succeeds if no backup file exists, making it safe
378 /// to call unconditionally after upgrades.
379 pub async fn cleanup_backup(&self) -> Result<()> {
380 if self.backup_path.exists() {
381 debug!("Cleaning up backup at {:?}", self.backup_path);
382 fs::remove_file(&self.backup_path).await.context("Failed to remove backup")?;
383 }
384 Ok(())
385 }
386
387 /// Check if a backup file currently exists.
388 ///
389 /// This is a synchronous check that verifies whether a backup file is
390 /// present at the expected location.
391 ///
392 /// # Returns
393 ///
394 /// - `true` - A backup file exists and can potentially be restored
395 /// - `false` - No backup file found at the expected location
396 ///
397 /// # Examples
398 ///
399 /// ```rust,no_run
400 /// use agpm_cli::upgrade::backup::BackupManager;
401 /// use std::path::PathBuf;
402 ///
403 /// let manager = BackupManager::new(PathBuf::from("./agpm"));
404 ///
405 /// if manager.backup_exists() {
406 /// println!("Backup available for rollback");
407 /// } else {
408 /// println!("No backup found");
409 /// }
410 /// ```
411 ///
412 /// # Note
413 ///
414 /// This method only checks for file existence, not validity or integrity
415 /// of the backup file. Use [`restore_backup()`](Self::restore_backup) to
416 /// verify the backup can actually be used.
417 pub fn backup_exists(&self) -> bool {
418 self.backup_path.exists()
419 }
420
421 /// Get the path where the backup file is stored.
422 ///
423 /// Returns the full path to the backup file location, which is useful
424 /// for logging, debugging, or manual backup management.
425 ///
426 /// # Returns
427 ///
428 /// A path reference to the backup file location.
429 ///
430 /// # Examples
431 ///
432 /// ```rust,no_run
433 /// use agpm_cli::upgrade::backup::BackupManager;
434 /// use std::path::PathBuf;
435 ///
436 /// let manager = BackupManager::new(PathBuf::from("/usr/local/bin/agpm"));
437 /// println!("Backup location: {}", manager.backup_path().display());
438 /// // Output: Backup location: /usr/local/bin/agpm.backup
439 /// ```
440 ///
441 /// # Use Cases
442 ///
443 /// - **Logging**: Include backup location in log messages
444 /// - **Debugging**: Help users locate backup files manually
445 /// - **Error Messages**: Show backup location when operations fail
446 /// - **Manual Recovery**: Allow users to manually restore backups
447 pub fn backup_path(&self) -> &Path {
448 &self.backup_path
449 }
450}