155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
"""
|
|
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
|