From 4d87b55b4068247805b0984b98f552af01930271 Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Thu, 20 Jul 2023 11:43:07 +0900 Subject: [PATCH] add new command `cp`. --- README.md | 45 ++++++++++++++++++- VERSION | 2 +- mdrsclient/__main__.py | 2 + mdrsclient/api/file.py | 13 +++++- mdrsclient/api/folder.py | 9 ++++ mdrsclient/commands/__init__.py | 2 + mdrsclient/commands/cp.py | 78 +++++++++++++++++++++++++++++++++ mdrsclient/commands/mv.py | 6 +-- mdrsclient/connection.py | 5 ++- 9 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 mdrsclient/commands/cp.py diff --git a/README.md b/README.md index 691a6ea..7d06e5a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # mdrs-client-python + The mdrs-client-python is python library and a command-line client for up- and downloading files to and from MDRS based repository. ## Installing + ``` pip install -e . ``` @@ -9,13 +11,17 @@ pip install -e . ## Example Usage ### config create + Create remote host configuration + ``` $ mdrs config create neurodata https://neurodata.riken.jp/api ``` ### login + Login to remote host + ``` $ mdrs login neurodata: Username: (enter your login name) @@ -23,25 +29,33 @@ Password: (enter your password) ``` ### logout + Logout from remote host + ``` $ mdrs logout neurodata: ``` ### whoami + Print current user name + ``` $ mdrs whoami neurodata: ``` ### labs + List all laboratories + ``` $ mdrs labs neurodata: ``` ### ls + List the folder contents + ``` $ mdrs ls neurodata:/NIU/Repository/ $ mdrs ls -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ @@ -50,20 +64,26 @@ $ mdrs ls -J -r neurodata:/NIU/Repository/Dataset1/ ``` ### mkdir + Create a new folder + ``` $ mdrs mkdir neurodata:/NIU/Repository/TEST ``` ### upload + Upload the file or directory + ``` $ mdrs upload ./sample.dat neurodata:/NIU/Repository/TEST/ $ mdrs upload -r ./dataset neurodata:/NIU/Repository/TEST/ ``` ### download + Download the file or folder + ``` $ mdrs download neurodata:/NIU/Repository/TEST/sample.dat ./ $ mdrs download -r neurodata:/NIU/Repository/TEST/dataset/ ./ @@ -71,21 +91,36 @@ $ mdrs download -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.dat ``` ### mv + Move or rename the file or folder + ``` -$ mdrs move neurodata:/NIU/Repository/TEST/sample.dat neurodata:/NIU/Repository/TEST2/sample2.dat -$ mdrs move neurodata:/NIU/Repository/TEST/dataset neurodata:/NIU/Repository/TEST2/ +$ mdrs mv neurodata:/NIU/Repository/TEST/sample.dat neurodata:/NIU/Repository/TEST2/sample2.dat +$ mdrs mv neurodata:/NIU/Repository/TEST/dataset neurodata:/NIU/Repository/TEST2/ +``` + +### cp + +Copy the file and folder + +``` +$ mdrs cp neurodata:/NIU/Repository/TEST/sample.dat neurodata:/NIU/Repository/TEST2/sample2.dat +$ mdrs cp -r neurodata:/NIU/Repository/TEST/dataset neurodata:/NIU/Repository/TEST2/ ``` ### rm + Remove the file or folder + ``` $ mdrs rm neurodata:/NIU/Repository/TEST2/sample2.dat $ mdrs rm -r neurodata:/NIU/Repository/TEST2/dataset ``` ### chacl + Change the folder access level + ``` $ mdrs chacl private neurodata:/NIU/Repository/Private $ mdrs chacl cbs_open -r neurodata:/NIU/Repository/CBS_Open @@ -93,21 +128,27 @@ $ mdrs chacl pw_open -r -p FOLDER_PASSWORD neurodata:/NIU/Repository/PW_Open ``` ### metadata + Get a folder metadata + ``` $ mdrs metadata neurodata:/NIU/Repository/TEST/ $ mdrs metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ ``` ### file-metadata + Get the file metadata + ``` $ mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat $ mdrs file-metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt ``` ### help + Show the help message and exit + ``` $ mdrs -h ``` diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/mdrsclient/__main__.py b/mdrsclient/__main__.py index 7ccd188..0ffd3df 100644 --- a/mdrsclient/__main__.py +++ b/mdrsclient/__main__.py @@ -4,6 +4,7 @@ import sys from mdrsclient.commands import ( ChaclCommand, ConfigCommand, + CpCommand, DownloadCommand, FileMetadataCommand, LabsCommand, @@ -36,6 +37,7 @@ def main() -> None: UploadCommand.register(parsers) DownloadCommand.register(parsers) MvCommand.register(parsers) + CpCommand.register(parsers) RmCommand.register(parsers) ChaclCommand.register(parsers) MetadataCommand.register(parsers) diff --git a/mdrsclient/api/file.py b/mdrsclient/api/file.py index 6ce3e97..ee59ef2 100644 --- a/mdrsclient/api/file.py +++ b/mdrsclient/api/file.py @@ -65,10 +65,19 @@ class FileApi(BaseApi): self._raise_response_error(response) return True - def move(self, file: File, folder_id: str) -> bool: + def move(self, file: File, folder_id: str, name: str) -> bool: # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) url = self.ENTRYPOINT + file.id + "/move/" - data: dict[str, str | int] = {"folder": folder_id, "name": file.name} + data: dict[str, str | int] = {"folder": folder_id, "name": name} + token_check(self.connection) + response = self.connection.post(url, data=data) + self._raise_response_error(response) + return True + + def copy(self, file: File, folder_id: str, name: str) -> bool: + # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) + url = self.ENTRYPOINT + file.id + "/copy/" + data: dict[str, str | int] = {"folder": folder_id, "name": name} token_check(self.connection) response = self.connection.post(url, data=data) self._raise_response_error(response) diff --git a/mdrsclient/api/folder.py b/mdrsclient/api/folder.py index fa407b9..e124912 100644 --- a/mdrsclient/api/folder.py +++ b/mdrsclient/api/folder.py @@ -103,6 +103,15 @@ class FolderApi(BaseApi): self._raise_response_error(response) return True + def copy(self, folder: FolderSimple, folder_id: str, name: str) -> bool: + # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) + url = self.ENTRYPOINT + folder.id + "/copy/" + data: dict[str, str | int] = {"parent": folder_id, "name": name} + token_check(self.connection) + response = self.connection.post(url, data=data) + self._raise_response_error(response) + return True + def metadata(self, id: str) -> dict[str, Any]: # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) url = self.ENTRYPOINT + id + "/metadata/" diff --git a/mdrsclient/commands/__init__.py b/mdrsclient/commands/__init__.py index 8fa1445..1af7650 100644 --- a/mdrsclient/commands/__init__.py +++ b/mdrsclient/commands/__init__.py @@ -1,5 +1,6 @@ from mdrsclient.commands.chacl import ChaclCommand from mdrsclient.commands.config import ConfigCommand +from mdrsclient.commands.cp import CpCommand from mdrsclient.commands.download import DownloadCommand from mdrsclient.commands.file_metadata import FileMetadataCommand from mdrsclient.commands.labs import LabsCommand @@ -16,6 +17,7 @@ from mdrsclient.commands.whoami import WhoamiCommand __all__ = [ "ConfigCommand", "ChaclCommand", + "CpCommand", "DownloadCommand", "FileMetadataCommand", "LabsCommand", diff --git a/mdrsclient/commands/cp.py b/mdrsclient/commands/cp.py new file mode 100644 index 0000000..6fdd084 --- /dev/null +++ b/mdrsclient/commands/cp.py @@ -0,0 +1,78 @@ +import os +from argparse import Namespace +from typing import Any +from unicodedata import normalize + +from mdrsclient.api import FileApi, FolderApi +from mdrsclient.commands.base import BaseCommand +from mdrsclient.exceptions import IllegalArgumentException + + +class CpCommand(BaseCommand): + @classmethod + def register(cls, parsers: Any) -> None: + cp_parser = parsers.add_parser("cp", help="copy the file and folder") + cp_parser.add_argument( + "-r", "--recursive", help="copy folders and their contents recursive", action="store_true" + ) + cp_parser.add_argument("src_path", help="source remote path (remote:/lab/path/src)") + cp_parser.add_argument("dest_path", help="destination remote path (remote:/lab/path/dest)") + cp_parser.set_defaults(func=cls.func) + + @classmethod + def func(cls, args: Namespace) -> None: + src_path = str(args.src_path) + dest_path = str(args.dest_path) + is_recursive = bool(args.recursive) + cls.cp(src_path, dest_path, is_recursive) + + @classmethod + def cp(cls, src_path: str, dest_path: str, is_recursive: bool) -> None: + (s_remote, s_laboratory_name, s_path) = cls._parse_remote_host_with_path(src_path) + (d_remote, d_laboratory_name, d_path) = cls._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_dirname = os.path.dirname(s_path) + s_basename = os.path.basename(s_path) + 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) + connection = cls._create_connection(s_remote) + laboratory = cls._find_laboratory(connection, s_laboratory_name) + s_parent_folder = cls._find_folder(connection, laboratory, s_dirname) + d_parent_folder = cls._find_folder(connection, laboratory, d_dirname) + s_file = s_parent_folder.find_file(s_basename) + if s_file is not None: + # source is file + d_file = d_parent_folder.find_file(d_basename) + if d_file is not None: + raise IllegalArgumentException(f"File `{d_basename}` already exists.") + d_sub_folder = d_parent_folder.find_sub_folder(d_basename) + if d_sub_folder is not None: + raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.") + file_api = FileApi(connection) + if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename: + file_api.copy(s_file, d_parent_folder.id, normalize("NFC", d_basename)) + else: + s_folder = s_parent_folder.find_sub_folder(s_basename) + if s_folder is None: + raise IllegalArgumentException(f"File or folder `{s_basename}` not found.") + # source is folder + if not is_recursive: + raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.") + if d_parent_folder.find_file(d_basename) is not None: + raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.") + d_folder = d_parent_folder.find_sub_folder(d_basename) + if d_folder is not None: + if d_folder.id == s_folder.id: + raise IllegalArgumentException(f"`{s_path}` and `{s_path}` are the same folder.") + raise IllegalArgumentException(f"Cannot move `{s_path}` to `{d_path}`: Folder not empty.") + folder_api = FolderApi(connection) + if s_parent_folder.id != d_parent_folder.id or s_basename != d_basename: + folder_api.copy(s_folder, d_parent_folder.id, normalize("NFC", d_basename)) diff --git a/mdrsclient/commands/mv.py b/mdrsclient/commands/mv.py index a7305a4..168abf4 100644 --- a/mdrsclient/commands/mv.py +++ b/mdrsclient/commands/mv.py @@ -9,7 +9,7 @@ from pydantic import TypeAdapter from mdrsclient.api import FileApi, FolderApi from mdrsclient.commands.base import BaseCommand from mdrsclient.exceptions import IllegalArgumentException -from mdrsclient.models import File, FolderSimple +from mdrsclient.models import FolderSimple class MvCommand(BaseCommand): @@ -58,9 +58,7 @@ class MvCommand(BaseCommand): raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{d_path}`.") file_api = FileApi(connection) if s_parent_folder.id != d_parent_folder.id or d_basename != s_basename: - d_file_dict = dataclasses.asdict(s_file) | {"name": normalize("NFC", d_basename)} - d_file = TypeAdapter(File).validate_python(d_file_dict) - file_api.move(d_file, d_parent_folder.id) + file_api.move(s_file, d_parent_folder.id, normalize("NFC", d_basename)) else: s_folder = s_parent_folder.find_sub_folder(s_basename) if s_folder is None: diff --git a/mdrsclient/connection.py b/mdrsclient/connection.py index 4a125ae..1124925 100644 --- a/mdrsclient/connection.py +++ b/mdrsclient/connection.py @@ -1,10 +1,13 @@ import platform import threading from io import BufferedReader -from typing import TypedDict, Unpack +from typing import TypedDict from requests import Response, Session +# Unpack is new in 3.11 +from typing_extensions import Unpack + from mdrsclient.__version__ import __version__ from mdrsclient.cache import CacheFile from mdrsclient.exceptions import MissingConfigurationException