Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion cterasdk/asynchronous/core/files/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .browser import CloudDrive # noqa: E402, F401
from .browser import CloudDrive, InvitationBrowser # noqa: E402, F401
46 changes: 44 additions & 2 deletions cterasdk/asynchronous/core/files/browser.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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()
4 changes: 4 additions & 0 deletions cterasdk/asynchronous/core/files/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Empty file.
18 changes: 18 additions & 0 deletions cterasdk/asynchronous/core/invitation/login.py
Original file line number Diff line number Diff line change
@@ -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"""
10 changes: 6 additions & 4 deletions cterasdk/cio/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -92,15 +92,17 @@ 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):
return self.reference.parts

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")
Expand Down
60 changes: 45 additions & 15 deletions cterasdk/cio/core/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
61 changes: 47 additions & 14 deletions cterasdk/cio/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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}')


Expand All @@ -84,25 +86,55 @@ 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):
"""
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):
Expand All @@ -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
Expand All @@ -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)}')

Expand All @@ -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)
Expand Down
Loading
Loading