diff --git a/.pylintrc b/.pylintrc index 5a30a827..98ea7298 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,7 +38,7 @@ persistent=yes # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. -suggestion-mode=yes +# suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. diff --git a/cterasdk/asynchronous/core/files/__init__.py b/cterasdk/asynchronous/core/files/__init__.py index 5550da3b..2cc40502 100644 --- a/cterasdk/asynchronous/core/files/__init__.py +++ b/cterasdk/asynchronous/core/files/__init__.py @@ -1 +1 @@ -from .browser import CloudDrive # noqa: E402, F401 +from .browser import CloudDrive, InvitationBrowser # noqa: E402, F401 diff --git a/cterasdk/asynchronous/core/files/browser.py b/cterasdk/asynchronous/core/files/browser.py index 6f09208c..a9751e7b 100644 --- a/cterasdk/asynchronous/core/files/browser.py +++ b/cterasdk/asynchronous/core/files/browser.py @@ -1,9 +1,10 @@ from .. import query from ....cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \ DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ - Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink -from ..base_command import BaseCommand + Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink, GetExternalShareInfo +from ....cio.core.types import InvitationPath from ....lib.storage import commonfs +from ..base_command import BaseCommand from . import io @@ -355,3 +356,44 @@ async def unshare(self, path): :param str path: Path of file/folder. """ return await UnShare(io.update_share, self._core, path).a_execute() + + +class InvitationBrowser: + + def __init__(self, core): + self._invitation = InvitationPath.from_context(core.invite) + self._core = core + self._file_browser = CloudDrive(core) + + def listdir(self, path=None): + return self._file_browser.listdir(self._invitation.join(path)) + + def walk(self, path=None): + return self._file_browser.walk(self._invitation.join(path)) + + async def properties(self, path): + return await self._file_browser.properties(self._invitation.join(path)) + + async def exists(self, path): + return await self._file_browser.exists(self._invitation.join(path)) + + async def mkdir(self, path): + return await self._file_browser.mkdir(self._invitation.join(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 upload(self, destination, handle, name=None, size=None): + return await self._file_browser.upload(self._invitation.join(destination), handle, name, size) + + async def upload_file(self, path, destination): + return await self._file_browser.upload_file(path, self._invitation.join(destination)) + + async def details(self): + return await GetExternalShareInfo(io.get_share_details, self._core, self._core.invite).a_execute() diff --git a/cterasdk/asynchronous/core/files/io.py b/cterasdk/asynchronous/core/files/io.py index adead1a5..cdde10a7 100644 --- a/cterasdk/asynchronous/core/files/io.py +++ b/cterasdk/asynchronous/core/files/io.py @@ -73,3 +73,7 @@ async def add_share_recipients(core, path, members): async def public_link(core, param): return await core.v1.api.execute('', 'createShare', param) + + +async def get_share_details(core, param): + return await core.v1.api.execute('', 'getShareDetails', param) diff --git a/cterasdk/asynchronous/core/invitation/__init__.py b/cterasdk/asynchronous/core/invitation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cterasdk/asynchronous/core/invitation/login.py b/cterasdk/asynchronous/core/invitation/login.py new file mode 100644 index 00000000..988987c1 --- /dev/null +++ b/cterasdk/asynchronous/core/invitation/login.py @@ -0,0 +1,18 @@ +import logging +from ..base_command import BaseCommand + + +logger = logging.getLogger('cterasdk.core') + + +class Login(BaseCommand): + """ + Portal Login APIs + """ + + async def login(self, key, value): + logger.info('Creating external session. %s', {'invite': value}) + await self._core.clients.v1.ctera.get('', params={key: value}) + + async def logout(self): + """No logout for external users""" diff --git a/cterasdk/cio/common.py b/cterasdk/cio/common.py index 07c59a7d..ca7e3234 100644 --- a/cterasdk/cio/common.py +++ b/cterasdk/cio/common.py @@ -60,7 +60,7 @@ def name(self): @property def parent(self): - return self.__class__(self._reference.parent.as_posix()) # pylint: disable=no-value-for-parameter + return self.__class__(self._scope, self._reference.parent.as_posix()) # pylint: disable=no-value-for-parameter @property def absolute(self): @@ -80,7 +80,7 @@ def is_relative_to(self, p): @resolver def relative_to(self, p): - return self.__class__(self.reference.relative_to(p).as_posix()) # pylint: disable=no-value-for-parameter + return self.__class__(self._scope, self.reference.relative_to(p).as_posix()) # pylint: disable=no-value-for-parameter @property def extension(self): @@ -92,7 +92,9 @@ def join(self, p): :param str p: Path. """ - return self.__class__(self.reference.joinpath(p).as_posix()) # pylint: disable=no-value-for-parameter + if p is not None: + return self.__class__(self._scope, self.reference.joinpath(p).as_posix()) # pylint: disable=no-value-for-parameter + return self @property def parts(self): @@ -100,7 +102,7 @@ def parts(self): def __getitem__(self, key): if isinstance(key, slice): - return self.__class__(self.parts[key]) # pylint: disable=no-value-for-parameter + return self.__class__(self._scope, self.parts[key]) # pylint: disable=no-value-for-parameter if isinstance(key, int): return self.parts[key] raise TypeError("Invalid argument type") diff --git a/cterasdk/cio/core/commands.py b/cterasdk/cio/core/commands.py index 52ffaf68..bb9d0b02 100644 --- a/cterasdk/cio/core/commands.py +++ b/cterasdk/cio/core/commands.py @@ -8,7 +8,7 @@ from ...common import Object, DateTimeUtils from ...core.enum import ProtectionLevel, CollaboratorType, SearchType, PortalAccountType, FileAccessMode, \ UploadError, ResourceScope, ResourceError, Context, Administrators -from ...core.types import PortalAccount, UserAccount, GroupAccount, Collaborator +from ...core.types import PortalAccount, UserAccount, GroupAccount, Collaborator, PortalInvitation from ... import exceptions from ...lib.storage import synfs, asynfs, commonfs from .types import SrcDstParam, CreateShareParam, ActionResourcesParam, FetchResourcesError, \ @@ -47,15 +47,16 @@ def ensure_user_access(user_session, path): The target path to validate. """ relative = path.relative - if user_session.account.role.name in Administrators: - if user_session.context == Context.admin: - if not administrator_namespace(path): - return False, exceptions.io.core.ContextError(relative) - if not user_session.account.role.access_end_user_folders and is_password_protected(path): - return False, exceptions.io.core.PrivilegeError(relative) - elif user_session.context == Context.ServicesPortal: - if not user_session.account.role.access_end_user_folders and is_password_protected(path): - return False, exceptions.io.core.PrivilegeError(relative) + if not user_session.context == Context.Invitations: + if user_session.account.role.name in Administrators: + if user_session.context == Context.admin: + if not administrator_namespace(path): + return False, exceptions.io.core.ContextError(relative) + if not user_session.account.role.access_end_user_folders and is_password_protected(path): + return False, exceptions.io.core.PrivilegeError(relative) + elif user_session.context == Context.ServicesPortal: + if not user_session.account.role.access_end_user_folders and is_password_protected(path): + return False, exceptions.io.core.PrivilegeError(relative) return True, None @@ -412,7 +413,7 @@ class OpenMany(PortalCommand): def __init__(self, function, receiver, resource, directory, *objects): super().__init__(function, receiver) - self.uid = str(resource.cloudFolderInfo.uid) + 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 @@ -589,7 +590,6 @@ def _generator(self): def generate(self): for path in self._generator(): try: - print('Enumerating: ', path or '.') for o in ResourceIterator(self._function, self._receiver, path, None, self.include_deleted, None, None).execute(): yield self._process_object(o) except (exceptions.io.core.ListDirectoryError, exceptions.io.core.PrivilegeError) as e: @@ -605,7 +605,7 @@ async def a_generate(self): def _process_object(self, o): if o.is_dir: - self.tree.append(o.path.relative) + self.tree.append(o.path) return o @staticmethod @@ -668,7 +668,7 @@ def _parents_generator(self): if self.parents: parts = self.path.parts for i in range(1, len(parts)): - yield automatic_resolution('/'.join(parts[:i]), self._receiver.context) + yield self.path[:i] else: yield self.path @@ -712,7 +712,7 @@ def _handle_response(self, r): if r == ResourceError.InvalidName: cause = exceptions.io.core.FilenameError(path) if r == ResourceError.PermissionDenied: - cause = exceptions.io.core.ReservedNameError(path) + cause = exceptions.io.core.PrivilegeError(path) raise error from cause @@ -745,6 +745,36 @@ def _handle_exception(self, e): raise exceptions.io.core.GetShareMetadataError(path) from e +class GetExternalShareInfo(PortalCommand): + + def __init__(self, function, receiver, invite): + super().__init__(function, receiver) + self.invite = invite + + def _before_command(self): + logger.info('Querying external share details: %s', self.invite) + + def get_parameter(self): + return self.invite + + def _execute(self): + with self.trace_execution(): + 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()) + + def _handle_response(self, r): + return PortalInvitation.from_server_object(r) + + def _handle_exception(self, e): + error = exceptions.io.core.ExternalShareError(self._receiver.uri) + if 'no longer valid' in e.error.response.error.msg: + raise error from exceptions.io.core.InvalidShareError(self.invite) + raise error + + class Link(PortalCommand): def __init__(self, function, receiver, path, access, expire_in): diff --git a/cterasdk/cio/core/types.py b/cterasdk/cio/core/types.py index 98b3a1bd..93e8345a 100644 --- a/cterasdk/cio/core/types.py +++ b/cterasdk/cio/core/types.py @@ -3,6 +3,7 @@ from datetime import datetime from ...common import Object from ..common import BasePath, BaseResource +from ...core.enum import Context from ...lib.iterator import DefaultResponse @@ -71,11 +72,12 @@ def _parse_from_str(path): :param str path: Path """ - groups = [f'(?P<{o.__name__}>{namespace})' for namespace, o in Namespaces.items()] + groups = [f'(?P<{c.__name__}>{c.expr})' for c in [ServicesPortalPath, GlobalAdminPath, InvitationPath]] regex = re.compile(f"^{'|'.join(groups)}") match = re.match(regex, path) if match: - return Namespaces[match.group()](path[match.end():]) + scope, reference = path[:match.end()], path[match.end():] + return resolve_namespace_from_href(scope)(scope, reference) raise ValueError(f'Could not determine object path: {path}') @@ -84,9 +86,11 @@ class ServicesPortalPath(PortalPath): ServicesPortal Path Object """ Namespace = '/ServicesPortal/webdav' + expr = Namespace - def __init__(self, reference): - super().__init__(ServicesPortalPath.Namespace, reference) + @staticmethod + def from_context(reference): + return ServicesPortalPath(ServicesPortalPath.Namespace, reference) class GlobalAdminPath(PortalPath): @@ -94,15 +98,43 @@ class GlobalAdminPath(PortalPath): Global Admin Path Object """ Namespace = '/admin/webdav' + expr = Namespace + + @staticmethod + def from_context(reference): + return ServicesPortalPath(GlobalAdminPath.Namespace, reference) + + +class InvitationPath(PortalPath): + """ + Invitation Path Object + """ + Namespace = '/invitations/webdav' + expr = f'{Namespace}/share/([a-zA-Z0-9]+)' + + @staticmethod + def from_context(reference): + return PortalPath.from_str(f'{InvitationPath.Namespace}/share/{reference}') + - def __init__(self, reference): - super().__init__(GlobalAdminPath.Namespace, reference) +def resolve_namespace_from_context(ctx): + if ctx == Context.admin: + return GlobalAdminPath + if ctx == Context.ServicesPortal: + return ServicesPortalPath + if ctx == Context.Invitations: + return InvitationPath + return None -Namespaces = { - ServicesPortalPath.Namespace: ServicesPortalPath, - GlobalAdminPath.Namespace: GlobalAdminPath -} +def resolve_namespace_from_href(href): + if href.startswith(GlobalAdminPath.Namespace): + return GlobalAdminPath + if href.startswith(ServicesPortalPath.Namespace): + return ServicesPortalPath + if href.startswith(InvitationPath.Namespace): + return InvitationPath + raise ValueError(f'Could not find namespace associated with href: {href}') def resolve(path, namespace=None): @@ -111,6 +143,7 @@ def resolve(path, namespace=None): :param object path: Path :param namespace: :class:`cterasdk.cio.core.types.ServicesPortalPath` or :class:`cterasdk.cio.core.types.GlobalAdminPath` (optional) + or :class:`cterasdk.cio.core.types.InvitationPath` (optional) """ if isinstance(path, PortalPath): return path @@ -123,7 +156,7 @@ def resolve(path, namespace=None): if namespace: if path is None or isinstance(path, str): - return namespace(path or '') + return namespace.from_context(path or '') raise ValueError(f'Error: Could not resolve path: {path}. Type: {type(path)}') @@ -144,14 +177,14 @@ def wrapper(): return wrapper() -def automatic_resolution(p, context=None): +def automatic_resolution(p, ctx=None): """ Automatic Resolution of Path Object :param object p: Path - :param str,optional context: Context (e.g. 'ServicesPortal' or 'admin') + :param str,optional ctx: Context (e.g. 'ServicesPortal', 'admin', or 'invitations') """ - namespace = Namespaces.get(f'/{context}/webdav', None) + namespace = resolve_namespace_from_context(ctx) if isinstance(p, (list, tuple)): return create_generator(p, namespace) diff --git a/cterasdk/cio/edge/types.py b/cterasdk/cio/edge/types.py index 5251848e..57f6e44d 100644 --- a/cterasdk/cio/edge/types.py +++ b/cterasdk/cio/edge/types.py @@ -9,8 +9,9 @@ class EdgePath(BasePath): """ Namespace = '/' - def __init__(self, reference): - super().__init__(EdgePath.Namespace, reference or '.') + @staticmethod + def from_context(reference): + return EdgePath(EdgePath.Namespace, reference) class EdgeResource(BaseResource): @@ -25,7 +26,6 @@ class EdgeResource(BaseResource): :ivar datetime.datetime last_modified: Last Modified :ivar str extension: Extension """ - Scheme = 'ctera-edge' def __init__(self, path, is_dir, size, created_at, last_modified): super().__init__(path.name, path, is_dir, size, last_modified) @@ -39,7 +39,7 @@ def decode_reference(href): @staticmethod def from_server_object(server_object): return EdgeResource( - EdgePath(EdgeResource.decode_reference(server_object.href)), + EdgePath.from_context(EdgeResource.decode_reference(server_object.href)), server_object.getcontenttype == 'httpd/unix-directory', server_object.getcontentlength, datetime.fromisoformat(server_object.creationdate), @@ -60,7 +60,7 @@ def resolve(path): return path.path if path is None or isinstance(path, str): - return EdgePath(path) + return EdgePath.from_context(path or '.') raise ValueError(f'Error: Could not resolve path: {path}. Type: {type(path)}') diff --git a/cterasdk/cli/__init__.py b/cterasdk/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cterasdk/cli/dav.py b/cterasdk/cli/dav.py new file mode 100644 index 00000000..95e14a4b --- /dev/null +++ b/cterasdk/cli/dav.py @@ -0,0 +1,163 @@ +import os +import sys +import asyncio +import argparse +from pathlib import Path +from ..core.enum import FileAccessMode +from ..lib.storage import asynfs, commonfs +from ..objects.asynchronous.invitation import AsyncInvitation +from ..exceptions.io.core import ListDirectoryError, GetMetadataError, ExternalShareError + + +async def handle_ls(args): + + attributes = [ + ("ID", 12), + ("VOLUME", 7), + ("SIZE", 10), + ("MODIFIED", 15), + ("DELETED", 8), + ("DIR", 4), + ("PATH", 50) + ] + + async with AsyncInvitation.from_uri(args.endpoint) as invitation: + func = invitation.files.walk if args.recursive else invitation.files.listdir + + rows = [ + ( + o.id, + o.volume.id, + o.size, + o.last_modified.strftime("%b %#d, %H:%M"), + 'true' if o.deleted else 'false', + 'd' if o.is_dir else 'f', + o.path.relative, + ) + async for o in func() + ] + + if rows: + formatter = " ".join(f"{{:<{width}}}" for _, width in attributes) + print(formatter.format(*(name for name, _ in attributes))) + for row in rows: + print(formatter.format(*row)) + + +async def handle_download(args): # pylint: disable=too-many-branches + target = Path(args.dest or os.getcwd()) + + try: + + Path.mkdir(target, exist_ok=True) + + 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)) + + async with AsyncInvitation.from_uri(args.endpoint) as invitation: + jobs = [] + + if not invitation.details.is_dir: # pylint: disable=too-many-nested-blocks + if args.src: + print(f"error: object not found error: '{args.src}'", file=sys.stderr) + sys.exit(1) + resource = await invitation.files.listdir().__anext__() # pylint: disable=unnecessary-dunder-call + jobs.append(download(invitation, resource, destination=target)) + else: + try: + resource = await invitation.files.properties(args.src) + if not resource.is_dir: + jobs.append(download(invitation, resource, destination=target)) + else: + enumerator = invitation.files.walk if args.recursive else invitation.files.listdir + objects = [r async for r in enumerator(args.src)] + if args.archive: + jobs.append(download(invitation, resource, objects, True, target.joinpath(args.src or invitation.invite))) + else: + for r in objects: + if r.is_dir: + await asynfs.mkdir(target.joinpath(r.path.relative), parents=True, exist_ok=True) + else: + jobs.append(download(invitation, r, destination=target)) + except (GetMetadataError, ListDirectoryError): + print(f"error: failed to obtain properties or list a directory: '{args.src}'", file=sys.stderr) + + if jobs: + await asyncio.gather(*jobs) + + except PermissionError: + print(f"error: permission denied: Cannot create files and folders at '{target}'.", file=sys.stderr) + except NotADirectoryError: + print(f"error: path conflict: '{target}' is a file, but a folder was expected.", file=sys.stderr) + except OSError as e: + print(f"error: an unexpected error occurred during download. {e}", file=sys.stderr) + + +async def handle_upload(args): + + for p in args.files: + if not commonfs.exists(p): + print(f"error: path not found: '{p}'", file=sys.stderr) + sys.exit(1) + + async with AsyncInvitation.from_uri(args.endpoint) as invitation: + if not invitation.details.is_dir: + print("error: destination is not a directory", file=sys.stderr) + sys.exit(1) + elif invitation.details.access not in [FileAccessMode.RW, FileAccessMode.UO]: + print(f"error: insufficient permissions to write to: {args.dest}", file=sys.stderr) + sys.exit(1) + + try: + destination = args.dest or '.' + if args.dest: + properties = await invitation.files.properties(args.dest) + if not properties.is_dir: + print(f"error: destination is not a directory: '{args.dest}'", file=sys.stderr) + sys.exit(1) + destination = properties.path.relative + + jobs = [] + for p in args.files: + jobs.append(invitation.files.upload_file(p, destination)) + + if jobs: + await asyncio.gather(*jobs) + + except (GetMetadataError, ExternalShareError): + print(f"error: failed to obtain properties for directory: '{args.dest}'", file=sys.stderr) + sys.exit(1) + + +async def main(): + parser = argparse.ArgumentParser(prog="cterasdk.io.dav") + subparsers = parser.add_subparsers(dest="command", required=True) + + ls = subparsers.add_parser("ls", help="list directory contents") + ls.add_argument("-e", "--endpoint", type=str, required=True, help="CTERA Portal endpoint, or external share") + ls.add_argument("-s", "--src", type=str, required=False, help="path to file or folder") + ls.add_argument("-R", "--recursive", action="store_true", help="list subdirectories recursively") + ls.set_defaults(func=handle_ls) + + download = subparsers.add_parser("download", help="download files or folders") + download.add_argument("-e", "--endpoint", type=str, required=True, help="CTERA Portal endpoint, or external share") + download.add_argument("-s", "--src", type=str, required=False, help="path to file or folder") + download.add_argument("-R", "--recursive", action="store_true", help="download sub-directories and files") + download.add_argument("-d", "--dest", type=str, help="destination path on local file system") + download.add_argument("-z", "--archive", action="store_true", help="download as zip archive (applicable to directories)") + download.set_defaults(func=handle_download) + + upload = subparsers.add_parser("upload", help="upload files") + upload.add_argument("files", type=str, nargs='+', help="one or more local files") + upload.add_argument("-e", "--endpoint", type=str, required=True, help="CTERA Portal endpoint, or external share") + upload.add_argument("-d", "--dest", type=str, required=False, help="destination folder") + upload.set_defaults(func=handle_upload) + + args = parser.parse_args() + await args.func(args) + + +def webdav(): + asyncio.run(main()) diff --git a/cterasdk/direct/cli.py b/cterasdk/cli/direct.py similarity index 95% rename from cterasdk/direct/cli.py rename to cterasdk/cli/direct.py index 9c424523..c80c41f4 100644 --- a/cterasdk/direct/cli.py +++ b/cterasdk/cli/direct.py @@ -9,7 +9,7 @@ from .. import settings from ..common import utils from ..lib.storage import commonfs -from .client import DirectIO +from ..direct.client import DirectIO from ..exceptions.direct import StreamError, DirectIOError from ..exceptions.transport import TLSError @@ -22,6 +22,7 @@ def validate_endpoint(endpoint): baseurl, port = yarl.URL(endpoint), 443 + assert baseurl.scheme in ('http', 'https'), 'Error: Endpoint scheme must be http or https.' logger.debug('Validating connection to host: %s on port: %s', baseurl.host, port) utils.tcp_connect(baseurl.host, port, timeout=5) return f'{baseurl}' diff --git a/cterasdk/core/buckets.py b/cterasdk/core/buckets.py index 74517da1..3f6fe725 100644 --- a/cterasdk/core/buckets.py +++ b/cterasdk/core/buckets.py @@ -102,7 +102,8 @@ def modify(self, current_name, new_name=None, read_only=None, dedicated_to=None, def list_buckets(self, include=None): """ List Buckets. - Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + :param list[str],optional include: List of fields to retrieve, defaults to ``['name']`` """ include = union(include or [], Buckets.default) diff --git a/cterasdk/core/enum.py b/cterasdk/core/enum.py index 45041b65..ed6809bd 100644 --- a/cterasdk/core/enum.py +++ b/cterasdk/core/enum.py @@ -4,9 +4,11 @@ class Context: :ivar str admin: Global admin context :ivar str ServicesPortal: Services Portal context + :ivar str Invitations: Invitations context """ admin = 'admin' ServicesPortal = 'ServicesPortal' + Invitations = 'invitations' class LogTopic: diff --git a/cterasdk/core/files/__init__.py b/cterasdk/core/files/__init__.py index 72af5ab3..f9d8a7a4 100644 --- a/cterasdk/core/files/__init__.py +++ b/cterasdk/core/files/__init__.py @@ -1 +1 @@ -from .browser import CloudDrive, Backups # noqa: E402, F401 +from .browser import CloudDrive, Backups, InvitationBrowser # noqa: E402, F401 diff --git a/cterasdk/core/files/browser.py b/cterasdk/core/files/browser.py index 2202ecc5..c02da87e 100644 --- a/cterasdk/core/files/browser.py +++ b/cterasdk/core/files/browser.py @@ -1,7 +1,8 @@ from .. import query from ...cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \ DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \ - Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink + Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink, GetExternalShareInfo +from ...cio.core.types import InvitationPath from ...lib.storage import commonfs from ..base_command import BaseCommand from . import io @@ -90,8 +91,7 @@ def properties(self, path): :rtype: cterasdk.cio.core.types.PortalResource :raises cterasdk.exceptions.io.core.GetMetadataError: Raised on error retrieving object metadata. """ - _, metadata = GetProperties(io.listdir, self._core, path, False).execute() - return metadata + return GetProperties(io.listdir, self._core, path, False).execute() def exists(self, path): """ @@ -353,3 +353,44 @@ def device_config(self, device, destination=None): """ 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() + + +class InvitationBrowser: + + def __init__(self, core): + self._invitation = InvitationPath.from_context(core.invite) + self._core = core + self._file_browser = CloudDrive(core) + + def listdir(self, path=None): + return self._file_browser.listdir(self._invitation.join(path)) + + def walk(self, path=None): + return self._file_browser.walk(self._invitation.join(path)) + + def properties(self, path): + return self._file_browser.properties(self._invitation.join(path)) + + def exists(self, path): + return self._file_browser.exists(self._invitation.join(path)) + + def mkdir(self, path): + return self._file_browser.mkdir(self._invitation.join(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 upload(self, destination, handle, name=None, size=None): + return self._file_browser.upload(self._invitation.join(destination), handle, name, size) + + def upload_file(self, path, destination): + return self._file_browser.upload_file(path, self._invitation.join(destination)) + + def details(self): + return GetExternalShareInfo(io.get_share_details, self._core, self._core.invite).execute() diff --git a/cterasdk/core/files/io.py b/cterasdk/core/files/io.py index dd222faf..48af06fd 100644 --- a/cterasdk/core/files/io.py +++ b/cterasdk/core/files/io.py @@ -73,3 +73,7 @@ def add_share_recipients(core, path, members): def public_link(core, param): return core.api.execute('', 'createShare', param) + + +def get_share_details(core, param): + return core.api.execute('', 'getShareDetails', param) diff --git a/cterasdk/core/invitation/__init__.py b/cterasdk/core/invitation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cterasdk/core/invitation/login.py b/cterasdk/core/invitation/login.py new file mode 100644 index 00000000..1afefe4a --- /dev/null +++ b/cterasdk/core/invitation/login.py @@ -0,0 +1,18 @@ +import logging +from ..base_command import BaseCommand + + +logger = logging.getLogger('cterasdk.core') + + +class Login(BaseCommand): + """ + Portal Login APIs + """ + + def login(self, key, value): + logger.info('Creating external session. %s', {'invite': value}) + self._core.clients.ctera.get('', params={key: value}) + + def logout(self): + """No logout for external users""" diff --git a/cterasdk/core/portals.py b/cterasdk/core/portals.py index b4338dbf..e4fe7c53 100644 --- a/cterasdk/core/portals.py +++ b/cterasdk/core/portals.py @@ -33,7 +33,8 @@ def get(self, name, include=None): def list_tenants(self, include=None, portal_type=None): """ List tenants. - Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] :param cterasdk.core.enum.PortalType portal_type: Portal type """ diff --git a/cterasdk/core/servers.py b/cterasdk/core/servers.py index d5a60924..0a4f19e3 100644 --- a/cterasdk/core/servers.py +++ b/cterasdk/core/servers.py @@ -46,7 +46,8 @@ def get(self, name, include=None): def list_servers(self, include=None): """ Retrieve the servers that comprise CTERA Portal. - Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + Restricted to the Global Administration Portal. Browse it using :py:func:`cterasdk.core.portals.browse_global_admin`. + :param list[str],optional include: List of fields to retrieve, defaults to ['name'] """ # browse administration diff --git a/cterasdk/core/types.py b/cterasdk/core/types.py index e6b740d2..a9827a66 100644 --- a/cterasdk/core/types.py +++ b/cterasdk/core/types.py @@ -889,3 +889,15 @@ def overwrite(apply_all=True): @staticmethod def rename(apply_all=True): return ConflictResolver(ConflictHandler.Rename, apply_all) + + +class PortalInvitation(Object): + + def __init__(self, access, is_dir): + super().__init__() + self.access = access + self.is_dir = is_dir + + @staticmethod + def from_server_object(server_object): + return PortalInvitation(server_object.mode, server_object.isDirectory) diff --git a/cterasdk/exceptions/io/core.py b/cterasdk/exceptions/io/core.py index a80df8e1..bc63ac6c 100644 --- a/cterasdk/exceptions/io/core.py +++ b/cterasdk/exceptions/io/core.py @@ -128,6 +128,18 @@ def __init__(self, filename): super().__init__(EREMOTEIO, 'Failed to retrieve collaboration-share metadata', filename) +class ExternalShareError(PathError): + + def __init__(self, filename): + super().__init__(EREMOTEIO, 'Failed to retrieve external share details', filename) + + +class InvalidShareError(PathError): + + def __init__(self, filename): + super().__init__(EREMOTEIO, 'This external share is invalid or has expired.', filename) + + class CreateLinkError(PathError): def __init__(self, filename): diff --git a/cterasdk/lib/session/base.py b/cterasdk/lib/session/base.py index 3cbfe1c5..d7ca1e74 100644 --- a/cterasdk/lib/session/base.py +++ b/cterasdk/lib/session/base.py @@ -6,11 +6,30 @@ from ...exceptions import CTERAException +class AccountType: + """ + Account Type + + :ivar str Internal: Internal + :ivar str External: External + """ + Internal = 'internal' + External = 'external' + + class BaseUser(Object): """Base User Account""" - def __init__(self, name, domain=None): + def __init__(self, type_of): super().__init__() + self.type = type_of + + +class InternalUser(BaseUser): + """Base User Account""" + + def __init__(self, name, domain=None): + super().__init__(AccountType.Internal) self.name = name self.domain = domain diff --git a/cterasdk/lib/session/core.py b/cterasdk/lib/session/core.py index efbb73c4..b1b72aee 100644 --- a/cterasdk/lib/session/core.py +++ b/cterasdk/lib/session/core.py @@ -1,11 +1,18 @@ import logging -from .base import BaseSession, BaseUser +from .base import BaseSession, BaseUser, AccountType, InternalUser from .types import Product from ...common import Object -from ...core.enum import Administrators +from ...core.enum import Administrators, Context -class PortalUser(BaseUser): +class ExternalUser(BaseUser): + """External User""" + + def __init__(self): + super().__init__(AccountType.External) + + +class PortalUser(InternalUser): """Local User""" def __init__(self, name, domain, tenant, role, authorizations): @@ -33,32 +40,34 @@ def __init__(self, address, context): self.context = context def _start_session(self, session): - logging.getLogger('cterasdk.core').debug('Starting Session.') - user_session = session.api.get('/currentSession') - current_tenant = session.api.get('/currentPortal') or Session.Administration - software_version = session.api.get('/version') - authorizations = session.roles.get(user_session.role) if user_session.role in Administrators else None - self._update_session(user_session, current_tenant, software_version, authorizations) + account = None + if session.context != Context.Invitations: + logging.getLogger('cterasdk.core').debug('Starting Session.') + user_session = session.api.get('/currentSession') + current_tenant = session.api.get('/currentPortal') or Session.Administration + software_version = session.api.get('/version') + authorizations = session.roles.get(user_session.role) if user_session.role in Administrators else None + account = PortalUser(user_session.username, user_session.domain, current_tenant or Session.Administration, + user_session.role, authorizations) + self._update_software_version(software_version) + else: + account = ExternalUser() + self._update_account(account) async def _async_start_session(self, session): logging.getLogger('cterasdk.core').debug('Starting Session.') - user_session = await session.v1.api.get('/currentSession') - current_tenant = session.v1.api.get('/currentPortal') - software_version = session.v1.api.get('/version') - authorizations = await session.roles.get(user_session.role) if user_session.role in Administrators else None - self._update_session(user_session, await current_tenant or Session.Administration, await software_version, authorizations) - - def _update_session(self, user_session, current_tenant, software_version, authorizations): - self._update_account( - PortalUser( - user_session.username, - user_session.domain, - current_tenant, - user_session.role, - authorizations - ) - ) - self._update_software_version(software_version) + account = None + if session.context != Context.Invitations: + user_session = await session.v1.api.get('/currentSession') + current_tenant = session.v1.api.get('/currentPortal') + software_version = session.v1.api.get('/version') + authorizations = await session.roles.get(user_session.role) if user_session.role in Administrators else None + account = PortalUser(user_session.username, user_session.domain, await current_tenant or Session.Administration, + user_session.role, authorizations) + self._update_software_version(await software_version) + else: + account = ExternalUser() + self._update_account(account) def _stop_session(self): # pylint: disable=no-self-use logging.getLogger('cterasdk.core').debug('Stopping Session.') diff --git a/cterasdk/lib/session/edge.py b/cterasdk/lib/session/edge.py index 4e351c92..8b9f0a87 100644 --- a/cterasdk/lib/session/edge.py +++ b/cterasdk/lib/session/edge.py @@ -1,5 +1,5 @@ import logging -from .base import BaseSession, BaseUser +from .base import BaseSession, InternalUser from .types import Product from ...common import Object @@ -15,11 +15,11 @@ def __init__(self, remote, source=None): self.source = source -class LocalUser(BaseUser): +class LocalUser(InternalUser): """Local User""" -class RemoteUser(BaseUser): +class RemoteUser(InternalUser): """Remote User""" def __init__(self, name, domain, tenant): diff --git a/cterasdk/lib/storage/asynfs.py b/cterasdk/lib/storage/asynfs.py index 48a4ad0e..ac9c322e 100644 --- a/cterasdk/lib/storage/asynfs.py +++ b/cterasdk/lib/storage/asynfs.py @@ -1,6 +1,7 @@ import logging import aiofiles -from .commonfs import write_new_version, ResultContext +import aiofiles.os +from .commonfs import write_new_version, ResultContext, expanduser logger = logging.getLogger('cterasdk.filesystem') @@ -36,3 +37,24 @@ async def overwrite(p, handle): async for chunk in handle.a_iter_content(chunk_size=8192): await fd.write(chunk) return p.as_posix() + + +async def mkdir(p, parents=False, exist_ok=True): + """ + Create a directory using aiofiles. + + :param str p: The path to create (string or Path object). + :param bool,optional parents: Create a path, including all parent directories + :param bool,optional exist_ok: Suppress error if directory exists + :return: Path + :rtype: str + :raises FileExistsError: If ``exist_ok`` is ``False`` and directory exists + """ + location = expanduser(p) + + if parents: + await aiofiles.os.makedirs(location, exist_ok=exist_ok) + else: + await aiofiles.os.mkdir(location, exist_ok=exist_ok) + + return location.as_posix() diff --git a/cterasdk/lib/storage/synfs.py b/cterasdk/lib/storage/synfs.py index 38814755..08bbdd12 100644 --- a/cterasdk/lib/storage/synfs.py +++ b/cterasdk/lib/storage/synfs.py @@ -1,5 +1,5 @@ import logging -from .commonfs import write_new_version, ResultContext +from .commonfs import write_new_version, ResultContext, expanduser logger = logging.getLogger('cterasdk.filesystem') @@ -36,3 +36,19 @@ def overwrite(p, handle): fd.write(chunk) logger.debug('Wrote: %s', p.as_posix()) return p.as_posix() + + +def mkdir(p, parents=False, exist_ok=True): + """ + Create a directory. + + :param str p: The path to create (string or Path object). + :param bool,optional parents: Create a path, including all parent directories + :param bool,optional exist_ok: Suppress error if directory exists + :return: Path + :rtype: str + :raises FileExistsError: If ``exist_ok`` is ``False`` and directory exists + """ + location = expanduser(p) + location.mkdir(parents=parents, exist_ok=exist_ok) + return location.as_posix() diff --git a/cterasdk/objects/asynchronous/invitation.py b/cterasdk/objects/asynchronous/invitation.py new file mode 100644 index 00000000..66d28033 --- /dev/null +++ b/cterasdk/objects/asynchronous/invitation.py @@ -0,0 +1,53 @@ +from .. import invitation +from ...clients import clients +from ..endpoints import EndpointBuilder +from .core import AsyncPortal +from ..asynchronous.core import files +from ...asynchronous.core.invitation import login + + +class AsyncInvitation(AsyncPortal): + + async def __aenter__(self): + await self.login() + return self + + def __init__(self, host, port, invite): + super().__init__(host, port) + self.invite = invite + self.clients.v1.api = self.default.clone(clients.AsyncAPI, EndpointBuilder.new(self.base, + self.context, f'/portalInvitation/share/{invite}')) + self.clients.io._webdav = self.default.clone(clients.AsyncWebDAV, EndpointBuilder.new(self.base, + self.context, f'/webdav/share/{invite}')) + self.clients.io._upload = self.default.clone(clients.AsyncUpload, EndpointBuilder.new(self.base, + self.context, f'/upload/share/{invite}')) + self.files = files.InvitationBrowser(self) + self.details = None + + @property + def context(self): + return 'invitations' + + def _authenticator(self, url): # pylint: disable=unused-argument + return True + + async def login(self): # pylint: disable=arguments-differ + await super().login('share', self.invite) + self.details = await self.files.details() + + @property + def uri(self): + return f'{self.clients.v1.ctera.baseurl}/?share={self.invite}' + + @property + def _login_object(self): + return login.Login(self) + + @staticmethod + def from_uri(uri): + host, port, invite = invitation.validate(uri) + return AsyncInvitation(host, port, invite) + + async def __aexit__(self, exc_type, exc, tb): + await self.logout() + return await super().__aexit__(exc_type, exc, tb) diff --git a/cterasdk/objects/invitation.py b/cterasdk/objects/invitation.py new file mode 100644 index 00000000..7667ddec --- /dev/null +++ b/cterasdk/objects/invitation.py @@ -0,0 +1,21 @@ +from .uri import components, parse_qsl + + +def validate(resource): + r = components(resource) + + assert r.scheme == "https", f"Error: Expected 'https' scheme, got '{r.scheme}'." + assert r.netloc, "Error: Could not identify network location." + assert r.path == "/invitations/", f"Error: Expected '/invitations/' path, got '{r.path}'." + + parameters = parse_qsl(r.query) + + if parameters and parameters[0][0] == "share": + invite = parameters[0][1] + + assert invite, "Error: Invitation identifier not found." + assert invite.isalnum(), f"Invitation identifier '{invite}' must be alphanumeric." + + return r.hostname, r.port, invite + + raise ValueError("Error: Could not find invitation identifer.") diff --git a/cterasdk/objects/services.py b/cterasdk/objects/services.py index 12db6dce..fe5e1571 100644 --- a/cterasdk/objects/services.py +++ b/cterasdk/objects/services.py @@ -1,5 +1,4 @@ from abc import abstractmethod - from . import endpoints, uri from .utils import URI from ..clients import clients diff --git a/cterasdk/objects/synchronous/invitation.py b/cterasdk/objects/synchronous/invitation.py new file mode 100644 index 00000000..35be9b8b --- /dev/null +++ b/cterasdk/objects/synchronous/invitation.py @@ -0,0 +1,53 @@ +from .. import invitation +from ...clients import clients +from ..endpoints import EndpointBuilder +from .core import Portal +from ...core import files +from ...core.invitation import login + + +class Invitation(Portal): + + def __enter__(self): + self.login() + return self + + def __init__(self, host, port, invite): + super().__init__(host, port) + self.invite = invite + self.clients.api = self.default.clone(clients.API, EndpointBuilder.new(self.base, + self.context, f'/portalInvitation/share/{invite}')) + self.clients.io._webdav = self.default.clone(clients.WebDAV, EndpointBuilder.new(self.base, + self.context, f'/webdav/share/{invite}')) + self.clients.io._upload = self.default.clone(clients.Upload, EndpointBuilder.new(self.base, + self.context, f'/upload/share/{invite}')) + self.files = files.InvitationBrowser(self) + self.details = None + + @property + def context(self): + return 'invitations' + + def _authenticator(self, url): # pylint: disable=unused-argument + return True + + def login(self): # pylint: disable=arguments-differ + super().login('share', self.invite) + self.details = self.files.details() + + @property + def uri(self): + return f'{self.clients.ctera.baseurl}/?share={self.invite}' + + @property + def _login_object(self): + return login.Login(self) + + @staticmethod + def from_uri(uri): + host, port, invite = invitation.validate(uri) + return Invitation(host, port, invite) + + def __exit__(self, exc_type, exc_value, exc_tb): + self.logout() + return super().__exit__(exc_type, exc_value, exc_tb) diff --git a/docs/source/UserGuides/CLI/Index.rst b/docs/source/UserGuides/CLI/Index.rst new file mode 100644 index 00000000..2b6aeaff --- /dev/null +++ b/docs/source/UserGuides/CLI/Index.rst @@ -0,0 +1,102 @@ +============= +CTERA SDK CLI +============= + +The CTERA Python SDK includes a set of built-in executable commands that are +installed automatically as part of the SDK package. These commands provide +direct access to common SDK functionality through a command-line interface. + +The following page describes the supported CLI commands and their usage. + +CTERA Direct/IO Download +------------------------ + +The ``cterasdk.io.direct.download`` command downloads a file using **CTERA Direct I/O**. + +The download is performed using the file's unique numeric **File ID** and +stores the file at a specified local path. Authentication can be performed +using either an Access/Secret key pair or a Bearer token. + +Arguments +^^^^^^^^^ + +``--endpoint`` / ``-e`` + CTERA Portal address (for example: ``corp.acme.ctera.com``). + +``--path`` / ``-p`` + Local destination path where the downloaded file will be saved + (for example: ``./download.zip``). + +``--file-id`` / ``-f`` + Numeric identifier of the file to download. + +``--access`` / ``-a`` + Access key used for authentication (optional). + +``--secret`` / ``-s`` + Secret key used together with the access key (optional). + +``--bearer`` / ``-b`` + Bearer authentication token (optional). + +``--no-verify-ssl`` / ``-k`` + Disable SSL certificate verification. Intended for testing or + environments using self-signed certificates. + +``--debug`` / ``-d`` + Enable verbose debug logging output. + +Examples +^^^^^^^^ + +Download a file using access and secret keys: + +**Bash (Linux / macOS)** + +.. code-block:: bash + + cterasdk.io.direct.download \ + --endpoint corp.acme.ctera.com \ + --file-id 847362915 \ + --path "./Statement.pdf" \ + --access AKIA7F3K9X2QPLM8D4ZT \ + --secret wJalrXUtnFEMI/K7MDENG/bPxRfiCY8zEXAMPLEKEY + +**Windows Command Prompt (cmd.exe)** + +.. code-block:: bash + + cterasdk.io.direct.download ^ + --endpoint corp.acme.ctera.com ^ + --file-id 847362915 ^ + --path ".\Statement.pdf" ^ + --access AKIA7F3K9X2QPLM8D4ZT ^ + --secret wJalrXUtnFEMI/K7MDENG/bPxRfiCY8zEXAMPLEKEY + + +CTERA Portal Invitation Browser +------------------------------- + +CLI to interact with external shares on CTERA Portal. + +.. code-block:: bash + + cterasdk.io.dav {ls,download,upload} [options] + +Commands +-------- + +Listing files and folders +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``cterasdk.io.dav ls -e ENDPOINT [-s SRC] [-R]`` + +Downloading files and folders +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``cterasdk.io.dav download -e ENDPOINT [-s SRC] [-R] [-d DEST] [-z]`` + +Uploading files +^^^^^^^^^^^^^^^ + +``cterasdk.io.dav upload -e ENDPOINT [-d DEST] files [files ...]`` diff --git a/docs/source/UserGuides/DataServices/DirectIO.rst b/docs/source/UserGuides/ContentServices/DirectIO.rst similarity index 98% rename from docs/source/UserGuides/DataServices/DirectIO.rst rename to docs/source/UserGuides/ContentServices/DirectIO.rst index 0f35d6df..e8009cb7 100644 --- a/docs/source/UserGuides/DataServices/DirectIO.rst +++ b/docs/source/UserGuides/ContentServices/DirectIO.rst @@ -43,7 +43,7 @@ Getting Started =============== In this example, a file is downloaded using its unique ID (e.g., ``12345``) and written to disk as ``example.pdf``. -For more information on how to obtain the File ID, name, refer to the `Event `_ object returned by the `Notification Service `_. +For more information on how to obtain the File ID, name, refer to the `Event `_ object returned by the `Notification Service `_. .. code-block:: python diff --git a/docs/source/UserGuides/ContentServices/Index.rst b/docs/source/UserGuides/ContentServices/Index.rst new file mode 100644 index 00000000..f113d91c --- /dev/null +++ b/docs/source/UserGuides/ContentServices/Index.rst @@ -0,0 +1,9 @@ +====================== +CTERA Content Services +====================== +.. toctree:: + :caption: CTERA Content Services + :maxdepth: 5 + + NotificationService + DirectIO diff --git a/docs/source/UserGuides/DataServices/NotificationService.rst b/docs/source/UserGuides/ContentServices/NotificationService.rst similarity index 100% rename from docs/source/UserGuides/DataServices/NotificationService.rst rename to docs/source/UserGuides/ContentServices/NotificationService.rst diff --git a/docs/source/UserGuides/DataServices/Index.rst b/docs/source/UserGuides/DataServices/Index.rst deleted file mode 100644 index 2839c60d..00000000 --- a/docs/source/UserGuides/DataServices/Index.rst +++ /dev/null @@ -1,9 +0,0 @@ -=================== -CTERA Data Services -=================== -.. toctree:: - :caption: CTERA Data Services - :maxdepth: 5 - - NotificationService - DirectIO diff --git a/docs/source/api/cterasdk.objects.asynchronous.core.rst b/docs/source/api/cterasdk.objects.asynchronous.core.rst new file mode 100644 index 00000000..8f2dafcd --- /dev/null +++ b/docs/source/api/cterasdk.objects.asynchronous.core.rst @@ -0,0 +1,7 @@ +cterasdk.objects.asynchronous.core module +========================================= + +.. automodule:: cterasdk.objects.asynchronous.core + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.objects.asynchronous.drive.rst b/docs/source/api/cterasdk.objects.asynchronous.drive.rst new file mode 100644 index 00000000..1a7e1b62 --- /dev/null +++ b/docs/source/api/cterasdk.objects.asynchronous.drive.rst @@ -0,0 +1,7 @@ +cterasdk.objects.asynchronous.drive module +========================================== + +.. automodule:: cterasdk.objects.asynchronous.drive + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.objects.asynchronous.edge.rst b/docs/source/api/cterasdk.objects.asynchronous.edge.rst new file mode 100644 index 00000000..f964e1a7 --- /dev/null +++ b/docs/source/api/cterasdk.objects.asynchronous.edge.rst @@ -0,0 +1,7 @@ +cterasdk.objects.asynchronous.edge module +========================================= + +.. automodule:: cterasdk.objects.asynchronous.edge + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.objects.asynchronous.invitation.rst b/docs/source/api/cterasdk.objects.asynchronous.invitation.rst new file mode 100644 index 00000000..95aefcf4 --- /dev/null +++ b/docs/source/api/cterasdk.objects.asynchronous.invitation.rst @@ -0,0 +1,7 @@ +cterasdk.objects.asynchronous.invitation module +=============================================== + +.. automodule:: cterasdk.objects.asynchronous.invitation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.objects.asynchronous.rst b/docs/source/api/cterasdk.objects.asynchronous.rst new file mode 100644 index 00000000..f3639496 --- /dev/null +++ b/docs/source/api/cterasdk.objects.asynchronous.rst @@ -0,0 +1,16 @@ +cterasdk.objects.asynchronous package +===================================== + +.. automodule:: cterasdk.objects.asynchronous + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + cterasdk.objects.asynchronous.edge + cterasdk.objects.asynchronous.core + cterasdk.objects.asynchronous.invitation diff --git a/docs/source/api/cterasdk.objects.rst b/docs/source/api/cterasdk.objects.rst index 41802078..f5c46691 100644 --- a/docs/source/api/cterasdk.objects.rst +++ b/docs/source/api/cterasdk.objects.rst @@ -11,7 +11,6 @@ Submodules .. toctree:: - cterasdk.objects.synchronous.drive - cterasdk.objects.synchronous.edge - cterasdk.objects.synchronous.core + cterasdk.objects.asynchronous + cterasdk.objects.synchronous diff --git a/docs/source/api/cterasdk.objects.synchronous.invitation.rst b/docs/source/api/cterasdk.objects.synchronous.invitation.rst new file mode 100644 index 00000000..bc2b10f3 --- /dev/null +++ b/docs/source/api/cterasdk.objects.synchronous.invitation.rst @@ -0,0 +1,7 @@ +cterasdk.objects.synchronous.invitation module +============================================== + +.. automodule:: cterasdk.objects.synchronous.invitation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/cterasdk.objects.synchronous.rst b/docs/source/api/cterasdk.objects.synchronous.rst new file mode 100644 index 00000000..897591a5 --- /dev/null +++ b/docs/source/api/cterasdk.objects.synchronous.rst @@ -0,0 +1,17 @@ +cterasdk.objects.synchronous package +==================================== + +.. automodule:: cterasdk.objects.synchronous + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + cterasdk.objects.synchronous.drive + cterasdk.objects.synchronous.edge + cterasdk.objects.synchronous.core + cterasdk.objects.synchronous.invitation diff --git a/docs/source/index.rst b/docs/source/index.rst index 947f2608..00e0c21c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,8 +18,8 @@ Key Features 2. Managing `Data Discovery and Migration Jobs `_ 3. File Data Management APIs for `CTERA Edge Filer `_ and `CTERA Portal `_. 4. Data Services Framework: - 1. `Subscribing to File System Notifications `_ - 2. `CTERA Direct IO `_, enabling parallel high-performnace retrieval directly from object storage. + 1. `Subscribing to File System Notifications `_ + 2. `CTERA Direct IO `_, enabling parallel high-performnace retrieval directly from object storage. .. _cterasdk-installation: @@ -147,7 +147,8 @@ Table of Contents UserGuides/Edge/Index UserGuides/Portal/Index - UserGuides/DataServices/Index + UserGuides/ContentServices/Index + UserGuides/CLI/Index UserGuides/Miscellaneous/Index api/cterasdk diff --git a/pyproject.toml b/pyproject.toml index ee178348..73b43ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,8 @@ Documentation = "https://ctera-python-sdk.readthedocs.io/en/latest/" "Source Code" = "https://github.com/ctera/ctera-python-sdk" [project.scripts] -"cterasdk.io.direct.download" = "cterasdk.direct.cli:download_object" +"cterasdk.io.direct.download" = "cterasdk.cli.direct:download_object" +"cterasdk.io.dav" = "cterasdk.cli.dav:webdav" [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]}