91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
120 lines
5.1 KiB
Python
120 lines
5.1 KiB
Python
"""Files interface — file operations inside the sandbox, backed by a Transport.
|
|
|
|
Wraps the computer-server file_handler commands (read_text, write_text,
|
|
read_bytes, write_bytes, file_exists, directory_exists, list_dir, create_dir,
|
|
delete_file, delete_dir, get_file_size).
|
|
|
|
Adds two helpers on top of the wire commands:
|
|
- ``upload(local_path, remote_path)`` — push a host file into the sandbox.
|
|
- ``download(remote_path, local_path)`` — pull a sandbox file to the host.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Optional, Union
|
|
|
|
from cua_sandbox.transport.base import Transport
|
|
|
|
|
|
@dataclass
|
|
class FileEntry:
|
|
name: str
|
|
path: str
|
|
is_dir: bool
|
|
size: Optional[int] = None
|
|
|
|
|
|
def _unwrap(result: Any, key: str, default: Any = None) -> Any:
|
|
"""computer-server returns dicts like {'success': True, 'content': ...}."""
|
|
if isinstance(result, dict):
|
|
return result.get(key, default)
|
|
return default
|
|
|
|
|
|
class Files:
|
|
"""File and directory operations inside the sandbox.
|
|
|
|
All paths are inside the sandbox unless otherwise noted.
|
|
"""
|
|
|
|
def __init__(self, transport: Transport):
|
|
self._t = transport
|
|
|
|
# ── existence / metadata ─────────────────────────────────────────────
|
|
async def exists(self, path: str) -> bool:
|
|
result = await self._t.send("file_exists", path=path)
|
|
return bool(_unwrap(result, "exists", False))
|
|
|
|
async def is_dir(self, path: str) -> bool:
|
|
result = await self._t.send("directory_exists", path=path)
|
|
return bool(_unwrap(result, "exists", False))
|
|
|
|
async def size(self, path: str) -> int:
|
|
result = await self._t.send("get_file_size", path=path)
|
|
return int(_unwrap(result, "size", 0) or 0)
|
|
|
|
# ── directories ──────────────────────────────────────────────────────
|
|
async def list(self, path: str) -> list[FileEntry]:
|
|
result = await self._t.send("list_dir", path=path)
|
|
raw = _unwrap(result, "files", []) or _unwrap(result, "entries", []) or []
|
|
out: list[FileEntry] = []
|
|
for item in raw:
|
|
if isinstance(item, str):
|
|
out.append(FileEntry(name=item, path=f"{path.rstrip('/')}/{item}", is_dir=False))
|
|
elif isinstance(item, dict):
|
|
out.append(
|
|
FileEntry(
|
|
name=item.get("name", ""),
|
|
path=item.get("path", f"{path.rstrip('/')}/{item.get('name', '')}"),
|
|
is_dir=bool(item.get("is_dir", False)),
|
|
size=item.get("size"),
|
|
)
|
|
)
|
|
return out
|
|
|
|
async def make_dir(self, path: str) -> None:
|
|
await self._t.send("create_dir", path=path)
|
|
|
|
async def remove_dir(self, path: str) -> None:
|
|
await self._t.send("delete_dir", path=path)
|
|
|
|
async def remove(self, path: str) -> None:
|
|
await self._t.send("delete_file", path=path)
|
|
|
|
# ── text I/O ─────────────────────────────────────────────────────────
|
|
async def read_text(self, path: str) -> str:
|
|
result = await self._t.send("read_text", path=path)
|
|
return _unwrap(result, "content", "") or ""
|
|
|
|
async def write_text(self, path: str, content: str) -> None:
|
|
await self._t.send("write_text", path=path, content=content)
|
|
|
|
# ── binary I/O ───────────────────────────────────────────────────────
|
|
async def read_bytes(self, path: str, offset: int = 0, length: Optional[int] = None) -> bytes:
|
|
kwargs: dict[str, Any] = {"path": path, "offset": offset}
|
|
if length is not None:
|
|
kwargs["length"] = length
|
|
result = await self._t.send("read_bytes", **kwargs)
|
|
b64 = _unwrap(result, "content_b64", "") or _unwrap(result, "content", "") or ""
|
|
if not b64:
|
|
return b""
|
|
return base64.b64decode(b64)
|
|
|
|
async def write_bytes(self, path: str, content: bytes) -> None:
|
|
b64 = base64.b64encode(content).decode("ascii")
|
|
await self._t.send("write_bytes", path=path, content_b64=b64)
|
|
|
|
# ── host ↔ sandbox transfer ──────────────────────────────────────────
|
|
async def upload(self, local_path: Union[str, Path], remote_path: str) -> None:
|
|
"""Push a file from the host into the sandbox."""
|
|
data = Path(local_path).read_bytes()
|
|
await self.write_bytes(remote_path, data)
|
|
|
|
async def download(self, remote_path: str, local_path: Union[str, Path]) -> None:
|
|
"""Pull a file from the sandbox down to the host."""
|
|
data = await self.read_bytes(remote_path)
|
|
Path(local_path).write_bytes(data)
|