Compare commits
29 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 | |||
fc01e32f0d | |||
7577b348a5 | |||
be99bcb250 | |||
2190bd795e | |||
76d1f88c4e |
@@ -24,6 +24,8 @@ Installing the addon
|
|||||||
|
|
||||||
* If you don't have one already, sign up for an account at
|
* If you don't have one already, sign up for an account at
|
||||||
the [Blender ID site](https://www.blender.org/id/).
|
the [Blender ID site](https://www.blender.org/id/).
|
||||||
|
* If you had a previous version of the addon installed, deactivate it
|
||||||
|
and restart Blender.
|
||||||
* Install and log in with the
|
* Install and log in with the
|
||||||
[Blender ID addon](https://developer.blender.org/diffusion/BIA/).
|
[Blender ID addon](https://developer.blender.org/diffusion/BIA/).
|
||||||
* Install the Blender Cloud addon in Blender (User Preferences →
|
* Install the Blender Cloud addon in Blender (User Preferences →
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'Blender Cloud',
|
'name': 'Blender Cloud',
|
||||||
'author': 'Sybren A. Stüvel and Francesco Siddi',
|
'author': 'Sybren A. Stüvel and Francesco Siddi',
|
||||||
'version': (1, 2, 0),
|
'version': (1, 3, 1),
|
||||||
'blender': (2, 77, 0),
|
'blender': (2, 77, 0),
|
||||||
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
|
'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 '
|
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
|
||||||
@@ -70,13 +70,17 @@ def register():
|
|||||||
sys.modules[modname] = module
|
sys.modules[modname] = module
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
reload_mod('blendfile')
|
||||||
|
reload_mod('home_project')
|
||||||
|
|
||||||
blender = reload_mod('blender')
|
blender = reload_mod('blender')
|
||||||
gui = reload_mod('gui')
|
gui = reload_mod('gui')
|
||||||
async_loop = reload_mod('async_loop')
|
async_loop = reload_mod('async_loop')
|
||||||
settings_sync = reload_mod('settings_sync')
|
settings_sync = reload_mod('settings_sync')
|
||||||
reload_mod('blendfile')
|
image_sharing = reload_mod('image_sharing')
|
||||||
else:
|
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.setup_asyncio_executor()
|
||||||
async_loop.register()
|
async_loop.register()
|
||||||
@@ -84,6 +88,7 @@ def register():
|
|||||||
gui.register()
|
gui.register()
|
||||||
blender.register()
|
blender.register()
|
||||||
settings_sync.register()
|
settings_sync.register()
|
||||||
|
image_sharing.register()
|
||||||
|
|
||||||
|
|
||||||
def _monkey_patch_requests():
|
def _monkey_patch_requests():
|
||||||
@@ -104,8 +109,9 @@ def _monkey_patch_requests():
|
|||||||
|
|
||||||
|
|
||||||
def unregister():
|
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()
|
settings_sync.unregister()
|
||||||
blender.unregister()
|
blender.unregister()
|
||||||
gui.unregister()
|
gui.unregister()
|
||||||
|
@@ -4,10 +4,11 @@ Separated from __init__.py so that we can import & run from non-Blender environm
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os.path
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
|
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
|
from . import pillar, gui
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ PILLAR_SERVER_URL = 'https://cloudapi.blender.org/'
|
|||||||
ADDON_NAME = 'blender_cloud'
|
ADDON_NAME = 'blender_cloud'
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
icons = None
|
||||||
|
|
||||||
|
|
||||||
def redraw(self, context):
|
def redraw(self, context):
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
@@ -26,7 +29,7 @@ def blender_syncable_versions(self, context):
|
|||||||
bss = context.window_manager.blender_sync_status
|
bss = context.window_manager.blender_sync_status
|
||||||
versions = bss.available_blender_versions
|
versions = bss.available_blender_versions
|
||||||
if not versions:
|
if not versions:
|
||||||
return [('', 'No settings stored in your home project.', '')]
|
return [('', 'No settings stored in your Blender Cloud', '')]
|
||||||
return [(v, v, '') for v in versions]
|
return [(v, v, '') for v in versions]
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +102,12 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
subtype='DIR_PATH',
|
subtype='DIR_PATH',
|
||||||
default='//textures')
|
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):
|
def draw(self, context):
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
@@ -114,28 +123,28 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
blender_id_profile = blender_id.get_active_profile()
|
blender_id_profile = blender_id.get_active_profile()
|
||||||
|
|
||||||
if blender_id is None:
|
if blender_id is None:
|
||||||
icon = 'ERROR'
|
msg_icon = 'ERROR'
|
||||||
text = 'This add-on requires Blender ID'
|
text = 'This add-on requires Blender ID'
|
||||||
help_text = 'Make sure that the Blender ID add-on is installed and activated'
|
help_text = 'Make sure that the Blender ID add-on is installed and activated'
|
||||||
elif not blender_id_profile:
|
elif not blender_id_profile:
|
||||||
icon = 'ERROR'
|
msg_icon = 'ERROR'
|
||||||
text = 'You are logged out.'
|
text = 'You are logged out.'
|
||||||
help_text = 'To login, go to the Blender ID add-on preferences.'
|
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:
|
elif bpy.app.debug and pillar.SUBCLIENT_ID not in blender_id_profile.subclients:
|
||||||
icon = 'QUESTION'
|
msg_icon = 'QUESTION'
|
||||||
text = 'No Blender Cloud credentials.'
|
text = 'No Blender Cloud credentials.'
|
||||||
help_text = ('You are logged in on Blender ID, but your credentials have not '
|
help_text = ('You are logged in on Blender ID, but your credentials have not '
|
||||||
'been synchronized with Blender Cloud yet. Press the Update '
|
'been synchronized with Blender Cloud yet. Press the Update '
|
||||||
'Credentials button.')
|
'Credentials button.')
|
||||||
else:
|
else:
|
||||||
icon = 'WORLD_DATA'
|
msg_icon = 'WORLD_DATA'
|
||||||
text = 'You are logged in as %s.' % blender_id_profile.username
|
text = 'You are logged in as %s.' % blender_id_profile.username
|
||||||
help_text = ('To logout or change profile, '
|
help_text = ('To logout or change profile, '
|
||||||
'go to the Blender ID add-on preferences.')
|
'go to the Blender ID add-on preferences.')
|
||||||
|
|
||||||
# Authentication stuff
|
# Authentication stuff
|
||||||
auth_box = layout.box()
|
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)
|
help_lines = textwrap.wrap(help_text, 80)
|
||||||
for line in help_lines:
|
for line in help_lines:
|
||||||
@@ -145,18 +154,18 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
|
|
||||||
# Texture browser stuff
|
# Texture browser stuff
|
||||||
texture_box = layout.box()
|
texture_box = layout.box()
|
||||||
texture_box.enabled = icon != 'ERROR'
|
texture_box.enabled = msg_icon != 'ERROR'
|
||||||
sub = texture_box.column()
|
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(self, "local_texture_dir", text='Default')
|
||||||
sub.prop(context.scene, "local_texture_dir", text='Current scene')
|
sub.prop(context.scene, "local_texture_dir", text='Current scene')
|
||||||
|
|
||||||
# Blender Sync stuff
|
# Blender Sync stuff
|
||||||
bss = context.window_manager.blender_sync_status
|
bss = context.window_manager.blender_sync_status
|
||||||
bsync_box = layout.box()
|
bsync_box = layout.box()
|
||||||
bsync_box.enabled = icon != 'ERROR'
|
bsync_box.enabled = msg_icon != 'ERROR'
|
||||||
row = bsync_box.row().split(percentage=0.33)
|
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 = {
|
icon_for_level = {
|
||||||
'INFO': 'NONE',
|
'INFO': 'NONE',
|
||||||
@@ -164,16 +173,21 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
'ERROR': 'ERROR',
|
'ERROR': 'ERROR',
|
||||||
'SUBSCRIBE': '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 = row.row()
|
||||||
message_container.label(bss.message, icon=icon)
|
message_container.label(bss.message, icon=msg_icon)
|
||||||
|
|
||||||
sub = bsync_box.column()
|
sub = bsync_box.column()
|
||||||
|
|
||||||
if bss.level == 'SUBSCRIBE':
|
if bss.level == 'SUBSCRIBE':
|
||||||
self.draw_subscribe_button(sub)
|
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):
|
def draw_subscribe_button(self, layout):
|
||||||
layout.operator('pillar.subscribe', icon='WORLD')
|
layout.operator('pillar.subscribe', icon='WORLD')
|
||||||
@@ -216,6 +230,8 @@ class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
|
|||||||
bl_idname = 'pillar.credentials_update'
|
bl_idname = 'pillar.credentials_update'
|
||||||
bl_label = 'Update credentials'
|
bl_label = 'Update credentials'
|
||||||
|
|
||||||
|
log = logging.getLogger('bpy.ops.%s' % bl_idname)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
# Only allow activation when the user is actually logged in.
|
# 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
|
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():
|
def register():
|
||||||
bpy.utils.register_class(BlenderCloudPreferences)
|
bpy.utils.register_class(BlenderCloudPreferences)
|
||||||
bpy.utils.register_class(PillarCredentialsUpdate)
|
bpy.utils.register_class(PillarCredentialsUpdate)
|
||||||
@@ -299,8 +348,12 @@ def register():
|
|||||||
|
|
||||||
WindowManager.blender_sync_status = PointerProperty(type=SyncStatusProperties)
|
WindowManager.blender_sync_status = PointerProperty(type=SyncStatusProperties)
|
||||||
|
|
||||||
|
load_custom_icons()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
unload_custom_icons()
|
||||||
|
|
||||||
gui.unregister()
|
gui.unregister()
|
||||||
|
|
||||||
bpy.utils.unregister_class(PillarCredentialsUpdate)
|
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')
|
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
|
||||||
self.report({'INFO'},
|
self.report({'INFO'},
|
||||||
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
|
'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,22 +16,30 @@ import asyncio
|
|||||||
import pillarsdk
|
import pillarsdk
|
||||||
from pillarsdk import exceptions as sdk_exceptions
|
from pillarsdk import exceptions as sdk_exceptions
|
||||||
from .pillar import pillar_call
|
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']
|
SETTINGS_FILES_TO_UPLOAD = ['userpref.blend', 'startup.blend']
|
||||||
|
|
||||||
# These are RNA keys inside the userpref.blend file, and their
|
# These are RNA keys inside the userpref.blend file, and their
|
||||||
# Python properties names.
|
# Python properties names. These settings will not be synced.
|
||||||
LOCAL_SETTINGS_RNA = [
|
LOCAL_SETTINGS_RNA = [
|
||||||
(b'dpi', 'system.dpi'),
|
(b'dpi', 'system.dpi'),
|
||||||
(b'virtual_pixel', 'system.virtual_pixel_mode'),
|
(b'virtual_pixel', 'system.virtual_pixel_mode'),
|
||||||
(b'compute_device_id', 'system.compute_device'),
|
(b'compute_device_id', 'system.compute_device'),
|
||||||
(b'compute_device_type', 'system.compute_device_type'),
|
(b'compute_device_type', 'system.compute_device_type'),
|
||||||
|
(b'fontdir', 'filepaths.font_directory'),
|
||||||
|
(b'textudir', 'filepaths.texture_directory'),
|
||||||
|
(b'renderdir', 'filepaths.render_output_directory'),
|
||||||
|
(b'pythondir', 'filepaths.script_directory'),
|
||||||
|
(b'sounddir', 'filepaths.sound_directory'),
|
||||||
(b'tempdir', 'filepaths.temporary_directory'),
|
(b'tempdir', 'filepaths.temporary_directory'),
|
||||||
|
(b'render_cachedir', 'filepaths.render_cache_directory'),
|
||||||
|
(b'i18ndir', 'filepaths.i18n_branches_directory'),
|
||||||
|
(b'image_editor', 'filepaths.image_editor'),
|
||||||
|
(b'anim_player', 'filepaths.animation_player'),
|
||||||
]
|
]
|
||||||
|
|
||||||
REQUIRES_ROLES_FOR_SYNC = set() # no roles needed.
|
REQUIRES_ROLES_FOR_SYNC = set() # no roles needed.
|
||||||
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' \
|
||||||
'#blender-addon) will synchronize your Blender settings here.'
|
'#blender-addon) will synchronize your Blender settings here.'
|
||||||
@@ -70,28 +78,6 @@ def async_set_blender_sync_status(set_status: str):
|
|||||||
return decorator
|
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,
|
async def find_sync_group_id(home_project_id: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
blender_version: str,
|
blender_version: str,
|
||||||
@@ -105,7 +91,7 @@ async def find_sync_group_id(home_project_id: str,
|
|||||||
# Find the top-level sync group node. This should have been
|
# Find the top-level sync group node. This should have been
|
||||||
# created by Pillar while creating the home project.
|
# created by Pillar while creating the home project.
|
||||||
try:
|
try:
|
||||||
sync_group, created = await find_or_create_node(
|
sync_group, created = await pillar.find_or_create_node(
|
||||||
where={'project': home_project_id,
|
where={'project': home_project_id,
|
||||||
'node_type': 'group',
|
'node_type': 'group',
|
||||||
'parent': None,
|
'parent': None,
|
||||||
@@ -122,7 +108,7 @@ async def find_sync_group_id(home_project_id: str,
|
|||||||
|
|
||||||
# Find/create the sub-group for the requested Blender version
|
# Find/create the sub-group for the requested Blender version
|
||||||
try:
|
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,
|
where={'project': home_project_id,
|
||||||
'node_type': 'group',
|
'node_type': 'group',
|
||||||
'parent': sync_group['_id'],
|
'parent': sync_group['_id'],
|
||||||
@@ -145,84 +131,6 @@ async def find_sync_group_id(home_project_id: str,
|
|||||||
return sync_group['_id'], sub_sync_group['_id']
|
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()
|
@functools.lru_cache()
|
||||||
async def available_blender_versions(home_project_id: str, user_id: str) -> list:
|
async def available_blender_versions(home_project_id: str, user_id: str) -> list:
|
||||||
bss = bpy.context.window_manager.blender_sync_status
|
bss = bpy.context.window_manager.blender_sync_status
|
||||||
@@ -241,7 +149,7 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list
|
|||||||
caching=False)
|
caching=False)
|
||||||
|
|
||||||
if sync_group is None:
|
if sync_group is None:
|
||||||
bss.report({'ERROR'}, 'No synced Blender settings in your home project')
|
bss.report({'ERROR'}, 'No synced Blender settings in your Blender Cloud')
|
||||||
log.debug('-- unable to find sync group for home_project_id=%r and user_id=%r',
|
log.debug('-- unable to find sync group for home_project_id=%r and user_id=%r',
|
||||||
home_project_id, user_id)
|
home_project_id, user_id)
|
||||||
return []
|
return []
|
||||||
@@ -259,7 +167,7 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list
|
|||||||
caching=False)
|
caching=False)
|
||||||
|
|
||||||
if not sync_nodes or not sync_nodes._items:
|
if not sync_nodes or not sync_nodes._items:
|
||||||
bss.report({'ERROR'}, 'No synced Blender settings in your home project')
|
bss.report({'ERROR'}, 'No synced Blender settings in your Blender Cloud.')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
versions = [node.name for node in sync_nodes._items]
|
versions = [node.name for node in sync_nodes._items]
|
||||||
@@ -374,7 +282,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
|
|||||||
|
|
||||||
# Find the home project.
|
# Find the home project.
|
||||||
try:
|
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:
|
except sdk_exceptions.ForbiddenAccess:
|
||||||
self.log.exception('Forbidden access to home project.')
|
self.log.exception('Forbidden access to home project.')
|
||||||
self.bss_report({'ERROR'}, 'Did not get access to home project.')
|
self.bss_report({'ERROR'}, 'Did not get access to home project.')
|
||||||
@@ -430,12 +338,23 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
|
|||||||
self.log.debug('Skipping non-existing %s', path)
|
self.log.debug('Skipping non-existing %s', path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if self.signalling_future.cancelled():
|
||||||
|
self.bss_report({'WARNING'}, 'Upload aborted.')
|
||||||
|
return
|
||||||
|
|
||||||
self.bss_report({'INFO'}, 'Uploading %s' % fname)
|
self.bss_report({'INFO'}, 'Uploading %s' % fname)
|
||||||
await attach_file_to_group(path,
|
try:
|
||||||
self.home_project_id,
|
await pillar.attach_file_to_group(path,
|
||||||
self.sync_group_versioned_id,
|
self.home_project_id,
|
||||||
self.user_id,
|
self.sync_group_versioned_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)
|
await self.action_refresh(context)
|
||||||
|
|
||||||
@@ -455,7 +374,8 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
|
|||||||
|
|
||||||
# If the sync group node doesn't exist, offer a list of groups that do.
|
# If the sync group node doesn't exist, offer a list of groups that do.
|
||||||
if self.sync_group_id is None:
|
if self.sync_group_id is None:
|
||||||
self.bss_report({'ERROR'}, 'There are no synced Blender settings in your home project.')
|
self.bss_report({'ERROR'},
|
||||||
|
'There are no synced Blender settings in your Blender Cloud.')
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.sync_group_versioned_id is None:
|
if self.sync_group_versioned_id is None:
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Primary requirements:
|
# Primary requirements:
|
||||||
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
|
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
|
||||||
lockfile==0.12.2
|
lockfile==0.12.2
|
||||||
pillarsdk==1.3.0
|
pillarsdk==1.4.0
|
||||||
wheel==0.29.0
|
wheel==0.29.0
|
||||||
|
|
||||||
# Secondary requirements:
|
# Secondary requirements:
|
||||||
|
6
setup.py
6
setup.py
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
@@ -14,6 +15,7 @@ from distutils.command.install_egg_info import install_egg_info
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
requirement_re = re.compile('[><=]+')
|
requirement_re = re.compile('[><=]+')
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
|
|
||||||
def set_default_path(var, default):
|
def set_default_path(var, default):
|
||||||
@@ -177,7 +179,7 @@ setup(
|
|||||||
'wheels': BuildWheels},
|
'wheels': BuildWheels},
|
||||||
name='blender_cloud',
|
name='blender_cloud',
|
||||||
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
|
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
|
||||||
version='1.2.0',
|
version='1.3.1',
|
||||||
author='Sybren A. Stüvel',
|
author='Sybren A. Stüvel',
|
||||||
author_email='sybren@stuvel.eu',
|
author_email='sybren@stuvel.eu',
|
||||||
packages=find_packages('.'),
|
packages=find_packages('.'),
|
||||||
|
Reference in New Issue
Block a user