From 791b3f480c94b6bbb1bcb60c3f8ba84823a4d945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 16 Jun 2016 17:19:49 +0200 Subject: [PATCH] Uploading setting files to home project works. --- blender_cloud/pillar.py | 65 ++++++++++++++++++++++++--- blender_cloud/settings_sync.py | 80 ++++++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 007eac6..0f95cac 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -4,6 +4,7 @@ import os import functools import logging from contextlib import closing, contextmanager +import urllib.parse import pathlib import requests @@ -109,6 +110,21 @@ def blender_id_profile() -> 'blender_id.BlenderIdProfile': return blender_id.get_active_profile() +def blender_id_subclient() -> dict: + """Returns the subclient dict, containing the 'subclient_user_id' and 'token' keys.""" + + profile = blender_id_profile() + if not profile: + raise UserNotLoggedInError() + + subclient = profile.subclients.get(SUBCLIENT_ID) + if not subclient: + raise CredentialsNotSyncedError() + + return subclient + + + def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api: """Returns the Pillar SDK API object for the current user. @@ -121,13 +137,7 @@ def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api: global _pillar_api # Only return the Pillar API object if the user is still logged in. - profile = blender_id_profile() - if not profile: - raise UserNotLoggedInError() - - subclient = profile.subclients.get(SUBCLIENT_ID) - if not subclient: - raise CredentialsNotSyncedError() + subclient = blender_id_subclient() if _pillar_api is None: # Allow overriding the endpoint before importing Blender-specific stuff. @@ -589,6 +599,47 @@ async def download_texture(texture_node, return await asyncio.gather(*downloaders, return_exceptions=True) +async def upload_file(project_id: str, file_path: pathlib.Path, *, + future: asyncio.Future) -> str: + """Uploads a file to the Blender Cloud, returning a file document ID.""" + + from .blender import PILLAR_SERVER_URL + + loop = asyncio.get_event_loop() + url = urllib.parse.urljoin(PILLAR_SERVER_URL, '/storage/stream/%s' % project_id) + + # Upload the file in a different thread. + def upload(): + auth_token = blender_id_subclient()['token'] + + with file_path.open(mode='rb') as infile: + return uncached_session.post(url, + files={'file': infile}, + auth=(auth_token, SUBCLIENT_ID)) + + # Check for cancellation even before we start our POST request + if is_cancelled(future): + log.debug('Uploading was cancelled before doing the POST') + raise asyncio.CancelledError('Uploading was cancelled') + + log.debug('Performing POST %s', url) + response = await loop.run_in_executor(None, upload) + log.debug('Status %i from POST %s', response.status_code, url) + response.raise_for_status() + + resp = response.json() + log.debug('Upload response: %s', resp) + + try: + file_id = resp['file_id'] + except KeyError: + log.error('No file ID in upload response: %s', resp) + raise PillarError('No file ID in upload response: %s' % resp) + + log.info('Uploaded %s to file ID %s', file_path, file_id) + return file_id + + def is_cancelled(future: asyncio.Future) -> bool: # assert future is not None # for debugging purposes. cancelled = future is not None and future.cancelled() diff --git a/blender_cloud/settings_sync.py b/blender_cloud/settings_sync.py index 67e2acc..a8b0683 100644 --- a/blender_cloud/settings_sync.py +++ b/blender_cloud/settings_sync.py @@ -3,11 +3,15 @@ import asyncio import logging import bpy +import pathlib + import pillarsdk from pillarsdk import exceptions as sdk_exceptions -from .pillar import pillar_call, check_pillar_credentials, PillarError +from .pillar import pillar_call, check_pillar_credentials, PillarError, upload_file from . import async_loop +SETTINGS_FILES_TO_UPLOAD = ['bookmarks.txt', 'recent-files.txt', 'userpref.blend', 'startup.blend'] + HOME_PROJECT_ENDPOINT = '/bcloud/home-project' SYNC_GROUP_NODE_NAME = 'Blender Sync' SYNC_GROUP_NODE_DESC = 'The [Blender Cloud Addon](https://cloud.blender.org/services' \ @@ -66,24 +70,60 @@ class PILLAR_OT_sync(async_loop.AsyncModalOperatorMixin, bpy.types.Operator): return {'PASS_THROUGH'} async def async_execute(self): - self.user_id = await check_pillar_credentials() + """Entry point of the asynchronous operator.""" + try: - group_id = await self.find_sync_group_id() - self.log.info('Found group node ID: %s', group_id) - except sdk_exceptions.ForbiddenAccess as ex: - self.log.exception('Unable to find Group ID') + self.user_id = await check_pillar_credentials() + try: + self.home_project_id = await get_home_project_id() + except sdk_exceptions.ForbiddenAccess: + self.log.exception('Forbidden access to home project.') + self.report({'ERROR'}, 'Did not get access to home project.') + self._state = 'QUIT' + return + + try: + self.sync_group_id = await self.find_sync_group_id() + self.log.info('Found group node ID: %s', self.sync_group_id) + except sdk_exceptions.ForbiddenAccess: + self.log.exception('Unable to find Group ID') + self.report({'ERROR'}, 'Unable to find sync folder.') + self._state = 'QUIT' + return + + if self.action == 'PUSH': + await self.action_push() + else: + self.report({'ERROR'}, 'Sorry, PULL not implemented yet.') + except Exception as ex: + self.log.exception('Unexpected exception caught.') + self.report({'ERROR'}, 'Unexpected error: %s' % ex) self._state = 'QUIT' + async def action_push(self): + """Sends files to the Pillar server.""" + + config_dir = pathlib.Path(bpy.utils.user_resource('CONFIG')) + + for fname in SETTINGS_FILES_TO_UPLOAD: + path = config_dir / fname + if not path.exists(): + self.log.debug('Skipping non-existing %s', path) + continue + + self.report({'INFO'}, 'Uploading %s' % fname) + await self.attach_file_to_group(path) + + self.report({'INFO'}, 'Settings pushed to Blender Cloud.') + async def find_sync_group_id(self) -> pillarsdk.Node: """Finds the group node in which to store sync assets. If the group node doesn't exist, it creates it. """ - home_proj_id = await get_home_project_id() - - node_props = {'project': home_proj_id, + node_props = {'project': self.home_project_id, 'node_type': 'group', 'parent': None, 'name': SYNC_GROUP_NODE_NAME, @@ -108,6 +148,28 @@ class PILLAR_OT_sync(async_loop.AsyncModalOperatorMixin, bpy.types.Operator): return sync_group['_id'] + async def attach_file_to_group(self, file_path: pathlib.Path) -> pillarsdk.Node: + """Creates an Asset node and attaches a file document to it.""" + + # First upload the file... + file_id = await upload_file(self.home_project_id, file_path, + future=self.signalling_future) + # Then attach it to a new node. + node_props = {'project': self.home_project_id, + 'node_type': 'asset', + 'parent': self.sync_group_id, + 'name': file_path.name, + 'properties': {'file': file_id}, + 'user': self.user_id} + node = pillarsdk.Node.new(node_props) + created_ok = await pillar_call(node.create) + if not created_ok: + log.error('Blender Cloud addon: unable to create asset node on the Cloud for file %s.', + file_path) + raise PillarError('Unable to create asset node on the Cloud for file %s' % file_path) + + return node + def draw_userpref_header(self: bpy.types.USERPREF_HT_header, context): """Adds some buttons to the userprefs header."""