""" Project serialization to/from ZIP files for pyPhotoAlbum """ import os import json import zipfile import shutil import tempfile from typing import Optional, Tuple from pathlib import Path from pyPhotoAlbum.project import Project # Version for serialization format - increment when making breaking changes SERIALIZATION_VERSION = "1.0" def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]: """ Save a project to a ZIP file, including all assets. Args: project: The Project instance to save zip_path: Path where the ZIP file should be created Returns: Tuple of (success: bool, error_message: Optional[str]) """ try: # Ensure .ppz extension if not zip_path.lower().endswith('.ppz'): zip_path += '.ppz' # Serialize project to dictionary project_data = project.serialize() # Add version information project_data['serialization_version'] = SERIALIZATION_VERSION # Create ZIP file with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: # Write project.json project_json = json.dumps(project_data, indent=2) zipf.writestr('project.json', project_json) # Add all files from the assets folder assets_folder = project.asset_manager.assets_folder if os.path.exists(assets_folder): for root, dirs, files in os.walk(assets_folder): for file in files: file_path = os.path.join(root, file) # Store with relative path from project folder arcname = os.path.relpath(file_path, project.folder_path) zipf.write(file_path, arcname) print(f"Project saved to {zip_path}") return True, None except Exception as e: error_msg = f"Error saving project: {str(e)}" print(error_msg) return False, error_msg def load_from_zip(zip_path: str, extract_to: Optional[str] = None) -> Tuple[Optional[Project], Optional[str]]: """ Load a project from a ZIP file. Args: zip_path: Path to the ZIP file to load extract_to: Optional directory to extract to. If None, uses a directory based on the ZIP filename in ./projects/ Returns: Tuple of (project: Optional[Project], error_message: Optional[str]) """ try: if not os.path.exists(zip_path): return None, f"ZIP file not found: {zip_path}" # Determine extraction directory if extract_to is None: # Extract to ./projects/{zipname}/ zip_basename = os.path.splitext(os.path.basename(zip_path))[0] extract_to = os.path.join("./projects", zip_basename) # Create extraction directory os.makedirs(extract_to, exist_ok=True) # Extract ZIP contents with zipfile.ZipFile(zip_path, 'r') as zipf: zipf.extractall(extract_to) # Load project.json project_json_path = os.path.join(extract_to, 'project.json') if not os.path.exists(project_json_path): return None, "Invalid project file: project.json not found" with open(project_json_path, 'r') as f: project_data = json.load(f) # Check version compatibility version = project_data.get('serialization_version', '1.0') if version != SERIALIZATION_VERSION: print(f"Warning: Loading project with version {version}, current version is {SERIALIZATION_VERSION}") # Create new project project_name = project_data.get('name', 'Untitled Project') project = Project(name=project_name, folder_path=extract_to) # Deserialize project data project.deserialize(project_data) # Update folder path to extraction location project.folder_path = extract_to project.asset_manager.project_folder = extract_to project.asset_manager.assets_folder = os.path.join(extract_to, "assets") print(f"Project loaded from {zip_path} to {extract_to}") return project, None except Exception as e: error_msg = f"Error loading project: {str(e)}" print(error_msg) return None, error_msg def get_project_info(zip_path: str) -> Optional[dict]: """ Get basic information about a project without fully loading it. Args: zip_path: Path to the ZIP file Returns: Dictionary with project info, or None if error """ try: with zipfile.ZipFile(zip_path, 'r') as zipf: # Read project.json project_json = zipf.read('project.json').decode('utf-8') project_data = json.loads(project_json) return { 'name': project_data.get('name', 'Unknown'), 'version': project_data.get('serialization_version', 'Unknown'), 'page_count': len(project_data.get('pages', [])), 'page_size_mm': project_data.get('page_size_mm', (0, 0)), 'working_dpi': project_data.get('working_dpi', 300), } except Exception as e: print(f"Error reading project info: {e}") return None