# BackupApp/backup_app/core/backup_operations.py import zipfile import os from pathlib import Path from typing import List, Tuple, Callable, Optional # Tuple format for file details: (filename: str, size_mb: float, full_path: str) FileDetail = Tuple[str, float, str] class BackupError(Exception): """Custom exception for errors during backup creation.""" pass def create_backup_archive( zip_file_path_str: str, source_root_dir_str: str, included_file_details: List[FileDetail], backup_description: str, progress_callback: Optional[Callable[[float, float, str], None]] = None ) -> None: """ Creates a ZIP archive with the specified files and description. Args: zip_file_path_str: Full path for the output ZIP file. source_root_dir_str: The root directory from which files were sourced. Used to determine the relative path in the archive. included_file_details: A list of tuples (filename, size_mb, full_path) for files to include in the backup. backup_description: A string description to be saved inside the ZIP archive. progress_callback: An optional function to call for progress updates. It receives (processed_size_mb, total_size_mb, arcname). Raises: BackupError: If an error occurs during ZIP creation. ValueError: If inputs are invalid (e.g., no files to backup). """ zip_file_path = Path(zip_file_path_str) source_root_path = Path(source_root_dir_str) if not included_file_details: raise ValueError("No files provided to include in the backup.") if not zip_file_path.parent.exists(): try: zip_file_path.parent.mkdir(parents=True, exist_ok=True) except OSError as e: raise BackupError(f"Failed to create destination directory {zip_file_path.parent}: {e}") total_size_to_zip_mb = sum(size for _, size, _ in included_file_details) processed_size_mb = 0.0 try: with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as backup_zip: # 1. Add files to be backed up for _, size_mb, full_file_path_str in included_file_details: full_file_path = Path(full_file_path_str) # arcname is the path as it will appear inside the ZIP archive arcname = full_file_path.relative_to(source_root_path) backup_zip.write(full_file_path, arcname=arcname) processed_size_mb += size_mb if progress_callback: progress_callback(processed_size_mb, total_size_to_zip_mb, str(arcname)) # 2. Add the description file # Using a distinct filename for the description. description_filename = "____backup_description.txt" if backup_description: # Only add if description is not empty backup_zip.writestr(description_filename, backup_description.encode('utf-8')) print(f"Info: Backup archive created successfully at {zip_file_path}") except FileNotFoundError as e: # Should not happen if files are from scanner raise BackupError(f"File not found during backup: {e}") except PermissionError as e: raise BackupError(f"Permission error during backup: {e}") except zipfile.BadZipFile as e: raise BackupError(f"Error creating zip file (BadZipFile): {e}") except OSError as e: # General OS error (disk full, etc.) raise BackupError(f"OS error during backup: {e}") except Exception as e: # Catch-all for other unexpected errors raise BackupError(f"An unexpected error occurred during backup: {e}")