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
274 lines
10 KiB
Python
274 lines
10 KiB
Python
from collections.abc import Awaitable, Callable
|
|
|
|
from mirage.cache.index import IndexCacheStore
|
|
from mirage.commands.builtin.utils.formatting import format_ls_long
|
|
from mirage.commands.builtin.utils.output import (format_optional_records,
|
|
format_records)
|
|
from mirage.io.types import IOResult
|
|
from mirage.types import FileStat, FileType, LsSortBy, PathSpec
|
|
from mirage.utils.errors import fs_strerror
|
|
from mirage.utils.key_prefix import rekey
|
|
from mirage.utils.path import rebase_one
|
|
|
|
|
|
def get_extension(name: str) -> str | None:
|
|
dot = name.rfind(".")
|
|
if dot == -1 or "/" in name[dot:]:
|
|
return None
|
|
return name[dot:]
|
|
|
|
|
|
def format_simple(entries: list[FileStat],
|
|
*,
|
|
classify: bool = False) -> list[str]:
|
|
out: list[str] = []
|
|
for e in entries:
|
|
is_dir = classify and e.type == FileType.DIRECTORY
|
|
out.append(e.name + "/" if is_dir else e.name)
|
|
return out
|
|
|
|
|
|
async def _file_entry(
|
|
path: PathSpec,
|
|
stat: Callable[[PathSpec, IndexCacheStore | None], Awaitable[FileStat]],
|
|
index: IndexCacheStore | None,
|
|
) -> FileStat | None:
|
|
try:
|
|
s = await stat(path, index)
|
|
except (FileNotFoundError, ValueError):
|
|
return None
|
|
if s.type == FileType.DIRECTORY:
|
|
return None
|
|
# GNU ls prints a file operand as given (`ls sub/x.txt` shows
|
|
# sub/x.txt, not x.txt); the row carries the operand spelling.
|
|
return s.model_copy(update={"name": path.raw_path})
|
|
|
|
|
|
async def walk(
|
|
path: PathSpec,
|
|
*,
|
|
readdir: Callable[[PathSpec, IndexCacheStore | None],
|
|
Awaitable[list[str]]],
|
|
stat: Callable[[PathSpec, IndexCacheStore | None], Awaitable[FileStat]],
|
|
all_files: bool = False,
|
|
sort_by: LsSortBy = LsSortBy.NAME,
|
|
reverse: bool = False,
|
|
recursive: bool = False,
|
|
list_dir: bool = False,
|
|
index: IndexCacheStore | None = None,
|
|
) -> tuple[list[FileStat], list[str]]:
|
|
warnings: list[str] = []
|
|
if list_dir:
|
|
try:
|
|
listed = await stat(path, index)
|
|
except (FileNotFoundError, ValueError) as exc:
|
|
detail = fs_strerror(exc) or exc
|
|
warnings.append(f"ls: cannot access '{path.raw_path}': {detail}")
|
|
return [], warnings
|
|
# GNU ls -d prints the operand as given.
|
|
return [listed.model_copy(update={"name": path.raw_path})], warnings
|
|
|
|
try:
|
|
entries = await readdir(path, index)
|
|
except (FileNotFoundError, ValueError, NotADirectoryError) as exc:
|
|
file_entry = await _file_entry(path, stat, index)
|
|
if file_entry is not None:
|
|
return [file_entry], warnings
|
|
warnings.append(
|
|
f"ls: cannot access '{path.raw_path}': {fs_strerror(exc) or exc}")
|
|
return [], warnings
|
|
|
|
if not entries:
|
|
file_entry = await _file_entry(path, stat, index)
|
|
if file_entry is not None:
|
|
return [file_entry], warnings
|
|
|
|
stats: list[FileStat] = []
|
|
for entry in entries:
|
|
entry_spec = PathSpec(virtual=entry,
|
|
directory=entry,
|
|
resolved=False,
|
|
resource_path=rekey(path.virtual,
|
|
path.resource_path, entry))
|
|
try:
|
|
s = await stat(entry_spec, index)
|
|
except (FileNotFoundError, ValueError) as exc:
|
|
warnings.append(
|
|
f"ls: cannot access '{entry}': {fs_strerror(exc) or exc}")
|
|
continue
|
|
if not all_files and s.name.startswith("."):
|
|
continue
|
|
stats.append(s)
|
|
|
|
if sort_by is LsSortBy.TIME:
|
|
stats.sort(key=lambda s: s.modified or "", reverse=not reverse)
|
|
elif sort_by is LsSortBy.SIZE:
|
|
stats.sort(key=lambda s: s.size or 0, reverse=not reverse)
|
|
else:
|
|
stats.sort(key=lambda s: s.name, reverse=reverse)
|
|
|
|
if recursive:
|
|
nested: list[FileStat] = []
|
|
for s in stats:
|
|
nested.append(s)
|
|
if s.type == FileType.DIRECTORY:
|
|
child_path = path.child(s.name)
|
|
child_spec = PathSpec(virtual=child_path,
|
|
directory=child_path,
|
|
resolved=False,
|
|
resource_path=rekey(
|
|
path.virtual, path.resource_path,
|
|
child_path))
|
|
sub, sub_ws = await walk(child_spec,
|
|
readdir=readdir,
|
|
stat=stat,
|
|
all_files=all_files,
|
|
sort_by=sort_by,
|
|
reverse=reverse,
|
|
recursive=True,
|
|
list_dir=False,
|
|
index=index)
|
|
nested.extend(sub)
|
|
warnings.extend(sub_ws)
|
|
stats = nested
|
|
|
|
return stats, warnings
|
|
|
|
|
|
async def walk_grouped(
|
|
path: PathSpec,
|
|
*,
|
|
readdir: Callable[[PathSpec, IndexCacheStore | None],
|
|
Awaitable[list[str]]],
|
|
stat: Callable[[PathSpec, IndexCacheStore | None], Awaitable[FileStat]],
|
|
all_files: bool = False,
|
|
sort_by: LsSortBy = LsSortBy.NAME,
|
|
reverse: bool = False,
|
|
index: IndexCacheStore | None = None,
|
|
) -> tuple[list[tuple[PathSpec, list[FileStat]]], list[str]]:
|
|
"""Recursive walk that returns one (dir, entries) group per directory
|
|
visited, in pre-order. Mirrors GNU `ls -R` output structure.
|
|
"""
|
|
groups: list[tuple[PathSpec, list[FileStat]]] = []
|
|
warnings: list[str] = []
|
|
here, sub_ws = await walk(path,
|
|
readdir=readdir,
|
|
stat=stat,
|
|
all_files=all_files,
|
|
sort_by=sort_by,
|
|
reverse=reverse,
|
|
recursive=False,
|
|
list_dir=False,
|
|
index=index)
|
|
warnings.extend(sub_ws)
|
|
groups.append((path, here))
|
|
for s in here:
|
|
if s.type == FileType.DIRECTORY:
|
|
child_path = path.child(s.name)
|
|
child_spec = PathSpec(virtual=child_path,
|
|
directory=child_path,
|
|
resolved=False,
|
|
resource_path=rekey(path.virtual,
|
|
path.resource_path,
|
|
child_path))
|
|
sub_groups, sub_ws2 = await walk_grouped(child_spec,
|
|
readdir=readdir,
|
|
stat=stat,
|
|
all_files=all_files,
|
|
sort_by=sort_by,
|
|
reverse=reverse,
|
|
index=index)
|
|
groups.extend(sub_groups)
|
|
warnings.extend(sub_ws2)
|
|
return groups, warnings
|
|
|
|
|
|
def _render_group(
|
|
results: list[str],
|
|
entries: list[FileStat],
|
|
*,
|
|
long: bool,
|
|
one_per_line: bool,
|
|
human: bool,
|
|
classify: bool,
|
|
) -> None:
|
|
if long and not one_per_line:
|
|
results.extend(format_ls_long(entries, human=human))
|
|
else:
|
|
results.extend(format_simple(entries, classify=classify))
|
|
|
|
|
|
async def ls(
|
|
paths: list[PathSpec],
|
|
*,
|
|
readdir: Callable[[PathSpec, IndexCacheStore | None],
|
|
Awaitable[list[str]]],
|
|
stat: Callable[[PathSpec, IndexCacheStore | None], Awaitable[FileStat]],
|
|
long: bool = False,
|
|
one_per_line: bool = False,
|
|
all_files: bool = False,
|
|
human: bool = False,
|
|
sort_by: LsSortBy = LsSortBy.NAME,
|
|
reverse: bool = False,
|
|
recursive: bool = False,
|
|
list_dir: bool = False,
|
|
classify: bool = False,
|
|
index: IndexCacheStore | None = None,
|
|
) -> tuple[bytes, IOResult]:
|
|
results: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
if recursive and not list_dir:
|
|
for p_idx, p in enumerate(paths):
|
|
groups, sub_ws = await walk_grouped(p,
|
|
readdir=readdir,
|
|
stat=stat,
|
|
all_files=all_files,
|
|
sort_by=sort_by,
|
|
reverse=reverse,
|
|
index=index)
|
|
warnings.extend(sub_ws)
|
|
for g_idx, (dir_spec, entries) in enumerate(groups):
|
|
if p_idx > 0 or g_idx > 0:
|
|
results.append("")
|
|
header = rebase_one(dir_spec.virtual, p.virtual, p.raw_path)
|
|
results.append(f"{header}:")
|
|
_render_group(results,
|
|
entries,
|
|
long=long,
|
|
one_per_line=one_per_line,
|
|
human=human,
|
|
classify=classify)
|
|
else:
|
|
for p in paths:
|
|
entries, sub_ws = await walk(p,
|
|
readdir=readdir,
|
|
stat=stat,
|
|
all_files=all_files,
|
|
sort_by=sort_by,
|
|
reverse=reverse,
|
|
recursive=False,
|
|
list_dir=list_dir,
|
|
index=index)
|
|
warnings.extend(sub_ws)
|
|
_render_group(results,
|
|
entries,
|
|
long=long,
|
|
one_per_line=one_per_line,
|
|
human=human,
|
|
classify=classify)
|
|
|
|
output = format_records(results)
|
|
stderr = format_optional_records(warnings)
|
|
exit_code = 1 if warnings and not results else 0
|
|
return output, IOResult(stderr=stderr, exit_code=exit_code)
|
|
|
|
|
|
__all__ = [
|
|
"format_simple",
|
|
"get_extension",
|
|
"ls",
|
|
"walk",
|
|
"walk_grouped",
|
|
]
|