Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

261 lines
9.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. =========
import dataclasses
import time
from collections.abc import Callable
from mirage.io import IOResult
from mirage.io.types import ByteSource
from mirage.types import FileType, PathSpec, word_text
from mirage.utils.path import CycleError, resolve_path
from mirage.workspace.mount.namespace import Namespace
from mirage.workspace.session import Session
from mirage.workspace.types import ExecutionNode
def _split_flags(
args: list[str | PathSpec],
known: str,
) -> tuple[set[str], list[str | PathSpec]]:
flags: set[str] = set()
operands: list[str | PathSpec] = []
parsing = True
for arg in args:
s = arg.virtual if isinstance(arg, PathSpec) else str(arg)
if parsing and s == "--":
parsing = False
continue
if (parsing and s != "-" and len(s) >= 2 and s.startswith("-")
and all(c in known for c in s[1:])):
flags.update(s[1:])
continue
parsing = False
operands.append(arg)
return flags, operands
def link_flags(args: list[str | PathSpec], known: str) -> set[str]:
flags, _ = _split_flags(args, known)
return flags
def _abs(arg: str | PathSpec, cwd: str) -> str:
if isinstance(arg, PathSpec):
return arg.virtual
return resolve_path(arg, cwd)
def handle_ln(
namespace: Namespace,
session: Session,
args: list[str | PathSpec],
) -> tuple[ByteSource | None, IOResult, ExecutionNode]:
flags, operands = _split_flags(args, "sfnv")
if len(operands) < 2:
err = b"ln: missing file operand\n"
return None, IOResult(exit_code=1,
stderr=err), ExecutionNode(command="ln",
exit_code=1,
stderr=err)
# GNU: with more than two operands the last must be a directory;
# namespace links never name directories, so this is always an error
# (an expanded multi-match glob source lands here).
if len(operands) > 2:
err = (f"ln: target '{word_text(operands[-1])}' "
f"is not a directory\n").encode()
return None, IOResult(exit_code=1,
stderr=err), ExecutionNode(command="ln",
exit_code=1,
stderr=err)
link_abs = _abs(operands[1], session.cwd)
target_typed = word_text(operands[0])
exists = namespace.is_link(link_abs) and "f" not in flags
if namespace.is_mount_root(link_abs) or exists:
err = (f"ln: failed to create symbolic link "
f"'{word_text(operands[1])}': File exists\n").encode()
return None, IOResult(exit_code=1,
stderr=err), ExecutionNode(command="ln",
exit_code=1,
stderr=err)
namespace.symlink(link_abs, target_typed, time.time())
out = None
if "v" in flags:
out = (f"'{word_text(operands[1])}' -> '{target_typed}'\n").encode()
return out, IOResult(), ExecutionNode(command="ln", exit_code=0)
def follow_paths(
namespace: Namespace,
items: list[str | PathSpec],
) -> list[str | PathSpec]:
"""Rewrite path operands through the symlink table (open(2) semantics).
Non-path items and paths that resolve to themselves pass through
untouched. A rewritten spec keeps the user-typed form in ``raw_path``
so error messages still name the operand as typed; the mount re-stamps
``resource_path`` at dispatch.
Args:
namespace (Namespace): addressing authority holding the link table.
items (list[str | PathSpec]): classified command parts.
Raises:
CycleError: when a path loops past the hop limit (ELOOP).
"""
out: list[str | PathSpec] = []
for item in items:
if not isinstance(item, PathSpec):
out.append(item)
continue
try:
virtual = namespace.follow(item.virtual)
except CycleError:
raise CycleError(item.raw_path) from None
if virtual == item.virtual:
out.append(item)
continue
out.append(
dataclasses.replace(item,
virtual=virtual,
directory=virtual[:virtual.rfind("/") + 1]
or "/",
resource_path=""))
return out
def strip_link_operands(
namespace: Namespace,
items: list[str | PathSpec],
) -> tuple[list[str | PathSpec], int]:
"""Unlink and drop ``rm`` operands that are symlinks.
GNU ``rm`` removes the link itself and never follows it; a dangling
link removes fine. Remaining operands stay for backend dispatch.
Args:
namespace (Namespace): addressing authority holding the link table.
items (list[str | PathSpec]): classified command parts.
Returns:
tuple[list[str | PathSpec], int]: surviving parts and the number
of link entries removed.
"""
removed = 0
kept: list[str | PathSpec] = []
for item in items:
if isinstance(item, PathSpec) and namespace.is_link(item.virtual):
namespace.unlink(item.virtual)
removed += 1
continue
kept.append(item)
return kept, removed
async def prepare_mv(
namespace: Namespace,
dispatch: Callable,
items: list[str | PathSpec],
) -> tuple[list[str | PathSpec], str | None, tuple[ByteSource | None, IOResult,
ExecutionNode] | None]:
"""Adjust a two-operand ``mv`` for symlink operands.
A link source renames the link entry itself (into a destination
directory when one exists, mirroring rename(2) preceded by mv's dst
stat). A link destination whose target is a directory is followed
(mv moves into it); any other link destination is replaced, so its
entry must drop once the backend move succeeds.
Args:
namespace (Namespace): addressing authority holding the link table.
dispatch (Callable): op dispatcher used to stat the destination.
items (list[str | PathSpec]): classified command parts.
Returns:
tuple: (possibly rewritten parts, link path to unlink after a
successful backend move, early result when the mv completed as a
pure namespace rename).
"""
paths = [p for p in items if isinstance(p, PathSpec)]
if len(paths) != 2:
return items, None, None
src, dst = paths
if namespace.is_link(src.virtual):
target_dst = dst.virtual
stat = await _stat_or_none(dispatch, dst)
if stat is not None and stat.type == FileType.DIRECTORY:
target_dst = (dst.virtual.rstrip("/") + "/" +
src.virtual.rsplit("/", 1)[-1])
namespace.unlink(target_dst)
namespace.rename(src.virtual, target_dst)
return items, None, (None, IOResult(),
ExecutionNode(command="mv", exit_code=0))
if namespace.is_link(dst.virtual):
followed = namespace.follow(dst.virtual)
stat = await _stat_or_none(dispatch, PathSpec.from_str_path(followed))
if stat is not None and stat.type == FileType.DIRECTORY:
return follow_paths(namespace, items), None, None
return items, dst.virtual, None
return items, None, None
async def _stat_or_none(dispatch: Callable, path: PathSpec):
"""Stat a path via dispatch, mapping a missing file to ``None``.
Args:
dispatch (Callable): op dispatcher.
path (PathSpec): path to stat.
"""
# A missing destination is an expected mv case (plain rename), not an
# error to surface.
try:
stat, _ = await dispatch("stat", path)
except FileNotFoundError:
return None
return stat
def handle_readlink(
namespace: Namespace,
session: Session,
args: list[str | PathSpec],
) -> tuple[ByteSource | None, IOResult, ExecutionNode]:
flags, operands = _split_flags(args, "fenm")
if not operands:
err = b"readlink: missing operand\n"
return None, IOResult(exit_code=1,
stderr=err), ExecutionNode(command="readlink",
exit_code=1,
stderr=err)
lines: list[str] = []
exit_code = 0
for op in operands:
target = namespace.readlink(_abs(op, session.cwd))
if target is None:
exit_code = 1
continue
lines.append(target)
if not lines:
return None, IOResult(exit_code=exit_code), ExecutionNode(
command="readlink", exit_code=exit_code)
if "n" in flags:
text = "".join(lines)
else:
text = "".join(line + "\n" for line in lines)
return text.encode(), IOResult(exit_code=exit_code), ExecutionNode(
command="readlink", exit_code=exit_code)