refactor: unify download item types and eliminate extension-based branching

Merge AssetDownloadItem, AssetInlineItem into SandboxDownloadItem with
optional 'content' field. All consumers now follow a clean pipeline:
  get items → accessor.resolve_items() → AppAssetService.to_download_items() → download

Key changes:
- SandboxDownloadItem gains content: bytes | None (entities.py)
- ZipSandbox.download_items() handles both inline (base64 heredoc) and
  remote (curl) via a single pipeline — no structural branching
- AssetDownloadService.build_download_script() takes unified list
- CachedContentAccessor.resolve_items() batch-enriches items from DB
  (extension-agnostic, no 'if md' checks needed)
- AppAssetService.to_download_items() converts AssetItem → SandboxDownloadItem
- DraftAppAssetsInitializer, package_and_upload, export_bundle simplified
- file_upload/node.py switched to SandboxDownloadItem
- Deleted AssetDownloadItem and AssetInlineItem classes
This commit is contained in:
Harry
2026-03-10 17:11:41 +08:00
parent 6ac730ec2e
commit 65e89520c0
19 changed files with 492 additions and 214 deletions

View File

@ -1,4 +1,11 @@
from .zip_sandbox import SandboxDownloadItem, SandboxFile, SandboxUploadItem, ZipSandbox
from __future__ import annotations
from typing import TYPE_CHECKING
from .entities import SandboxDownloadItem, SandboxFile, SandboxUploadItem
if TYPE_CHECKING:
from .zip_sandbox import ZipSandbox
__all__ = [
"SandboxDownloadItem",
@ -6,3 +13,11 @@ __all__ = [
"SandboxUploadItem",
"ZipSandbox",
]
def __getattr__(name: str):
if name == "ZipSandbox":
from .zip_sandbox import ZipSandbox
return ZipSandbox
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@ -0,0 +1,39 @@
"""Data classes for ZipSandbox file operations.
Separated from ``zip_sandbox.py`` so that lightweight consumers (tests,
shell-script builders) can import the types without pulling in the full
sandbox provider chain.
"""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(frozen=True)
class SandboxDownloadItem:
"""Unified download/inline item for sandbox file operations.
For remote files, *url* is set and the item is fetched via ``curl``.
For inline content, *content* is set and the bytes are written directly
into the VM via ``upload_file`` — no network round-trip.
"""
path: str
url: str = ""
content: bytes | None = field(default=None, repr=False)
@dataclass(frozen=True)
class SandboxUploadItem:
"""Item for uploading: sandbox path -> URL."""
path: str
url: str
@dataclass(frozen=True)
class SandboxFile:
"""A handle to a file in the sandbox."""
path: str

View File

@ -1,7 +1,8 @@
from __future__ import annotations
import base64
import posixpath
from dataclasses import dataclass
import shlex
from io import BytesIO
from pathlib import PurePosixPath
from types import TracebackType
@ -20,34 +21,12 @@ from core.virtual_environment.__base.virtual_environment import VirtualEnvironme
from services.sandbox.sandbox_provider_service import SandboxProviderService
from .cli_strategy import CliZipStrategy
from .entities import SandboxDownloadItem, SandboxFile, SandboxUploadItem
from .node_strategy import NodeZipStrategy
from .python_strategy import PythonZipStrategy
from .strategy import ZipStrategy
@dataclass(frozen=True)
class SandboxDownloadItem:
"""Item for downloading: URL -> sandbox path."""
url: str
path: str
@dataclass(frozen=True)
class SandboxUploadItem:
"""Item for uploading: sandbox path -> URL."""
path: str
url: str
@dataclass(frozen=True)
class SandboxFile:
"""A handle to a file in the sandbox."""
path: str
class ZipSandbox:
"""A sandbox for archive (zip) operations.
@ -221,6 +200,12 @@ class ZipSandbox:
# ========== Download operations ==========
def download_items(self, items: list[SandboxDownloadItem], *, dest_dir: str = ".") -> list[str]:
"""Download or write items into the sandbox via a single pipeline.
Remote items (with *url*) are fetched via ``curl``. Inline items
(with *content*) are written via ``base64 -d`` heredoc. Both go
through the same pipeline — no branching at the structural level.
"""
if not items:
return []
@ -238,7 +223,10 @@ class ZipSandbox:
out_dir = posixpath.dirname(out_path)
if out_dir not in ("", "."):
p.add(["mkdir", "-p", out_dir], error_message="Failed to create download directory")
p.add(["curl", "-fsSL", item.url, "-o", out_path], error_message="Failed to download file")
p.add(
self.to_download_command(item, out_path),
error_message=f"Failed to write {item.path}",
)
try:
p.execute(timeout=self._DEFAULT_TIMEOUT_SECONDS, raise_on_error=True)
@ -247,6 +235,14 @@ class ZipSandbox:
return out_paths
@staticmethod
def to_download_command(item: SandboxDownloadItem, out_path: str) -> list[str]:
"""Return the shell command to materialise *item* at *out_path*."""
if item.content is not None:
encoded = base64.b64encode(item.content).decode("ascii")
return ["sh", "-c", f"base64 -d <<'_B64_' > {shlex.quote(out_path)}\n{encoded}\n_B64_"]
return ["curl", "-fsSL", item.url, "-o", out_path]
def download_archive(self, archive_url: str, *, path: str = "input.tar.gz") -> str:
path = self._normalize_path(path)