import os from dataclasses import dataclass from unicodedata import normalize from mdrsclient.api import FilesApi, FoldersApi from mdrsclient.cache import CacheInterface from mdrsclient.config import ConfigInterface from mdrsclient.connection import MDRSConnection from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.models import File, Folder, FolderSimple, Laboratory from mdrsclient.models.file import find_file from mdrsclient.services import MdrsService @dataclass(frozen=True) class TransferEndpoints: """Both ends of a `cp` or `mv`, resolved: the parents, their contents, and the names.""" laboratory: Laboratory s_path: str s_basename: str s_parent_folder: Folder s_parent_files: list[File] d_path: str d_basename: str d_parent_folder: Folder d_parent_files: list[File] def is_same_place(self) -> bool: """Whether the destination names what the source already is, leaving nothing to do.""" return self.s_parent_folder.id == self.d_parent_folder.id and self.s_basename == self.d_basename class MdrsClient(MdrsService): """Service layer client for MDRS.""" def __init__(self, connection: MDRSConnection | None, config_class: type[ConfigInterface] | None = None): super().__init__(connection, config_class) @classmethod def from_remote( cls, remote: str, cache: CacheInterface | None = None, config: ConfigInterface | None = None ) -> "MdrsClient": return cls(cls.create_connection(remote, cache, config)) def mkdir(self, remote_path: str) -> None: remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path) r_path = r_path.rstrip("/") r_dirname = os.path.dirname(r_path) r_basename = os.path.basename(r_path) laboratory = self.find_laboratory(laboratory_name) parent_folder = self.find_folder(laboratory, r_dirname) files = self.find_files(parent_folder.id) if parent_folder.find_sub_folder(r_basename) is not None or find_file(files, r_basename) is not None: raise IllegalArgumentException(f"Cannot create folder `{r_path}`: File exists.") folder_api = FoldersApi(self.connection) folder_api.create(normalize("NFC", r_basename), parent_folder.id) def rm(self, remote_path: str, is_recursive: bool = False) -> None: remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path) r_path = r_path.rstrip("/") r_dirname = os.path.dirname(r_path) r_basename = os.path.basename(r_path) laboratory = self.find_laboratory(laboratory_name) parent_folder = self.find_folder(laboratory, r_dirname) parent_files = self.find_files(parent_folder.id) file = find_file(parent_files, r_basename) if file is not None: file_api = FilesApi(self.connection) file_api.destroy(file) else: folder = parent_folder.find_sub_folder(r_basename) if folder is None: raise IllegalArgumentException(f"Cannot remove `{r_path}`: No such file or folder.") if not is_recursive: raise IllegalArgumentException(f"Cannot remove `{r_path}`: Is a folder.") folder_api = FoldersApi(self.connection) folder_api.destroy(folder.id, True) def ls(self, remote_path: str, password: str | None = None) -> tuple[Folder, list[File]]: folder, laboratory = self.resolve_folder(remote_path, password) files = self.find_files(folder.id) return folder, files def __resolve_transfer(self, src_path: str, dest_path: str) -> TransferEndpoints: """Resolve both ends of a transfer, refusing one that crosses a remote or a laboratory.""" s_remote, s_laboratory_name, s_path = self.parse_remote_host_with_path(src_path) d_remote, d_laboratory_name, d_path = self.parse_remote_host_with_path(dest_path) if s_remote != d_remote: raise IllegalArgumentException("Remote host mismatched.") if s_laboratory_name != d_laboratory_name: raise IllegalArgumentException("Laboratory mismatched.") s_path = s_path.rstrip("/") s_basename = os.path.basename(s_path) # A destination ending in a separator names a folder to put the source into, and # keeps the source's own name. if d_path.endswith("/"): d_dirname = d_path d_basename = s_basename else: d_dirname = os.path.dirname(d_path) d_basename = os.path.basename(d_path) laboratory = self.find_laboratory(s_laboratory_name) s_parent_folder = self.find_folder(laboratory, os.path.dirname(s_path)) d_parent_folder = self.find_folder(laboratory, d_dirname) return TransferEndpoints( laboratory=laboratory, s_path=s_path, s_basename=s_basename, s_parent_folder=s_parent_folder, s_parent_files=self.find_files(s_parent_folder.id), d_path=d_path, d_basename=d_basename, d_parent_folder=d_parent_folder, d_parent_files=self.find_files(d_parent_folder.id), ) @staticmethod def __check_file_destination(ends: TransferEndpoints) -> None: """Refuse a destination that a file cannot take the place of.""" if find_file(ends.d_parent_files, ends.d_basename) is not None: raise IllegalArgumentException(f"File `{ends.d_basename}` already exists.") if ends.d_parent_folder.find_sub_folder(ends.d_basename) is not None: raise IllegalArgumentException( f"Cannot overwrite non-folder `{ends.d_basename}` with folder `{ends.d_path}`." ) @staticmethod def __check_folder_destination(ends: TransferEndpoints, s_folder: FolderSimple) -> None: """Refuse a destination that a folder cannot take the place of.""" if find_file(ends.d_parent_files, ends.d_basename) is not None: raise IllegalArgumentException( f"Cannot overwrite non-folder `{ends.d_basename}` with folder `{ends.s_path}`." ) d_folder = ends.d_parent_folder.find_sub_folder(ends.d_basename) if d_folder is not None: if d_folder.id == s_folder.id: raise IllegalArgumentException(f"`{ends.s_path}` and `{ends.s_path}` are the same folder.") raise IllegalArgumentException(f"Cannot move `{ends.s_path}` to `{ends.d_path}`: Folder not empty.") @staticmethod def __find_source_folder(ends: TransferEndpoints) -> FolderSimple: s_folder = ends.s_parent_folder.find_sub_folder(ends.s_basename) if s_folder is None: raise IllegalArgumentException(f"File or folder `{ends.s_basename}` not found.") return s_folder def cp(self, src_path: str, dest_path: str, is_recursive: bool = False) -> None: ends = self.__resolve_transfer(src_path, dest_path) s_file = find_file(ends.s_parent_files, ends.s_basename) if s_file is not None: self.__check_file_destination(ends) if not ends.is_same_place(): FilesApi(self.connection).copy(s_file, ends.d_parent_folder.id, normalize("NFC", ends.d_basename)) return s_folder = self.__find_source_folder(ends) if not is_recursive: raise IllegalArgumentException(f"Cannot copy `{ends.s_path}`: Is a folder.") self.__check_folder_destination(ends, s_folder) if not ends.is_same_place(): FoldersApi(self.connection).copy(s_folder, ends.d_parent_folder.id, normalize("NFC", ends.d_basename)) def mv(self, src_path: str, dest_path: str) -> None: ends = self.__resolve_transfer(src_path, dest_path) s_file = find_file(ends.s_parent_files, ends.s_basename) if s_file is not None: self.__check_file_destination(ends) if not ends.is_same_place(): FilesApi(self.connection).move(s_file, ends.d_parent_folder.id, normalize("NFC", ends.d_basename)) return s_folder = self.__find_source_folder(ends) self.__check_folder_destination(ends, s_folder) if not ends.is_same_place(): FoldersApi(self.connection).move(s_folder, ends.d_parent_folder.id, normalize("NFC", ends.d_basename)) def chacl( self, remote_path: str, access_level: int, is_recursive: bool = False, password: str | None = None ) -> None: remote, laboratory_name, r_path = self.parse_remote_host_with_path(remote_path) r_path = r_path.rstrip("/") laboratory = self.find_laboratory(laboratory_name) folder = self.find_folder(laboratory, r_path) folder_api = FoldersApi(self.connection) folder_api.acl(folder.id, access_level, is_recursive, password) def metadata(self, remote_path: str, password: str | None = None) -> dict: folder, laboratory = self.resolve_folder(remote_path, password) folder_api = FoldersApi(self.connection) return folder_api.metadata(folder.id) def file_metadata(self, remote_path: str, password: str | None = None) -> dict: folder, laboratory, r_basename = self.resolve_file(remote_path, password) files = self.find_files(folder.id) file = find_file(files, r_basename) if file is None: raise IllegalArgumentException(f"File `{r_basename}` not found.") file_api = FilesApi(self.connection) return file_api.metadata(file) def upload( self, local_path: str, remote_path: str, is_recursive: bool = False, is_skip_if_exists: bool = False ) -> None: from mdrsclient.transfer import Uploader uploader = Uploader(self) uploader.upload(local_path, remote_path, is_recursive, is_skip_if_exists) def download( self, remote_path: str, local_path: str, is_recursive: bool = False, is_skip_if_exists: bool = False, password: str | None = None, excludes: list[str] | None = None, ) -> None: from mdrsclient.transfer import Downloader downloader = Downloader(self) downloader.download(remote_path, local_path, is_recursive, is_skip_if_exists, password, excludes) def version(self) -> str: from mdrsclient.__version__ import __version__ return f"mdrs {__version__}" def config_create(self, remote: str, url: str) -> None: remote = self.parse_remote_host(remote) config = self.config_class(remote) if config.url is not None: raise IllegalArgumentException(f"Remote host `{remote}` is already exists.") else: config.url = url def config_update(self, remote: str, url: str) -> None: remote = self.parse_remote_host(remote) config = self.config_class(remote) if config.url is None: raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") else: config.url = url def config_list(self) -> list: config = self.config_class("") return config.list() def config_delete(self, remote: str) -> None: remote = self.parse_remote_host(remote) config = self.config_class(remote) if config.url is None: raise IllegalArgumentException(f"Remote host `{remote}` is not exists.") else: del config.url