Compare commits

..

26 Commits

Author SHA1 Message Date
388a059400 Bumped version to 1.3.2 2016-07-15 14:02:01 +02:00
80d2b5b2e7 Move "Share on Cloud" button from image header to menu. 2016-07-15 14:01:21 +02:00
53ab2fc6df Bumped version to 1.3.1 2016-07-14 11:50:19 +02:00
1e2c74e82d Made screenshot the default target for image sharing.
This way the spacebar-menu takes a screenshot of the current area and
shares it. The other targets need a 'name' property set, so those won't
work from the spacebar-menu anyway.

I also added some extra options for the screenshotting, to mirror the
bpy.ops.screen.screenshot() operator options.

The full-window screenshot operator is now also placed in the Window menu.
2016-07-14 11:49:30 +02:00
ecb8f8575f Added missing logger 2016-07-14 11:47:50 +02:00
acd62b4917 Added screenshot functionality 2016-07-14 11:13:09 +02:00
65faeba7b0 Bumped requirement pillarsdk 1.3.0 → 1.4.0 2016-07-13 11:06:51 +02:00
8f8e14b66e Bumped version to 1.3.0 2016-07-12 18:00:00 +02:00
250939dc32 Added custom cloud icon 2016-07-08 17:00:44 +02:00
2e617287fd Remove now-unused PILLAR_WEB_SERVER_URL 2016-07-08 17:00:21 +02:00
36bbead1e1 Handling 413 Request Entity Too Large while uploading synced settings.
Non-subscribers are limited in the file size they can upload.
2016-07-08 12:38:58 +02:00
89a9055aa4 No longer use theatre_link, and show URL in GUI 2016-07-07 17:03:28 +02:00
6339f75406 Fix: added some missing return statements 2016-07-07 16:14:51 +02:00
a9aa961b92 Open browser at the short URL. 2016-07-07 15:43:36 +02:00
4da601be0c Share image after uploading it to Pillar. 2016-07-07 15:19:21 +02:00
3c9e4e2873 Give users the option to open a webbrowser after sharing an image.
The addon now also uses the home project URL from the project itself,
rather than hard-coding it.
2016-07-07 11:43:01 +02:00
4762f0292d Added support for sharing packed images. 2016-07-07 11:09:30 +02:00
959e83229b Allow execution of the file sharing operator
(rather than requiring INVOKE_DEFAULT)
2016-07-06 16:25:31 +02:00
662b6cf221 Choose default target='DATABLOCK' 2016-07-06 16:25:10 +02:00
96616dbdff Always create new nodes on the cloud, and prevent cache issue.
The cache issue: this caused etag mismatches when sharing a file multiple
times using always_create_new_node=False. Even though we don't use this
option right now, it should be easy to enable.
2016-07-06 16:24:55 +02:00
dbbffcc28e Some protection against sharing dirty image datablocks.
We can save dirty files, either to disk or the cloud, but I think that's
a bad idea to:

- Share unsaved data to the cloud; users can assume it's saved
  to disk and close blender, losing their file.
- Save unsaved data first; this can overwrite a file a user
  didn't want to overwrite.

The clearest way is simply to refuse to handle dirty datablocks.
2016-07-06 16:23:33 +02:00
0a1f1972da Support for uploading render results. 2016-07-06 15:48:55 +02:00
c9a92dd5d1 Added start of image sharing.
Sharing an image datablock works, if it has been saved and not packed.
Directly sharing a file, and dirty/packed datablocks are for a future
commit.
2016-07-06 15:20:50 +02:00
1c2def3b84 Moved some code from settings_sync.py to home_project.py and pillar.py 2016-07-05 17:26:26 +02:00
e29b61b649 Using pillarsdk.Node.create_asset_from_file() 2016-07-05 16:47:37 +02:00
1d1c8cf3d6 Bumped version to 1.2.2 2016-06-30 18:43:51 +02:00
9 changed files with 509 additions and 130 deletions

View File

@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender Cloud',
'author': 'Sybren A. Stüvel and Francesco Siddi',
'version': (1, 2, 1),
'version': (1, 3, 2),
'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
@@ -70,13 +70,17 @@ def register():
sys.modules[modname] = module
return module
reload_mod('blendfile')
reload_mod('home_project')
blender = reload_mod('blender')
gui = reload_mod('gui')
async_loop = reload_mod('async_loop')
settings_sync = reload_mod('settings_sync')
reload_mod('blendfile')
image_sharing = reload_mod('image_sharing')
else:
from . import blender, gui, async_loop, settings_sync, blendfile
from . import (blender, gui, async_loop, settings_sync, blendfile, home_project,
image_sharing)
async_loop.setup_asyncio_executor()
async_loop.register()
@@ -84,6 +88,7 @@ def register():
gui.register()
blender.register()
settings_sync.register()
image_sharing.register()
def _monkey_patch_requests():
@@ -104,8 +109,9 @@ def _monkey_patch_requests():
def unregister():
from . import blender, gui, async_loop, settings_sync
from . import blender, gui, async_loop, settings_sync, image_sharing
image_sharing.unregister()
settings_sync.unregister()
blender.unregister()
gui.unregister()

View File

@@ -4,10 +4,11 @@ Separated from __init__.py so that we can import & run from non-Blender environm
"""
import logging
import os.path
import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty
from . import pillar, gui
@@ -17,6 +18,8 @@ PILLAR_SERVER_URL = 'https://cloudapi.blender.org/'
ADDON_NAME = 'blender_cloud'
log = logging.getLogger(__name__)
icons = None
def redraw(self, context):
context.area.tag_redraw()
@@ -99,6 +102,12 @@ class BlenderCloudPreferences(AddonPreferences):
subtype='DIR_PATH',
default='//textures')
open_browser_after_share = BoolProperty(
name='Open browser after sharing file',
description='When enabled, Blender will open a webbrowser',
default=True
)
def draw(self, context):
import textwrap
@@ -114,28 +123,28 @@ class BlenderCloudPreferences(AddonPreferences):
blender_id_profile = blender_id.get_active_profile()
if blender_id is None:
icon = 'ERROR'
msg_icon = 'ERROR'
text = 'This add-on requires Blender ID'
help_text = 'Make sure that the Blender ID add-on is installed and activated'
elif not blender_id_profile:
icon = 'ERROR'
msg_icon = 'ERROR'
text = 'You are logged out.'
help_text = 'To login, go to the Blender ID add-on preferences.'
elif bpy.app.debug and pillar.SUBCLIENT_ID not in blender_id_profile.subclients:
icon = 'QUESTION'
msg_icon = 'QUESTION'
text = 'No Blender Cloud credentials.'
help_text = ('You are logged in on Blender ID, but your credentials have not '
'been synchronized with Blender Cloud yet. Press the Update '
'Credentials button.')
else:
icon = 'WORLD_DATA'
msg_icon = 'WORLD_DATA'
text = 'You are logged in as %s.' % blender_id_profile.username
help_text = ('To logout or change profile, '
'go to the Blender ID add-on preferences.')
# Authentication stuff
auth_box = layout.box()
auth_box.label(text=text, icon=icon)
auth_box.label(text=text, icon=msg_icon)
help_lines = textwrap.wrap(help_text, 80)
for line in help_lines:
@@ -145,18 +154,18 @@ class BlenderCloudPreferences(AddonPreferences):
# Texture browser stuff
texture_box = layout.box()
texture_box.enabled = icon != 'ERROR'
texture_box.enabled = msg_icon != 'ERROR'
sub = texture_box.column()
sub.label(text='Local directory for downloaded textures')
sub.label(text='Local directory for downloaded textures', icon_value=icon('CLOUD'))
sub.prop(self, "local_texture_dir", text='Default')
sub.prop(context.scene, "local_texture_dir", text='Current scene')
# Blender Sync stuff
bss = context.window_manager.blender_sync_status
bsync_box = layout.box()
bsync_box.enabled = icon != 'ERROR'
bsync_box.enabled = msg_icon != 'ERROR'
row = bsync_box.row().split(percentage=0.33)
row.label('Blender Sync with Blender Cloud')
row.label('Blender Sync with Blender Cloud', icon_value=icon('CLOUD'))
icon_for_level = {
'INFO': 'NONE',
@@ -164,17 +173,22 @@ class BlenderCloudPreferences(AddonPreferences):
'ERROR': 'ERROR',
'SUBSCRIBE': 'ERROR',
}
icon = icon_for_level[bss.level] if bss.message else 'NONE'
msg_icon = icon_for_level[bss.level] if bss.message else 'NONE'
message_container = row.row()
message_container.label(bss.message, icon=icon)
message_container.label(bss.message, icon=msg_icon)
sub = bsync_box.column()
if bss.level == 'SUBSCRIBE':
self.draw_subscribe_button(sub)
else:
self.draw_sync_buttons(sub, bss)
# Image Share stuff
share_box = layout.box()
share_box.label('Image Sharing on Blender Cloud', icon_value=icon('CLOUD'))
texture_box.enabled = msg_icon != 'ERROR'
share_box.prop(self, 'open_browser_after_share')
def draw_subscribe_button(self, layout):
layout.operator('pillar.subscribe', icon='WORLD')
@@ -216,6 +230,8 @@ class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
bl_idname = 'pillar.credentials_update'
bl_label = 'Update credentials'
log = logging.getLogger('bpy.ops.%s' % bl_idname)
@classmethod
def poll(cls, context):
# Only allow activation when the user is actually logged in.
@@ -273,6 +289,39 @@ def preferences() -> BlenderCloudPreferences:
return bpy.context.user_preferences.addons[ADDON_NAME].preferences
def load_custom_icons():
global icons
if icons is not None:
# Already loaded
return
import bpy.utils.previews
icons = bpy.utils.previews.new()
my_icons_dir = os.path.join(os.path.dirname(__file__), 'icons')
icons.load('CLOUD', os.path.join(my_icons_dir, 'icon-cloud.png'), 'IMAGE')
def unload_custom_icons():
global icons
if icons is None:
# Already unloaded
return
bpy.utils.previews.remove(icons)
icons = None
def icon(icon_name: str) -> int:
"""Returns the icon ID for the named icon.
Use with layout.operator('pillar.image_share', icon_value=icon('CLOUD'))
"""
return icons[icon_name].icon_id
def register():
bpy.utils.register_class(BlenderCloudPreferences)
bpy.utils.register_class(PillarCredentialsUpdate)
@@ -299,8 +348,12 @@ def register():
WindowManager.blender_sync_status = PointerProperty(type=SyncStatusProperties)
load_custom_icons()
def unregister():
unload_custom_icons()
gui.unregister()
bpy.utils.unregister_class(PillarCredentialsUpdate)

View File

@@ -0,0 +1,32 @@
import logging
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
log = logging.getLogger(__name__)
HOME_PROJECT_ENDPOINT = '/bcloud/home-project'
async def get_home_project(params=None) -> pillarsdk.Project:
"""Returns the home project."""
log.debug('Getting home project')
try:
return await pillar_call(pillarsdk.Project.find_from_endpoint,
HOME_PROJECT_ENDPOINT, params=params)
except sdk_exceptions.ForbiddenAccess:
log.warning('Access to the home project was denied. '
'Double-check that you are logged in with valid BlenderID credentials.')
raise
except sdk_exceptions.ResourceNotFound:
log.warning('No home project available.')
raise
async def get_home_project_id() -> str:
"""Returns just the ID of the home project."""
home_proj = await get_home_project({'projection': {'_id': 1}})
home_proj_id = home_proj['_id']
return home_proj_id

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,323 @@
import logging
import os.path
import tempfile
import datetime
import bpy
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
from . import async_loop, pillar, home_project, blender
REQUIRES_ROLES_FOR_IMAGE_SHARING = {'subscriber', 'demo'}
IMAGE_SHARING_GROUP_NODE_NAME = 'Image sharing'
log = logging.getLogger(__name__)
async def find_image_sharing_group_id(home_project_id, user_id):
# Find the top-level image sharing group node.
try:
share_group, created = await pillar.find_or_create_node(
where={'project': home_project_id,
'node_type': 'group',
'parent': None,
'name': IMAGE_SHARING_GROUP_NODE_NAME},
additional_create_props={
'user': user_id,
'properties': {},
},
projection={'_id': 1},
may_create=True)
except pillar.PillarError:
log.exception('Pillar error caught')
raise pillar.PillarError('Unable to find image sharing folder on the Cloud')
return share_group['_id']
class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin,
bpy.types.Operator):
bl_idname = 'pillar.image_share'
bl_label = 'Share an image/screenshot via Blender Cloud'
bl_description = 'Uploads an image for sharing via Blender Cloud'
log = logging.getLogger('bpy.ops.%s' % bl_idname)
home_project_id = None
home_project_url = 'home'
share_group_id = None # top-level share group node ID
user_id = None
target = bpy.props.EnumProperty(
items=[
('FILE', 'File', 'Share an image file'),
('DATABLOCK', 'Datablock', 'Share an image datablock'),
('SCREENSHOT', 'Screenshot', 'Share a screenshot'),
],
name='target',
default='SCREENSHOT')
name = bpy.props.StringProperty(name='name',
description='File or datablock name to sync')
screenshot_show_multiview = bpy.props.BoolProperty(
name='screenshot_show_multiview',
description='Enable Multi-View',
default=False)
screenshot_use_multiview = bpy.props.BoolProperty(
name='screenshot_use_multiview',
description='Use Multi-View',
default=False)
screenshot_full = bpy.props.BoolProperty(
name='screenshot_full',
description='Full Screen, Capture the whole window (otherwise only capture the active area)',
default=False)
def invoke(self, context, event):
# Do a quick test on datablock dirtyness. If it's not packed and dirty,
# the user should save it first.
if self.target == 'DATABLOCK':
if not self.name:
self.report({'ERROR'}, 'No name given of the datablock to share.')
return {'CANCELLED'}
datablock = bpy.data.images[self.name]
if datablock.type == 'IMAGE' and datablock.is_dirty and not datablock.packed_file:
self.report({'ERROR'}, 'Datablock is dirty, save it first.')
return {'CANCELLED'}
async_loop.AsyncModalOperatorMixin.invoke(self, context, event)
self.log.info('Starting sharing')
self._new_async_task(self.async_execute(context))
return {'RUNNING_MODAL'}
def execute(self, context):
return self.invoke(context, None)
async def async_execute(self, context):
"""Entry point of the asynchronous operator."""
self.report({'INFO'}, 'Communicating with Blender Cloud')
try:
# Refresh credentials
try:
self.user_id = await self.check_credentials(context,
REQUIRES_ROLES_FOR_IMAGE_SHARING)
self.log.debug('Found user ID: %s', self.user_id)
except pillar.NotSubscribedToCloudError:
self.log.exception('User not subscribed to cloud.')
self.report({'ERROR'}, 'Please subscribe to the Blender Cloud.')
self._state = 'QUIT'
return
except pillar.CredentialsNotSyncedError:
self.log.exception('Error checking/refreshing credentials.')
self.report({'ERROR'}, 'Please log in on Blender ID first.')
self._state = 'QUIT'
return
# Find the home project.
try:
home_proj = await home_project.get_home_project({
'projection': {'_id': 1, 'url': 1}
})
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
except sdk_exceptions.ResourceNotFound:
self.report({'ERROR'}, 'Home project not found.')
self._state = 'QUIT'
return
self.home_project_id = home_proj['_id']
self.home_project_url = home_proj['url']
try:
gid = await find_image_sharing_group_id(self.home_project_id,
self.user_id)
self.share_group_id = gid
self.log.debug('Found group node ID: %s', self.share_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
await self.share_image(context)
except Exception as ex:
self.log.exception('Unexpected exception caught.')
self.report({'ERROR'}, 'Unexpected error %s: %s' % (type(ex), ex))
self._state = 'QUIT'
async def share_image(self, context):
"""Sends files to the Pillar server."""
if self.target == 'FILE':
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
node = await self.upload_file(self.name)
elif self.target == 'SCREENSHOT':
node = await self.upload_screenshot(context)
else:
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
node = await self.upload_datablock(context)
self.report({'INFO'}, 'Upload complete, creating link to share.')
share_info = await pillar_call(node.share)
url = share_info.get('short_link')
context.window_manager.clipboard = url
self.report({'INFO'}, 'The link has been copied to your clipboard: %s' % url)
await self.maybe_open_browser(url)
async def upload_file(self, filename: str, fileobj=None) -> pillarsdk.Node:
"""Uploads a file to the cloud, attached to the image sharing node.
Returns the node.
"""
self.log.info('Uploading file %s', filename)
node = await pillar_call(pillarsdk.Node.create_asset_from_file,
self.home_project_id,
self.share_group_id,
'image',
filename,
extra_where={'user': self.user_id},
always_create_new_node=True,
fileobj=fileobj,
caching=False)
node_id = node['_id']
self.log.info('Created node %s', node_id)
self.report({'INFO'}, 'File succesfully uploaded to the cloud!')
return node
async def maybe_open_browser(self, url):
prefs = blender.preferences()
if not prefs.open_browser_after_share:
return
import webbrowser
self.log.info('Opening browser at %s', url)
webbrowser.open_new_tab(url)
async def upload_datablock(self, context) -> pillarsdk.Node:
"""Saves a datablock to file if necessary, then upload.
Returns the node.
"""
self.log.info("Uploading datablock '%s'" % self.name)
datablock = bpy.data.images[self.name]
if datablock.type == 'RENDER_RESULT':
# Construct a sensible name for this render.
filename = '%s-%s-render%s' % (
os.path.splitext(os.path.basename(context.blend_data.filepath))[0],
context.scene.name,
context.scene.render.file_extension)
return await self.upload_via_tempdir(datablock, filename)
if datablock.packed_file is not None:
return await self.upload_packed_file(datablock)
if datablock.is_dirty:
# We can handle dirty datablocks like this if we want.
# However, I (Sybren) do NOT think it's a good idea to:
# - Share unsaved data to the cloud; users can assume it's saved
# to disk and close blender, losing their file.
# - Save unsaved data first; this can overwrite a file a user
# didn't want to overwrite.
filename = bpy.path.basename(datablock.filepath)
return await self.upload_via_tempdir(datablock, filename)
filepath = bpy.path.abspath(datablock.filepath)
return await self.upload_file(filepath)
async def upload_via_tempdir(self, datablock, filename_on_cloud) -> pillarsdk.Node:
"""Saves the datablock to file, and uploads it to the cloud.
Saving is done to a temporary directory, which is removed afterwards.
Returns the node.
"""
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, filename_on_cloud)
self.log.debug('Saving %s to %s', datablock, filepath)
datablock.save_render(filepath)
return await self.upload_file(filepath)
async def upload_packed_file(self, datablock) -> pillarsdk.Node:
"""Uploads a packed file directly from memory.
Returns the node.
"""
import io
filename = '%s.%s' % (datablock.name, datablock.file_format.lower())
fileobj = io.BytesIO(datablock.packed_file.data)
fileobj.seek(0) # ensure PillarSDK reads the file from the beginning.
self.log.info('Uploading packed file directly from memory to %r.', filename)
return await self.upload_file(filename, fileobj=fileobj)
async def upload_screenshot(self, context) -> pillarsdk.Node:
"""Takes a screenshot, saves it to a temp file, and uploads it."""
self.name = datetime.datetime.now().strftime('Screenshot-%Y-%m-%d-%H:%M:%S.png')
self.report({'INFO'}, "Uploading %s '%s'" % (self.target.lower(), self.name))
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, self.name)
self.log.debug('Saving screenshot to %s', filepath)
bpy.ops.screen.screenshot(filepath=filepath,
show_multiview=self.screenshot_show_multiview,
use_multiview=self.screenshot_use_multiview,
full=self.screenshot_full)
return await self.upload_file(filepath)
def image_editor_menu(self, context):
image = context.space_data.image
box = self.layout.row()
if image and image.has_data:
text = 'Share on Blender Cloud'
if image.type == 'IMAGE' and image.is_dirty and not image.packed_file:
box.enabled = False
text = 'Save image before sharing on Blender Cloud'
props = box.operator(PILLAR_OT_image_share.bl_idname, text=text,
icon_value=blender.icon('CLOUD'))
props.target = 'DATABLOCK'
props.name = image.name
def window_menu(self, context):
props = self.layout.operator(PILLAR_OT_image_share.bl_idname,
text='Share screenshot via Blender Cloud',
icon_value=blender.icon('CLOUD'))
props.target = 'SCREENSHOT'
props.screenshot_full = True
def register():
bpy.utils.register_class(PILLAR_OT_image_share)
bpy.types.IMAGE_MT_image.append(image_editor_menu)
bpy.types.INFO_MT_window.append(window_menu)
def unregister():
bpy.utils.unregister_class(PILLAR_OT_image_share)
bpy.types.IMAGE_MT_image.remove(image_editor_menu)
bpy.types.INFO_MT_window.remove(window_menu)

View File

@@ -700,3 +700,58 @@ class PillarOperatorMixin:
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
self.report({'INFO'},
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
async def find_or_create_node(where: dict,
additional_create_props: dict = None,
projection: dict = None,
may_create: bool = True) -> (pillarsdk.Node, bool):
"""Finds a node by the `filter_props`, creates it using the additional props.
:returns: tuple (node, created), where 'created' is a bool indicating whether
a new node was created, or an exising one is returned.
"""
params = {
'where': where,
}
if projection:
params['projection'] = projection
found_node = await pillar_call(pillarsdk.Node.find_first, params, caching=False)
if found_node is not None:
return found_node, False
if not may_create:
return None, False
# Augment the node properties to form a complete node.
node_props = where.copy()
if additional_create_props:
node_props.update(additional_create_props)
log.debug('Creating new node %s', node_props)
created_node = pillarsdk.Node.new(node_props)
created_ok = await pillar_call(created_node.create)
if not created_ok:
log.error('Blender Cloud addon: unable to create node on the Cloud.')
raise PillarError('Unable to create node on the Cloud')
return created_node, True
async def attach_file_to_group(file_path: pathlib.Path,
home_project_id: str,
group_node_id: str,
user_id: str = None) -> pillarsdk.Node:
"""Creates an Asset node and attaches a file document to it."""
node = await pillar_call(pillarsdk.Node.create_asset_from_file,
home_project_id,
group_node_id,
'file',
str(file_path),
extra_where=user_id and {'user': user_id})
return node

View File

@@ -16,7 +16,7 @@ import asyncio
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
from . import async_loop, pillar, cache, blendfile
from . import async_loop, pillar, cache, blendfile, home_project
SETTINGS_FILES_TO_UPLOAD = ['userpref.blend', 'startup.blend']
@@ -40,7 +40,6 @@ LOCAL_SETTINGS_RNA = [
]
REQUIRES_ROLES_FOR_SYNC = set() # no roles needed.
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' \
'#blender-addon) will synchronize your Blender settings here.'
@@ -79,28 +78,6 @@ def async_set_blender_sync_status(set_status: str):
return decorator
async def get_home_project(params=None) -> pillarsdk.Project:
"""Returns the home project."""
log.debug('Getting home project')
try:
return await pillar_call(pillarsdk.Project.find_from_endpoint,
HOME_PROJECT_ENDPOINT, params=params)
except sdk_exceptions.ForbiddenAccess:
log.warning('Access to the home project was denied. '
'Double-check that you are logged in with valid BlenderID credentials.')
raise
except sdk_exceptions.ResourceNotFound:
log.warning('No home project available.')
raise
async def get_home_project_id():
home_proj = await get_home_project({'projection': {'_id': 1}})
home_proj_id = home_proj['_id']
return home_proj_id
async def find_sync_group_id(home_project_id: str,
user_id: str,
blender_version: str,
@@ -114,7 +91,7 @@ async def find_sync_group_id(home_project_id: str,
# Find the top-level sync group node. This should have been
# created by Pillar while creating the home project.
try:
sync_group, created = await find_or_create_node(
sync_group, created = await pillar.find_or_create_node(
where={'project': home_project_id,
'node_type': 'group',
'parent': None,
@@ -131,7 +108,7 @@ async def find_sync_group_id(home_project_id: str,
# Find/create the sub-group for the requested Blender version
try:
sub_sync_group, created = await find_or_create_node(
sub_sync_group, created = await pillar.find_or_create_node(
where={'project': home_project_id,
'node_type': 'group',
'parent': sync_group['_id'],
@@ -154,84 +131,6 @@ async def find_sync_group_id(home_project_id: str,
return sync_group['_id'], sub_sync_group['_id']
async def find_or_create_node(where: dict,
additional_create_props: dict = None,
projection: dict = None,
may_create: bool = True) -> (pillarsdk.Node, bool):
"""Finds a node by the `filter_props`, creates it using the additional props.
:returns: tuple (node, created), where 'created' is a bool indicating whether
a new node was created, or an exising one is returned.
"""
params = {
'where': where,
}
if projection:
params['projection'] = projection
found_node = await pillar_call(pillarsdk.Node.find_first, params, caching=False)
created = False
if found_node is None:
if not may_create:
return None, False
log.info('Creating new sync group node')
# Augment the node properties to form a complete node.
node_props = where.copy()
if additional_create_props:
node_props.update(additional_create_props)
found_node = pillarsdk.Node.new(node_props)
created_ok = await pillar_call(found_node.create)
if not created_ok:
log.error('Blender Cloud addon: unable to create node on the Cloud.')
raise pillar.PillarError('Unable to create node on the Cloud')
created = True
return found_node, created
async def attach_file_to_group(file_path: pathlib.Path,
home_project_id: str,
group_node_id: str,
user_id: str,
*,
future=None) -> pillarsdk.Node:
"""Creates an Asset node and attaches a file document to it."""
# First upload the file...
file_id = await pillar.upload_file(home_project_id, file_path,
future=future)
# Then attach it to a node.
node, created = await find_or_create_node(
where={
'project': home_project_id,
'node_type': 'asset',
'parent': group_node_id,
'name': file_path.name,
'user': user_id},
additional_create_props={
'properties': {'file': file_id},
})
if not created:
# Update the existing node.
node.properties = {'file': file_id}
updated_ok = await pillar_call(node.update)
if not updated_ok:
log.error(
'Blender Cloud addon: unable to update asset node on the Cloud for file %s.',
file_path)
raise pillar.PillarError(
'Unable to update asset node on the Cloud for file %s' % file_path.name)
return node
@functools.lru_cache()
async def available_blender_versions(home_project_id: str, user_id: str) -> list:
bss = bpy.context.window_manager.blender_sync_status
@@ -383,7 +282,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
# Find the home project.
try:
self.home_project_id = await get_home_project_id()
self.home_project_id = await home_project.get_home_project_id()
except sdk_exceptions.ForbiddenAccess:
self.log.exception('Forbidden access to home project.')
self.bss_report({'ERROR'}, 'Did not get access to home project.')
@@ -439,12 +338,23 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
self.log.debug('Skipping non-existing %s', path)
continue
if self.signalling_future.cancelled():
self.bss_report({'WARNING'}, 'Upload aborted.')
return
self.bss_report({'INFO'}, 'Uploading %s' % fname)
await attach_file_to_group(path,
try:
await pillar.attach_file_to_group(path,
self.home_project_id,
self.sync_group_versioned_id,
self.user_id,
future=self.signalling_future)
self.user_id)
except sdk_exceptions.RequestEntityTooLarge as ex:
self.log.error('File too big to upload: %s' % ex)
self.log.error('To upload larger files, please subscribe to Blender Cloud.')
self.bss_report({'SUBSCRIBE'}, 'File %s too big to upload. '
'Subscribe for unlimited space.' % fname)
self._state = 'QUIT'
return
await self.action_refresh(context)

View File

@@ -1,7 +1,7 @@
# Primary requirements:
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
lockfile==0.12.2
pillarsdk==1.3.0
pillarsdk==1.4.0
wheel==0.29.0
# Secondary requirements:

View File

@@ -179,7 +179,7 @@ setup(
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.2.1',
version='1.3.2',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),