bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
193 lines
8.6 KiB
Python
193 lines
8.6 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
from typing import Awaitable, Callable
|
|
|
|
from mirage.cache.index import IndexCacheStore
|
|
from mirage.commands.builtin.utils.copy import (backend_key_default,
|
|
copy_targets, is_directory,
|
|
path_exists)
|
|
from mirage.io.types import ByteSource, IOResult
|
|
from mirage.types import FileType, PathSpec
|
|
|
|
|
|
async def walk(readdir: Callable, stat: Callable, root: str,
|
|
index: IndexCacheStore | None) -> list[tuple[str, bool]]:
|
|
"""List a tree as ``(path, is_dir)`` pairs, parents before children.
|
|
|
|
The dir/file type is captured here, while the tree is intact, so a caller
|
|
that deletes as it goes (mv) never re-stats a path whose virtual parent dir
|
|
has since vanished (e.g. on S3). Used only by the primitive (no native
|
|
``copy``) path; backends that inject ``copy``/``find`` never reach it.
|
|
|
|
Args:
|
|
readdir (Callable): Lists a directory's full child paths.
|
|
stat (Callable): Stats a path; ``.type`` distinguishes directories.
|
|
root (str): Full path of the tree root.
|
|
index (IndexCacheStore | None): Optional cache index for stat/readdir.
|
|
"""
|
|
info = await stat(root, index)
|
|
if info.type != FileType.DIRECTORY:
|
|
return [(root, False)]
|
|
entries = [(root, True)]
|
|
queue = [root]
|
|
while queue:
|
|
directory = queue.pop(0)
|
|
for child in await readdir(directory, index):
|
|
child_info = await stat(child, index)
|
|
is_dir = child_info.type == FileType.DIRECTORY
|
|
entries.append((child, is_dir))
|
|
if is_dir:
|
|
queue.append(child)
|
|
return entries
|
|
|
|
|
|
async def cp(
|
|
paths: list[PathSpec],
|
|
*,
|
|
stat: Callable[..., Awaitable[object]],
|
|
recursive: bool,
|
|
n: bool,
|
|
v: bool,
|
|
copy: Callable[..., Awaitable[None]] | None = None,
|
|
find: Callable[..., Awaitable[list[str]]] | None = None,
|
|
find_type: str = "f",
|
|
read_bytes: Callable[..., Awaitable[bytes]] | None = None,
|
|
write: Callable[..., Awaitable[None]] | None = None,
|
|
mkdir: Callable[..., Awaitable[None]] | None = None,
|
|
readdir: Callable[..., Awaitable[list[str]]] | None = None,
|
|
index: IndexCacheStore | None = None,
|
|
backend_key: Callable[[PathSpec], str] | None = None,
|
|
dir_copy: Callable[..., Awaitable[None]] | None = None,
|
|
) -> tuple[ByteSource | None, IOResult]:
|
|
"""Copy sources to a destination, fanning out into a directory.
|
|
|
|
A backend injects its native ``copy``/``find`` for an efficient same-store
|
|
copy. When ``copy`` is omitted (cross-mount), the primitive path is used
|
|
instead: the tree is walked via ``readdir``/``stat`` and each entry is a
|
|
``mkdir`` (directory) or a ``write`` of ``read_bytes`` (file).
|
|
|
|
Args:
|
|
paths (list[PathSpec]): Source operands followed by the destination.
|
|
stat (Callable): Stats a path; raises when missing.
|
|
recursive (bool): Whether to copy directories recursively.
|
|
n (bool): No-clobber; skip targets that already exist.
|
|
v (bool): Verbose; emit one ``src -> target`` line per write.
|
|
copy (Callable | None): Native single-entry copy; None for primitive.
|
|
find (Callable | None): Native tree lister, with ``copy``.
|
|
find_type (str): File-type selector passed to ``find``.
|
|
read_bytes (Callable | None): Whole-file reader (primitive path).
|
|
write (Callable | None): Byte writer, for the primitive path.
|
|
mkdir (Callable | None): Directory creator, for the primitive path.
|
|
readdir (Callable | None): Directory lister, for the primitive walk.
|
|
index (IndexCacheStore | None): Cache for the destination dir probe.
|
|
backend_key (Callable | None): Maps a path to its backend storage key
|
|
for the same-file and into-own-subtree guards; defaults to the
|
|
normalized mount-relative path.
|
|
|
|
Returns:
|
|
tuple[ByteSource | None, IOResult]: Verbose output and recorded
|
|
writes, with per-source coreutils errors on stderr and exit code 1
|
|
when any source failed.
|
|
"""
|
|
key_of = backend_key if backend_key is not None else backend_key_default
|
|
*sources, dst = paths
|
|
dst_is_dir = await is_directory(stat, dst, index)
|
|
writes: dict[str, bytes] = {}
|
|
reads: dict[str, bytes] = {}
|
|
lines: list[str] = []
|
|
errors: list[str] = []
|
|
for src, target in copy_targets(sources, dst, dst_is_dir):
|
|
if not await path_exists(stat, src):
|
|
errors.append(f"cp: cannot stat '{src.virtual}': "
|
|
"No such file or directory")
|
|
continue
|
|
if key_of(src) == key_of(target):
|
|
errors.append(f"cp: '{src.virtual}' and '{target.virtual}' "
|
|
"are the same file")
|
|
continue
|
|
if recursive and key_of(target).startswith(key_of(src) + "/"):
|
|
errors.append(f"cp: cannot copy a directory, '{src.virtual}', "
|
|
f"into itself, '{target.virtual}'")
|
|
continue
|
|
if not recursive and await is_directory(stat, src, index):
|
|
errors.append("cp: -r not specified; omitting directory "
|
|
f"'{src.virtual}'")
|
|
continue
|
|
if recursive:
|
|
src_base = src.mount_path.rstrip("/")
|
|
dst_base = target.mount_path.rstrip("/")
|
|
if copy is None:
|
|
for entry, is_dir in await walk(readdir, stat, src.virtual,
|
|
index):
|
|
entry_dst = dst_base + entry[len(src_base):]
|
|
if is_dir:
|
|
if not await is_directory(stat, entry_dst, index):
|
|
await mkdir(entry_dst)
|
|
writes[entry_dst] = b""
|
|
if v:
|
|
lines.append(f"'{entry}' -> '{entry_dst}'")
|
|
continue
|
|
if n and await path_exists(stat, entry_dst):
|
|
continue
|
|
data = await read_bytes(entry)
|
|
await write(entry_dst, data=data)
|
|
reads[entry] = data
|
|
writes[entry_dst] = b""
|
|
if v:
|
|
lines.append(f"'{entry}' -> '{entry_dst}'")
|
|
continue
|
|
if dir_copy is not None:
|
|
if n and await path_exists(stat, target):
|
|
continue
|
|
await dir_copy(src, target)
|
|
for entry in await find(src, type=find_type):
|
|
entry_dst = dst_base + entry[len(src_base):]
|
|
writes[entry_dst] = b""
|
|
if v:
|
|
lines.append(f"'{entry}' -> '{entry_dst}'")
|
|
continue
|
|
for entry in await find(src, type=find_type):
|
|
entry_dst = dst_base + entry[len(src_base):]
|
|
if n and await path_exists(stat, entry_dst):
|
|
continue
|
|
await copy(entry, entry_dst)
|
|
writes[entry_dst] = b""
|
|
if v:
|
|
lines.append(f"'{entry}' -> '{entry_dst}'")
|
|
continue
|
|
if n and await path_exists(stat, target):
|
|
continue
|
|
if copy is None:
|
|
# write takes bytes, not a stream: the file is materialized here.
|
|
data = await read_bytes(src)
|
|
await write(target, data=data)
|
|
reads[src.virtual] = data
|
|
else:
|
|
await copy(src, target)
|
|
writes[target.mount_path] = b""
|
|
if v:
|
|
lines.append(f"'{src.virtual}' -> '{target.virtual}'")
|
|
output = "\n".join(lines) + "\n" if lines else None
|
|
stderr = ("\n".join(errors) + "\n").encode() if errors else None
|
|
# Sources that streamed through the client are recorded as reads so
|
|
# apply_io can populate the file cache: a cp is also a full read.
|
|
return output.encode() if output else None, IOResult(
|
|
writes=writes,
|
|
reads=dict(reads),
|
|
cache=list(reads),
|
|
stderr=stderr,
|
|
exit_code=1 if errors else 0,
|
|
)
|