Files
mdrs-client-python/mdrsclient/commands/ls.py
T
orrisroot a2bc9bb642 feat: build access level labels from their bits and project names
Labels came from a fixed table matched by subset, so Storage (no bits)
matched any level the table lacked, and a level was named after the first
entry it contained: the server's Brain/MINDS 2.0 folders (0x10000) showed
as Storage and 0x10104 as CBS or PW Open.

Labels are now composed from the bits. Projects (bit 16 and up) take the
names the server reports to their members through users/current/, and
otherwise Project<n>; restrict slot 1 is CBS and any other Restrict<n>.
A level with no meaning is shown as Unknown (0x...). chacl reads keys
built the same way, refuses public and storage before any request, and
lists the user's projects with their keys when given an invalid level.
2026-11-13 16:53:25 +09:00

239 lines
10 KiB
Python

import json
from argparse import Namespace
from dataclasses import field
from typing import Any, Final
from pydantic.dataclasses import dataclass
from mdrsclient.api import FoldersApi
from mdrsclient.client import MdrsClient
from mdrsclient.commands.base import BaseCommand
from mdrsclient.config import build_download_url
from mdrsclient.exceptions import UnauthorizedException
from mdrsclient.models import File, Folder, FolderSimple, Laboratory
class Config:
arbitrary_types_allowed = True
frozen = True
@dataclass(config=Config)
class LsCommandContext:
prefix: str
client: MdrsClient
laboratory: Laboratory
password: str
is_json: bool
is_quiet: bool
is_recursive: bool
projects: dict[int, str] = field(default_factory=dict)
class LsCommand(BaseCommand):
@classmethod
def register(cls, parsers: Any) -> None:
ls_parser = parsers.add_parser("ls", help="list the folder contents")
ls_parser.add_argument("-p", "--password", help="password to use when open locked folder")
ls_parser.add_argument("-J", "--json", help="turn on json output", action="store_true")
ls_parser.add_argument(
"-q",
"--quiet",
help="don't output header row. this option is forced if the -r option is specified",
action="store_true",
)
ls_parser.add_argument("-r", "--recursive", help="list the folder contents recursive", action="store_true")
ls_parser.add_argument("remote_path", help="remote folder path (remote:/lab/path/)")
ls_parser.set_defaults(func=cls.func)
@classmethod
def func(cls, args: Namespace) -> None:
remote_path = str(args.remote_path)
password = str(args.password) if args.password else None
is_json = bool(args.json)
is_recursive = bool(args.recursive)
is_quiet = bool(args.quiet) if not is_recursive else True
cls.ls(remote_path, password, is_json, is_recursive, is_quiet)
@classmethod
def ls(cls, remote_path: str, password: str | None, is_json: bool, is_recursive: bool, is_quiet: bool) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
from mdrsclient.client import MdrsClient
client = MdrsClient.from_remote(remote)
cls._ls_logic(client, remote_path, password, is_json, is_recursive, is_quiet)
return
@classmethod
def _ls_logic(
cls,
client: MdrsClient,
remote_path: str,
password: str | None,
is_json: bool,
is_recursive: bool,
is_quiet: bool,
) -> None:
remote = remote_path.split(":", 1)[0] if ":" in remote_path else ""
folder, laboratory = client.resolve_folder(remote_path, password)
laboratory_name = laboratory.name
files = client.find_files(folder.id)
context = LsCommandContext(
f"{remote}:/{laboratory_name}",
client,
laboratory,
password if password is not None else "",
is_json,
is_quiet,
is_recursive,
client.project_names(),
)
if context.is_json:
cls._ls_json(context, folder, files)
else:
cls._ls_plain(context, folder, files)
@classmethod
def _ls_json(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
print(json.dumps(cls._folder2dict(context, folder, files), ensure_ascii=False))
LABELS: Final[dict[str, str]] = {
"type": "Type",
"acl": "Access",
"laboratory": "Laboratory",
"size": "Size",
"date": "Date",
"name": "Name",
}
@classmethod
def _column_widths(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> dict[str, int]:
"""Width of each column: the widest of its heading and everything printed under it."""
length = {key: len(label) if not context.is_quiet else 0 for key, label in cls.LABELS.items()}
for sub_folder in folder.sub_folders:
sub_laboratory_name = cls._laboratory_name(context, sub_folder.laboratory_id)
length["acl"] = max(length["acl"], len(sub_folder.access_level_label(context.projects)))
length["laboratory"] = max(length["laboratory"], len(sub_laboratory_name))
length["size"] = max(length["size"], len(str(sub_folder.size)))
length["date"] = max(length["date"], len(sub_folder.updated_at_name))
length["name"] = max(length["name"], len(sub_folder.name))
for file in files:
length["size"] = max(length["size"], len(str(file.size)))
length["date"] = max(length["date"], len(file.updated_at_name))
length["name"] = max(length["name"], len(file.name))
length["acl"] = max(length["acl"], len(folder.access_level_label(context.projects)))
length["laboratory"] = max(length["laboratory"], len(context.laboratory.name))
return length
@classmethod
def _ls_plain_children(cls, context: LsCommandContext, folder: Folder) -> None:
"""List each sub-folder in turn, passing over the ones the caller cannot open."""
print("")
folder_api = FoldersApi(context.client.connection)
for sub_folder in sorted(folder.sub_folders, key=lambda x: x.name):
try:
if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password)
sub_detail = folder_api.retrieve(sub_folder.id)
sub_files = context.client.find_files(sub_folder.id)
except UnauthorizedException:
continue
cls._ls_plain(context, sub_detail, sub_files)
@classmethod
def _ls_plain(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
label = cls.LABELS
length = cls._column_widths(context, folder, files)
header = (
f"{label['type']:{length['type']}}\t{label['acl']:{length['acl']}}\t"
f"{label['laboratory']:{length['laboratory']}}\t{label['size']:{length['size']}}\t"
f"{label['date']:{length['date']}}\t{label['name']:{length['name']}}"
)
if context.is_recursive:
print(f"{context.prefix}{folder.path}:")
print(f"total {sum(f.size for f in files)}")
if not context.is_quiet:
print(header)
print("-" * len(header.expandtabs()))
for sub_folder in sorted(folder.sub_folders, key=lambda x: x.name):
sub_laboratory_name = cls._laboratory_name(context, sub_folder.laboratory_id)
sub_folder_type = "[d]" if sub_folder.lock is False else "[l]"
sub_folder_acl = sub_folder.access_level_label(context.projects)
print(
f"{sub_folder_type:{length['type']}}\t{sub_folder_acl:{length['acl']}}\t"
f"{sub_laboratory_name:{length['laboratory']}}\t{sub_folder.size:{length['size']}}\t"
f"{sub_folder.updated_at_name:{length['date']}}\t{sub_folder.name:{length['name']}}"
)
for file in sorted(files, key=lambda x: x.name):
print(
f"{'[f]':{length['type']}}\t{folder.access_level_label(context.projects):{length['acl']}}\t"
f"{context.laboratory.name:{length['laboratory']}}\t{file.size:{length['size']}}\t"
f"{file.updated_at_name:{length['date']}}\t{file.name:{length['name']}}"
)
if context.is_recursive:
cls._ls_plain_children(context, folder)
@classmethod
def _folder2dict(
cls, context: LsCommandContext, folder: Folder | FolderSimple, files: list[File]
) -> dict[str, Any]:
data: dict[str, Any] = {
"id": folder.id,
"pid": folder.pid,
"name": folder.name,
"size": folder.size,
"access_level": folder.access_level_label(context.projects),
"lock": folder.lock,
"laboratory": cls._laboratory_name(context, folder.laboratory_id),
"description": folder.description,
"created_at": folder.created_at,
"updated_at": folder.updated_at,
}
if isinstance(folder, Folder):
folder_api = FoldersApi(context.client.connection)
data["metadata"] = folder_api.metadata(folder.id)
if context.is_recursive:
sub_folders: list[dict[str, Any]] = []
for sub_folder in sorted(folder.sub_folders, key=lambda x: x.name):
try:
if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password)
folder2 = folder_api.retrieve(sub_folder.id)
files2 = context.client.find_files(sub_folder.id)
sub_folders.append(cls._folder2dict(context, folder2, files2))
except UnauthorizedException:
pass
data["sub_folders"] = sub_folders
else:
data["sub_folders"] = list(
map(lambda x: cls._folder2dict(context, x, []), sorted(folder.sub_folders, key=lambda x: x.name))
)
data["files"] = list(map(lambda x: cls._file2dict(context, x), sorted(files, key=lambda x: x.name)))
return data
@classmethod
def _file2dict(cls, context: LsCommandContext, file: File) -> dict[str, Any]:
data: dict[str, Any] = {
"id": file.id,
"name": file.name,
"type": file.type,
"size": file.size,
# "thumbnail": file.thumbnail,
"description": file.description,
"metadata": file.metadata,
"download_url": build_download_url(context.client.connection.url, file.download_url),
"created_at": file.created_at,
"updated_at": file.updated_at,
}
return data
@classmethod
def _laboratory_name(cls, context: LsCommandContext, laboratory_id: int) -> str:
laboratory = context.client.connection.laboratories.find_by_id(laboratory_id)
return laboratory.name if laboratory is not None else "(invalid)"