Spaces:
Running
Running
File size: 2,881 Bytes
d3a26e1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | """Notebook export helpers.
Spec references:
- `specs/04_interfaces.md`: implements `export_notebook_zip()`.
- `specs/07_security.md`: export remains scoped to one user's notebook root.
- `specs/10_test_plan.md`: export logic is explicit and unit-testable.
"""
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import zipfile
from notebooklm_clone.notebooks import get_notebook
from notebooklm_clone.storage import notebook_root, safe_join, user_root
class ExportError(Exception):
"""Base exception for notebook export failures."""
class ExportIOError(ExportError):
"""Raised when notebook export files cannot be created."""
def _utc_timestamp() -> str:
"""Return a timestamp suitable for export filenames."""
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
def _zip_name(notebook_id: str) -> str:
"""Build a deterministic export filename for a notebook."""
return f"{notebook_id}_{_utc_timestamp()}.zip"
def _should_exclude(path: Path) -> bool:
"""Return whether a file should be excluded as a transient artifact.
Spec references:
- User requirement: exclude large transient files if necessary.
"""
return path.name.endswith(".lock") or path.name.endswith(".sqlite-wal") or path.name.endswith(
".sqlite-shm"
)
def export_notebook_zip(username: str, notebook_id: str) -> Path:
"""Zip one notebook directory and return the archive path.
Spec references:
- `specs/04_interfaces.md`: implements `export_notebook_zip()`.
- `specs/07_security.md`: keeps export paths within the user's storage root.
Raises:
ExportIOError: If the notebook archive cannot be created.
"""
# Verifies notebook ownership and existence before export.
get_notebook(username, notebook_id)
source_root: Path = notebook_root(username, notebook_id)
destination_root: Path = user_root(username)
zip_path: Path = safe_join(destination_root, _zip_name(notebook_id))
try:
with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
for file_path in sorted(source_root.rglob("*")):
if not file_path.is_file():
continue
if _should_exclude(file_path):
continue
archive_name: Path = file_path.relative_to(source_root)
archive.write(file_path, arcname=str(archive_name))
except OSError as exc:
raise ExportIOError(f"Failed to create notebook export archive: {zip_path}") from exc
except ValueError as exc:
raise ExportIOError(f"Failed to package notebook export archive: {zip_path}") from exc
except zipfile.BadZipFile as exc:
raise ExportIOError(f"Failed to finalize notebook export archive: {zip_path}") from exc
return zip_path
|