From 408f017e811adc51d3ed58b0508c3aadaee93f40 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Mon, 23 Aug 2021 14:50:00 +0200 Subject: [PATCH] removed updater 1.6.1 - removed: Auto Updater that was failing since 2.93 - prefs: add a checkbox to disable the "load base palette button" in UI. --- CHANGELOG.md | 5 + README_FR.md | 2 +- UI_tools.py | 16 +- __init__.py | 61 +- addon_updater.py | 1673 ------------------------------------------ addon_updater_ops.py | 1503 ------------------------------------- 6 files changed, 31 insertions(+), 3229 deletions(-) delete mode 100644 addon_updater.py delete mode 100644 addon_updater_ops.py diff --git a/CHANGELOG.md b/CHANGELOG.md index eb5cb65..da8c263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +1.6.1 + +- removed: Auto Updater that was failing since 2.93 +- prefs: add a checkbox to disable the "load base palette button" in UI. + 1.6.0 - feat: Namespace upgrade diff --git a/README_FR.md b/README_FR.md index 0e0b93b..b998eb3 100644 --- a/README_FR.md +++ b/README_FR.md @@ -127,7 +127,7 @@ TODO: profiter du système de render layers (per layer) pour faire un meilleur b **Check links** (pop une fenêtre) - Permet de lister les liens de la scène, voir si il y en a des cassés et ouvrir le dossier d'un lien existant. -**Auto update** - Un système de mise à jour est diponible dans les addon prefs, autocheck possible (désactivé par défaut). Utilise [CGcookie addon updater](https://github.com/CGCookie/blender-addon-updater)) + ## raccourci supplémentaires diff --git a/UI_tools.py b/UI_tools.py index e4f238d..589e3b1 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -1,4 +1,4 @@ -from . import addon_updater_ops +# from . import addon_updater_ops from .utils import get_addon_prefs import bpy from pathlib import Path @@ -44,9 +44,10 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel): def draw(self, context): layout = self.layout # layout.use_property_split = True + prefs = get_addon_prefs() rd = context.scene.render # check for update - addon_updater_ops.check_for_update_background() + # addon_updater_ops.check_for_update_background() # layout.label(text='View options:') ## flip X cam @@ -147,10 +148,11 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel): row.operator('gp.rename_data_from_obj', text='Rename all').rename_all = True ## Check base palette - if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")): - box = col.box() - box.label(text='Missing base material setup', icon='INFO') - box.operator('gp.load_default_palette') + if prefs.warn_base_palette and prefs.palette_path: + if not all(x in [m.name for m in context.object.data.materials if m] for x in ("line", "invisible")): + box = col.box() + box.label(text='Missing base material setup', icon='INFO') + box.operator('gp.load_default_palette') else: col.label(text='No GP object selected') @@ -177,7 +179,7 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel): col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon) # Mention update as notice - addon_updater_ops.update_notice_box_ui(self, context) + # addon_updater_ops.update_notice_box_ui(self, context) # row = layout.row(align=False) diff --git a/__init__.py b/__init__.py index 6241a9c..989cdda 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (1, 6, 0), +"version": (1, 6, 1), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -24,7 +24,7 @@ bl_info = { "category": "3D View", } -from . import addon_updater_ops +# from . import addon_updater_ops from .utils import * from .functions import * @@ -124,48 +124,12 @@ class GPTB_prefs(bpy.types.AddonPreferences): pref_tabs : EnumProperty( items=(('PREF', "Preferences", "Change some preferences of the modal"), ('MAN_OPS', "Operator", "Operator to add Manually"), - # ('TUTO', "Tutorial", "How to use the tool"), ('CHECKS', "Check List", "Customise what should happend when hitting 'check fix' button"), - ('UPDATE', "Update", "Check and apply updates"), + # ('UPDATE', "Update", "Check and apply updates"), + # ('TUTO', "Tutorial", "How to use the tool"), # ('KEYMAP', "Keymap", "customise the default keymap"), ), default='PREF') - - ## addon pref updater props - - auto_check_update : BoolProperty( - name="Auto-check for Update", - description="If enabled, auto-check for updates using an interval", - default=False, - ) - - updater_intrval_months : IntProperty( - name='Months', - description="Number of months between checking for updates", - default=0, - min=0 - ) - updater_intrval_days : IntProperty( - name='Days', - description="Number of days between checking for updates", - default=7, - min=0, - max=31 - ) - updater_intrval_hours : IntProperty( - name='Hours', - description="Number of hours between checking for updates", - default=0, - min=0, - max=23 - ) - updater_intrval_minutes : IntProperty( - name='Minutes', - description="Number of minutes between checking for updates", - default=0, - min=0, - max=59 - ) ## addon prefs @@ -212,7 +176,13 @@ class GPTB_prefs(bpy.types.AddonPreferences): palette_path : StringProperty( name="Palettes directory", description="Path to palette containing palette.json files to save and load", - default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path + default="", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path + + warn_base_palette : BoolProperty( + name="Warn if base palette isn't loaded", + description="Display a button to load palette base.json if current grease pencil has a no 'line' and 'invisible' materials", + default=True, + ) use_env_brushes : BoolProperty( name="Use Project Brushes", @@ -364,6 +334,7 @@ class GPTB_prefs(bpy.types.AddonPreferences): ## Palette subbox.prop(self, 'use_env_palettes', text='Use Palettes Environnement Path') subbox.prop(self, 'palette_path') + subbox.prop(self, 'warn_base_palette') ## Brushes subbox.prop(self, 'use_env_brushes', text='Use Brushes Environnement Path') @@ -490,8 +461,8 @@ class GPTB_prefs(bpy.types.AddonPreferences): col.prop(self.fixprops, "lock_object_mode") # row.label(text='lock the active camera if not a draw cam (and if not "layout" in blendfile name)') - if self.pref_tabs == 'UPDATE': - addon_updater_ops.update_settings_ui(self, context) + # if self.pref_tabs == 'UPDATE': + # addon_updater_ops.update_settings_ui(self, context) @@ -543,7 +514,7 @@ GPT_OT_auto_tint_gp_layers, def register(): - addon_updater_ops.register(bl_info) + # addon_updater_ops.register(bl_info) # bpy.types.Scene.gpfixprops = bpy.props.PointerProperty(type = GP_PG_FixSettings) # used in prefs for cls in classes: bpy.utils.register_class(cls) @@ -586,7 +557,7 @@ def unregister(): bpy.app.handlers.save_pre.remove(remap_relative) keymaps.unregister() - addon_updater_ops.unregister() + # addon_updater_ops.unregister() for cls in reversed(classes): bpy.utils.unregister_class(cls) UI_tools.unregister() diff --git a/addon_updater.py b/addon_updater.py deleted file mode 100644 index 064c269..0000000 --- a/addon_updater.py +++ /dev/null @@ -1,1673 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -""" -See documentation for usage -https://github.com/CGCookie/blender-addon-updater - -""" - -__version__ = "1.0.8" - -import errno -import platform -import ssl -import urllib.request -import urllib -import os -import json -import zipfile -import shutil -import threading -import fnmatch -from datetime import datetime, timedelta - -# blender imports, used in limited cases -import bpy -import addon_utils - -# ----------------------------------------------------------------------------- -# Define error messages/notices & hard coded globals -# ----------------------------------------------------------------------------- - -# currently not used -DEFAULT_TIMEOUT = 10 -DEFAULT_PER_PAGE = 30 - - -# ----------------------------------------------------------------------------- -# The main class -# ----------------------------------------------------------------------------- - -class Singleton_updater(object): - """ - This is the singleton class to reference a copy from, - it is the shared module level class - """ - def __init__(self): - - self._engine = GitlabEngine()#GithubEngine() - self._user = None - self._repo = None - self._website = None - self._current_version = None - self._subfolder_path = None - self._tags = [] - self._tag_latest = None - self._tag_names = [] - self._latest_release = None - self._use_releases = False - self._include_branches = False - self._include_branch_list = ['master'] - self._include_branch_autocheck = False - self._manual_only = False - self._version_min_update = None - self._version_max_update = None - - # by default, backup current addon if new is being loaded - self._backup_current = True - self._backup_ignore_patterns = None - - # set patterns for what files to overwrite on update - self._overwrite_patterns = ["*.py","*.pyc"] - self._remove_pre_update_patterns = [] - - # by default, don't auto enable/disable the addon on update - # as it is slightly less stable/won't always fully reload module - self._auto_reload_post_update = False - - # settings relating to frequency and whether to enable auto background check - self._check_interval_enable = False - self._check_interval_months = 0 - self._check_interval_days = 1 - self._check_interval_hours = 0 - self._check_interval_minutes = 0 - - # runtime variables, initial conditions - self._verbose = False - self._fake_install = False - self._async_checking = False # only true when async daemon started - self._update_ready = None - self._update_link = None - self._update_version = None - self._source_zip = None - self._check_thread = None - self._select_link = None - self.skip_tag = None - - # get from module data - self._addon = __package__.lower() - self._addon_package = __package__ # must not change - self._updater_path = os.path.join(os.path.dirname(__file__), - self._addon+"_updater") - self._addon_root = os.path.dirname(__file__) - self._json = {} - self._error = None - self._error_msg = None - self._prefiltered_tag_count = 0 - - # UI code only, ie not used within this module but still useful - # properties to have - - # to verify a valid import, in place of placeholder import - self.showpopups = True # used in UI to show or not show update popups - self.invalidupdater = False - - # pre-assign basic select-link function - def select_link_function(self, tag): - return tag["zipball_url"] - - self._select_link = select_link_function - - - # ------------------------------------------------------------------------- - # Getters and setters - # ------------------------------------------------------------------------- - - - @property - def addon(self): - return self._addon - @addon.setter - def addon(self, value): - self._addon = str(value) - - @property - def api_url(self): - return self._engine.api_url - @api_url.setter - def api_url(self, value): - if self.check_is_url(value) == False: - raise ValueError("Not a valid URL: " + value) - self._engine.api_url = value - - @property - def async_checking(self): - return self._async_checking - - @property - def auto_reload_post_update(self): - return self._auto_reload_post_update - @auto_reload_post_update.setter - def auto_reload_post_update(self, value): - try: - self._auto_reload_post_update = bool(value) - except: - raise ValueError("Must be a boolean value") - - @property - def backup_current(self): - return self._backup_current - @backup_current.setter - def backup_current(self, value): - if value == None: - self._backup_current = False - return - else: - self._backup_current = value - - @property - def backup_ignore_patterns(self): - return self._backup_ignore_patterns - @backup_ignore_patterns.setter - def backup_ignore_patterns(self, value): - if value == None: - self._backup_ignore_patterns = None - return - elif type(value) != type(['list']): - raise ValueError("Backup pattern must be in list format") - else: - self._backup_ignore_patterns = value - - @property - def check_interval(self): - return (self._check_interval_enable, - self._check_interval_months, - self._check_interval_days, - self._check_interval_hours, - self._check_interval_minutes) - - @property - def current_version(self): - return self._current_version - @current_version.setter - def current_version(self, tuple_values): - if tuple_values==None: - self._current_version = None - return - elif type(tuple_values) is not tuple: - try: - tuple(tuple_values) - except: - raise ValueError( - "Not a tuple! current_version must be a tuple of integers") - for i in tuple_values: - if type(i) is not int: - raise ValueError( - "Not an integer! current_version must be a tuple of integers") - self._current_version = tuple(tuple_values) - - @property - def engine(self): - return self._engine.name - @engine.setter - def engine(self, value): - if value.lower()=="github": - self._engine = GithubEngine() - elif value.lower()=="gitlab": - self._engine = GitlabEngine() - elif value.lower()=="bitbucket": - self._engine = BitbucketEngine() - else: - raise ValueError("Invalid engine selection") - - @property - def error(self): - return self._error - - @property - def error_msg(self): - return self._error_msg - - @property - def fake_install(self): - return self._fake_install - @fake_install.setter - def fake_install(self, value): - if type(value) != type(False): - raise ValueError("fake_install must be a boolean value") - self._fake_install = bool(value) - - # not currently used - @property - def include_branch_autocheck(self): - return self._include_branch_autocheck - @include_branch_autocheck.setter - def include_branch_autocheck(self, value): - try: - self._include_branch_autocheck = bool(value) - except: - raise ValueError("include_branch_autocheck must be a boolean value") - - @property - def include_branch_list(self): - return self._include_branch_list - @include_branch_list.setter - def include_branch_list(self, value): - try: - if value == None: - self._include_branch_list = ['master'] - elif type(value) != type(['master']) or value==[]: - raise ValueError("include_branch_list should be a list of valid branches") - else: - self._include_branch_list = value - except: - raise ValueError("include_branch_list should be a list of valid branches") - - @property - def include_branches(self): - return self._include_branches - @include_branches.setter - def include_branches(self, value): - try: - self._include_branches = bool(value) - except: - raise ValueError("include_branches must be a boolean value") - - @property - def json(self): - if self._json == {}: - self.set_updater_json() - return self._json - - @property - def latest_release(self): - if self._latest_release == None: - return None - return self._latest_release - - @property - def manual_only(self): - return self._manual_only - @manual_only.setter - def manual_only(self, value): - try: - self._manual_only = bool(value) - except: - raise ValueError("manual_only must be a boolean value") - - @property - def overwrite_patterns(self): - return self._overwrite_patterns - @overwrite_patterns.setter - def overwrite_patterns(self, value): - if value == None: - self._overwrite_patterns = ["*.py","*.pyc"] - elif type(value) != type(['']): - raise ValueError("overwrite_patterns needs to be in a list format") - else: - self._overwrite_patterns = value - - @property - def private_token(self): - return self._engine.token - @private_token.setter - def private_token(self, value): - if value==None: - self._engine.token = None - else: - self._engine.token = str(value) - - @property - def remove_pre_update_patterns(self): - return self._remove_pre_update_patterns - @remove_pre_update_patterns.setter - def remove_pre_update_patterns(self, value): - if value == None: - self._remove_pre_update_patterns = [] - elif type(value) != type(['']): - raise ValueError("remove_pre_update_patterns needs to be in a list format") - else: - self._remove_pre_update_patterns = value - - @property - def repo(self): - return self._repo - @repo.setter - def repo(self, value): - try: - self._repo = str(value) - except: - raise ValueError("User must be a string") - - @property - def select_link(self): - return self._select_link - @select_link.setter - def select_link(self, value): - # ensure it is a function assignment, with signature: - # input self, tag; returns link name - if not hasattr(value, "__call__"): - raise ValueError("select_link must be a function") - self._select_link = value - - @property - def stage_path(self): - return self._updater_path - @stage_path.setter - def stage_path(self, value): - if value == None: - if self._verbose: print("Aborting assigning stage_path, it's null") - return - elif value != None and not os.path.exists(value): - try: - os.makedirs(value) - except: - if self._verbose: print("Error trying to staging path") - return - self._updater_path = value - - @property - def subfolder_path(self): - return self._subfolder_path - @subfolder_path.setter - def subfolder_path(self, value): - self._subfolder_path = value - - @property - def tags(self): - if self._tags == []: - return [] - tag_names = [] - for tag in self._tags: - tag_names.append(tag["name"]) - return tag_names - - @property - def tag_latest(self): - if self._tag_latest == None: - return None - return self._tag_latest["name"] - - @property - def update_link(self): - return self._update_link - - @property - def update_ready(self): - return self._update_ready - - @property - def update_version(self): - return self._update_version - - @property - def use_releases(self): - return self._use_releases - @use_releases.setter - def use_releases(self, value): - try: - self._use_releases = bool(value) - except: - raise ValueError("use_releases must be a boolean value") - - @property - def user(self): - return self._user - @user.setter - def user(self, value): - try: - self._user = str(value) - except: - raise ValueError("User must be a string value") - - @property - def verbose(self): - return self._verbose - @verbose.setter - def verbose(self, value): - try: - self._verbose = bool(value) - if self._verbose == True: - print(self._addon+" updater verbose is enabled") - except: - raise ValueError("Verbose must be a boolean value") - - @property - def version_max_update(self): - return self._version_max_update - @version_max_update.setter - def version_max_update(self, value): - if value == None: - self._version_max_update = None - return - if type(value) != type((1,2,3)): - raise ValueError("Version maximum must be a tuple") - for subvalue in value: - if type(subvalue) != int: - raise ValueError("Version elements must be integers") - self._version_max_update = value - - @property - def version_min_update(self): - return self._version_min_update - @version_min_update.setter - def version_min_update(self, value): - if value == None: - self._version_min_update = None - return - if type(value) != type((1,2,3)): - raise ValueError("Version minimum must be a tuple") - for subvalue in value: - if type(subvalue) != int: - raise ValueError("Version elements must be integers") - self._version_min_update = value - - @property - def website(self): - return self._website - @website.setter - def website(self, value): - if self.check_is_url(value) == False: - raise ValueError("Not a valid URL: " + value) - self._website = value - - - # ------------------------------------------------------------------------- - # Parameter validation related functions - # ------------------------------------------------------------------------- - - - def check_is_url(self, url): - if not ("http://" in url or "https://" in url): - return False - if "." not in url: - return False - return True - - def get_tag_names(self): - tag_names = [] - self.get_tags() - for tag in self._tags: - tag_names.append(tag["name"]) - return tag_names - - def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0): - # enabled = False, default initially will not check against frequency - # if enabled, default is then 2 weeks - - if type(enable) is not bool: - raise ValueError("Enable must be a boolean value") - if type(months) is not int: - raise ValueError("Months must be an integer value") - if type(days) is not int: - raise ValueError("Days must be an integer value") - if type(hours) is not int: - raise ValueError("Hours must be an integer value") - if type(minutes) is not int: - raise ValueError("Minutes must be an integer value") - - if enable==False: - self._check_interval_enable = False - else: - self._check_interval_enable = True - - self._check_interval_months = months - self._check_interval_days = days - self._check_interval_hours = hours - self._check_interval_minutes = minutes - - # declare how the class gets printed - - def __repr__(self): - return "".format(a=__file__) - - def __str__(self): - return "Updater, with user: {a}, repository: {b}, url: {c}".format( - a=self._user, - b=self._repo, c=self.form_repo_url()) - - - # ------------------------------------------------------------------------- - # API-related functions - # ------------------------------------------------------------------------- - - def form_repo_url(self): - return self._engine.form_repo_url(self) - - def form_tags_url(self): - return self._engine.form_tags_url(self) - - def form_branch_url(self, branch): - return self._engine.form_branch_url(branch, self) - - def get_tags(self): - request = self.form_tags_url() - if self._verbose: print("Getting tags from server") - - # get all tags, internet call - all_tags = self._engine.parse_tags(self.get_api(request), self) - if all_tags is not None: - self._prefiltered_tag_count = len(all_tags) - else: - self._prefiltered_tag_count = 0 - all_tags = [] - - # pre-process to skip tags - if self.skip_tag != None: - self._tags = [tg for tg in all_tags if self.skip_tag(self, tg)==False] - else: - self._tags = all_tags - - # get additional branches too, if needed, and place in front - # Does NO checking here whether branch is valid - if self._include_branches == True: - temp_branches = self._include_branch_list.copy() - temp_branches.reverse() - for branch in temp_branches: - request = self.form_branch_url(branch) - include = { - "name":branch.title(), - "zipball_url":request - } - self._tags = [include] + self._tags # append to front - - if self._tags == None: - # some error occurred - self._tag_latest = None - self._tags = [] - return - elif self._prefiltered_tag_count == 0 and self._include_branches == False: - self._tag_latest = None - if self._error == None: # if not None, could have had no internet - self._error = "No releases found" - self._error_msg = "No releases or tags found on this repository" - if self._verbose: print("No releases or tags found on this repository") - elif self._prefiltered_tag_count == 0 and self._include_branches == True: - if not self._error: self._tag_latest = self._tags[0] - if self._verbose: - branch = self._include_branch_list[0] - print("{} branch found, no releases".format(branch), self._tags[0]) - elif (len(self._tags)-len(self._include_branch_list)==0 and self._include_branches==True) \ - or (len(self._tags)==0 and self._include_branches==False) \ - and self._prefiltered_tag_count > 0: - self._tag_latest = None - self._error = "No releases available" - self._error_msg = "No versions found within compatible version range" - if self._verbose: print("No versions found within compatible version range") - else: - if self._include_branches == False: - self._tag_latest = self._tags[0] - if self._verbose: print("Most recent tag found:",self._tags[0]['name']) - else: - # don't return branch if in list - n = len(self._include_branch_list) - self._tag_latest = self._tags[n] # guaranteed at least len()=n+1 - if self._verbose: print("Most recent tag found:",self._tags[n]['name']) - - - # all API calls to base url - def get_raw(self, url): - # print("Raw request:", url) - request = urllib.request.Request(url) - try: - context = ssl._create_unverified_context() - except: - # some blender packaged python versions don't have this, largely - # useful for local network setups otherwise minimal impact - context = None - - # setup private request headers if appropriate - if self._engine.token != None: - if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN',self._engine.token) - else: - if self._verbose: print("Tokens not setup for engine yet") - - # Always set user agent - request.add_header('User-Agent', "Python/"+str(platform.python_version())) - - # run the request - try: - if context: - result = urllib.request.urlopen(request, context=context) - else: - result = urllib.request.urlopen(request) - except urllib.error.HTTPError as e: - if str(e.code) == "403": - self._error = "HTTP error (access denied)" - self._error_msg = str(e.code) + " - server error response" - print(self._error, self._error_msg) - else: - self._error = "HTTP error" - self._error_msg = str(e.code) - print(self._error, self._error_msg) - self._update_ready = None - except urllib.error.URLError as e: - reason = str(e.reason) - if "TLSV1_ALERT" in reason or "SSL" in reason.upper(): - self._error = "Connection rejected, download manually" - self._error_msg = reason - print(self._error, self._error_msg) - else: - self._error = "URL error, check internet connection" - self._error_msg = reason - print(self._error, self._error_msg) - self._update_ready = None - return None - else: - result_string = result.read() - result.close() - return result_string.decode() - - - # result of all api calls, decoded into json format - def get_api(self, url): - # return the json version - get = None - get = self.get_raw(url) - if get != None: - try: - return json.JSONDecoder().decode(get) - except Exception as e: - self._error = "API response has invalid JSON format" - self._error_msg = str(e.reason) - self._update_ready = None - print(self._error, self._error_msg) - return None - else: - return None - - - # create a working directory and download the new files - def stage_repository(self, url): - - local = os.path.join(self._updater_path,"update_staging") - error = None - - # make/clear the staging folder - # ensure the folder is always "clean" - if self._verbose: print("Preparing staging folder for download:\n",local) - if os.path.isdir(local) == True: - try: - shutil.rmtree(local) - os.makedirs(local) - except: - error = "failed to remove existing staging directory" - else: - try: - os.makedirs(local) - except: - error = "failed to create staging directory" - - if error != None: - if self._verbose: print("Error: Aborting update, "+error) - self._error = "Update aborted, staging path error" - self._error_msg = "Error: {}".format(error) - return False - - if self._backup_current==True: - self.create_backup() - if self._verbose: print("Now retrieving the new source zip") - - self._source_zip = os.path.join(local,"source.zip") - - if self._verbose: print("Starting download update zip") - try: - request = urllib.request.Request(url) - context = ssl._create_unverified_context() - - # setup private token if appropriate - if self._engine.token != None: - if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN',self._engine.token) - else: - if self._verbose: print("Tokens not setup for selected engine yet") - - # Always set user agent - request.add_header('User-Agent', "Python/"+str(platform.python_version())) - - self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip) - # add additional checks on file size being non-zero - if self._verbose: print("Successfully downloaded update zip") - return True - except Exception as e: - self._error = "Error retrieving download, bad link?" - self._error_msg = "Error: {}".format(e) - if self._verbose: - print("Error retrieving download, bad link?") - print("Error: {}".format(e)) - return False - - - def create_backup(self): - if self._verbose: print("Backing up current addon folder") - local = os.path.join(self._updater_path,"backup") - tempdest = os.path.join(self._addon_root, - os.pardir, - self._addon+"_updater_backup_temp") - - if self._verbose: print("Backup destination path: ",local) - - if os.path.isdir(local): - try: - shutil.rmtree(local) - except: - if self._verbose:print("Failed to removed previous backup folder, contininuing") - - # remove the temp folder; shouldn't exist but could if previously interrupted - if os.path.isdir(tempdest): - try: - shutil.rmtree(tempdest) - except: - if self._verbose:print("Failed to remove existing temp folder, contininuing") - # make the full addon copy, which temporarily places outside the addon folder - if self._backup_ignore_patterns != None: - shutil.copytree( - self._addon_root,tempdest, - ignore=shutil.ignore_patterns(*self._backup_ignore_patterns)) - else: - shutil.copytree(self._addon_root,tempdest) - shutil.move(tempdest,local) - - # save the date for future ref - now = datetime.now() - self._json["backup_date"] = "{m}-{d}-{yr}".format( - m=now.strftime("%B"),d=now.day,yr=now.year) - self.save_updater_json() - - def restore_backup(self): - if self._verbose: print("Restoring backup") - - if self._verbose: print("Backing up current addon folder") - backuploc = os.path.join(self._updater_path,"backup") - tempdest = os.path.join(self._addon_root, - os.pardir, - self._addon+"_updater_backup_temp") - tempdest = os.path.abspath(tempdest) - - # make the copy - shutil.move(backuploc,tempdest) - shutil.rmtree(self._addon_root) - os.rename(tempdest,self._addon_root) - - self._json["backup_date"] = "" - self._json["just_restored"] = True - self._json["just_updated"] = True - self.save_updater_json() - - self.reload_addon() - - def unpack_staged_zip(self,clean=False): - """Unzip the downloaded file, and validate contents""" - if os.path.isfile(self._source_zip) == False: - if self._verbose: print("Error, update zip not found") - self._error = "Install failed" - self._error_msg = "Downloaded zip not found" - return -1 - - # clear the existing source folder in case previous files remain - outdir = os.path.join(self._updater_path, "source") - try: - shutil.rmtree(outdir) - if self._verbose: - print("Source folder cleared") - except: - pass - - # Create parent directories if needed, would not be relevant unless - # installing addon into another location or via an addon manager - try: - os.mkdir(outdir) - except Exception as err: - print("Error occurred while making extract dir:") - print(str(err)) - self._error = "Install failed" - self._error_msg = "Failed to make extract directory" - return -1 - - if not os.path.isdir(outdir): - print("Failed to create source directory") - self._error = "Install failed" - self._error_msg = "Failed to create extract directory" - return -1 - - if self._verbose: - print("Begin extracting source from zip:", self._source_zip) - zfile = zipfile.ZipFile(self._source_zip, "r") - - if not zfile: - if self._verbose: - print("Resulting file is not a zip, cannot extract") - self._error = "Install failed" - self._error_msg = "Resulting file is not a zip, cannot extract" - return -1 - - # Now extract directly from the first subfolder (not root) - # this avoids adding the first subfolder to the path length, - # which can be too long if the download has the SHA in the name - zsep = '/' #os.sep # might just always be / even on windows - for name in zfile.namelist(): - if zsep not in name: - continue - top_folder = name[:name.index(zsep)+1] - if name == top_folder + zsep: - continue # skip top level folder - subpath = name[name.index(zsep)+1:] - if name.endswith(zsep): - try: - os.mkdir(os.path.join(outdir, subpath)) - if self._verbose: - print("Extract - mkdir: ", os.path.join(outdir, subpath)) - except OSError as exc: - if exc.errno != errno.EEXIST: - self._error = "Install failed" - self._error_msg = "Could not create folder from zip" - return -1 - else: - with open(os.path.join(outdir, subpath), "wb") as outfile: - data = zfile.read(name) - outfile.write(data) - if self._verbose: - print("Extract - create:", os.path.join(outdir, subpath)) - - if self._verbose: - print("Extracted source") - - unpath = os.path.join(self._updater_path, "source") - if not os.path.isdir(unpath): - self._error = "Install failed" - self._error_msg = "Extracted path does not exist" - print("Extracted path does not exist: ", unpath) - return -1 - - if self._subfolder_path: - self._subfolder_path.replace('/', os.path.sep) - self._subfolder_path.replace('\\', os.path.sep) - - # either directly in root of zip/one subfolder, or use specified path - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: - dirlist = os.listdir(unpath) - if len(dirlist)>0: - if self._subfolder_path == "" or self._subfolder_path == None: - unpath = os.path.join(unpath, dirlist[0]) - else: - unpath = os.path.join(unpath, self._subfolder_path) - - # smarter check for additional sub folders for a single folder - # containing __init__.py - if os.path.isfile(os.path.join(unpath,"__init__.py")) == False: - if self._verbose: - print("not a valid addon found") - print("Paths:") - print(dirlist) - self._error = "Install failed" - self._error_msg = "No __init__ file found in new source" - return -1 - - # merge code with running addon directory, using blender default behavior - # plus any modifiers indicated by user (e.g. force remove/keep) - self.deepMergeDirectory(self._addon_root, unpath, clean) - - # Now save the json state - # Change to True, to trigger the handler on other side - # if allowing reloading within same blender instance - self._json["just_updated"] = True - self.save_updater_json() - self.reload_addon() - self._update_ready = False - return 0 - - - def deepMergeDirectory(self,base,merger,clean=False): - """Merge folder 'merger' into folder 'base' without deleting existing""" - if not os.path.exists(base): - if self._verbose: - print("Base path does not exist:", base) - return -1 - elif not os.path.exists(merger): - if self._verbose: - print("Merger path does not exist") - return -1 - - # paths to be aware of and not overwrite/remove/etc - staging_path = os.path.join(self._updater_path,"update_staging") - backup_path = os.path.join(self._updater_path,"backup") - - # If clean install is enabled, clear existing files ahead of time - # note: will not delete the update.json, update folder, staging, or staging - # but will delete all other folders/files in addon directory - error = None - if clean==True: - try: - # implement clearing of all folders/files, except the - # updater folder and updater json - # Careful, this deletes entire subdirectories recursively... - # make sure that base is not a high level shared folder, but - # is dedicated just to the addon itself - if self._verbose: print("clean=True, clearing addon folder to fresh install state") - - # remove root files and folders (except update folder) - files = [f for f in os.listdir(base) if os.path.isfile(os.path.join(base,f))] - folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base,f))] - - for f in files: - os.remove(os.path.join(base,f)) - print("Clean removing file {}".format(os.path.join(base,f))) - for f in folders: - if os.path.join(base,f)==self._updater_path: continue - shutil.rmtree(os.path.join(base,f)) - print("Clean removing folder and contents {}".format(os.path.join(base,f))) - - except Exception as err: - error = "failed to create clean existing addon folder" - print(error, str(err)) - - # Walk through the base addon folder for rules on pre-removing - # but avoid removing/altering backup and updater file - for path, dirs, files in os.walk(base): - # prune ie skip updater folder - dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]] - for file in files: - for ptrn in self.remove_pre_update_patterns: - if fnmatch.filter([file],ptrn): - try: - fl = os.path.join(path,file) - os.remove(fl) - if self._verbose: print("Pre-removed file "+file) - except OSError: - print("Failed to pre-remove "+file) - - # Walk through the temp addon sub folder for replacements - # this implements the overwrite rules, which apply after - # the above pre-removal rules. This also performs the - # actual file copying/replacements - for path, dirs, files in os.walk(merger): - # verify this structure works to prune updater sub folder overwriting - dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]] - relPath = os.path.relpath(path, merger) - destPath = os.path.join(base, relPath) - if not os.path.exists(destPath): - os.makedirs(destPath) - for file in files: - # bring in additional logic around copying/replacing - # Blender default: overwrite .py's, don't overwrite the rest - destFile = os.path.join(destPath, file) - srcFile = os.path.join(path, file) - - # decide whether to replace if file already exists, and copy new over - if os.path.isfile(destFile): - # otherwise, check each file to see if matches an overwrite pattern - replaced=False - for ptrn in self._overwrite_patterns: - if fnmatch.filter([file],ptrn): - replaced=True - break - if replaced: - os.remove(destFile) - os.rename(srcFile, destFile) - if self._verbose: print("Overwrote file "+os.path.basename(destFile)) - else: - if self._verbose: print("Pattern not matched to "+os.path.basename(destFile)+", not overwritten") - else: - # file did not previously exist, simply move it over - os.rename(srcFile, destFile) - if self._verbose: print("New file "+os.path.basename(destFile)) - - # now remove the temp staging folder and downloaded zip - try: - shutil.rmtree(staging_path) - except: - error = "Error: Failed to remove existing staging directory, consider manually removing "+staging_path - if self._verbose: print(error) - - - def reload_addon(self): - # if post_update false, skip this function - # else, unload/reload addon & trigger popup - if self._auto_reload_post_update == False: - print("Restart blender to reload addon and complete update") - return - - if self._verbose: print("Reloading addon...") - addon_utils.modules(refresh=True) - bpy.utils.refresh_script_paths() - - # not allowed in restricted context, such as register module - # toggle to refresh - bpy.ops.wm.addon_disable(module=self._addon_package) - bpy.ops.wm.addon_refresh() - bpy.ops.wm.addon_enable(module=self._addon_package) - - - # ------------------------------------------------------------------------- - # Other non-api functions and setups - # ------------------------------------------------------------------------- - - def clear_state(self): - self._update_ready = None - self._update_link = None - self._update_version = None - self._source_zip = None - self._error = None - self._error_msg = None - - # custom urlretrieve implementation - def urlretrieve(self, urlfile, filepath): - chunk = 1024*8 - f = open(filepath, "wb") - while 1: - data = urlfile.read(chunk) - if not data: - #print("done.") - break - f.write(data) - #print("Read %s bytes"%len(data)) - f.close() - - - def version_tuple_from_text(self,text): - if text == None: return () - - # should go through string and remove all non-integers, - # and for any given break split into a different section - segments = [] - tmp = '' - for l in str(text): - if l.isdigit()==False: - if len(tmp)>0: - segments.append(int(tmp)) - tmp = '' - else: - tmp+=l - if len(tmp)>0: - segments.append(int(tmp)) - - if len(segments)==0: - if self._verbose: print("No version strings found text: ",text) - if self._include_branches == False: - return () - else: - return (text) - return tuple(segments) - - # called for running check in a background thread - def check_for_update_async(self, callback=None): - - if self._json != None and "update_ready" in self._json and self._json["version_text"]!={}: - if self._json["update_ready"] == True: - self._update_ready = True - self._update_link = self._json["version_text"]["link"] - self._update_version = str(self._json["version_text"]["version"]) - # cached update - callback(True) - return - - # do the check - if self._check_interval_enable == False: - return - elif self._async_checking == True: - if self._verbose: print("Skipping async check, already started") - return # already running the bg thread - elif self._update_ready == None: - self.start_async_check_update(False, callback) - - - def check_for_update_now(self, callback=None): - - self._error = None - self._error_msg = None - - if self._verbose: - print("Check update pressed, first getting current status") - if self._async_checking == True: - if self._verbose: print("Skipping async check, already started") - return # already running the bg thread - elif self._update_ready == None: - self.start_async_check_update(True, callback) - else: - self._update_ready = None - self.start_async_check_update(True, callback) - - - # this function is not async, will always return in sequential fashion - # but should have a parent which calls it in another thread - def check_for_update(self, now=False): - if self._verbose: print("Checking for update function") - - # clear the errors if any - self._error = None - self._error_msg = None - - # avoid running again in, just return past result if found - # but if force now check, then still do it - if self._update_ready != None and now == False: - return (self._update_ready,self._update_version,self._update_link) - - if self._current_version == None: - raise ValueError("current_version not yet defined") - if self._repo == None: - raise ValueError("repo not yet defined") - if self._user == None: - raise ValueError("username not yet defined") - - self.set_updater_json() # self._json - - if now == False and self.past_interval_timestamp()==False: - if self._verbose: - print("Aborting check for updated, check interval not reached") - return (False, None, None) - - # check if using tags or releases - # note that if called the first time, this will pull tags from online - if self._fake_install == True: - if self._verbose: - print("fake_install = True, setting fake version as ready") - self._update_ready = True - self._update_version = "(999,999,999)" - self._update_link = "http://127.0.0.1" - - return (self._update_ready, self._update_version, self._update_link) - - # primary internet call - self.get_tags() # sets self._tags and self._tag_latest - - self._json["last_check"] = str(datetime.now()) - self.save_updater_json() - - # can be () or ('master') in addition to branches, and version tag - new_version = self.version_tuple_from_text(self.tag_latest) - - if len(self._tags)==0: - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - if self._include_branches == False: - link = self.select_link(self, self._tags[0]) - else: - n = len(self._include_branch_list) - if len(self._tags)==n: - # effectively means no tags found on repo - # so provide the first one as default - link = self.select_link(self, self._tags[0]) - else: - link = self.select_link(self, self._tags[n]) - - if new_version == (): - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - elif str(new_version).lower() in self._include_branch_list: - # handle situation where master/whichever branch is included - # however, this code effectively is not triggered now - # as new_version will only be tag names, not branch names - if self._include_branch_autocheck == False: - # don't offer update as ready, - # but set the link for the default - # branch for installing - self._update_ready = False - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) - else: - raise ValueError("include_branch_autocheck: NOT YET DEVELOPED") - # bypass releases and look at timestamp of last update - # from a branch compared to now, see if commit values - # match or not. - - else: - # situation where branches not included - - if new_version > self._current_version: - - self._update_ready = True - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) - - # elif new_version != self._current_version: - # self._update_ready = False - # self._update_version = new_version - # self._update_link = link - # self.save_updater_json() - # return (True, new_version, link) - - # if no update, set ready to False from None - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - - - def set_tag(self, name): - """Assign the tag name and url to update to""" - tg = None - for tag in self._tags: - if name == tag["name"]: - tg = tag - break - if tg: - new_version = self.version_tuple_from_text(self.tag_latest) - self._update_version = new_version - self._update_link = self.select_link(self, tg) - elif self._include_branches and name in self._include_branch_list: - # scenario if reverting to a specific branch name instead of tag - tg = name - link = self.form_branch_url(tg) - self._update_version = name # this will break things - self._update_link = link - if not tg: - raise ValueError("Version tag not found: "+name) - - - def run_update(self,force=False,revert_tag=None,clean=False,callback=None): - """Runs an install, update, or reversion of an addon from online source - - Arguments: - force: Install assigned link, even if self.update_ready is False - revert_tag: Version to install, if none uses detected update link - clean: not used, but in future could use to totally refresh addon - callback: used to run function on update completion - """ - self._json["update_ready"] = False - self._json["ignore"] = False # clear ignore flag - self._json["version_text"] = {} - - if revert_tag != None: - self.set_tag(revert_tag) - self._update_ready = True - - # clear the errors if any - self._error = None - self._error_msg = None - - if self._verbose: print("Running update") - - if self._fake_install == True: - # change to True, to trigger the reload/"update installed" handler - if self._verbose: - print("fake_install=True") - print("Just reloading and running any handler triggers") - self._json["just_updated"] = True - self.save_updater_json() - if self._backup_current == True: - self.create_backup() - self.reload_addon() - self._update_ready = False - res = True # fake "success" zip download flag - - elif force==False: - if self._update_ready != True: - if self._verbose: - print("Update stopped, new version not ready") - if callback: - callback( - self._addon_package, - "Update stopped, new version not ready") - return "Update stopped, new version not ready" - elif self._update_link == None: - # this shouldn't happen if update is ready - if self._verbose: - print("Update stopped, update link unavailable") - if callback: - callback( - self._addon_package, - "Update stopped, update link unavailable") - return "Update stopped, update link unavailable" - - if self._verbose and revert_tag==None: - print("Staging update") - elif self._verbose: - print("Staging install") - - res = self.stage_repository(self._update_link) - if res !=True: - print("Error in staging repository: "+str(res)) - if callback != None: - callback(self._addon_package, self._error_msg) - return self._error_msg - res = self.unpack_staged_zip(clean) - if res<0: - if callback: - callback(self._addon_package, self._error_msg) - return res - - else: - if self._update_link == None: - if self._verbose: - print("Update stopped, could not get link") - return "Update stopped, could not get link" - if self._verbose: - print("Forcing update") - - res = self.stage_repository(self._update_link) - if res !=True: - print("Error in staging repository: "+str(res)) - if callback: - callback(self._addon_package, self._error_msg) - return self._error_msg - res = self.unpack_staged_zip(clean) - if res<0: - return res - # would need to compare against other versions held in tags - - # run the front-end's callback if provided - if callback: - callback(self._addon_package) - - # return something meaningful, 0 means it worked - return 0 - - - def past_interval_timestamp(self): - if self._check_interval_enable == False: - return True # ie this exact feature is disabled - - if "last_check" not in self._json or self._json["last_check"] == "": - return True - - now = datetime.now() - last_check = datetime.strptime(self._json["last_check"], - "%Y-%m-%d %H:%M:%S.%f") - next_check = last_check - offset = timedelta( - days=self._check_interval_days + 30*self._check_interval_months, - hours=self._check_interval_hours, - minutes=self._check_interval_minutes - ) - - delta = (now - offset) - last_check - if delta.total_seconds() > 0: - if self._verbose: - print("{} Updater: Time to check for updates!".format(self._addon)) - return True - - if self._verbose: - print("{} Updater: Determined it's not yet time to check for updates".format(self._addon)) - return False - - def get_json_path(self): - """Returns the full path to the JSON state file used by this updater. - - Will also rename old file paths to addon-specific path if found - """ - json_path = os.path.join(self._updater_path, - "{}_updater_status.json".format(self._addon_package)) - old_json_path = os.path.join(self._updater_path, "updater_status.json") - - # rename old file if it exists - try: - os.rename(old_json_path, json_path) - except FileNotFoundError: - pass - except Exception as err: - print("Other OS error occurred while trying to rename old JSON") - print(err) - return json_path - - def set_updater_json(self): - """Load or initialize JSON dictionary data for updater state""" - if self._updater_path == None: - raise ValueError("updater_path is not defined") - elif os.path.isdir(self._updater_path) == False: - os.makedirs(self._updater_path) - - jpath = self.get_json_path() - if os.path.isfile(jpath): - with open(jpath) as data_file: - self._json = json.load(data_file) - if self._verbose: - print("{} Updater: Read in JSON settings from file".format( - self._addon)) - else: - # set data structure - self._json = { - "last_check":"", - "backup_date":"", - "update_ready":False, - "ignore":False, - "just_restored":False, - "just_updated":False, - "version_text":{} - } - self.save_updater_json() - - - def save_updater_json(self): - # first save the state - if self._update_ready == True: - if type(self._update_version) == type((0,0,0)): - self._json["update_ready"] = True - self._json["version_text"]["link"]=self._update_link - self._json["version_text"]["version"]=self._update_version - else: - self._json["update_ready"] = False - self._json["version_text"] = {} - else: - self._json["update_ready"] = False - self._json["version_text"] = {} - - jpath = self.get_json_path() - outf = open(jpath,'w') - data_out = json.dumps(self._json, indent=4) - outf.write(data_out) - outf.close() - if self._verbose: - print(self._addon+": Wrote out updater JSON settings to file, with the contents:") - print(self._json) - - def json_reset_postupdate(self): - self._json["just_updated"] = False - self._json["update_ready"] = False - self._json["version_text"] = {} - self.save_updater_json() - - def json_reset_restore(self): - self._json["just_restored"] = False - self._json["update_ready"] = False - self._json["version_text"] = {} - self.save_updater_json() - self._update_ready = None # reset so you could check update again - - def ignore_update(self): - self._json["ignore"] = True - self.save_updater_json() - - - # ------------------------------------------------------------------------- - # ASYNC stuff - # ------------------------------------------------------------------------- - - def start_async_check_update(self, now=False, callback=None): - """Start a background thread which will check for updates""" - if self._async_checking is True: - return - if self._verbose: - print("{} updater: Starting background checking thread".format( - self._addon)) - check_thread = threading.Thread(target=self.async_check_update, - args=(now,callback,)) - check_thread.daemon = True - self._check_thread = check_thread - check_thread.start() - - def async_check_update(self, now, callback=None): - """Perform update check, run as target of background thread""" - self._async_checking = True - if self._verbose: - print("{} BG thread: Checking for update now in background".format( - self._addon)) - - try: - self.check_for_update(now=now) - except Exception as exception: - print("Checking for update error:") - print(exception) - if not self._error: - self._update_ready = False - self._update_version = None - self._update_link = None - self._error = "Error occurred" - self._error_msg = "Encountered an error while checking for updates" - - self._async_checking = False - self._check_thread = None - - if self._verbose: - print("{} BG thread: Finished checking for update, doing callback".format(self._addon)) - if callback: - callback(self._update_ready) - - def stop_async_check_update(self): - """Method to give impression of stopping check for update. - - Currently does nothing but allows user to retry/stop blocking UI from - hitting a refresh button. This does not actually stop the thread, as it - will complete after the connection timeout regardless. If the thread - does complete with a successful response, this will be still displayed - on next UI refresh (ie no update, or update available). - """ - if self._check_thread != None: - if self._verbose: print("Thread will end in normal course.") - # however, "There is no direct kill method on a thread object." - # better to let it run its course - #self._check_thread.stop() - self._async_checking = False - self._error = None - self._error_msg = None - - -# ----------------------------------------------------------------------------- -# Updater Engines -# ----------------------------------------------------------------------------- - - -class BitbucketEngine(object): - """Integration to Bitbucket API for git-formatted repositories""" - - def __init__(self): - self.api_url = 'https://api.bitbucket.org' - self.token = None - self.name = "bitbucket" - - def form_repo_url(self, updater): - return self.api_url+"/2.0/repositories/"+updater.user+"/"+updater.repo - - def form_tags_url(self, updater): - return self.form_repo_url(updater) + "/refs/tags?sort=-name" - - def form_branch_url(self, branch, updater): - return self.get_zip_url(branch, updater) - - def get_zip_url(self, name, updater): - return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format( - user=updater.user, - repo=updater.repo, - name=name) - - def parse_tags(self, response, updater): - if response == None: - return [] - return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} for tag in response["values"]] - - -class GithubEngine(object): - """Integration to Github API""" - - def __init__(self): - self.api_url = 'https://api.github.com' - self.token = None - self.name = "github" - - def form_repo_url(self, updater): - return "{}{}{}{}{}".format(self.api_url,"/repos/",updater.user, - "/",updater.repo) - - def form_tags_url(self, updater): - if updater.use_releases: - return "{}{}".format(self.form_repo_url(updater),"/releases") - else: - return "{}{}".format(self.form_repo_url(updater),"/tags") - - def form_branch_list_url(self, updater): - return "{}{}".format(self.form_repo_url(updater),"/branches") - - def form_branch_url(self, branch, updater): - return "{}{}{}".format(self.form_repo_url(updater), - "/zipball/",branch) - - def parse_tags(self, response, updater): - if response == None: - return [] - return response - - -class GitlabEngine(object): - """Integration to GitLab API""" - - def __init__(self): - self.api_url = 'https://gitlab.com' - self.token = None - self.name = "gitlab" - - def form_repo_url(self, updater): - return "{}{}{}".format(self.api_url,"/api/v4/projects/",updater.repo) - - def form_tags_url(self, updater): - return "{}{}".format(self.form_repo_url(updater),"/repository/tags") - - def form_branch_list_url(self, updater): - # does not validate branch name. - return "{}{}".format( - self.form_repo_url(updater), - "/repository/branches") - - def form_branch_url(self, branch, updater): - # Could clash with tag names and if it does, it will - # download TAG zip instead of branch zip to get - # direct path, would need. - return "{}{}{}".format( - self.form_repo_url(updater), - "/repository/archive.zip?sha=", - branch) - - def get_zip_url(self, sha, updater): - return "{base}/repository/archive.zip?sha={sha}".format( - base=self.form_repo_url(updater), - sha=sha) - - # def get_commit_zip(self, id, updater): - # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id - - def parse_tags(self, response, updater): - if response == None: - return [] - return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response] - - -# ----------------------------------------------------------------------------- -# The module-shared class instance, -# should be what's imported to other files -# ----------------------------------------------------------------------------- - -Updater = Singleton_updater() diff --git a/addon_updater_ops.py b/addon_updater_ops.py deleted file mode 100644 index 4a7369e..0000000 --- a/addon_updater_ops.py +++ /dev/null @@ -1,1503 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -"""Blender UI integrations for the addon updater. - -Implements draw calls, popups, and operators that use the addon_updater. -""" - -import os - -import bpy -from bpy.app.handlers import persistent - -# updater import, import safely -# Prevents popups for users with invalid python installs e.g. missing libraries -try: - from .addon_updater import Updater as updater -except Exception as e: - print("ERROR INITIALIZING UPDATER") - print(str(e)) - class Singleton_updater_none(object): - def __init__(self): - self.addon = None - self.verbose = False - self.invalidupdater = True # used to distinguish bad install - self.error = None - self.error_msg = None - self.async_checking = None - def clear_state(self): - self.addon = None - self.verbose = False - self.invalidupdater = True - self.error = None - self.error_msg = None - self.async_checking = None - def run_update(self): pass - def check_for_update(self): pass - updater = Singleton_updater_none() - updater.error = "Error initializing updater module" - updater.error_msg = str(e) - -# Must declare this before classes are loaded -# otherwise the bl_idname's will not match and have errors. -# Must be all lowercase and no spaces -updater.addon = "gp_toolbox" - - -# ----------------------------------------------------------------------------- -# Blender version utils -# ----------------------------------------------------------------------------- - - -def make_annotations(cls): - """Add annotation attribute to class fields to avoid Blender 2.8 warnings""" - if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): - return cls - bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} - if bl_props: - if '__annotations__' not in cls.__dict__: - setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] - for k, v in bl_props.items(): - annotations[k] = v - delattr(cls, k) - return cls - - -def layout_split(layout, factor=0.0, align=False): - """Intermediate method for pre and post blender 2.8 split UI function""" - if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): - return layout.split(percentage=factor, align=align) - return layout.split(factor=factor, align=align) - - -def get_user_preferences(context=None): - """Intermediate method for pre and post blender 2.8 grabbing preferences""" - if not context: - context = bpy.context - prefs = None - if hasattr(context, "user_preferences"): - prefs = context.user_preferences.addons.get(__package__, None) - elif hasattr(context, "preferences"): - prefs = context.preferences.addons.get(__package__, None) - if prefs: - return prefs.preferences - # To make the addon stable and non-exception prone, return None - # raise Exception("Could not fetch user preferences") - return None - - -# ----------------------------------------------------------------------------- -# Updater operators -# ----------------------------------------------------------------------------- - - -# simple popup for prompting checking for update & allow to install if available -class addon_updater_install_popup(bpy.types.Operator): - """Check and install update if available""" - bl_label = "Update {x} addon".format(x=updater.addon) - bl_idname = updater.addon+".updater_install_popup" - bl_description = "Popup menu to check and display current updates available" - bl_options = {'REGISTER', 'INTERNAL'} - - # if true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( - name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", - default=False, - options={'HIDDEN'} - ) - ignore_enum = bpy.props.EnumProperty( - name="Process update", - description="Decide to install, ignore, or defer new addon update", - items=[ - ("install","Update Now","Install update now"), - ("ignore","Ignore", "Ignore this update to prevent future popups"), - ("defer","Defer","Defer choice till next blender session") - ], - options={'HIDDEN'} - ) - - def check (self, context): - return True - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - if updater.invalidupdater == True: - layout.label(text="Updater module error") - return - elif updater.update_ready == True: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Update {} ready!".format(str(updater.update_version)), - icon="LOOP_FORWARDS") - col.label(text="Choose 'Update Now' & press OK to install, ",icon="BLANK1") - col.label(text="or click outside window to defer",icon="BLANK1") - row = col.row() - row.prop(self,"ignore_enum",expand=True) - col.split() - elif updater.update_ready == False: - col = layout.column() - col.scale_y = 0.7 - col.label(text="No updates available") - col.label(text="Press okay to dismiss dialog") - # add option to force install - else: - # case: updater.update_ready = None - # we have not yet checked for the update - layout.label(text="Check for update now?") - - # potentially in future, could have UI for 'check to select old version' - # to revert back to. - - def execute(self,context): - - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - - if updater.manual_only==True: - bpy.ops.wm.url_open(url=updater.website) - elif updater.update_ready == True: - - # action based on enum selection - if self.ignore_enum=='defer': - return {'FINISHED'} - elif self.ignore_enum=='ignore': - updater.ignore_update() - return {'FINISHED'} - #else: "install update now!" - - res = updater.run_update( - force=False, - callback=post_update_callback, - clean=self.clean_install) - # should return 0, if not something happened - if updater.verbose: - if res==0: - print("Updater returned successful") - else: - print("Updater returned {}, error occurred".format(res)) - elif updater.update_ready == None: - _ = updater.check_for_update(now=True) - - # re-launch this dialog - atr = addon_updater_install_popup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - else: - if updater.verbose: - print("Doing nothing, not ready for update") - return {'FINISHED'} - - -# User preference check-now operator -class addon_updater_check_now(bpy.types.Operator): - bl_label = "Check now for "+updater.addon+" update" - bl_idname = updater.addon+".updater_check_now" - bl_description = "Check now for an update to the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - def execute(self,context): - if updater.invalidupdater == True: - return {'CANCELLED'} - - if updater.async_checking == True and updater.error == None: - # Check already happened - # Used here to just avoid constant applying settings below - # Ignoring if error, to prevent being stuck on the error screen - return {'CANCELLED'} - - # apply the UI settings - settings = get_user_preferences(context) - if not settings: - if updater.verbose: - print("Could not get {} preferences, update check skipped".format( - __package__)) - return {'CANCELLED'} - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update - - # input is an optional callback function - # this function should take a bool input, if true: update ready - # if false, no update ready - updater.check_for_update_now(ui_refresh) - - return {'FINISHED'} - - -class addon_updater_update_now(bpy.types.Operator): - bl_label = "Update "+updater.addon+" addon now" - bl_idname = updater.addon+".updater_update_now" - bl_description = "Update to the latest version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - # if true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( - name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", - default=False, - options={'HIDDEN'} - ) - - def execute(self,context): - - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - - if updater.manual_only == True: - bpy.ops.wm.url_open(url=updater.website) - if updater.update_ready == True: - # if it fails, offer to open the website instead - try: - res = updater.run_update( - force=False, - callback=post_update_callback, - clean=self.clean_install) - - # should return 0, if not something happened - if updater.verbose: - if res==0: print("Updater returned successful") - else: print("Updater returned "+str(res)+", error occurred") - except Exception as e: - updater._error = "Error trying to run update" - updater._error_msg = str(e) - atr = addon_updater_install_manually.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - elif updater.update_ready == None: - (update_ready, version, link) = updater.check_for_update(now=True) - # re-launch this dialog - atr = addon_updater_install_popup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - - elif updater.update_ready == False: - self.report({'INFO'}, "Nothing to update") - return {'CANCELLED'} - else: - self.report({'ERROR'}, "Encountered problem while trying to update") - return {'CANCELLED'} - - return {'FINISHED'} - - -class addon_updater_update_target(bpy.types.Operator): - bl_label = updater.addon+" version target" - bl_idname = updater.addon+".updater_update_target" - bl_description = "Install a targeted version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - def target_version(self, context): - # in case of error importing updater - if updater.invalidupdater == True: - ret = [] - - ret = [] - i=0 - for tag in updater.tags: - ret.append( (tag,tag,"Select to install "+tag) ) - i+=1 - return ret - - target = bpy.props.EnumProperty( - name="Target version to install", - description="Select the version to install", - items=target_version - ) - - # if true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( - name="Clean install", - description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", - default=False, - options={'HIDDEN'} - ) - - @classmethod - def poll(cls, context): - if updater.invalidupdater == True: return False - return updater.update_ready != None and len(updater.tags)>0 - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - if updater.invalidupdater == True: - layout.label(text="Updater error") - return - split = layout_split(layout, factor=0.66) - subcol = split.column() - subcol.label(text="Select install version") - subcol = split.column() - subcol.prop(self, "target", text="") - - - def execute(self,context): - - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - - res = updater.run_update( - force=False, - revert_tag=self.target, - callback=post_update_callback, - clean=self.clean_install) - - # should return 0, if not something happened - if res==0: - if updater.verbose: - print("Updater returned successful") - else: - if updater.verbose: - print("Updater returned "+str(res)+", error occurred") - return {'CANCELLED'} - - return {'FINISHED'} - - -class addon_updater_install_manually(bpy.types.Operator): - """As a fallback, direct the user to download the addon manually""" - bl_label = "Install update manually" - bl_idname = updater.addon+".updater_install_manually" - bl_description = "Proceed to manually install update" - bl_options = {'REGISTER', 'INTERNAL'} - - error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} - ) - - def invoke(self, context, event): - return context.window_manager.invoke_popup(self) - - def draw(self, context): - layout = self.layout - - if updater.invalidupdater == True: - layout.label(text="Updater error") - return - - # use a "failed flag"? it shows this label if the case failed. - if self.error!="": - col = layout.column() - col.scale_y = 0.7 - col.label(text="There was an issue trying to auto-install",icon="ERROR") - col.label(text="Press the download button below and install",icon="BLANK1") - col.label(text="the zip file like a normal addon.",icon="BLANK1") - else: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Install the addon manually") - col.label(text="Press the download button below and install") - col.label(text="the zip file like a normal addon.") - - # if check hasn't happened, i.e. accidentally called this menu - # allow to check here - - row = layout.row() - - if updater.update_link != None: - row.operator("wm.url_open", - text="Direct download").url=updater.update_link - else: - row.operator("wm.url_open", - text="(failed to retrieve direct download)") - row.enabled = False - - if updater.website != None: - row = layout.row() - row.operator("wm.url_open",text="Open website").url=\ - updater.website - else: - row = layout.row() - row.label(text="See source website to download the update") - - def execute(self,context): - return {'FINISHED'} - - -class addon_updater_updated_successful(bpy.types.Operator): - """Addon in place, popup telling user it completed or what went wrong""" - bl_label = "Installation Report" - bl_idname = updater.addon+".updater_update_successful" - bl_description = "Update installation response" - bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} - - error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} - ) - - def invoke(self, context, event): - return context.window_manager.invoke_props_popup(self, event) - - def draw(self, context): - layout = self.layout - - if updater.invalidupdater == True: - layout.label(text="Updater error") - return - - saved = updater.json - if self.error != "": - col = layout.column() - col.scale_y = 0.7 - col.label(text="Error occurred, did not install", icon="ERROR") - if updater.error_msg: - msg = updater.error_msg - else: - msg = self.error - col.label(text=str(msg), icon="BLANK1") - rw = col.row() - rw.scale_y = 2 - rw.operator("wm.url_open", - text="Click for manual download.", - icon="BLANK1" - ).url=updater.website - # manual download button here - elif updater.auto_reload_post_update == False: - # tell user to restart blender - if "just_restored" in saved and saved["just_restored"] == True: - col = layout.column() - col.label(text="Addon restored", icon="RECOVER_LAST") - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") - updater.json_reset_restore() - else: - col = layout.column() - col.label(text="Addon successfully installed", icon="FILE_TICK") - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") - - else: - # reload addon, but still recommend they restart blender - if "just_restored" in saved and saved["just_restored"] == True: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Addon restored", icon="RECOVER_LAST") - col.label(text="Consider restarting blender to fully reload.", - icon="BLANK1") - updater.json_reset_restore() - else: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Addon successfully installed", icon="FILE_TICK") - col.label(text="Consider restarting blender to fully reload.", - icon="BLANK1") - - def execute(self, context): - return {'FINISHED'} - - -class addon_updater_restore_backup(bpy.types.Operator): - """Restore addon from backup""" - bl_label = "Restore backup" - bl_idname = updater.addon+".updater_restore_backup" - bl_description = "Restore addon from backup" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - try: - return os.path.isdir(os.path.join(updater.stage_path,"backup")) - except: - return False - - def execute(self, context): - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - updater.restore_backup() - return {'FINISHED'} - - -class addon_updater_ignore(bpy.types.Operator): - """Prevent future update notice popups""" - bl_label = "Ignore update" - bl_idname = updater.addon+".updater_ignore" - bl_description = "Ignore update to prevent future popups" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - if updater.invalidupdater == True: - return False - elif updater.update_ready == True: - return True - else: - return False - - def execute(self, context): - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - updater.ignore_update() - self.report({"INFO"},"Open addon preferences for updater options") - return {'FINISHED'} - - -class addon_updater_end_background(bpy.types.Operator): - """Stop checking for update in the background""" - bl_label = "End background check" - bl_idname = updater.addon+".end_background_check" - bl_description = "Stop checking for update in the background" - bl_options = {'REGISTER', 'INTERNAL'} - - # @classmethod - # def poll(cls, context): - # if updater.async_checking == True: - # return True - # else: - # return False - - def execute(self, context): - # in case of error importing updater - if updater.invalidupdater == True: - return {'CANCELLED'} - updater.stop_async_check_update() - return {'FINISHED'} - - -# ----------------------------------------------------------------------------- -# Handler related, to create popups -# ----------------------------------------------------------------------------- - - -# global vars used to prevent duplicate popup handlers -ran_autocheck_install_popup = False -ran_update_sucess_popup = False - -# global var for preventing successive calls -ran_background_check = False - -@persistent -def updater_run_success_popup_handler(scene): - global ran_update_sucess_popup - ran_update_sucess_popup = True - - # in case of error importing updater - if updater.invalidupdater == True: - return - - try: - if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_success_popup_handler) - else: - bpy.app.handlers.depsgraph_update_post.remove( - updater_run_success_popup_handler) - except: - pass - - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - - -@persistent -def updater_run_install_popup_handler(scene): - global ran_autocheck_install_popup - ran_autocheck_install_popup = True - - # in case of error importing updater - if updater.invalidupdater == True: - return - - try: - if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_install_popup_handler) - else: - bpy.app.handlers.depsgraph_update_post.remove( - updater_run_install_popup_handler) - except: - pass - - if "ignore" in updater.json and updater.json["ignore"] == True: - return # don't do popup if ignore pressed - # elif type(updater.update_version) != type((0,0,0)): - # # likely was from master or another branch, shouldn't trigger popup - # updater.json_reset_restore() - # return - elif "version_text" in updater.json and "version" in updater.json["version_text"]: - version = updater.json["version_text"]["version"] - ver_tuple = updater.version_tuple_from_text(version) - - if ver_tuple < updater.current_version: - # user probably manually installed to get the up to date addon - # in here. Clear out the update flag using this function - if updater.verbose: - print("{} updater: appears user updated, clearing flag".format(\ - updater.addon)) - updater.json_reset_restore() - return - atr = addon_updater_install_popup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - - -def background_update_callback(update_ready): - """Passed into the updater, background thread updater""" - global ran_autocheck_install_popup - - # in case of error importing updater - if updater.invalidupdater == True: - return - if updater.showpopups == False: - return - if update_ready != True: - return - - # see if we need add to the update handler to trigger the popup - handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - handlers = bpy.app.handlers.scene_update_post - else: # 2.8x - handlers = bpy.app.handlers.depsgraph_update_post - in_handles = updater_run_install_popup_handler in handlers - - if in_handles or ran_autocheck_install_popup: - return - - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_install_popup_handler) - else: # 2.8x - bpy.app.handlers.depsgraph_update_post.append( - updater_run_install_popup_handler) - ran_autocheck_install_popup = True - - -def post_update_callback(module_name, res=None): - """Callback for once the run_update function has completed - - Only makes sense to use this if "auto_reload_post_update" == False, - i.e. don't auto-restart the addon - - Arguments: - module_name: returns the module name from updater, but unused here - res: If an error occurred, this is the detail string - """ - - # in case of error importing updater - if updater.invalidupdater == True: - return - - if res==None: - # this is the same code as in conditional at the end of the register function - # ie if "auto_reload_post_update" == True, comment out this code - if updater.verbose: - print("{} updater: Running post update callback".format(updater.addon)) - - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - global ran_update_sucess_popup - ran_update_sucess_popup = True - else: - # some kind of error occurred and it was unable to install, - # offer manual download instead - atr = addon_updater_updated_successful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT',error=res) - return - - -def ui_refresh(update_status): - # find a way to just re-draw self? - # callback intended for trigger by async thread - for windowManager in bpy.data.window_managers: - for window in windowManager.windows: - for area in window.screen.areas: - area.tag_redraw() - - -def check_for_update_background(): - """Function for asynchronous background check. - - *Could* be called on register, but would be bad practice. - """ - if updater.invalidupdater == True: - return - global ran_background_check - if ran_background_check == True: - # Global var ensures check only happens once - return - elif updater.update_ready != None or updater.async_checking == True: - # Check already happened - # Used here to just avoid constant applying settings below - return - - # apply the UI settings - settings = get_user_preferences(bpy.context) - if not settings: - return - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update - - # input is an optional callback function - # this function should take a bool input, if true: update ready - # if false, no update ready - if updater.verbose: - print("{} updater: Running background check for update".format(\ - updater.addon)) - updater.check_for_update_async(background_update_callback) - ran_background_check = True - - -def check_for_update_nonthreaded(self, context): - """Can be placed in front of other operators to launch when pressed""" - if updater.invalidupdater == True: - return - - # only check if it's ready, ie after the time interval specified - # should be the async wrapper call here - settings = get_user_preferences(bpy.context) - if not settings: - if updater.verbose: - print("Could not get {} preferences, update check skipped".format( - __package__)) - return - updater.set_check_interval(enable=settings.auto_check_update, - months=settings.updater_intrval_months, - days=settings.updater_intrval_days, - hours=settings.updater_intrval_hours, - minutes=settings.updater_intrval_minutes - ) # optional, if auto_check_update - - (update_ready, version, link) = updater.check_for_update(now=False) - if update_ready == True: - atr = addon_updater_install_popup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT') - else: - if updater.verbose: print("No update ready") - self.report({'INFO'}, "No update ready") - - -def showReloadPopup(): - """For use in register only, to show popup after re-enabling the addon - - Must be enabled by developer - """ - if updater.invalidupdater == True: - return - saved_state = updater.json - global ran_update_sucess_popup - - has_state = saved_state != None - just_updated = "just_updated" in saved_state - updated_info = saved_state["just_updated"] - - if not (has_state and just_updated and updated_info): - return - - updater.json_reset_postupdate() # so this only runs once - - # no handlers in this case - if updater.auto_reload_post_update == False: - return - - # see if we need add to the update handler to trigger the popup - handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - handlers = bpy.app.handlers.scene_update_post - else: # 2.8x - handlers = bpy.app.handlers.depsgraph_update_post - in_handles = updater_run_success_popup_handler in handlers - - if in_handles or ran_update_sucess_popup is True: - return - - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_success_popup_handler) - else: # 2.8x - bpy.app.handlers.depsgraph_update_post.append( - updater_run_success_popup_handler) - ran_update_sucess_popup = True - - -# ----------------------------------------------------------------------------- -# Example UI integrations -# ----------------------------------------------------------------------------- - - -def update_notice_box_ui(self, context): - """ Panel - Update Available for placement at end/beginning of panel - - After a check for update has occurred, this function will draw a box - saying an update is ready, and give a button for: update now, open website, - or ignore popup. Ideal to be placed at the end / beginning of a panel - """ - - if updater.invalidupdater == True: - return - - saved_state = updater.json - if updater.auto_reload_post_update == False: - if "just_updated" in saved_state and saved_state["just_updated"] == True: - layout = self.layout - box = layout.box() - col = box.column() - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender", - icon="ERROR") - col.label(text="to complete update") - - return - - # if user pressed ignore, don't draw the box - if "ignore" in updater.json and updater.json["ignore"] == True: - return - if updater.update_ready != True: - return - - layout = self.layout - box = layout.box() - col = box.column(align=True) - col.label(text="Update ready!",icon="ERROR") - col.separator() - row = col.row(align=True) - split = row.split(align=True) - colL = split.column(align=True) - colL.scale_y = 1.5 - colL.operator(addon_updater_ignore.bl_idname,icon="X",text="Ignore") - colR = split.column(align=True) - colR.scale_y = 1.5 - if updater.manual_only==False: - colR.operator(addon_updater_update_now.bl_idname, - text="Update", icon="LOOP_FORWARDS") - col.operator("wm.url_open", text="Open website").url = updater.website - #col.operator("wm.url_open",text="Direct download").url=updater.update_link - col.operator(addon_updater_install_manually.bl_idname, - text="Install manually") - else: - #col.operator("wm.url_open",text="Direct download").url=updater.update_link - col.operator("wm.url_open", text="Get it now").url = updater.website - - -def update_settings_ui(self, context, element=None): - """Preferences - for drawing with full width inside user preferences - - Create a function that can be run inside user preferences panel for prefs UI - Place inside UI draw using: addon_updater_ops.updaterSettingsUI(self, context) - or by: addon_updater_ops.updaterSettingsUI(context) - """ - - # element is a UI element, such as layout, a row, column, or box - if element==None: - element = self.layout - box = element.box() - - # in case of error importing updater - if updater.invalidupdater == True: - box.label(text="Error initializing updater code:") - box.label(text=updater.error_msg) - return - settings = get_user_preferences(context) - if not settings: - box.label(text="Error getting updater preferences", icon='ERROR') - return - - # auto-update settings - box.label(text="Updater Settings") - row = box.row() - - # special case to tell user to restart blender, if set that way - if updater.auto_reload_post_update == False: - saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"] == True: - row.alert = True - row.operator( - "wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") - return - - split = layout_split(row, factor=0.4) - subcol = split.column() - subcol.prop(settings, "auto_check_update") - subcol = split.column() - - if settings.auto_check_update==False: - subcol.enabled = False - subrow = subcol.row() - subrow.label(text="Interval between checks") - subrow = subcol.row(align=True) - checkcol = subrow.column(align=True) - checkcol.prop(settings,"updater_intrval_months") - checkcol = subrow.column(align=True) - checkcol.prop(settings,"updater_intrval_days") - checkcol = subrow.column(align=True) - - # Consider un-commenting for local dev (e.g. to set shorter intervals) - # checkcol.prop(settings,"updater_intrval_hours") - # checkcol = subrow.column(align=True) - # checkcol.prop(settings,"updater_intrval_minutes") - - # checking / managing updates - row = box.row() - col = row.column() - if updater.error != None: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - if "ssl" in updater.error_msg.lower(): - split.enabled = True - split.operator(addon_updater_install_manually.bl_idname, - text=updater.error) - else: - split.enabled = False - split.operator(addon_updater_check_now.bl_idname, - text=updater.error) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready == None and updater.async_checking == False: - col.scale_y = 2 - col.operator(addon_updater_check_now.bl_idname) - elif updater.update_ready == None: # async is running - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Checking...") - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_end_background.bl_idname, - text = "", icon="X") - - elif updater.include_branches==True and \ - len(updater.tags)==len(updater.include_branch_list) and \ - updater.manual_only==False: - # no releases found, but still show the appropriate branch - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update directly to "+str(updater.include_branch_list[0])) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready==True and updater.manual_only==False: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update now to "+str(updater.update_version)) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready==True and updater.manual_only==True: - col.scale_y = 2 - col.operator("wm.url_open", - text="Download "+str(updater.update_version)).url=updater.website - else: # i.e. that updater.update_ready == False - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Addon is up to date") - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - if updater.manual_only == False: - col = row.column(align=True) - #col.operator(addon_updater_update_target.bl_idname, - if updater.include_branches == True and len(updater.include_branch_list)>0: - branch = updater.include_branch_list[0] - col.operator(addon_updater_update_target.bl_idname, - text="Install latest {} / old version".format(branch)) - else: - col.operator(addon_updater_update_target.bl_idname, - text="Reinstall / install old version") - lastdate = "none found" - backuppath = os.path.join(updater.stage_path,"backup") - if "backup_date" in updater.json and os.path.isdir(backuppath): - if updater.json["backup_date"] == "": - lastdate = "Date not found" - else: - lastdate = updater.json["backup_date"] - backuptext = "Restore addon backup ({})".format(lastdate) - col.operator(addon_updater_restore_backup.bl_idname, text=backuptext) - - row = box.row() - row.scale_y = 0.7 - lastcheck = updater.json["last_check"] - if updater.error != None and updater.error_msg != None: - row.label(text=updater.error_msg) - elif lastcheck != "" and lastcheck != None: - lastcheck = lastcheck[0: lastcheck.index(".") ] - row.label(text="Last update check: " + lastcheck) - else: - row.label(text="Last update check: Never") - - -def update_settings_ui_condensed(self, context, element=None): - """Preferences - Condensed drawing within preferences - - Alternate draw for user preferences or other places, does not draw a box - """ - - # element is a UI element, such as layout, a row, column, or box - if element==None: - element = self.layout - row = element.row() - - # in case of error importing updater - if updater.invalidupdater == True: - row.label(text="Error initializing updater code:") - row.label(text=updater.error_msg) - return - settings = get_user_preferences(context) - if not settings: - row.label(text="Error getting updater preferences", icon='ERROR') - return - - # special case to tell user to restart blender, if set that way - if updater.auto_reload_post_update == False: - saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"] == True: - row.alert = True # mark red - row.operator( - "wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") - return - - col = row.column() - if updater.error != None: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - if "ssl" in updater.error_msg.lower(): - split.enabled = True - split.operator(addon_updater_install_manually.bl_idname, - text=updater.error) - else: - split.enabled = False - split.operator(addon_updater_check_now.bl_idname, - text=updater.error) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready == None and updater.async_checking == False: - col.scale_y = 2 - col.operator(addon_updater_check_now.bl_idname) - elif updater.update_ready == None: # async is running - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Checking...") - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_end_background.bl_idname, - text = "", icon="X") - - elif updater.include_branches==True and \ - len(updater.tags)==len(updater.include_branch_list) and \ - updater.manual_only==False: - # no releases found, but still show the appropriate branch - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update directly to "+str(updater.include_branch_list[0])) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready==True and updater.manual_only==False: - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_update_now.bl_idname, - text="Update now to "+str(updater.update_version)) - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - elif updater.update_ready==True and updater.manual_only==True: - col.scale_y = 2 - col.operator("wm.url_open", - text="Download "+str(updater.update_version)).url=updater.website - else: # i.e. that updater.update_ready == False - subcol = col.row(align=True) - subcol.scale_y = 1 - split = subcol.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text="Addon is up to date") - split = subcol.split(align=True) - split.scale_y = 2 - split.operator(addon_updater_check_now.bl_idname, - text = "", icon="FILE_REFRESH") - - row = element.row() - row.prop(settings, "auto_check_update") - - row = element.row() - row.scale_y = 0.7 - lastcheck = updater.json["last_check"] - if updater.error != None and updater.error_msg != None: - row.label(text=updater.error_msg) - elif lastcheck != "" and lastcheck != None: - lastcheck = lastcheck[0: lastcheck.index(".") ] - row.label(text="Last check: " + lastcheck) - else: - row.label(text="Last check: Never") - - -def skip_tag_function(self, tag): - """A global function for tag skipping - - A way to filter which tags are displayed, - e.g. to limit downgrading too far - input is a tag text, e.g. "v1.2.3" - output is True for skipping this tag number, - False if the tag is allowed (default for all) - Note: here, "self" is the acting updater shared class instance - """ - - # in case of error importing updater - if self.invalidupdater == True: - return False - - # ---- write any custom code here, return true to disallow version ---- # - # - # # Filter out e.g. if 'beta' is in name of release - # if 'beta' in tag.lower(): - # return True - # ---- write any custom code above, return true to disallow version --- # - - if self.include_branches == True: - for branch in self.include_branch_list: - if tag["name"].lower() == branch: return False - - # function converting string to tuple, ignoring e.g. leading 'v' - tupled = self.version_tuple_from_text(tag["name"]) - if type(tupled) != type( (1,2,3) ): return True - - # select the min tag version - change tuple accordingly - if self.version_min_update != None: - if tupled < self.version_min_update: - return True # skip if current version below this - - # select the max tag version - if self.version_max_update != None: - if tupled >= self.version_max_update: - return True # skip if current version at or above this - - # in all other cases, allow showing the tag for updating/reverting - return False - - -def select_link_function(self, tag): - """Only customize if trying to leverage "attachments" in *GitHub* releases - - A way to select from one or multiple attached donwloadable files from the - server, instead of downloading the default release/tag source code - """ - - # -- Default, universal case (and is the only option for GitLab/Bitbucket) - link = tag["zipball_url"] - - # -- Example: select the first (or only) asset instead source code -- - #if "assets" in tag and "browser_download_url" in tag["assets"][0]: - # link = tag["assets"][0]["browser_download_url"] - - # -- Example: select asset based on OS, where multiple builds exist -- - # # not tested/no error checking, modify to fit your own needs! - # # assume each release has three attached builds: - # # release_windows.zip, release_OSX.zip, release_linux.zip - # # This also would logically not be used with "branches" enabled - # if platform.system() == "Darwin": # ie OSX - # link = [asset for asset in tag["assets"] if 'OSX' in asset][0] - # elif platform.system() == "Windows": - # link = [asset for asset in tag["assets"] if 'windows' in asset][0] - # elif platform.system() == "Linux": - # link = [asset for asset in tag["assets"] if 'linux' in asset][0] - - return link - - -# ----------------------------------------------------------------------------- -# Register, should be run in the register module itself -# ----------------------------------------------------------------------------- - - -classes = ( - addon_updater_install_popup, - addon_updater_check_now, - addon_updater_update_now, - addon_updater_update_target, - addon_updater_install_manually, - addon_updater_updated_successful, - addon_updater_restore_backup, - addon_updater_ignore, - addon_updater_end_background -) - - -def register(bl_info): - """Registering the operators in this module""" - # safer failure in case of issue loading module - if updater.error: - print("Exiting updater registration, " + updater.error) - return - updater.clear_state() # clear internal vars, avoids reloading oddities - - # confirm your updater "engine" (Github is default if not specified) - # updater.engine = "Github" - updater.engine = "GitLab" - # updater.engine = "Bitbucket" - - # If using private repository, indicate the token here - # Must be set after assigning the engine. - # **WARNING** Depending on the engine, this token can act like a password!! - # Only provide a token if the project is *non-public*, see readme for - # other considerations and suggestions from a security standpoint - updater.private_token = "" # "tokenstring" - - # choose your own username, must match website (not needed for GitLab) - updater.user = "" - - # choose your own repository, must match git name - updater.repo = "23569019" - - #updater.addon = # define at top of module, MUST be done first - - # Website for manual addon download, optional but recommended to set - updater.website = "https://gitlab.com/autour-de-minuit/blender/gp_toolbox" - - # Addon subfolder path - # "sample/path/to/addon" - # default is "" or None, meaning root - updater.subfolder_path = "" - - # used to check/compare versions - updater.current_version = bl_info["version"] - - # Optional, to hard-set update frequency, use this here - however, - # this demo has this set via UI properties. - # updater.set_check_interval( - # enable=False,months=0,days=0,hours=0,minutes=2) - - # Optional, consider turning off for production or allow as an option - # This will print out additional debugging info to the console - updater.verbose = False # make False for production default - - # Optional, customize where the addon updater processing subfolder is, - # essentially a staging folder used by the updater on its own - # Needs to be within the same folder as the addon itself - # Need to supply a full, absolute path to folder - # updater.updater_path = # set path of updater folder, by default: - # /addons/{__package__}/{__package__}_updater - - # auto create a backup of the addon when installing other versions - updater.backup_current = True # True by default - - # Sample ignore patterns for when creating backup of current during update - updater.backup_ignore_patterns = ["__pycache__",".gitignore",".git","*.json"] - # Alternate example patterns - # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] - - # Patterns for files to actively overwrite if found in new update - # file and are also found in the currently installed addon. Note that - - # by default (ie if set to []), updates are installed in the same way as blender: - # .py files are replaced, but other file types (e.g. json, txt, blend) - # will NOT be overwritten if already present in current install. Thus - # if you want to automatically update resources/non py files, add them - # as a part of the pattern list below so they will always be overwritten by an - # update. If a pattern file is not found in new update, no action is taken - # This does NOT detele anything, only defines what is allowed to be overwritten - updater.overwrite_patterns = ["*.png","*.jpg","README.md","LICENSE.txt"] - # updater.overwrite_patterns = [] - # other examples: - # ["*"] means ALL files/folders will be overwritten by update, was the behavior pre updater v1.0.4 - # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect if user installs update manually without deleting the existing addon first - # e.g. if existing install and update both have a resource.blend file, the existing installed one will remain - # ["some.py"] means if some.py is found in addon update, it will overwrite any existing some.py in current addon install, if any - # ["*.json"] means all json files found in addon update will overwrite those of same name in current install - # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all pngs will be overwritten by update - - # Patterns for files to actively remove prior to running update - # Useful if wanting to remove old code due to changes in filenames - # that otherwise would accumulate. Note: this runs after taking - # a backup (if enabled) but before placing in new update. If the same - # file name removed exists in the update, then it acts as if pattern - # is placed in the overwrite_patterns property. Note this is effectively - # ignored if clean=True in the run_update method - updater.remove_pre_update_patterns = ["*.py", "*.pyc"] - # Note setting ["*"] here is equivalent to always running updates with - # clean = True in the run_update method, ie the equivalent of a fresh, - # new install. This would also delete any resources or user-made/modified - # files setting ["__pycache__"] ensures the pycache folder is always removed - # The configuration of ["*.py","*.pyc"] is a safe option as this - # will ensure no old python files/caches remain in event different addon - # versions have different filenames or structures - - # Allow branches like 'master' as an option to update to, regardless - # of release or version. - # Default behavior: releases will still be used for auto check (popup), - # but the user has the option from user preferences to directly - # update to the master branch or any other branches specified using - # the "install {branch}/older version" operator. - updater.include_branches = True - - # (GitHub only) This options allows the user to use releases over tags for data, - # which enables pulling down release logs/notes, as well as specify installs from - # release-attached zips (instead of just the auto-packaged code generated with - # a release/tag). Setting has no impact on BitBucket or GitLab repos - updater.use_releases = False - # note: Releases always have a tag, but a tag may not always be a release - # Therefore, setting True above will filter out any non-annoted tags - # note 2: Using this option will also display the release name instead of - # just the tag name, bear this in mind given the skip_tag_function filtering above - - # if using "include_branches", - # updater.include_branch_list defaults to ['master'] branch if set to none - # example targeting another multiple branches allowed to pull from - # updater.include_branch_list = ['master', 'dev'] # example with two branches - updater.include_branch_list = None # None is the equivalent to setting ['master'] - - # Only allow manual install, thus prompting the user to open - # the addon's web page to download, specifically: updater.website - # Useful if only wanting to get notification of updates but not - # directly install. - updater.manual_only = False - - # Used for development only, "pretend" to install an update to test - # reloading conditions - updater.fake_install = False # Set to true to test callback/reloading - - # Show popups, ie if auto-check for update is enabled or a previous - # check for update in user preferences found a new version, show a popup - # (at most once per blender session, and it provides an option to ignore - # for future sessions); default behavior is set to True - updater.showpopups = True - # note: if set to false, there will still be an "update ready" box drawn - # using the `update_notice_box_ui` panel function. - - # Override with a custom function on what tags - # to skip showing for updater; see code for function above. - # Set the min and max versions allowed to install. - # Optional, default None - # min install (>=) will install this and higher - updater.version_min_update = (0,9,2) - # updater.version_min_update = None # if not wanting to define a min - - # max install (<) will install strictly anything lower - # updater.version_max_update = (9,9,9) - updater.version_max_update = None # set to None if not wanting to set max - - # Function defined above, customize as appropriate per repository - updater.skip_tag = skip_tag_function # min and max used in this function - - # Function defined above, customize as appropriate per repository; not required - updater.select_link = select_link_function - - # The register line items for all operators/panels - # If using bpy.utils.register_module(__name__) to register elsewhere - # in the addon, delete these lines (also from unregister) - for cls in classes: - # apply annotations to remove Blender 2.8 warnings, no effect on 2.7 - make_annotations(cls) - # comment out this line if using bpy.utils.register_module(__name__) - bpy.utils.register_class(cls) - - # special situation: we just updated the addon, show a popup - # to tell the user it worked - # should be enclosed in try/catch in case other issues arise - showReloadPopup() - - -def unregister(): - for cls in reversed(classes): - # comment out this line if using bpy.utils.unregister_module(__name__) - bpy.utils.unregister_class(cls) - - # clear global vars since they may persist if not restarting blender - updater.clear_state() # clear internal vars, avoids reloading oddities - - global ran_autocheck_install_popup - ran_autocheck_install_popup = False - - global ran_update_sucess_popup - ran_update_sucess_popup = False - - global ran_background_check - ran_background_check = False