Compare commits
24 Commits
version-1.
...
version-1.
Author | SHA1 | Date | |
---|---|---|---|
53ab2fc6df | |||
1e2c74e82d | |||
ecb8f8575f | |||
acd62b4917 | |||
65faeba7b0 | |||
8f8e14b66e | |||
250939dc32 | |||
2e617287fd | |||
36bbead1e1 | |||
89a9055aa4 | |||
6339f75406 | |||
a9aa961b92 | |||
4da601be0c | |||
3c9e4e2873 | |||
4762f0292d | |||
959e83229b | |||
662b6cf221 | |||
96616dbdff | |||
dbbffcc28e | |||
0a1f1972da | |||
c9a92dd5d1 | |||
1c2def3b84 | |||
e29b61b649 | |||
1d1c8cf3d6 |
@@ -21,7 +21,7 @@
|
||||
bl_info = {
|
||||
'name': 'Blender Cloud',
|
||||
'author': 'Sybren A. Stüvel and Francesco Siddi',
|
||||
'version': (1, 2, 1),
|
||||
'version': (1, 3, 1),
|
||||
'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()
|
||||
|
@@ -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,16 +173,21 @@ 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)
|
||||
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)
|
||||
|
32
blender_cloud/home_project.py
Normal file
32
blender_cloud/home_project.py
Normal 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
|
BIN
blender_cloud/icons/icon-cloud.png
Normal file
BIN
blender_cloud/icons/icon-cloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
323
blender_cloud/image_sharing.py
Normal file
323
blender_cloud/image_sharing.py
Normal 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_HT_header.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_HT_header.remove(image_editor_menu)
|
||||
bpy.types.INFO_MT_window.remove(window_menu)
|
@@ -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
|
||||
|
@@ -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,
|
||||
self.home_project_id,
|
||||
self.sync_group_versioned_id,
|
||||
self.user_id,
|
||||
future=self.signalling_future)
|
||||
try:
|
||||
await pillar.attach_file_to_group(path,
|
||||
self.home_project_id,
|
||||
self.sync_group_versioned_id,
|
||||
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)
|
||||
|
||||
|
@@ -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:
|
||||
|
2
setup.py
2
setup.py
@@ -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.1',
|
||||
author='Sybren A. Stüvel',
|
||||
author_email='sybren@stuvel.eu',
|
||||
packages=find_packages('.'),
|
||||
|
Reference in New Issue
Block a user