Uploading setting files to home project works.
This commit is contained in:
parent
efb1456596
commit
791b3f480c
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
Reference in New Issue
Block a user