chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# ========= 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)
|
||||
Reference in New Issue
Block a user