File size: 3,080 Bytes
b7cdd54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from uuid import uuid4
from zipfile import ZIP_DEFLATED, ZipFile

from .file_service import (
    SYSTEM_FILENAMES,
    build_file_tree_from_paths,
    build_required_docs,
    ensure_within_directory,
    sanitize_relative_path,
    slugify,
)


def create_project_zip(preview: dict[str, Any], generated_dir: Path) -> dict[str, str]:
    generated_dir.mkdir(parents=True, exist_ok=True)

    project_name = str(preview.get("projectName") or "Generated Project")
    slug = slugify(project_name)
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
    unique_suffix = uuid4().hex[:8]
    bundle_name = f"{slug}-{timestamp}-{unique_suffix}"

    project_dir = generated_dir / bundle_name
    project_dir.mkdir(parents=True, exist_ok=False)

    _write_project_source_files(project_dir, preview)
    bundle_info: dict[str, Any] = {}

    actual_paths = [
        path.relative_to(project_dir).as_posix()
        for path in sorted(project_dir.rglob("*"))
        if path.is_file()
    ]
    bundle_info["actualFileTree"] = build_file_tree_from_paths(actual_paths)

    required_docs = build_required_docs(preview, bundle_info)
    full_paths = sorted(set(actual_paths + list(required_docs.keys())))
    bundle_info["actualFileTree"] = build_file_tree_from_paths(full_paths)
    required_docs = build_required_docs(preview, bundle_info)
    for doc_name, content in required_docs.items():
        target_path = project_dir / doc_name
        _write_text_file(project_dir, target_path, content)

    zip_path = generated_dir / f"{bundle_name}.zip"
    with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as zip_file:
        for file_path in sorted(project_dir.rglob("*")):
            if file_path.is_file():
                arcname = Path(bundle_name) / file_path.relative_to(project_dir)
                zip_file.write(file_path, arcname=arcname)

    return {
        "filename": zip_path.name,
        "downloadUrl": f"/downloads/{zip_path.name}",
    }


def _write_project_source_files(project_dir: Path, preview: dict[str, Any]) -> None:
    written_paths: set[Path] = set()
    for file_entry in preview.get("files", []):
        relative_path = sanitize_relative_path(str(file_entry.get("path", "")))
        if relative_path in written_paths or (
            relative_path.name in SYSTEM_FILENAMES and relative_path.parent == Path(".")
        ):
            continue

        content = str(file_entry.get("content", ""))
        target_path = project_dir / relative_path
        _write_text_file(project_dir, target_path, content)
        written_paths.add(relative_path)


def _write_text_file(project_dir: Path, target_path: Path, content: str) -> None:
    target_path.parent.mkdir(parents=True, exist_ok=True)
    if not ensure_within_directory(project_dir, target_path):
        raise ValueError(f"Refusing to write outside the generated project folder: {target_path}")
    target_path.write_text(content, encoding="utf-8")