Uploading setting files to home project works.

This commit is contained in:
Sybren A. Stüvel 2016-06-16 17:19:49 +02:00
parent efb1456596
commit 791b3f480c
2 changed files with 129 additions and 16 deletions

View File

@ -4,6 +4,7 @@ import os
import functools import functools
import logging import logging
from contextlib import closing, contextmanager from contextlib import closing, contextmanager
import urllib.parse
import pathlib import pathlib
import requests import requests
@ -109,6 +110,21 @@ def blender_id_profile() -> 'blender_id.BlenderIdProfile':
return blender_id.get_active_profile() 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: def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api:
"""Returns the Pillar SDK API object for the current user. """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 global _pillar_api
# Only return the Pillar API object if the user is still logged in. # Only return the Pillar API object if the user is still logged in.
profile = blender_id_profile() subclient = blender_id_subclient()
if not profile:
raise UserNotLoggedInError()
subclient = profile.subclients.get(SUBCLIENT_ID)
if not subclient:
raise CredentialsNotSyncedError()
if _pillar_api is None: if _pillar_api is None:
# Allow overriding the endpoint before importing Blender-specific stuff. # 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) 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: def is_cancelled(future: asyncio.Future) -> bool:
# assert future is not None # for debugging purposes. # assert future is not None # for debugging purposes.
cancelled = future is not None and future.cancelled() cancelled = future is not None and future.cancelled()

View File

@ -3,11 +3,15 @@ import asyncio
import logging import logging
import bpy import bpy
import pathlib
import pillarsdk import pillarsdk
from pillarsdk import exceptions as sdk_exceptions 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 from . import async_loop
SETTINGS_FILES_TO_UPLOAD = ['bookmarks.txt', 'recent-files.txt', 'userpref.blend', 'startup.blend']
HOME_PROJECT_ENDPOINT = '/bcloud/home-project' HOME_PROJECT_ENDPOINT = '/bcloud/home-project'
SYNC_GROUP_NODE_NAME = 'Blender Sync' SYNC_GROUP_NODE_NAME = 'Blender Sync'
SYNC_GROUP_NODE_DESC = 'The [Blender Cloud Addon](https://cloud.blender.org/services' \ 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'} return {'PASS_THROUGH'}
async def async_execute(self): async def async_execute(self):
self.user_id = await check_pillar_credentials() """Entry point of the asynchronous operator."""
try: try:
group_id = await self.find_sync_group_id() self.user_id = await check_pillar_credentials()
self.log.info('Found group node ID: %s', group_id) try:
except sdk_exceptions.ForbiddenAccess as ex: self.home_project_id = await get_home_project_id()
self.log.exception('Unable to find Group 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' 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: async def find_sync_group_id(self) -> pillarsdk.Node:
"""Finds the group node in which to store sync assets. """Finds the group node in which to store sync assets.
If the group node doesn't exist, it creates it. If the group node doesn't exist, it creates it.
""" """
home_proj_id = await get_home_project_id() node_props = {'project': self.home_project_id,
node_props = {'project': home_proj_id,
'node_type': 'group', 'node_type': 'group',
'parent': None, 'parent': None,
'name': SYNC_GROUP_NODE_NAME, 'name': SYNC_GROUP_NODE_NAME,
@ -108,6 +148,28 @@ class PILLAR_OT_sync(async_loop.AsyncModalOperatorMixin, bpy.types.Operator):
return sync_group['_id'] 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): def draw_userpref_header(self: bpy.types.USERPREF_HT_header, context):
"""Adds some buttons to the userprefs header.""" """Adds some buttons to the userprefs header."""