diff --git a/cterasdk/asynchronous/core/files/browser.py b/cterasdk/asynchronous/core/files/browser.py index a9751e7b..3b8a7ff4 100644 --- a/cterasdk/asynchronous/core/files/browser.py +++ b/cterasdk/asynchronous/core/files/browser.py @@ -1,6 +1,6 @@ from .. import query -from ....cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \ - DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ +from ....cio.core.commands import Open, Upload, Download, EnsureDirectory, \ + UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink, GetExternalShareInfo from ....cio.core.types import InvitationPath from ....lib.storage import commonfs @@ -11,60 +11,39 @@ class FileBrowser(BaseCommand): """Async File Browser API.""" - async def handle(self, path): + async def handle(self, path, objects): """ Get a file handle. :param str path: Path to a file. + :param list[str],optional objects: Files and folders to include. :returns: File handle. :rtype: object - :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain file handle. + :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain a file handle. + :raises cterasdk.exceptions.io.core.GetMetadataError: If the directory was not found. + :raises cterasdk.exceptions.io.core.NotADirectoryException: If the target path is not a directory. """ - return await Open(io.handle, self._core, path).a_execute() + async with GetProperties(io.listdir, self._core, path) as properties: + return await Open(io.handle_many if properties.is_dir else io.handle, self._core, + properties.path, properties, objects).a_execute() - async def handle_many(self, directory, *objects): - """ - Get a ZIP archive file handle. - - :param str directory: Path to a folder. - :param args objects: List of files and folders to include. - :returns: File handle. - :rtype: object - :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found. - :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory. - """ - async with EnsureDirectory(io.listdir, self._core, directory) as (_, resource): - return await OpenMany(io.handle_many, self._core, resource, directory, *objects).a_execute() - - async def download(self, path, destination=None): + async def download(self, path, objects=None, destination=None): """ Download a file. :param str path: Path. - :param str, optional destination: File destination. If directory, original filename preserved. Defaults to default directory. - :returns: Path to local file. + :param list[str],optional objects: List of files and / or directory names to download. + :param str, optional destination: File destination. If a directory is provided, the original filename is preserved. + Defaults to the default download directory. + :returns: Path to the local file. :rtype: str - :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain file handle. + :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain a file handle. + :raises cterasdk.exceptions.io.core.GetMetadataError: If the directory was not found. + :raises cterasdk.exceptions.io.core.NotADirectoryException: If the target path is not a directory. """ - return await Download(io.handle, self._core, path, destination).a_execute() - - async def download_many(self, directory, objects, destination=None): - """ - Download selected files and/or directories as a ZIP archive. - - .. warning:: - Only existing files and directories will be included in the resulting ZIP file. - - :param str directory: Path to a folder. - :param list[str] objects: List of files and / or directory names to download. - :param str destination: Optional path to destination file or directory. Defaults to default download directory. - :returns: Path to local file. - :rtype: str - :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found. - :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory. - """ - async with EnsureDirectory(io.listdir, self._core, directory) as (_, resource): - return await DownloadMany(io.handle_many, self._core, resource, directory, objects, destination).a_execute() + async with GetProperties(io.listdir, self._core, path) as properties: + return await Download(io.handle_many if properties.is_dir else io.handle, self._core, + properties.path, properties, objects, destination).a_execute() async def listdir(self, path=None, include_deleted=False): """ @@ -383,11 +362,8 @@ async def mkdir(self, path): async def makedirs(self, path): return await self._file_browser.makedirs(self._invitation.join(path)) - async def download(self, path, destination=None): - return await self._file_browser.download(self._invitation.join(path), destination) - - async def download_many(self, directory, objects, destination=None): - return await self._file_browser.download_many(self._invitation.join(directory), objects, destination) + async def download(self, path, objects=None, destination=None): + return await self._file_browser.download(self._invitation.join(path), objects, destination) async def upload(self, destination, handle, name=None, size=None): return await self._file_browser.upload(self._invitation.join(destination), handle, name, size) diff --git a/cterasdk/cio/core/commands.py b/cterasdk/cio/core/commands.py index bb9d0b02..ff6f471a 100644 --- a/cterasdk/cio/core/commands.py +++ b/cterasdk/cio/core/commands.py @@ -356,24 +356,42 @@ def obtain_current_accounts(collaborators): class Open(PortalCommand): """Open file""" - def __init__(self, function, receiver, path): + def __init__(self, function, receiver, path, properties, objects): super().__init__(function, receiver) self.path = automatic_resolution(path, receiver.context) + self.properties = properties + self.objects = objects or [] def get_parameter(self): - return self.path.relative_encode + if self.properties and self.properties.is_dir: + param = Object() + param.paths = [self.path.join(filename).absolute_encode for filename in self.objects] if self.objects else [self.path.absolute] + param.snapshot = None + param.password = None + param.portalName = None + param.showDeleted = False + uid = ( + str(self.properties.volume.id) + if self._receiver.context != Context.Invitations + else f'share/{self._receiver.invite}' + ) + return uid, encode_request_parameter(param) + return self.path.relative_encode, # pylint: disable=trailing-comma-tuple def _before_command(self): raise_or_suppress_access_error(self._receiver, self.path) - logger.info('Getting handle: %s', self.path) + if self.properties and self.properties.is_dir and self.objects: + logger.info('Getting handle: %s', [self.path.join(o).relative for o in self.objects]) + else: + logger.info('Getting handle: %s', self.path) def _execute(self): with self.trace_execution(): - return self._function(self._receiver, self.get_parameter()) + return self._function(self._receiver, *self.get_parameter()) async def _a_execute(self): with self.trace_execution(): - return await self._function(self._receiver, self.get_parameter()) + return await self._function(self._receiver, *self.get_parameter()) def _handle_exception(self, e): path = self.path.relative @@ -385,86 +403,34 @@ def _handle_exception(self, e): class Download(PortalCommand): - def __init__(self, function, receiver, path, destination): + def __init__(self, function, receiver, path, properties=None, objects=None, destination=None): super().__init__(function, receiver) self.path = automatic_resolution(path, receiver.context) - self.destination = destination - - def get_parameter(self): - return commonfs.determine_directory_and_filename(self.path.reference, destination=self.destination) - - def _before_command(self): - logger.info('Downloading: %s', self.path) - - def _execute(self): - directory, name = self.get_parameter() - with self.trace_execution(): - with Open(self._function, self._receiver, self.path) as handle: - return synfs.write(directory, name, handle) - - async def _a_execute(self): - directory, name = self.get_parameter() - with self.trace_execution(): - async with Open(self._function, self._receiver, self.path) as handle: - return await asynfs.write(directory, name, handle) - - -class OpenMany(PortalCommand): - - def __init__(self, function, receiver, resource, directory, *objects): - super().__init__(function, receiver) - self.uid = str(resource.cloudFolderInfo.uid) if receiver.context != Context.Invitations else f'share/{receiver.invite}' - self.directory = automatic_resolution(directory, receiver.context) - self.objects = objects - - def _before_command(self): - raise_or_suppress_access_error(self._receiver, self.directory) - logger.info('Getting handle: %s', [self.directory.join(o).relative for o in self.objects]) - - def get_parameter(self): - param = Object() - param.paths = [self.directory.join(filename).absolute_encode for filename in self.objects] - param.snapshot = None - param.password = None - param.portalName = None - param.showDeleted = False - return encode_request_parameter(param) - - def _execute(self): - with self.trace_execution(): - return self._function(self._receiver, self.uid, self.get_parameter()) - - async def _a_execute(self): - with self.trace_execution(): - return await self._function(self._receiver, self.uid, self.get_parameter()) - - -class DownloadMany(PortalCommand): - - def __init__(self, function, receiver, resource, directory, objects, destination): - super().__init__(function, receiver) - self.resource = resource - self.directory = automatic_resolution(directory, receiver.context) + self.properties = properties self.objects = objects self.destination = destination def get_parameter(self): - return commonfs.determine_directory_and_filename(self.directory.reference, self.objects, destination=self.destination, archive=True) + archive = self.properties.is_dir if self.properties else False + return commonfs.determine_directory_and_filename(self.path.reference, self.objects, + self.destination, archive) def _before_command(self): - for o in self.objects: - logger.info('Downloading: %s', self.directory.join(o).relative) + if self.properties and self.properties.is_dir and self.objects: + logger.info('Downloading: %s', [self.path.join(o).relative for o in self.objects]) + else: + logger.info('Downloading: %s', self.path) def _execute(self): directory, name = self.get_parameter() with self.trace_execution(): - with OpenMany(self._function, self._receiver, self.resource, self.directory, *self.objects) as handle: + with Open(self._function, self._receiver, self.path, self.properties, self.objects) as handle: return synfs.write(directory, name, handle) async def _a_execute(self): directory, name = self.get_parameter() with self.trace_execution(): - async with OpenMany(self._function, self._receiver, self.resource, self.directory, *self.objects) as handle: + async with Open(self._function, self._receiver, self.path, self.properties, self.objects) as handle: return await asynfs.write(directory, name, handle) diff --git a/cterasdk/cli/dav.py b/cterasdk/cli/dav.py index 95e14a4b..71484a82 100644 --- a/cterasdk/cli/dav.py +++ b/cterasdk/cli/dav.py @@ -53,8 +53,8 @@ async def handle_download(args): # pylint: disable=too-many-branches async def download(invitation, resource, objects=None, archive=False, destination=None): if objects is not None or archive: - return await invitation.files.download_many(resource.path.relative, [o.name for o in objects], f'{destination}.zip') - return await invitation.files.download(resource.path.relative, destination.joinpath(resource.path.relative)) + return await invitation.files.download(resource.path.relative, [o.name for o in objects], f'{destination}.zip') + return await invitation.files.download(resource.path.relative, destination=destination.joinpath(resource.path.relative)) async with AsyncInvitation.from_uri(args.endpoint) as invitation: jobs = [] diff --git a/cterasdk/core/files/browser.py b/cterasdk/core/files/browser.py index c02da87e..f84bae98 100644 --- a/cterasdk/core/files/browser.py +++ b/cterasdk/core/files/browser.py @@ -1,6 +1,6 @@ from .. import query -from ...cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \ - DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ +from ...cio.core.commands import Open, Upload, Download, EnsureDirectory, \ + UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink, GetExternalShareInfo from ...cio.core.types import InvitationPath from ...lib.storage import commonfs @@ -11,61 +11,39 @@ class FileBrowser(BaseCommand): """CTERA Portal File Browser API.""" - def handle(self, path): + def handle(self, path, objects=None): """ Get a file handle. :param str path: Path to a file. + :param list[str],optional objects: Files and folders to include. :returns: File handle. :rtype: object :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain a file handle. - """ - return Open(io.handle, self._core, path).execute() - - def handle_many(self, directory, *objects): - """ - Get a ZIP archive file handle. - - :param str directory: Path to a folder. - :param args objects: Files and folders to include. - :returns: File handle. - :rtype: object :raises cterasdk.exceptions.io.core.GetMetadataError: If the directory was not found. :raises cterasdk.exceptions.io.core.NotADirectoryException: If the target path is not a directory. """ - with EnsureDirectory(io.listdir, self._core, directory) as (_, resource): - return OpenMany(io.handle_many, self._core, resource, directory, *objects).execute() + with GetProperties(io.listdir, self._core, path) as properties: + return Open(io.handle_many if properties.is_dir else io.handle, self._core, + properties.path, properties, objects).execute() - def download(self, path, destination=None): + def download(self, path, objects=None, destination=None): """ Download a file. :param str path: Path. + :param list[str],optional objects: List of files and / or directory names to download. :param str, optional destination: File destination. If a directory is provided, the original filename is preserved. Defaults to the default download directory. :returns: Path to the local file. :rtype: str :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain a file handle. - """ - return Download(io.handle, self._core, path, destination).execute() - - def download_many(self, directory, objects, destination=None): - """ - Download selected files and/or directories as a ZIP archive. - - .. warning:: - Only existing files and directories will be included in the resulting ZIP file. - - :param str directory: Path to a folder. - :param list[str] objects: List of files and / or directory names to download. - :param str destination: Optional path to destination file or directory. Defaults to the default download directory. - :returns: Path to the local file. - :rtype: str :raises cterasdk.exceptions.io.core.GetMetadataError: If the directory was not found. :raises cterasdk.exceptions.io.core.NotADirectoryException: If the target path is not a directory. """ - with EnsureDirectory(io.listdir, self._core, directory) as (_, resource): - return DownloadMany(io.handle_many, self._core, resource, directory, objects, destination).execute() + with GetProperties(io.listdir, self._core, path) as properties: + return Download(io.handle_many if properties.is_dir else io.handle, self._core, + properties.path, properties, objects, destination).execute() def listdir(self, path=None, include_deleted=False): """ @@ -352,7 +330,8 @@ def device_config(self, device, destination=None): :raises cterasdk.exceptions.io.core.OpenError: Raised on error obtaining file handle. """ destination = destination if destination is not None else f'{commonfs.downloads()}/{device}.xml' - return Download(io.handle, self._core, f'backups/{device}/Device Configuration/db.xml', destination).execute() + return Download(io.handle, self._core, + f'backups/{device}/Device Configuration/db.xml', destination=destination).execute() class InvitationBrowser: @@ -380,11 +359,8 @@ def mkdir(self, path): def makedirs(self, path): return self._file_browser.makedirs(self._invitation.join(path)) - def download(self, path, destination=None): - return self._file_browser.download(self._invitation.join(path), destination) - - def download_many(self, directory, objects, destination=None): - return self._file_browser.download_many(self._invitation.join(directory), objects, destination) + def download(self, path, objects=None, destination=None): + return self._file_browser.download(self._invitation.join(path), objects, destination) def upload(self, destination, handle, name=None, size=None): return self._file_browser.upload(self._invitation.join(destination), handle, name, size) diff --git a/cterasdk/lib/storage/commonfs.py b/cterasdk/lib/storage/commonfs.py index bf5d3dd1..98791210 100644 --- a/cterasdk/lib/storage/commonfs.py +++ b/cterasdk/lib/storage/commonfs.py @@ -96,7 +96,7 @@ def determine_zip_archive_name(directory, objects): :rtype: str """ - if len(objects) > 1: + if not objects or len(objects) > 1: path = Path(directory) else: path = Path(objects[0]) diff --git a/docs/source/UserGuides/Portal/Files.rst b/docs/source/UserGuides/Portal/Files.rst index cf4a7792..5ab62b13 100644 --- a/docs/source/UserGuides/Portal/Files.rst +++ b/docs/source/UserGuides/Portal/Files.rst @@ -195,13 +195,14 @@ File Handles .. code-block:: python + """Retrieve a handle for a file""" handle = files.handle('My Files/Keystone Project.docx') -.. automethod:: cterasdk.core.files.browser.FileBrowser.handle_many + """Retrieve a handle for a folder""" + handle = files.handle('My Files/Project X') -.. code-block:: python - - handle = files.handle_many('My Files', 'Keystone Project.docx', 'Images', 'Notes.txt') + """Retrieve a handle for individual files within a folder""" + handle = files.handle('My Files', ['Keystone Project.docx', 'Images', 'Notes.txt']) Downloading Files ----------------- @@ -211,14 +212,14 @@ Downloading Files .. code-block:: python - local_path = files.download('My Files/Keystone Project.docx') + """Download a file""" + path = files.download('My Files/Keystone Project.docx') -.. automethod:: cterasdk.core.files.browser.FileBrowser.download_many - :noindex: - -.. code-block:: python + """Download a folder""" + path = files.download('My Files/Project X') - zip_path = files.download_many('My Files', ['Keystone Project.docx', 'Images'], destination='/tmp/MyFiles.zip') + """Download individual files within a folder""" + zip_archive = files.download('My Files', ['Keystone Project.docx', 'Images'], destination='/tmp/MyFiles.zip') Create Directories ------------------ diff --git a/tests/ut/core/user/test_special_characters.py b/tests/ut/core/user/test_special_characters.py index 69db0f96..26a5beed 100644 --- a/tests/ut/core/user/test_special_characters.py +++ b/tests/ut/core/user/test_special_characters.py @@ -35,6 +35,7 @@ def setUp(self): self._special_dir = 'My Files/100% Complete' self._special_filename = 'file_100%_done.txt' self._special_path = f'{self._special_dir}/{self._special_filename}' + self._mock_properties_response = self.patch_call('cterasdk.cio.core.commands.GetProperties._handle_response') def _expected_encoded_absolute(self, path): return f'{self._base}/{quote(path)}' @@ -44,6 +45,7 @@ def _expected_encoded_absolute(self, path): def test_handle_encodes_special_characters(self): for filename in self.SPECIAL_FILENAMES: path = f'My Files/{filename}' + self._mock_properties_response.return_value = munch.Munch({'is_dir': False, 'path': path}) self._init_services() mock_download = mock.MagicMock() self._services.io._webdav.download = mock_download # pylint: disable=protected-access @@ -57,6 +59,7 @@ def test_handle_encodes_special_characters(self): def test_handle_percent_in_directory(self): for directory in self.SPECIAL_DIRECTORIES: path = f'{directory}/document.txt' + self._mock_properties_response.return_value = munch.Munch({'is_dir': False, 'path': path}) self._init_services() mock_download = mock.MagicMock() self._services.io._webdav.download = mock_download # pylint: disable=protected-access