diff --git a/blender_cloud/blender.py b/blender_cloud/blender.py index af3fcef..b425d6f 100644 --- a/blender_cloud/blender.py +++ b/blender_cloud/blender.py @@ -11,6 +11,9 @@ from bpy.props import StringProperty from . import pillar, gui +PILLAR_SERVER_URL = 'https://cloudapi.blender.org/' +# PILLAR_SERVER_URL = 'http://localhost:5000/' + ADDON_NAME = 'blender_cloud' log = logging.getLogger(__name__) @@ -23,8 +26,8 @@ class BlenderCloudPreferences(AddonPreferences): pillar_server = bpy.props.StringProperty( name='Blender Cloud Server', description='URL of the Blender Cloud backend server', - default='https://cloudapi.blender.org/', - get=lambda self: 'https://cloudapi.blender.org/' + default=PILLAR_SERVER_URL, + get=lambda self: PILLAR_SERVER_URL ) # TODO: Move to the Scene properties? @@ -125,24 +128,16 @@ class PillarCredentialsUpdate(Operator): self.report({'ERROR'}, 'No active profile found') return {'CANCELLED'} - endpoint = preferences().pillar_server.rstrip('/') - - # Create a subclient token and send it to Pillar. - try: - blender_id.create_subclient_token(pillar.SUBCLIENT_ID, endpoint) - except blender_id.BlenderIdCommError as ex: - log.exception('Error sending subclient-specific token to Blender ID') - self.report({'ERROR'}, 'Failed to sync Blender ID to %s' % endpoint) - return {'CANCELLED'} - - # Test the new URL - pillar._pillar_api = None try: loop = asyncio.get_event_loop() - loop.run_until_complete(pillar.get_project_uuid('textures')) # Any query will do. + loop.run_until_complete(pillar.refresh_pillar_credentials()) + except blender_id.BlenderIdCommError as ex: + log.exception('Error sending subclient-specific token to Blender ID') + self.report({'ERROR'}, 'Failed to sync Blender ID to Blender Cloud') + return {'CANCELLED'} except Exception as ex: log.exception('Error in test call to Pillar') - self.report({'ERROR'}, 'Failed test connection to %s' % endpoint) + self.report({'ERROR'}, 'Failed test connection to Blender Cloud') return {'CANCELLED'} self.report({'INFO'}, 'Blender Cloud credentials & endpoint URL updated.') diff --git a/blender_cloud/gui.py b/blender_cloud/gui.py index dd3a74e..8b0436b 100644 --- a/blender_cloud/gui.py +++ b/blender_cloud/gui.py @@ -182,7 +182,7 @@ class BlenderCloudBrowser(bpy.types.Operator): _draw_handle = None - _state = 'BROWSING' + _state = 'INITIALIZING' project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID node = None # The Node object we're currently showing, or None if we're at the project top. @@ -229,7 +229,7 @@ class BlenderCloudBrowser(bpy.types.Operator): self.current_display_content = [] self.loaded_images = set() - self.browse_assets() + self.check_credentials() context.window_manager.modal_handler_add(self) self.timer = context.window_manager.event_timer_add(1 / 30, context.window) @@ -285,6 +285,35 @@ class BlenderCloudBrowser(bpy.types.Operator): return {'RUNNING_MODAL'} + def check_credentials(self): + self._state = 'CHECKING_CREDENTIALS' + self.log.debug('Checking credentials') + self._new_async_task(self._check_credentials()) + + async def _check_credentials(self): + """Checks credentials with Pillar, and if ok goes to the BROWSING state.""" + + try: + await pillar.check_pillar_credentials() + except pillar.CredentialsNotSyncedError: + self.log.info('Credentials not synced, re-syncing automatically.') + else: + self.log.info('Credentials okay, browsing assets.') + await self.async_download_previews() + return + + try: + await pillar.refresh_pillar_credentials() + except pillar.UserNotLoggedInError: + self.error('User not logged in on Blender ID.') + else: + self.log.info('Credentials refreshed and ok, browsing assets.') + await self.async_download_previews() + return + + raise pillar.UserNotLoggedInError() + # self._new_async_task(self._check_credentials()) + def descend_node(self, node): """Descends the node hierarchy by visiting this node. @@ -386,7 +415,10 @@ class BlenderCloudBrowser(bpy.types.Operator): else: raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid) - async def async_download_previews(self, thumbnails_directory): + async def async_download_previews(self): + self._state = 'BROWSING' + + thumbnails_directory = self.thumbnails_cache self.log.info('Asynchronously downloading previews to %r', thumbnails_directory) self.clear_images() @@ -396,7 +428,8 @@ class BlenderCloudBrowser(bpy.types.Operator): def thumbnail_loaded(node, file_desc, thumb_path): self.update_menu_item(node, file_desc, thumb_path, file_desc['filename']) - # Download either by group_texture node UUID or by project UUID (which shows all top-level nodes) + # Download either by group_texture node UUID or by project UUID (which + # shows all top-level nodes) if self.node_uuid: self.log.debug('Getting subnodes for parent node %r', self.node_uuid) children = await pillar.get_nodes(parent_node_uuid=self.node_uuid, @@ -435,9 +468,8 @@ class BlenderCloudBrowser(bpy.types.Operator): future=self.signalling_future) def browse_assets(self): - self._state = 'BROWSING' self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid) - self._new_async_task(self.async_download_previews(self.thumbnails_cache)) + self._new_async_task(self.async_download_previews()) def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future=None): """Stops the currently running async task, and starts another one.""" @@ -457,6 +489,7 @@ class BlenderCloudBrowser(bpy.types.Operator): """Draws the GUI with OpenGL.""" drawers = { + 'CHECKING_CREDENTIALS': self._draw_checking_credentials, 'BROWSING': self._draw_browser, 'DOWNLOADING_TEXTURE': self._draw_downloading, 'EXCEPTION': self._draw_exception, @@ -531,14 +564,24 @@ class BlenderCloudBrowser(bpy.types.Operator): def _draw_downloading(self, context): """OpenGL drawing code for the DOWNLOADING_TEXTURE state.""" - content_height, content_width = self._window_size(context) + self._draw_text_on_colour(context, + 'Downloading texture from Blender Cloud', + (0.0, 0.0, 0.2, 0.6)) + def _draw_checking_credentials(self, context): + """OpenGL drawing code for the CHECKING_CREDENTIALS state.""" + + self._draw_text_on_colour(context, + 'Checking login credentials', + (0.0, 0.0, 0.2, 0.6)) + + def _draw_text_on_colour(self, context, text, bgcolour): + content_height, content_width = self._window_size(context) bgl.glEnable(bgl.GL_BLEND) - bgl.glColor4f(0.0, 0.0, 0.2, 0.6) + bgl.glColor4f(*bgcolour) bgl.glRectf(0, 0, content_width, content_height) font_id = 0 - text = "Downloading texture from Blender Cloud" bgl.glColor4f(1.0, 1.0, 1.0, 1.0) blf.size(font_id, 20, 72) text_width, text_height = blf.dimensions(font_id, text) diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 105cc9f..193a444 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -34,6 +34,13 @@ class UserNotLoggedInError(RuntimeError): return 'UserNotLoggedInError' +class CredentialsNotSyncedError(UserNotLoggedInError): + """Raised when the user may be logged in on Blender ID, but has no Blender Cloud token.""" + + def __str__(self): + return 'CredentialsNotSyncedError' + + class PillarError(RuntimeError): """Raised when there is some issue with the communication with Pillar. @@ -118,7 +125,7 @@ def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api: subclient = profile.subclients.get(SUBCLIENT_ID) if not subclient: - raise UserNotLoggedInError() + raise CredentialsNotSyncedError() if _pillar_api is None: # Allow overriding the endpoint before importing Blender-specific stuff. @@ -130,7 +137,7 @@ def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api: _pillar_api = pillarsdk.Api(endpoint=pillar_endpoint, username=subclient['subclient_user_id'], - password=None, + password=SUBCLIENT_ID, token=subclient['token']) return _pillar_api @@ -148,6 +155,51 @@ async def pillar_call(pillar_func, *args, **kwargs): return await loop.run_in_executor(None, partial) +async def check_pillar_credentials(): + """Tries to obtain the user at Pillar using the user's credentials. + + :raises UserNotLoggedInError: when the user is not logged in on Blender ID. + :raises CredentialsNotSyncedError: when the user is logged in on Blender ID but + doesn't have a valid subclient token for Pillar. + """ + + profile = blender_id_profile() + if not profile: + raise UserNotLoggedInError() + + subclient = profile.subclients.get(SUBCLIENT_ID) + if not subclient: + raise CredentialsNotSyncedError() + + try: + await get_project_uuid('textures') # Any query will do. + except pillarsdk.UnauthorizedAccess: + raise CredentialsNotSyncedError() + + +async def refresh_pillar_credentials(): + """Refreshes the authentication token on Pillar. + + :raises blender_id.BlenderIdCommError: when Blender ID refuses to send a token to Pillar. + :raises Exception: when the Pillar credential check fails. + """ + + global _pillar_api + + import blender_id + + from . import blender + pillar_endpoint = blender.preferences().pillar_server.rstrip('/') + + # Create a subclient token and send it to Pillar. + # May raise a blender_id.BlenderIdCommError + blender_id.create_subclient_token(SUBCLIENT_ID, pillar_endpoint) + + # Test the new URL + _pillar_api = None + await get_project_uuid('textures') # Any query will do. + + async def get_project_uuid(project_url: str) -> str: """Returns the UUID for the project, given its '/p/' string.""" diff --git a/requirements.txt b/requirements.txt index 3423789..1c8c9d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # Primary requirements: CacheControl==0.11.6 lockfile==0.12.2 -pillarsdk==0.1.0 +pillarsdk==0.1.1 wheel==0.29.0 # Secondary requirements: