From c34ffb66db9b0dbb635887a711c889234c533e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 21 Nov 2017 15:00:23 +0100 Subject: [PATCH] Added CLI script to share images to the Cloud. Assumes that you are logged in on Blender ID with the Blender ID Add-on. Haven't tested what happens when you've never shared any images yet. --- cloud_share_img.py | 299 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100755 cloud_share_img.py diff --git a/cloud_share_img.py b/cloud_share_img.py new file mode 100755 index 0000000..1081a3a --- /dev/null +++ b/cloud_share_img.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +"""CLI command for sharing an image via Blender Cloud. + +Assumes that you are logged in on Blender ID with the Blender ID Add-on. + +The user_config_dir and user_data_dir functions come from + https://github.com/ActiveState/appdirs/blob/master/appdirs.py and + are licensed under the MIT license. +""" + +import argparse +import json +import mimetypes +import os.path +import pprint +import sys +import webbrowser + +from urllib.parse import urljoin + +import requests + +cli = argparse.Namespace() # CLI args from argparser +sess = requests.Session() +IMAGE_SHARING_GROUP_NODE_NAME = 'Image sharing' + +if sys.platform.startswith('java'): + import platform + + os_name = platform.java_ver()[3][0] + if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc. + system = 'win32' + elif os_name.startswith('Mac'): # "Mac OS X", etc. + system = 'darwin' + else: # "Linux", "SunOS", "FreeBSD", etc. + # Setting this to "linux2" is not ideal, but only Windows or Mac + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = 'linux2' +else: + system = sys.platform + + +def request(method: str, rel_url: str, **kwargs) -> requests.Response: + kwargs.setdefault('auth', (cli.token, '')) + url = urljoin(cli.server_url, rel_url) + return sess.request(method, url, **kwargs) + + +def get(rel_url: str, **kwargs) -> requests.Response: + return request('GET', rel_url, **kwargs) + + +def post(rel_url: str, **kwargs) -> requests.Response: + return request('POST', rel_url, **kwargs) + + +def find_user_id() -> str: + """Returns the current user ID.""" + + print(15 * '=', 'User info', 15 * '=') + resp = get('/api/users/me') + resp.raise_for_status() + + user_info = resp.json() + print('You are logged in as %(full_name)s (%(_id)s)' % user_info) + + return user_info['_id'] + + +def find_home_project() -> dict: + resp = get('/api/bcloud/home-project') + resp.raise_for_status() + + proj = resp.json() + print('Your home project ID is %s' % proj['_id']) + return proj + + +def find_image_sharing_group_id(home_project_id, user_id) -> str: + # Find the top-level image sharing group node. + + node_doc = {'project': home_project_id, + 'node_type': 'group', + 'parent': None, + 'name': IMAGE_SHARING_GROUP_NODE_NAME} + + resp = get('/api/nodes', params={'where': json.dumps(node_doc)}) + resp.raise_for_status() + items = resp.json()['_items'] + + if not items: + print('Share group not found, creating one.') + node_doc.update({ + 'user': user_id, + 'properties': {}, + }) + resp = post('/api/nodes', json=node_doc) + resp.raise_for_status() + share_group = resp.json() + else: + share_group = items[0] + + # print('Share group:', share_group) + return share_group['_id'] + + +def upload_image(): + # user_id = find_user_id() + # home_proj = find_home_project() + # home_project_id = home_proj['_id'] + # group_id = find_image_sharing_group_id(home_project_id, user_id) + + user_id = '564cf2b1c379cf10c4aaceaf' + home_project_id = '577278e5c379cf03400ffb1e' + group_id = '5785f33bc379cf31436a5c20' + basename = os.path.basename(cli.imgfile) + print('Sharing group ID is %s' % group_id) + + # Upload the image to the project. + print('Uploading %r' % cli.imgfile) + mimetype, _ = mimetypes.guess_type(cli.imgfile, strict=False) + with open(cli.imgfile, mode='rb') as infile: + resp = post('api/storage/stream/%s' % home_project_id, + files={'file': (basename, infile, mimetype)}) + resp.raise_for_status() + file_upload_resp = resp.json() + file_upload_status = file_upload_resp.get('_status') or file_upload_resp.get('status') + if file_upload_status != 'ok': + raise ValueError('Received bad status %s from Pillar: %s' % + (file_upload_status, json.dumps(file_upload_resp))) + file_id = file_upload_resp['file_id'] + print('File ID is', file_id) + + # Create the asset node + asset_node = { + 'project': home_project_id, + 'node_type': 'asset', + 'name': basename, + 'parent': group_id, + 'properties': { + 'content_type': mimetype, + 'file': file_id, + }, + } + resp = post('api/nodes', json=asset_node) + resp.raise_for_status() + node_info = resp.json() + node_id = node_info['_id'] + print('Created asset node', node_id) + + # Share the node to get a public URL. + resp = post('api/nodes/%s/share' % node_id) + resp.raise_for_status() + share_info = resp.json() + print(json.dumps(share_info, indent=4)) + + url = share_info.get('short_link') + print('Opening %s in a browser' % url) + webbrowser.open_new_tab(url) + + +def find_credentials(): + """Finds BlenderID credentials. + + :rtype: str + :returns: the authentication token to use. + """ + import glob + + # Find BlenderID profile file. + configpath = user_config_dir('blender', 'Blender Foundation', roaming=True) + found = glob.glob(os.path.join(configpath, '*')) + for confpath in reversed(sorted(found)): + profiles_path = os.path.join(confpath, 'config', 'blender_id', 'profiles.json') + if not os.path.exists(profiles_path): + continue + + print('Reading credentials from %s' % profiles_path) + with open(profiles_path) as infile: + profiles = json.load(infile, encoding='utf8') + if profiles: + break + else: + print('Unable to find Blender ID credentials. Log in with the Blender ID add-on in ' + 'Blender first.') + raise SystemExit() + + active_profile = profiles[u'active_profile'] + profile = profiles[u'profiles'][active_profile] + print('Logging in as %s' % profile[u'username']) + + return profile[u'token'] + + +def main(): + global cli + + parser = argparse.ArgumentParser() + parser.add_argument('imgfile', help='The image file to share.') + parser.add_argument('-u', '--server-url', default='https://cloud.blender.org/', + help='URL of the Flamenco server.') + parser.add_argument('-t', '--token', + help='Authentication token to use. If not given, your token from the ' + 'Blender ID add-on is used.') + + cli = parser.parse_args() + if not cli.token: + cli.token = find_credentials() + + upload_image() + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + Typical user config directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by default "~/.config/". + """ + if system in {"win32", "darwin"}: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by default "~/.local/share/". + """ + if system == "win32": + raise RuntimeError("Sorry, Windows is not supported for now.") + elif system == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +if __name__ == '__main__': + main()