From 81e0ed44f7f8619f5c3220c484c59489625747ac Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Thu, 21 Aug 2025 21:05:32 -0400 Subject: [PATCH 1/2] raise session expired exception instead of not logged in catch upload errors and raise exceptions update direct-io docs --- cterasdk/asynchronous/core/files/io.py | 6 ++- cterasdk/cio/core.py | 36 ++++++++++++++--- cterasdk/cio/edge.py | 4 +- cterasdk/clients/clients.py | 6 ++- cterasdk/clients/decorators.py | 1 + cterasdk/core/enum.py | 15 +++++++ cterasdk/core/files/io.py | 9 +++-- cterasdk/exceptions/io.py | 40 +++++++++++++++++++ cterasdk/lib/tasks.py | 1 + .../UserGuides/DataServices/DirectIO.rst | 4 +- 10 files changed, 104 insertions(+), 18 deletions(-) diff --git a/cterasdk/asynchronous/core/files/io.py b/cterasdk/asynchronous/core/files/io.py index c41b5968..f391286f 100644 --- a/cterasdk/asynchronous/core/files/io.py +++ b/cterasdk/asynchronous/core/files/io.py @@ -40,6 +40,8 @@ async def versions(core, path): async def walk(core, scope, path, include_deleted=False): + target = fs.CorePath.instance(scope, path) + await ensure_directory(core, target) paths = [fs.CorePath.instance(scope, path)] while len(paths) > 0: path = paths.pop(0) @@ -53,7 +55,7 @@ async def walk(core, scope, path, include_deleted=False): async def mkdir(core, path): with fs.makedir(path) as param: response = await core.v1.api.execute('', 'makeCollection', param) - fs.accept_response(response) + fs.accept_error(response) async def makedirs(core, path): @@ -173,7 +175,7 @@ async def wrapper(core): """ uid, filename, directory = await _validate_destination(core, name, destination) with fs.upload(core, filename, directory, size, fd) as param: - return await core.io.upload(str(uid), param) + return fs.validate_transfer_success(await core.io.upload(str(uid), param), destination.join(name).reference.as_posix()) return wrapper diff --git a/cterasdk/cio/core.py b/cterasdk/cio/core.py index 77e6537f..3ea3f340 100644 --- a/cterasdk/cio/core.py +++ b/cterasdk/cio/core.py @@ -3,9 +3,12 @@ from contextlib import contextmanager from ..objects.uri import quote, unquote from ..common import Object, DateTimeUtils -from ..core.enum import ProtectionLevel, CollaboratorType, SearchType, PortalAccountType, FileAccessMode, FileAccessError +from ..core.enum import ProtectionLevel, CollaboratorType, SearchType, PortalAccountType, FileAccessMode, FileAccessError, \ + UploadError from ..core.types import PortalAccount, UserAccount, GroupAccount -from ..exceptions.io import ResourceExistsError, PathValidationError, NameSyntaxError, ReservedNameError, RestrictedRoot +from ..exceptions.io import ResourceExistsError, PathValidationError, NameSyntaxError, \ + ReservedNameError, RestrictedRoot, InsufficientPermission +from ..exceptions.io import UploadException, OutOfQuota, RejectedByPolicy, NoStorageBucket, WindowsACLError from ..lib.iterator import DefaultResponse from . import common @@ -181,7 +184,7 @@ def build(self): class FetchResourcesResponse(DefaultResponse): def __init__(self, response): - accept_response(response.errorType) + accept_error(response.errorType) super().__init__(response) @property @@ -291,7 +294,7 @@ def handle(path): def destination_prerequisite_conditions(destination, name): - if not destination.reference.root: + if not len(destination.reference.parts) > 0: raise RestrictedRoot() if any(c in name for c in ['\\', '/', ':', '?', '&', '<', '>', '"', '|']): raise NameSyntaxError() @@ -311,6 +314,26 @@ def upload(core, name, destination, size, fd): yield param +def validate_transfer_success(response, path): + if response.rc: + logger.error('Upload of file: "%s" failed.', path) + if response.msg == UploadError.UserQuotaViolation: + raise OutOfQuota('User', path) + if response.msg == UploadError.PortalQuotaViolation: + raise OutOfQuota('Team Portal', path) + if response.msg == UploadError.FolderQuotaViolation: + raise OutOfQuota('Cloud drive folder', path) + if response.msg == UploadError.RejectedByPolicy: + raise RejectedByPolicy(path) + if response.msg == UploadError.WindowsACL: + raise WindowsACLError(path) + if response.msg.startswith(UploadError.NoStorageBucket): + raise NoStorageBucket(path) + raise UploadException(f'Upload failed. Reason: {response.msg}', path) + if not response.rc and response.msg == 'OK': + logger.info('Upload successful. Saved to: %s', path) + + @contextmanager def handle_many(directory, objects): param = Object() @@ -508,7 +531,7 @@ def obtain_current_accounts(param): return current_accounts -def accept_response(error_type): +def accept_error(error_type): """ Check if response contains an error. """ @@ -516,7 +539,8 @@ def accept_response(error_type): FileAccessError.FileWithTheSameNameExist: ResourceExistsError(), FileAccessError.DestinationNotExists: PathValidationError(), FileAccessError.InvalidName: NameSyntaxError(), - FileAccessError.ReservedName: ReservedNameError() + FileAccessError.ReservedName: ReservedNameError(), + FileAccessError.PermissionDenied: InsufficientPermission() }.get(error_type, None) try: if error: diff --git a/cterasdk/cio/edge.py b/cterasdk/cio/edge.py index b5ede1e4..2b3242e9 100644 --- a/cterasdk/cio/edge.py +++ b/cterasdk/cio/edge.py @@ -69,7 +69,7 @@ def makedir(path): yield path.absolute except CTERAException as error: try: - accept_response(error.response.message.msg, directory) + accept_error(error.response.message.msg, directory) except ResourceExistsError: logger.info('Directory already exists: %s', directory) logger.info('Directory created: %s', directory) @@ -126,7 +126,7 @@ def upload(name, destination, fd): yield param -def accept_response(response, reference): +def accept_error(response, reference): error = { "File exists": ResourceExistsError(), "Creating a folder in this location is forbidden": RestrictedPathError(), diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index 20ac3602..d8b59834 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -49,7 +49,8 @@ async def download_zip(self, path, data, **kwargs): class AsyncUpload(AsyncClient): async def upload(self, path, data, **kwargs): - return await super().form_data(path, data, **kwargs) + response = await super().form_data(path, data, **kwargs) + return await response.xml() class AsyncWebDAV(AsyncClient): @@ -253,7 +254,8 @@ def download_zip(self, path, data, **kwargs): class Upload(Client): def upload(self, path, data, **kwargs): - return super().form_data(path, data, **kwargs) + response = super().form_data(path, data, **kwargs) + return response.xml() class WebDAV(Client): diff --git a/cterasdk/clients/decorators.py b/cterasdk/clients/decorators.py index d8d42162..5f414764 100644 --- a/cterasdk/clients/decorators.py +++ b/cterasdk/clients/decorators.py @@ -16,6 +16,7 @@ def authenticate_then_execute(self, *args, **kwargs): except SessionExpired: logger.error('Session expired.') self.cookies.clear() + raise logger.error('Not logged in.') raise NotLoggedIn() return authenticate_then_execute diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 027e68a7..1ff8c9b0 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -646,6 +646,21 @@ class Reports: FolderGroups = 'folderGroupsStatisticsReport' +class UploadError: + """ + Upload Error + + :ivar QuotaViolation: User is out of quota. + :ivar RejectedByPolicy: Rejected by Cloud Drive policy rule. + """ + FolderQuotaViolation = 'Folder is out of quota' + UserQuotaViolation = 'User is out of quota' + PortalQuotaViolation = 'Portal is out of quota' + RejectedByPolicy = "Rejected by Cloud Drive policy rule" + NoStorageBucket = "No available storage location" + WindowsACL = "Illegal access to NTACL folder" + + class FileAccessError: """ File Access Error diff --git a/cterasdk/core/files/io.py b/cterasdk/core/files/io.py index ff660e13..6d04b97a 100644 --- a/cterasdk/core/files/io.py +++ b/cterasdk/core/files/io.py @@ -41,8 +41,9 @@ def versions(core, path): def walk(core, scope, path, include_deleted=False): - ensure_directory(core, path) - paths = [fs.CorePath.instance(scope, path)] + target = fs.CorePath.instance(scope, path) + ensure_directory(core, target) + paths = [target] while len(paths) > 0: path = paths.pop(0) entries = listdir(core, path, include_deleted=include_deleted) @@ -55,7 +56,7 @@ def walk(core, scope, path, include_deleted=False): def mkdir(core, path): with fs.makedir(path) as param: response = core.api.execute('', 'makeCollection', param) - fs.accept_response(response) + fs.accept_error(response) def makedirs(core, path): @@ -175,7 +176,7 @@ def wrapper(core): """ uid, filename, directory = _validate_destination(core, name, destination) with fs.upload(core, filename, directory, size, fd) as param: - return core.io.upload(str(uid), param) + return fs.validate_transfer_success(core.io.upload(str(uid), param), destination.join(name).reference.as_posix()) return wrapper diff --git a/cterasdk/exceptions/io.py b/cterasdk/exceptions/io.py index e997f631..a0fc22a8 100644 --- a/cterasdk/exceptions/io.py +++ b/cterasdk/exceptions/io.py @@ -58,3 +58,43 @@ class RestrictedRoot(RemoteStorageException): def __init__(self): super().__init__('Storing files to the root directory is forbidden.', '/') + + +class InsufficientPermission(RemoteStorageException): + + def __init__(self): + super().__init__('Permission denied: You must have appropriate permissions to access this resource.') + + +class UploadException(RemoteStorageException): + """ + Upload Exception + + :ivar str path: Path + """ + def __init__(self, message, path): + super().__init__(f'Upload failed: {message}', path) + + +class OutOfQuota(UploadException): + + def __init__(self, entity, path): + super().__init__(f'{entity} is out of quota.', path) + + +class RejectedByPolicy(UploadException): + + def __init__(self, path): + super().__init__('Rejected by Cloud Drive policy rule.', path) + + +class NoStorageBucket(UploadException): + + def __init__(self, path): + super().__init__('No available storage location.', path) + + +class WindowsACLError(UploadException): + + def __init__(self, path): + super().__init__('Unable to store file in a Windows ACL-enabled cloud folder..', path) diff --git a/cterasdk/lib/tasks.py b/cterasdk/lib/tasks.py index 30c262c9..61511ab2 100644 --- a/cterasdk/lib/tasks.py +++ b/cterasdk/lib/tasks.py @@ -4,6 +4,7 @@ import asyncio from abc import ABC, abstractmethod +from ..common import Object from ..common.enum import TaskRunningStatus from ..exceptions.common import TaskWaitTimeoutError, AwaitableTaskException from ..exceptions.transport import HTTPError diff --git a/docs/source/UserGuides/DataServices/DirectIO.rst b/docs/source/UserGuides/DataServices/DirectIO.rst index 68a11a29..0f35d6df 100644 --- a/docs/source/UserGuides/DataServices/DirectIO.rst +++ b/docs/source/UserGuides/DataServices/DirectIO.rst @@ -85,7 +85,7 @@ During testing, you may need to disable TLS verification if the Portal or Object Blocks API ========== -.. automethod:: cterasdk.direct.client.Client.blocks +.. automethod:: cterasdk.direct.client.DirectIO.blocks :noindex: @@ -122,7 +122,7 @@ Blocks API Streamer API ============ -.. automethod:: cterasdk.direct.client.Client.streamer +.. automethod:: cterasdk.direct.client.DirectIO.streamer :noindex: .. code-block:: python From 34bc2404c4fab01d9045229236f857f054cad469 Mon Sep 17 00:00:00 2001 From: Saimon Michelson Date: Thu, 21 Aug 2025 21:22:18 -0400 Subject: [PATCH 2/2] resolve lint errors --- cterasdk/core/enum.py | 2 +- cterasdk/exceptions/io.py | 10 +++++----- cterasdk/lib/tasks.py | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 1ff8c9b0..0c4da94d 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -649,7 +649,7 @@ class Reports: class UploadError: """ Upload Error - + :ivar QuotaViolation: User is out of quota. :ivar RejectedByPolicy: Rejected by Cloud Drive policy rule. """ diff --git a/cterasdk/exceptions/io.py b/cterasdk/exceptions/io.py index a0fc22a8..ed7c34bf 100644 --- a/cterasdk/exceptions/io.py +++ b/cterasdk/exceptions/io.py @@ -73,28 +73,28 @@ class UploadException(RemoteStorageException): :ivar str path: Path """ def __init__(self, message, path): - super().__init__(f'Upload failed: {message}', path) + super().__init__(f'Upload failed: {message}.', path) class OutOfQuota(UploadException): def __init__(self, entity, path): - super().__init__(f'{entity} is out of quota.', path) + super().__init__(f'{entity} is out of quota', path) class RejectedByPolicy(UploadException): def __init__(self, path): - super().__init__('Rejected by Cloud Drive policy rule.', path) + super().__init__('Rejected by Cloud Drive policy rule', path) class NoStorageBucket(UploadException): def __init__(self, path): - super().__init__('No available storage location.', path) + super().__init__('No available storage location', path) class WindowsACLError(UploadException): def __init__(self, path): - super().__init__('Unable to store file in a Windows ACL-enabled cloud folder..', path) + super().__init__('Unable to store file in a Windows ACL-enabled cloud folder', path) diff --git a/cterasdk/lib/tasks.py b/cterasdk/lib/tasks.py index 61511ab2..30c262c9 100644 --- a/cterasdk/lib/tasks.py +++ b/cterasdk/lib/tasks.py @@ -4,7 +4,6 @@ import asyncio from abc import ABC, abstractmethod -from ..common import Object from ..common.enum import TaskRunningStatus from ..exceptions.common import TaskWaitTimeoutError, AwaitableTaskException from ..exceptions.transport import HTTPError