85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
# 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}") |