13 Commits

21 changed files with 204 additions and 73 deletions

View File

@ -16,7 +16,11 @@
"mdrsclient", "mdrsclient",
"neurodata", "neurodata",
"Neuroinformatics", "Neuroinformatics",
"orcid",
"RIKEN" "RIKEN"
], ],
"ignorePaths": [".env", "__pycache__"] "ignorePaths": [
".env",
"__pycache__"
]
} }

1
.gitignore vendored
View File

@ -161,3 +161,4 @@ cython_debug/
# mdrs-cli # mdrs-cli
.neurodatacli.config .neurodatacli.config
poetry.toml

View File

@ -60,7 +60,7 @@ List the folder contents
```shell ```shell
mdrs ls neurodata:/NIU/Repository/ mdrs ls neurodata:/NIU/Repository/
mdrs ls -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ mdrs ls -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/
mdrs ls -r neurodata:/NIU/Repository/Dataset1/ mdrs ls -r neurodata:/NIU/Repository/Dataset1/
mdrs ls -J -r neurodata:/NIU/Repository/Dataset1/ mdrs ls -J -r neurodata:/NIU/Repository/Dataset1/
``` ```
@ -80,7 +80,7 @@ Upload the file or directory
```shell ```shell
mdrs upload ./sample.dat neurodata:/NIU/Repository/TEST/ mdrs upload ./sample.dat neurodata:/NIU/Repository/TEST/
mdrs upload -r ./dataset neurodata:/NIU/Repository/TEST/ mdrs upload -r ./dataset neurodata:/NIU/Repository/TEST/
mdrs upload -r -s ./dataset neurodata:/NIU/Repository/TEST/ mdrs upload -r --skip-if-exists ./dataset neurodata:/NIU/Repository/TEST/
``` ```
### download ### download
@ -90,7 +90,9 @@ Download the file or folder
```shell ```shell
mdrs download neurodata:/NIU/Repository/TEST/sample.dat ./ mdrs download neurodata:/NIU/Repository/TEST/sample.dat ./
mdrs download -r neurodata:/NIU/Repository/TEST/dataset/ ./ mdrs download -r neurodata:/NIU/Repository/TEST/dataset/ ./
mdrs download -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.dat ./ mdrs download -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.dat ./
mdrs download -r --exclude /NIU/Repository/TEST/dataset/skip neurodata:/NIU/Repository/TEST/dataset/ ./
mdrs download -r --skip-if-exists neurodata:/NIU/Repository/TEST/dataset/ ./
``` ```
### mv ### mv
@ -127,7 +129,7 @@ Change the folder access level
```shell ```shell
mdrs chacl private neurodata:/NIU/Repository/Private mdrs chacl private neurodata:/NIU/Repository/Private
mdrs chacl cbs_open -r neurodata:/NIU/Repository/CBS_Open mdrs chacl cbs_open -r neurodata:/NIU/Repository/CBS_Open
mdrs chacl pw_open -r -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open mdrs chacl pw_open -r -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open
``` ```
### metadata ### metadata
@ -136,7 +138,7 @@ Get a folder metadata
```shell ```shell
mdrs metadata neurodata:/NIU/Repository/TEST/ mdrs metadata neurodata:/NIU/Repository/TEST/
mdrs metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/ mdrs metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/
``` ```
### file-metadata ### file-metadata
@ -145,7 +147,7 @@ Get the file metadata
```shell ```shell
mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat mdrs file-metadata neurodata:/NIU/Repository/TEST/dataset/sample.dat
mdrs file-metadata -p PW_OPEN_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt mdrs file-metadata -p SHARING_PASSWORD neurodata:/NIU/Repository/PW_Open/Readme.txt
``` ```
### help ### help

View File

@ -1 +1 @@
1.3.6 1.3.12

View File

@ -1,5 +1,6 @@
import argparse import argparse
import sys import sys
from json import JSONDecodeError
from mdrsclient.commands import ( from mdrsclient.commands import (
ChaclCommand, ChaclCommand,
@ -52,6 +53,9 @@ def main() -> None:
except MDRSException as e: except MDRSException as e:
print(f"Error: {e}") print(f"Error: {e}")
sys.exit(2) sys.exit(2)
except JSONDecodeError:
print("Unexpected response returned. Please check the configuration or the server's operational status.")
sys.exit(2)
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(130) sys.exit(130)

View File

@ -17,10 +17,26 @@ class FilesApiCreateResponse:
id: str id: str
@dataclass(frozen=True)
class FilesApiListResponse:
count: int
next: str | None
previous: str | None
results: list[File]
class FilesApi(BaseApi): class FilesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/files/" ENTRYPOINT: Final[str] = "v3/files/"
FALLBACK_MIMETYPE: Final[str] = "application/octet-stream" FALLBACK_MIMETYPE: Final[str] = "application/octet-stream"
def list(self, folder_id: str, page_num: int) -> FilesApiListResponse:
url = self.ENTRYPOINT
token_check(self.connection)
params: dict[str, str | int] = {"folder_id": folder_id, "page": page_num}
response = self.connection.get(url, params=params)
self._raise_response_error(response)
return TypeAdapter(FilesApiListResponse).validate_python(response.json())
def retrieve(self, id: str) -> File: def retrieve(self, id: str) -> File:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name) # print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + id + "/" url = self.ENTRYPOINT + id + "/"

View File

@ -20,8 +20,10 @@ class UsersCurrentResponseLaboratory:
class UsersApiCurrentResponse: class UsersApiCurrentResponse:
id: int id: int
username: str username: str
full_name: str first_name: str
last_name: str
email: str email: str
orcid_id: str
laboratories: list[UsersCurrentResponseLaboratory] laboratories: list[UsersCurrentResponseLaboratory]
is_staff: bool is_staff: bool
is_active: bool is_active: bool

View File

@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
from typing import Any from typing import Any
from unicodedata import normalize from unicodedata import normalize
from mdrsclient.api import FoldersApi, LaboratoriesApi from mdrsclient.api import FilesApi, FoldersApi, LaboratoriesApi
from mdrsclient.config import ConfigFile from mdrsclient.config import ConfigFile
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import ( from mdrsclient.exceptions import (
@ -12,7 +12,8 @@ from mdrsclient.exceptions import (
UnauthorizedException, UnauthorizedException,
UnexpectedException, UnexpectedException,
) )
from mdrsclient.models import Folder, Laboratory from mdrsclient.models import File, Folder, Laboratory
from mdrsclient.utils import page_num_from_url
class BaseCommand(ABC): class BaseCommand(ABC):
@ -52,6 +53,19 @@ class BaseCommand(ABC):
folder_api.auth(folders[0].id, password) folder_api.auth(folders[0].id, password)
return folder_api.retrieve(folders[0].id) return folder_api.retrieve(folders[0].id)
@classmethod
def _find_files(cls, connection: MDRSConnection, folder_id: str) -> list[File]:
files_api = FilesApi(connection)
page = 1
results_file = []
while page:
result = files_api.list(folder_id, page)
results_file.extend(result.results)
page = 0
if result.next:
page = page_num_from_url(result.next)
return results_file
@classmethod @classmethod
def _parse_remote_host(cls, path: str) -> str: def _parse_remote_host(cls, path: str) -> str:
path_array = path.split(":") path_array = path.split(":")

View File

@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FilesApi, FoldersApi from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class CpCommand(BaseCommand): class CpCommand(BaseCommand):
@ -46,11 +47,13 @@ class CpCommand(BaseCommand):
connection = cls._create_connection(s_remote) connection = cls._create_connection(s_remote)
laboratory = cls._find_laboratory(connection, s_laboratory_name) laboratory = cls._find_laboratory(connection, s_laboratory_name)
s_parent_folder = cls._find_folder(connection, laboratory, s_dirname) s_parent_folder = cls._find_folder(connection, laboratory, s_dirname)
s_parent_files = cls._find_files(connection, s_parent_folder.id)
d_parent_folder = cls._find_folder(connection, laboratory, d_dirname) d_parent_folder = cls._find_folder(connection, laboratory, d_dirname)
s_file = s_parent_folder.find_file(s_basename) d_parent_files = cls._find_files(connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None: if s_file is not None:
# source is file # source is file
d_file = d_parent_folder.find_file(d_basename) d_file = find_file(d_parent_files, d_basename)
if d_file is not None: if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.") raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename) d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
@ -66,7 +69,7 @@ class CpCommand(BaseCommand):
# source is folder # source is folder
if not is_recursive: if not is_recursive:
raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.") raise IllegalArgumentException(f"Cannot copy `{s_path}`: Is a folder.")
if d_parent_folder.find_file(d_basename) is not None: if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.") raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename) d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None: if d_folder is not None:

View File

@ -9,7 +9,8 @@ from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, UnexpectedException from mdrsclient.exceptions import IllegalArgumentException, UnexpectedException
from mdrsclient.models import File from mdrsclient.models import File, Folder, Laboratory
from mdrsclient.models.file import find_file
from mdrsclient.settings import CONCURRENT from mdrsclient.settings import CONCURRENT
@ -22,6 +23,7 @@ class DownloadFileInfo:
@dataclass @dataclass
class DownloadContext: class DownloadContext:
hasError: bool hasError: bool
isSkipIfExists: bool
files: list[DownloadFileInfo] files: list[DownloadFileInfo]
@ -32,6 +34,15 @@ class DownloadCommand(BaseCommand):
download_parser.add_argument( download_parser.add_argument(
"-r", "--recursive", help="download folders and their contents recursive", action="store_true" "-r", "--recursive", help="download folders and their contents recursive", action="store_true"
) )
download_parser.add_argument(
"-s",
"--skip-if-exists",
help="skip the download if file is already downloaded and file size is the same",
action="store_true",
)
download_parser.add_argument(
"-e", "--exclude", help="exclude to download path matched file or folders", action="append"
)
download_parser.add_argument("-p", "--password", help="password to use when open locked folder") download_parser.add_argument("-p", "--password", help="password to use when open locked folder")
download_parser.add_argument("remote_path", help="remote file path (remote:/lab/path/file)") download_parser.add_argument("remote_path", help="remote file path (remote:/lab/path/file)")
download_parser.add_argument("local_path", help="local folder path (/foo/bar/)") download_parser.add_argument("local_path", help="local folder path (/foo/bar/)")
@ -42,11 +53,21 @@ class DownloadCommand(BaseCommand):
remote_path = str(args.remote_path) remote_path = str(args.remote_path)
local_path = str(args.local_path) local_path = str(args.local_path)
is_recursive = bool(args.recursive) is_recursive = bool(args.recursive)
is_skip_if_exists = bool(args.skip_if_exists)
password = str(args.password) if args.password else None password = str(args.password) if args.password else None
cls.download(remote_path, local_path, is_recursive, password) excludes = list(map(lambda x: str(x).rstrip("/").lower(), args.exclude)) if args.exclude is not None else []
cls.download(remote_path, local_path, is_recursive, is_skip_if_exists, password, excludes)
@classmethod @classmethod
def download(cls, remote_path: str, local_path: str, is_recursive: bool, password: str | None) -> None: def download(
cls,
remote_path: str,
local_path: str,
is_recursive: bool,
is_skip_if_exists: bool,
password: str | None,
excludes: list[str],
) -> None:
(remote, laboratory_name, r_path) = cls._parse_remote_host_with_path(remote_path) (remote, laboratory_name, r_path) = cls._parse_remote_host_with_path(remote_path)
r_path = r_path.rstrip("/") r_path = r_path.rstrip("/")
r_dirname = os.path.dirname(r_path) r_dirname = os.path.dirname(r_path)
@ -57,9 +78,12 @@ class DownloadCommand(BaseCommand):
raise IllegalArgumentException(f"Local directory `{local_path}` not found.") raise IllegalArgumentException(f"Local directory `{local_path}` not found.")
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
r_parent_folder = cls._find_folder(connection, laboratory, r_dirname, password) r_parent_folder = cls._find_folder(connection, laboratory, r_dirname, password)
file = r_parent_folder.find_file(r_basename) r_parent_files = cls._find_files(connection, r_parent_folder.id)
file = find_file(r_parent_files, r_basename)
if file is not None: if file is not None:
context = DownloadContext(False, []) if cls.__check_excludes(excludes, laboratory, r_parent_folder, file):
return
context = DownloadContext(False, is_skip_if_exists, [])
l_path = os.path.join(l_dirname, r_basename) l_path = os.path.join(l_dirname, r_basename)
context.files.append(DownloadFileInfo(file, l_path)) context.files.append(DownloadFileInfo(file, l_path))
cls.__multiple_download(connection, context) cls.__multiple_download(connection, context)
@ -70,41 +94,68 @@ class DownloadCommand(BaseCommand):
if not is_recursive: if not is_recursive:
raise IllegalArgumentException(f"Cannot download `{r_path}`: Is a folder.") raise IllegalArgumentException(f"Cannot download `{r_path}`: Is a folder.")
folder_api = FoldersApi(connection) folder_api = FoldersApi(connection)
cls.__multiple_download_pickup_recursive_files(connection, folder_api, folder.id, l_dirname) cls.__multiple_download_pickup_recursive_files(
connection, folder_api, laboratory, folder.id, l_dirname, excludes, is_skip_if_exists
)
@classmethod @classmethod
def __multiple_download_pickup_recursive_files( def __multiple_download_pickup_recursive_files(
cls, connection: MDRSConnection, folder_api: FoldersApi, folder_id: str, basedir: str cls,
connection: MDRSConnection,
folder_api: FoldersApi,
laboratory: Laboratory,
folder_id: str,
basedir: str,
excludes: list[str],
is_skip_if_exists: bool,
) -> None: ) -> None:
context = DownloadContext(False, []) context = DownloadContext(False, is_skip_if_exists, [])
folder = folder_api.retrieve(folder_id) folder = folder_api.retrieve(folder_id)
files = cls._find_files(connection, folder.id)
dirname = os.path.join(basedir, folder.name) dirname = os.path.join(basedir, folder.name)
if cls.__check_excludes(excludes, laboratory, folder, None):
return
if not os.path.exists(dirname): if not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)
print(dirname) print(dirname)
for file in folder.files: for file in files:
if cls.__check_excludes(excludes, laboratory, folder, file):
continue
path = os.path.join(dirname, file.name) path = os.path.join(dirname, file.name)
context.files.append(DownloadFileInfo(file, path)) context.files.append(DownloadFileInfo(file, path))
cls.__multiple_download(connection, context) cls.__multiple_download(connection, context)
if context.hasError: if context.hasError:
raise UnexpectedException("Some files failed to download.") raise UnexpectedException("Some files failed to download.")
for sub_folder in folder.sub_folders: for sub_folder in folder.sub_folders:
cls.__multiple_download_pickup_recursive_files(connection, folder_api, sub_folder.id, dirname) cls.__multiple_download_pickup_recursive_files(
connection, folder_api, laboratory, sub_folder.id, dirname, excludes, is_skip_if_exists
)
@classmethod @classmethod
def __multiple_download(cls, connection: MDRSConnection, context: DownloadContext) -> None: def __multiple_download(cls, connection: MDRSConnection, context: DownloadContext) -> None:
file_api = FilesApi(connection) file_api = FilesApi(connection)
with ThreadPoolExecutor(max_workers=CONCURRENT) as pool: with ThreadPoolExecutor(max_workers=CONCURRENT) as pool:
results = pool.map(lambda x: cls.__multiple_download_worker(file_api, x), context.files) results = pool.map(
lambda x: cls.__multiple_download_worker(file_api, x, context.isSkipIfExists), context.files
)
hasError = next(filter(lambda x: x is False, results), None) hasError = next(filter(lambda x: x is False, results), None)
if hasError is not None: if hasError is not None:
context.hasError = True context.hasError = True
@classmethod @classmethod
def __multiple_download_worker(cls, file_api: FilesApi, info: DownloadFileInfo) -> bool: def __multiple_download_worker(cls, file_api: FilesApi, info: DownloadFileInfo, is_skip_if_exists: bool) -> bool:
if not is_skip_if_exists or not os.path.exists(info.path) or info.file.size != os.path.getsize(info.path):
try: try:
file_api.download(info.file, info.path) file_api.download(info.file, info.path)
except Exception: except Exception:
print(f"Failed: ${info.path}")
if os.path.isfile(info.path):
os.remove(info.path)
return False return False
print(info.path) print(info.path)
return True return True
@classmethod
def __check_excludes(cls, excludes: list[str], laboratory: Laboratory, folder: Folder, file: File | None) -> bool:
path = f"/{laboratory.name}{folder.path}{file.name if file is not None else ''}".rstrip("/").lower()
return path in excludes

View File

@ -6,6 +6,7 @@ from typing import Any
from mdrsclient.api import FilesApi from mdrsclient.api import FilesApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class FileMetadataCommand(BaseCommand): class FileMetadataCommand(BaseCommand):
@ -31,7 +32,8 @@ class FileMetadataCommand(BaseCommand):
connection = cls._create_connection(remote) connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_dirname, password) folder = cls._find_folder(connection, laboratory, r_dirname, password)
file = folder.find_file(r_basename) files = cls._find_files(connection, folder.id)
file = find_file(files, r_basename)
if file is None: if file is None:
raise IllegalArgumentException(f"File `{r_basename}` not found.") raise IllegalArgumentException(f"File `{r_basename}` not found.")
file_api = FilesApi(connection) file_api = FilesApi(connection)

View File

@ -4,7 +4,7 @@ from typing import Any
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from mdrsclient.api import FoldersApi from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import UnauthorizedException from mdrsclient.exceptions import UnauthorizedException
@ -67,17 +67,18 @@ class LsCommand(BaseCommand):
is_recursive, is_recursive,
) )
folder = cls._find_folder(connection, laboratory, r_path, password) folder = cls._find_folder(connection, laboratory, r_path, password)
files = cls._find_files(connection, folder.id)
if context.is_json: if context.is_json:
cls._ls_json(context, folder) cls._ls_json(context, folder, files)
else: else:
cls._ls_plain(context, folder) cls._ls_plain(context, folder, files)
@classmethod @classmethod
def _ls_json(cls, context: LsCommandContext, folder: Folder) -> None: def _ls_json(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
print(json.dumps(cls._folder2dict(context, folder), ensure_ascii=False)) print(json.dumps(cls._folder2dict(context, folder, files), ensure_ascii=False))
@classmethod @classmethod
def _ls_plain(cls, context: LsCommandContext, folder: Folder) -> None: def _ls_plain(cls, context: LsCommandContext, folder: Folder, files: list[File]) -> None:
label = { label = {
"type": "Type", "type": "Type",
"acl": "Access", "acl": "Access",
@ -97,7 +98,7 @@ class LsCommand(BaseCommand):
length["size"] = max(length["size"], len(str(folder.size))) length["size"] = max(length["size"], len(str(folder.size)))
length["date"] = max(length["date"], len(sub_folder.updated_at_name)) length["date"] = max(length["date"], len(sub_folder.updated_at_name))
length["name"] = max(length["name"], len(sub_folder.name)) length["name"] = max(length["name"], len(sub_folder.name))
for file in folder.files: for file in files:
length["size"] = max(length["size"], len(str(file.size))) length["size"] = max(length["size"], len(str(file.size)))
length["date"] = max(length["date"], len(file.updated_at_name)) length["date"] = max(length["date"], len(file.updated_at_name))
length["name"] = max(length["name"], len(file.name)) length["name"] = max(length["name"], len(file.name))
@ -111,7 +112,7 @@ class LsCommand(BaseCommand):
if context.is_recursive: if context.is_recursive:
print(f"{context.prefix}{folder.path}:") print(f"{context.prefix}{folder.path}:")
print(f"total {sum(f.size for f in folder.files)}") print(f"total {sum(f.size for f in files)}")
if not context.is_quick: if not context.is_quick:
print(header) print(header)
@ -125,7 +126,7 @@ class LsCommand(BaseCommand):
f"{sub_laboratory_name:{length['laboratory']}}\t{sub_folder.size:{length['size']}}\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']}}" f"{sub_folder.updated_at_name:{length['date']}}\t{sub_folder.name:{length['name']}}"
) )
for file in sorted(folder.files, key=lambda x: x.name): for file in sorted(files, key=lambda x: x.name):
print( print(
f"{'[f]':{length['type']}}\t{folder.access_level_name:{length['acl']}}\t" f"{'[f]':{length['type']}}\t{folder.access_level_name:{length['acl']}}\t"
f"{context.laboratory.name:{length['laboratory']}}\t{file.size:{length['size']}}\t" f"{context.laboratory.name:{length['laboratory']}}\t{file.size:{length['size']}}\t"
@ -140,12 +141,15 @@ class LsCommand(BaseCommand):
if sub_folder.lock: if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password) folder_api.auth(sub_folder.id, context.password)
folder = folder_api.retrieve(sub_folder.id) folder = folder_api.retrieve(sub_folder.id)
cls._ls_plain(context, folder) files = cls._find_files(context.connection, sub_folder.id)
cls._ls_plain(context, folder, files)
except UnauthorizedException: except UnauthorizedException:
pass pass
@classmethod @classmethod
def _folder2dict(cls, context: LsCommandContext, folder: Folder | FolderSimple) -> dict[str, Any]: def _folder2dict(
cls, context: LsCommandContext, folder: Folder | FolderSimple, files: list[File]
) -> dict[str, Any]:
data: dict[str, Any] = { data: dict[str, Any] = {
"id": folder.id, "id": folder.id,
"pid": folder.pid, "pid": folder.pid,
@ -168,15 +172,16 @@ class LsCommand(BaseCommand):
if sub_folder.lock: if sub_folder.lock:
folder_api.auth(sub_folder.id, context.password) folder_api.auth(sub_folder.id, context.password)
folder2 = folder_api.retrieve(sub_folder.id) folder2 = folder_api.retrieve(sub_folder.id)
sub_folders.append(cls._folder2dict(context, folder2)) files2 = cls._find_files(context.connection, sub_folder.id)
sub_folders.append(cls._folder2dict(context, folder2, files2))
except UnauthorizedException: except UnauthorizedException:
pass pass
data["sub_folders"] = sub_folders data["sub_folders"] = sub_folders
else: else:
data["sub_folders"] = list( data["sub_folders"] = list(
map(lambda x: cls._folder2dict(context, x), sorted(folder.sub_folders, key=lambda x: x.name)) 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(folder.files, key=lambda x: x.name))) data["files"] = list(map(lambda x: cls._file2dict(context, x), sorted(files, key=lambda x: x.name)))
return data return data
@classmethod @classmethod

View File

@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FoldersApi from mdrsclient.api import FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class MkdirCommand(BaseCommand): class MkdirCommand(BaseCommand):
@ -29,7 +30,8 @@ class MkdirCommand(BaseCommand):
connection = cls._create_connection(remote) connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
parent_folder = cls._find_folder(connection, laboratory, r_dirname) parent_folder = cls._find_folder(connection, laboratory, r_dirname)
if parent_folder.find_sub_folder(r_basename) is not None or parent_folder.find_file(r_basename) is not None: files = cls._find_files(connection, 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.") raise IllegalArgumentException(f"Cannot create folder `{r_path}`: File exists.")
folder_api = FoldersApi(connection) folder_api = FoldersApi(connection)
folder_api.create(normalize("NFC", r_basename), parent_folder.id) folder_api.create(normalize("NFC", r_basename), parent_folder.id)

View File

@ -6,6 +6,7 @@ from unicodedata import normalize
from mdrsclient.api import FilesApi, FoldersApi from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class MvCommand(BaseCommand): class MvCommand(BaseCommand):
@ -42,11 +43,13 @@ class MvCommand(BaseCommand):
connection = cls._create_connection(s_remote) connection = cls._create_connection(s_remote)
laboratory = cls._find_laboratory(connection, s_laboratory_name) laboratory = cls._find_laboratory(connection, s_laboratory_name)
s_parent_folder = cls._find_folder(connection, laboratory, s_dirname) s_parent_folder = cls._find_folder(connection, laboratory, s_dirname)
s_parent_files = cls._find_files(connection, s_parent_folder.id)
d_parent_folder = cls._find_folder(connection, laboratory, d_dirname) d_parent_folder = cls._find_folder(connection, laboratory, d_dirname)
s_file = s_parent_folder.find_file(s_basename) d_parent_files = cls._find_files(connection, d_parent_folder.id)
s_file = find_file(s_parent_files, s_basename)
if s_file is not None: if s_file is not None:
# source is file # source is file
d_file = d_parent_folder.find_file(d_basename) d_file = find_file(d_parent_files, d_basename)
if d_file is not None: if d_file is not None:
raise IllegalArgumentException(f"File `{d_basename}` already exists.") raise IllegalArgumentException(f"File `{d_basename}` already exists.")
d_sub_folder = d_parent_folder.find_sub_folder(d_basename) d_sub_folder = d_parent_folder.find_sub_folder(d_basename)
@ -60,7 +63,7 @@ class MvCommand(BaseCommand):
if s_folder is None: if s_folder is None:
raise IllegalArgumentException(f"File or folder `{s_basename}` not found.") raise IllegalArgumentException(f"File or folder `{s_basename}` not found.")
# source is folder # source is folder
if d_parent_folder.find_file(d_basename) is not None: if find_file(d_parent_files, d_basename) is not None:
raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.") raise IllegalArgumentException(f"Cannot overwrite non-folder `{d_basename}` with folder `{s_path}`.")
d_folder = d_parent_folder.find_sub_folder(d_basename) d_folder = d_parent_folder.find_sub_folder(d_basename)
if d_folder is not None: if d_folder is not None:

View File

@ -5,6 +5,7 @@ from typing import Any
from mdrsclient.api import FilesApi, FoldersApi from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.models.file import find_file
class RmCommand(BaseCommand): class RmCommand(BaseCommand):
@ -32,7 +33,8 @@ class RmCommand(BaseCommand):
connection = cls._create_connection(remote) connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
parent_folder = cls._find_folder(connection, laboratory, r_dirname) parent_folder = cls._find_folder(connection, laboratory, r_dirname)
file = parent_folder.find_file(r_basename) parent_files = cls._find_files(connection, parent_folder.id)
file = find_file(parent_files, r_basename)
if file is not None: if file is not None:
file_api = FilesApi(connection) file_api = FilesApi(connection)
file_api.destroy(file) file_api.destroy(file)

View File

@ -9,13 +9,15 @@ from mdrsclient.api import FilesApi, FoldersApi
from mdrsclient.commands.base import BaseCommand from mdrsclient.commands.base import BaseCommand
from mdrsclient.connection import MDRSConnection from mdrsclient.connection import MDRSConnection
from mdrsclient.exceptions import IllegalArgumentException, MDRSException from mdrsclient.exceptions import IllegalArgumentException, MDRSException
from mdrsclient.models import Folder from mdrsclient.models import File, Folder
from mdrsclient.models.file import find_file
from mdrsclient.settings import CONCURRENT from mdrsclient.settings import CONCURRENT
@dataclass(frozen=True) @dataclass(frozen=True)
class UploadFileInfo: class UploadFileInfo:
folder: Folder folder: Folder
files: list[File]
path: str path: str
@ -53,6 +55,7 @@ class UploadCommand(BaseCommand):
connection = cls._create_connection(remote) connection = cls._create_connection(remote)
laboratory = cls._find_laboratory(connection, laboratory_name) laboratory = cls._find_laboratory(connection, laboratory_name)
folder = cls._find_folder(connection, laboratory, r_path) folder = cls._find_folder(connection, laboratory, r_path)
files = cls._find_files(connection, folder.id)
infos: list[UploadFileInfo] = [] infos: list[UploadFileInfo] = []
if os.path.isdir(l_path): if os.path.isdir(l_path):
if not is_recursive: if not is_recursive:
@ -60,6 +63,8 @@ class UploadCommand(BaseCommand):
folder_api = FoldersApi(connection) folder_api = FoldersApi(connection)
folder_map: dict[str, Folder] = {} folder_map: dict[str, Folder] = {}
folder_map[r_path] = folder folder_map[r_path] = folder
files_map: dict[str, list[File]] = {}
files_map[r_path] = files
l_basename = os.path.basename(l_path) l_basename = os.path.basename(l_path)
for dirpath, _, filenames in os.walk(l_path, followlinks=True): for dirpath, _, filenames in os.walk(l_path, followlinks=True):
sub = l_basename if dirpath == l_path else os.path.join(l_basename, os.path.relpath(dirpath, l_path)) sub = l_basename if dirpath == l_path else os.path.join(l_basename, os.path.relpath(dirpath, l_path))
@ -68,7 +73,10 @@ class UploadCommand(BaseCommand):
# prepare destination parent path # prepare destination parent path
d_parent_dirname = os.path.dirname(d_dirname) d_parent_dirname = os.path.dirname(d_dirname)
if folder_map.get(d_parent_dirname) is None: if folder_map.get(d_parent_dirname) is None:
folder_map[d_parent_dirname] = cls._find_folder(connection, laboratory, d_parent_dirname) parent_folder = cls._find_folder(connection, laboratory, d_parent_dirname)
folder_map[d_parent_dirname] = parent_folder
parent_files = cls._find_files(connection, parent_folder.id)
files_map[d_parent_dirname] = parent_files
# prepare destination path # prepare destination path
if folder_map.get(d_dirname) is None: if folder_map.get(d_dirname) is None:
d_folder = folder_map[d_parent_dirname].find_sub_folder(d_basename) d_folder = folder_map[d_parent_dirname].find_sub_folder(d_basename)
@ -78,13 +86,16 @@ class UploadCommand(BaseCommand):
d_folder_id = d_folder.id d_folder_id = d_folder.id
print(d_dirname) print(d_dirname)
folder_map[d_dirname] = folder_api.retrieve(d_folder_id) folder_map[d_dirname] = folder_api.retrieve(d_folder_id)
files_map[d_dirname] = cls._find_files(connection, d_folder_id)
if d_folder is None: if d_folder is None:
folder_map[d_parent_dirname].sub_folders.append(folder_map[d_dirname]) folder_map[d_parent_dirname].sub_folders.append(folder_map[d_dirname])
# register upload file list # register upload file list
for filename in filenames: for filename in filenames:
infos.append(UploadFileInfo(folder_map[d_dirname], os.path.join(dirpath, filename))) infos.append(
UploadFileInfo(folder_map[d_dirname], files_map[d_dirname], os.path.join(dirpath, filename))
)
else: else:
infos.append(UploadFileInfo(folder, l_path)) infos.append(UploadFileInfo(folder, files, l_path))
cls.__multiple_upload(connection, infos, is_skip_if_exists) cls.__multiple_upload(connection, infos, is_skip_if_exists)
@classmethod @classmethod
@ -98,7 +109,7 @@ class UploadCommand(BaseCommand):
@classmethod @classmethod
def __multiple_upload_worker(cls, file_api: FilesApi, info: UploadFileInfo, is_skip_if_exists: bool) -> None: def __multiple_upload_worker(cls, file_api: FilesApi, info: UploadFileInfo, is_skip_if_exists: bool) -> None:
basename = os.path.basename(info.path) basename = os.path.basename(info.path)
file = info.folder.find_file(basename) file = find_file(info.files, basename)
try: try:
if file is None: if file is None:
file_api.create(info.folder.id, info.path) file_api.create(info.folder.id, info.path)

View File

@ -2,7 +2,7 @@ import configparser
import os import os
from typing import Final from typing import Final
import validators # type: ignore import validators
from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.exceptions import IllegalArgumentException
from mdrsclient.settings import CONFIG_DIRNAME from mdrsclient.settings import CONFIG_DIRNAME
@ -41,7 +41,7 @@ class ConfigFile:
@url.setter @url.setter
def url(self, url: str) -> None: def url(self, url: str) -> None:
if not validators.url(url): # type: ignore if not validators.url(url):
raise IllegalArgumentException("malformed URI sequence") raise IllegalArgumentException("malformed URI sequence")
self.__load() self.__load()
if self.__config.has_section(self.remote): if self.__config.has_section(self.remote):

View File

@ -1,4 +1,5 @@
from typing import Any from typing import Any
from unicodedata import normalize
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
@ -25,3 +26,8 @@ class File:
@property @property
def updated_at_name(self) -> str: def updated_at_name(self) -> str:
return iso8601_to_user_friendly(self.updated_at) return iso8601_to_user_friendly(self.updated_at)
def find_file(files: list[File], name: str) -> File | None:
_name = normalize("NFC", name).lower()
return next((x for x in files if x.name.lower() == _name), None)

View File

@ -3,7 +3,6 @@ from unicodedata import normalize
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from mdrsclient.models.file import File
from mdrsclient.models.utils import iso8601_to_user_friendly from mdrsclient.models.utils import iso8601_to_user_friendly
@ -78,13 +77,8 @@ class FolderSimple:
class Folder(FolderSimple): class Folder(FolderSimple):
metadata: list[dict[str, Any]] metadata: list[dict[str, Any]]
sub_folders: list[FolderSimple] sub_folders: list[FolderSimple]
files: list[File]
path: str path: str
def find_sub_folder(self, name: str) -> FolderSimple | None: def find_sub_folder(self, name: str) -> FolderSimple | None:
_name = normalize("NFC", name).lower() _name = normalize("NFC", name).lower()
return next((x for x in self.sub_folders if x.name.lower() == _name), None) return next((x for x in self.sub_folders if x.name.lower() == _name), None)
def find_file(self, name: str) -> File | None:
_name = normalize("NFC", name).lower()
return next((x for x in self.files if x.name.lower() == _name), None)

View File

@ -1,5 +1,6 @@
import os import os
from typing import IO, Any from typing import IO, Any
from urllib.parse import parse_qs, urlparse
if os.name == "nt": if os.name == "nt":
import msvcrt import msvcrt
@ -21,3 +22,10 @@ class FileLock:
msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1) msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
elif os.name == "posix": elif os.name == "posix":
fcntl.flock(file.fileno(), fcntl.LOCK_UN) fcntl.flock(file.fileno(), fcntl.LOCK_UN)
def page_num_from_url(url: str) -> int | None:
parsed_url = urlparse(url)
params = parse_qs(parsed_url.query)
page = params.get("page", [None])[0]
return int(page) if page is not None else None

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "mdrs-client-python" name = "mdrs-client-python"
version = "1.3.6" version = "1.3.12"
description = "The mdrs-client-python is python library and a command-line client for up- and downloading files to and from MDRS based repository." description = "The mdrs-client-python is python library and a command-line client for up- and downloading files to and from MDRS based repository."
authors = ["Yoshihiro OKUMURA <yoshihiro.okumura@riken.jp>"] authors = ["Yoshihiro OKUMURA <yoshihiro.okumura@riken.jp>"]
license = "MIT" license = "MIT"
@ -13,6 +13,7 @@ classifiers=[
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"OSI Approved :: MIT License", "OSI Approved :: MIT License",
"Topic :: Utilities", "Topic :: Utilities",
] ]
@ -24,18 +25,18 @@ packages = [
python = "^3.10" python = "^3.10"
requests = "^2.32.3" requests = "^2.32.3"
requests-toolbelt = "^1.0.0" requests-toolbelt = "^1.0.0"
python-dotenv = "^1.0.1" python-dotenv = "^1.1.0"
pydantic = "^2.8.2" pydantic = "^2.11.4"
pydantic-settings = "^2.3.4" pydantic-settings = "^2.9.1"
PyJWT = "^2.8.0" PyJWT = "^2.10.1"
validators = "^0.22.0" validators = "^0.34.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^24.2.2" black = "^24.10.0"
flake8 = "^7.1.0" flake8 = "^7.2.0"
Flake8-pyproject = "^1.2.3" Flake8-pyproject = "^1.2.3"
isort = "^5.13.2" isort = "^5.13.2"
pyright = "^1.1.370" pyright = "^1.1.400"
[tool.poetry.scripts] [tool.poetry.scripts]
mdrs = 'mdrsclient.__main__:main' mdrs = 'mdrsclient.__main__:main'