a719e14a6c
- Added connection timeouts everywhere:
SQLAlchemy engines: connect_args={'timeout': 30}
sqlite3 direct connects: timeout=30 across scripts
- Enabled WAL mode on local disks and made it conditional via an environment flag.
-Added small retry/backoff in read-only UI probes to tolerate transient “database is locked.”
Increased Network share support (NFS/SMB):
- Introduced NETWORK_SHARE_MODE to disable WAL and suppress permission-changing operations that can break on network filesystems.
- Suppressed recursive chown when NETWORK_SHARE_MODE is true:
Python:
scripts/auto_library.py: skip chown for /config and /calibre-library during app.db/new library setup
scripts/ingest_processor.py: skip chown of library in set_library_permissions
scripts/convert_library.py: skip chown of convert-library.log; skip library chown
scripts/kindle_epub_fixer.py: skip chown of epub-fixer.log
cps/updater.py: disable os.chown during updates when flag is enabled
Shell:
scripts/setup-cwa.sh: guard chown of /etc/s6-overlay
root/etc/s6-overlay/s6-rc.d/cwa-init/run: guard initial chown of /config and cps/cache; guard requiredDirs loop
Documentation and configuration:
- README: Added “Network shares and SQLite WAL mode” section; documented that NETWORK_SHARE_MODE disables WAL and chown behavior for safety on network shares.
- docker-compose.yml and docker-compose.yml.dev: Exposed NETWORK_SHARE_MODE (default false) in environment.
Fixed a minor YAML indentation issue during compose edits.
Impacts on the Project:
- Fewer “database is locked” errors due to timeouts, gentle retries, and WAL on local disks.
- Safer behavior on NFS/SMB: WAL disabled and chown suppressed when NETWORK_SHARE_MODE=true.
- Clearer guidance for users via README and compose templates.
[bug] chown failed
Fixes #175
Calibre-Web-automated [bug]
Fixes #530
634 lines
27 KiB
Python
Executable File
634 lines
27 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
||
# Calibre-Web Automated – fork of Calibre-Web
|
||
# Copyright (C) 2018-2025 Calibre-Web contributors
|
||
# Copyright (C) 2024-2025 Calibre-Web Automated contributors
|
||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
# See CONTRIBUTORS for full list of authors.
|
||
|
||
import sys
|
||
import os
|
||
import datetime
|
||
import json
|
||
import shutil
|
||
import threading
|
||
import time
|
||
import zipfile
|
||
from io import BytesIO
|
||
import requests
|
||
|
||
from flask_babel import format_datetime
|
||
from flask_babel import gettext as _
|
||
|
||
from . import constants, logger # config, web_server
|
||
from .file_helper import get_temp_dir
|
||
|
||
|
||
log = logger.create()
|
||
_REPOSITORY_API_URL = 'https://api.github.com/repos/janeczku/calibre-web'
|
||
|
||
|
||
def is_sha1(sha1):
|
||
if len(sha1) != 40:
|
||
return False
|
||
try:
|
||
int(sha1, 16)
|
||
except ValueError:
|
||
return False
|
||
return True
|
||
|
||
|
||
class Updater(threading.Thread):
|
||
|
||
def __init__(self):
|
||
threading.Thread.__init__(self)
|
||
self.web_server = None
|
||
self.config = None
|
||
self.paused = False
|
||
self.can_run = threading.Event()
|
||
self.pause()
|
||
self.status = -1
|
||
self.updateIndex = None
|
||
|
||
def init_updater(self, config, web_server):
|
||
self.config = config
|
||
self.web_server = web_server
|
||
|
||
def get_current_version_info(self):
|
||
if self.config.config_updatechannel == constants.UPDATE_STABLE:
|
||
return self._stable_version_info()
|
||
return self._nightly_version_info()
|
||
|
||
def get_available_updates(self, request_method):
|
||
if self.config.config_updatechannel == constants.UPDATE_STABLE:
|
||
return self._stable_available_updates(request_method)
|
||
return self._nightly_available_updates(request_method)
|
||
|
||
def do_work(self):
|
||
try:
|
||
self.status = 1
|
||
log.debug('Download update file')
|
||
headers = {'Accept': 'application/vnd.github.v3+json'}
|
||
r = requests.get(self._get_request_path(), stream=True, headers=headers, timeout=(10, 600))
|
||
r.raise_for_status()
|
||
|
||
self.status = 2
|
||
log.debug('Opening zipfile')
|
||
z = zipfile.ZipFile(BytesIO(r.content))
|
||
self.status = 3
|
||
log.debug('Extracting zipfile')
|
||
tmp_dir = get_temp_dir()
|
||
z.extractall(tmp_dir)
|
||
folder_name = os.path.join(tmp_dir, z.namelist()[0])[:-1]
|
||
if not os.path.isdir(folder_name):
|
||
self.status = 11
|
||
log.info('Extracted contents of zipfile not found in temp folder')
|
||
self.pause()
|
||
return False
|
||
self.status = 4
|
||
log.debug('Replacing files')
|
||
if self.update_source(folder_name, constants.BASE_DIR):
|
||
self.status = 6
|
||
log.debug('Preparing restart of server')
|
||
time.sleep(2)
|
||
self.web_server.stop(True)
|
||
self.status = 7
|
||
time.sleep(2)
|
||
return True
|
||
else:
|
||
self.status = 13
|
||
|
||
except requests.exceptions.HTTPError as ex:
|
||
log.error('HTTP Error %s', ex)
|
||
self.status = 8
|
||
except requests.exceptions.ConnectionError:
|
||
log.error('Connection error')
|
||
self.status = 9
|
||
except requests.exceptions.Timeout:
|
||
log.error('Timeout while establishing connection')
|
||
self.status = 10
|
||
except (requests.exceptions.RequestException, zipfile.BadZipFile):
|
||
self.status = 11
|
||
log.error('General error')
|
||
except (IOError, OSError) as ex:
|
||
self.status = 12
|
||
log.error('Possible Reason for error: update file could not be saved in temp dir')
|
||
log.error_or_exception(ex)
|
||
self.pause()
|
||
return False
|
||
|
||
def run(self):
|
||
while True:
|
||
self.can_run.wait()
|
||
if self.status > -1:
|
||
if self.do_work():
|
||
break # stop loop and end thread for restart
|
||
else:
|
||
break
|
||
|
||
def pause(self):
|
||
self.can_run.clear()
|
||
|
||
# should just resume the thread
|
||
def resume(self):
|
||
self.can_run.set()
|
||
|
||
def stop(self):
|
||
self.status = -2
|
||
self.can_run.set()
|
||
|
||
def get_update_status(self):
|
||
return self.status
|
||
|
||
@classmethod
|
||
def file_to_list(cls, filelist):
|
||
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
|
||
|
||
@classmethod
|
||
def one_minus_two(cls, one, two):
|
||
return [x for x in one if x not in set(two)]
|
||
|
||
@classmethod
|
||
def reduce_dirs(cls, delete_files, new_list):
|
||
new_delete = []
|
||
for filename in delete_files:
|
||
parts = filename.split(os.sep)
|
||
sub = ''
|
||
for part in parts:
|
||
sub = os.path.join(sub, part)
|
||
if sub == '':
|
||
sub = os.sep
|
||
count = 0
|
||
for song in new_list:
|
||
if song.startswith(sub):
|
||
count += 1
|
||
break
|
||
if count == 0:
|
||
if sub != '\\':
|
||
new_delete.append(sub)
|
||
break
|
||
return list(set(new_delete))
|
||
|
||
@classmethod
|
||
def reduce_files(cls, remove_items, exclude_items):
|
||
rf = []
|
||
for item in remove_items:
|
||
if not item.startswith(exclude_items):
|
||
rf.append(item)
|
||
return rf
|
||
|
||
@classmethod
|
||
def check_permissions(cls, root_src_dir, root_dst_dir, log_function):
|
||
access = True
|
||
remove_path = len(root_src_dir) + 1
|
||
for src_dir, __, files in os.walk(root_src_dir):
|
||
root_dir = os.path.join(root_dst_dir, src_dir[remove_path:])
|
||
# Skip non-existing folders on check
|
||
if not os.path.isdir(root_dir):
|
||
continue
|
||
if not os.access(root_dir, os.R_OK | os.W_OK):
|
||
log_function("Missing permissions for {}".format(root_dir))
|
||
access = False
|
||
for file_ in files:
|
||
curr_file = os.path.join(root_dir, file_)
|
||
# Skip non-existing files on check
|
||
if not os.path.isfile(curr_file): # or curr_file.startswith('.'):
|
||
continue
|
||
if not os.access(curr_file, os.R_OK | os.W_OK):
|
||
log_function("Missing permissions for {}".format(curr_file))
|
||
access = False
|
||
return access
|
||
|
||
@classmethod
|
||
def move_all_files(cls, root_src_dir, root_dst_dir):
|
||
permission = None
|
||
new_permissions = os.stat(root_dst_dir)
|
||
log.debug('Performing Update on OS-System: %s', sys.platform)
|
||
change_permissions = not (sys.platform == "win32" or sys.platform == "darwin")
|
||
# Skip permission changes entirely in network share mode
|
||
try:
|
||
nsm = os.getenv("NETWORK_SHARE_MODE", "false").strip().lower() in ("1", "true", "yes", "on")
|
||
if nsm:
|
||
change_permissions = False
|
||
log.debug('NETWORK_SHARE_MODE=true detected; skipping chown operations during update')
|
||
except Exception:
|
||
pass
|
||
for src_dir, __, files in os.walk(root_src_dir):
|
||
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
|
||
if not os.path.exists(dst_dir):
|
||
try:
|
||
os.makedirs(dst_dir)
|
||
log.debug('Create directory: {}'.format(dst_dir))
|
||
except OSError as e:
|
||
log.error('Failed creating folder: {} with error {}'.format(dst_dir, e))
|
||
if change_permissions:
|
||
try:
|
||
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
|
||
except OSError as e:
|
||
old_permissions = os.stat(dst_dir)
|
||
log.error('Failed changing permissions of %s. Before: %s:%s After %s:%s error: %s',
|
||
dst_dir, old_permissions.st_uid, old_permissions.st_gid,
|
||
new_permissions.st_uid, new_permissions.st_gid, e)
|
||
for file_ in files:
|
||
src_file = os.path.join(src_dir, file_)
|
||
dst_file = os.path.join(dst_dir, file_)
|
||
if os.path.exists(dst_file):
|
||
if change_permissions:
|
||
permission = os.stat(dst_file)
|
||
try:
|
||
os.remove(dst_file)
|
||
log.debug('Remove file before copy: {}'.format(dst_file))
|
||
except OSError as e:
|
||
log.error('Failed removing file: {} with error {}'.format(dst_file, e))
|
||
else:
|
||
if change_permissions:
|
||
permission = new_permissions
|
||
try:
|
||
shutil.move(src_file, dst_dir)
|
||
log.debug('Move File %s to %s', src_file, dst_dir)
|
||
except OSError as ex:
|
||
log.error('Failed moving file from {} to {} with error {}'.format(src_file, dst_dir, ex))
|
||
if change_permissions:
|
||
try:
|
||
os.chown(dst_file, permission.st_uid, permission.st_gid)
|
||
except OSError as e:
|
||
old_permissions = os.stat(dst_file)
|
||
log.error('Failed changing permissions of %s. Before: %s:%s After %s:%s error: %s',
|
||
dst_file, old_permissions.st_uid, old_permissions.st_gid,
|
||
permission.st_uid, permission.st_gid, e)
|
||
return
|
||
|
||
def update_source(self, source, destination):
|
||
# destination files
|
||
old_list = list()
|
||
exclude = self._add_excluded_files(log.info)
|
||
additional_path = self.is_venv()
|
||
if additional_path:
|
||
exclude.append(additional_path)
|
||
exclude = tuple(exclude)
|
||
# check if we are in a package, rename cps.py to __init__.py and __main__.py
|
||
if constants.HOME_CONFIG:
|
||
shutil.copy(os.path.join(source, 'cps.py'), os.path.join(source, '__main__.py'))
|
||
shutil.move(os.path.join(source, 'cps.py'), os.path.join(source, '__init__.py'))
|
||
|
||
for root, dirs, files in os.walk(destination, topdown=True):
|
||
for name in files:
|
||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||
for name in dirs:
|
||
old_list.append(os.path.join(root, name).replace(destination, ''))
|
||
# source files
|
||
new_list = list()
|
||
for root, dirs, files in os.walk(source, topdown=True):
|
||
for name in files:
|
||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||
for name in dirs:
|
||
new_list.append(os.path.join(root, name).replace(source, ''))
|
||
|
||
delete_files = self.one_minus_two(old_list, new_list)
|
||
|
||
rf = self.reduce_files(delete_files, exclude)
|
||
|
||
remove_items = self.reduce_dirs(rf, new_list)
|
||
|
||
if self.check_permissions(source, destination, log.debug):
|
||
self.move_all_files(source, destination)
|
||
|
||
for item in remove_items:
|
||
item_path = os.path.join(destination, item[1:])
|
||
if os.path.isdir(item_path):
|
||
log.debug("Delete dir %s", item_path)
|
||
shutil.rmtree(item_path, ignore_errors=True)
|
||
else:
|
||
try:
|
||
os.remove(item_path)
|
||
log.debug("Delete file %s", item_path)
|
||
except OSError:
|
||
log.debug("Could not remove: %s", item_path)
|
||
shutil.rmtree(source, ignore_errors=True)
|
||
return True
|
||
else:
|
||
log.debug("Permissions missing for update")
|
||
return False
|
||
|
||
@staticmethod
|
||
def is_venv():
|
||
if (hasattr(sys, 'real_prefix')) or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
|
||
return os.sep + os.path.relpath(sys.prefix, constants.BASE_DIR)
|
||
else:
|
||
return False
|
||
|
||
@classmethod
|
||
def _nightly_version_info(cls):
|
||
if is_sha1(constants.NIGHTLY_VERSION[0]) and len(constants.NIGHTLY_VERSION[1]) > 0:
|
||
log.debug("Nightly version: {}, {}".format(constants.NIGHTLY_VERSION[0], constants.NIGHTLY_VERSION[1]))
|
||
return {'version': constants.NIGHTLY_VERSION[0], 'datetime': constants.NIGHTLY_VERSION[1]}
|
||
return False
|
||
|
||
@classmethod
|
||
def _stable_version_info(cls):
|
||
log.debug("Stable version: {}".format(constants.STABLE_VERSION))
|
||
return {'version': constants.STABLE_VERSION }
|
||
|
||
@classmethod
|
||
def dry_run(cls):
|
||
cls._add_excluded_files(print)
|
||
cls.check_permissions(constants.BASE_DIR, constants.BASE_DIR, print)
|
||
print("\n*** Finished ***")
|
||
|
||
@staticmethod
|
||
def _populate_parent_commits(update_data, status, tz, parents):
|
||
try:
|
||
parent_commit = update_data['parents'][0]
|
||
# limit the maximum search depth
|
||
remaining_parents_cnt = 10
|
||
except (IndexError, KeyError):
|
||
remaining_parents_cnt = None
|
||
parent_commit = None
|
||
|
||
if remaining_parents_cnt is not None:
|
||
while True:
|
||
if remaining_parents_cnt == 0:
|
||
break
|
||
|
||
# check if we are more than one update behind if so, go up the tree
|
||
if parent_commit['sha'] != status['current_commit_hash']:
|
||
try:
|
||
headers = {'Accept': 'application/vnd.github.v3+json'}
|
||
r = requests.get(parent_commit['url'], headers=headers, timeout=10)
|
||
r.raise_for_status()
|
||
parent_data = r.json()
|
||
|
||
parent_commit_date = datetime.datetime.strptime(
|
||
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||
parent_commit_date = format_datetime(
|
||
parent_commit_date, format='short')
|
||
|
||
parents.append([parent_commit_date,
|
||
parent_data['message'].replace('\r\n', '<p>').replace('\n', '<p>')])
|
||
parent_commit = parent_data['parents'][0]
|
||
remaining_parents_cnt -= 1
|
||
except Exception:
|
||
# it isn't crucial if we can't get information about the parent
|
||
break
|
||
else:
|
||
# parent is our current version
|
||
break
|
||
return parents
|
||
|
||
@staticmethod
|
||
def _load_nightly_data(repository_url, commit, status):
|
||
update_data = dict()
|
||
try:
|
||
headers = {'Accept': 'application/vnd.github.v3+json'}
|
||
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'],
|
||
headers=headers,
|
||
timeout=10)
|
||
r.raise_for_status()
|
||
update_data = r.json()
|
||
except requests.exceptions.HTTPError as e:
|
||
status['message'] = _('HTTP Error') + ' ' + str(e)
|
||
except requests.exceptions.ConnectionError:
|
||
status['message'] = _('Connection error')
|
||
except requests.exceptions.Timeout:
|
||
status['message'] = _('Timeout while establishing connection')
|
||
except (requests.exceptions.RequestException, ValueError):
|
||
status['message'] = _('General error')
|
||
return status, update_data
|
||
|
||
@staticmethod
|
||
def _add_excluded_files(log_function):
|
||
excluded_files = [
|
||
os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
|
||
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json',
|
||
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv',
|
||
os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2', os.sep + '.key',
|
||
os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR',
|
||
os.sep + 'gmail.json', os.sep + 'exclude.txt', os.sep + 'cps' + os.sep + 'cache'
|
||
]
|
||
try:
|
||
with open(os.path.join(constants.BASE_DIR, "exclude.txt"), "r") as f:
|
||
lines = f.readlines()
|
||
for line in lines:
|
||
processed_line = line.strip("\n\r ").strip("\"'").lstrip("\\/ ").\
|
||
replace("\\", os.sep).replace("/", os.sep)
|
||
if os.path.exists(os.path.join(constants.BASE_DIR, processed_line)):
|
||
excluded_files.append(os.sep + processed_line)
|
||
else:
|
||
log_function("File list for updater: {} not found".format(line))
|
||
except (PermissionError, FileNotFoundError):
|
||
log_function("Excluded file list for updater not found, or not accessible")
|
||
return excluded_files
|
||
|
||
def _nightly_available_updates(self, request_method):
|
||
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
|
||
if request_method == "GET":
|
||
repository_url = _REPOSITORY_API_URL
|
||
status, commit = self._load_remote_data(repository_url + '/git/refs/heads/master')
|
||
parents = []
|
||
if status['message'] != '':
|
||
return json.dumps(status)
|
||
if 'object' not in commit or 'url' not in commit['object']:
|
||
status['message'] = _('Unexpected data while reading update information')
|
||
return json.dumps(status)
|
||
try:
|
||
if commit['object']['sha'] == status['current_commit_hash']:
|
||
status.update({
|
||
'update': False,
|
||
'success': True,
|
||
'message': _('No update available. You already have the latest version installed')
|
||
})
|
||
return json.dumps(status)
|
||
except (TypeError, KeyError):
|
||
status['message'] = _('Unexpected data while reading update information')
|
||
return json.dumps(status)
|
||
|
||
# a new update is available
|
||
status['update'] = True
|
||
status, update_data = self._load_nightly_data(repository_url, commit, status)
|
||
|
||
if status['message'] != '':
|
||
return json.dumps(status)
|
||
|
||
# if 'committer' in update_data and 'message' in update_data:
|
||
try:
|
||
log.debug("A new update is available.")
|
||
status['success'] = True
|
||
status['message'] = _(
|
||
'A new update is available. Click on the button below to update to the latest version.')
|
||
|
||
new_commit_date = datetime.datetime.strptime(
|
||
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
|
||
parents.append(
|
||
[
|
||
format_datetime(new_commit_date, format='short'),
|
||
update_data['message'],
|
||
update_data['sha']
|
||
]
|
||
)
|
||
# it only makes sense to analyze the parents if we know the current commit hash
|
||
if status['current_commit_hash'] != '':
|
||
parents = self._populate_parent_commits(update_data, status, tz, parents)
|
||
status['history'] = parents[::-1]
|
||
except (IndexError, KeyError):
|
||
status['success'] = False
|
||
status['message'] = _('Could not fetch update information')
|
||
log.error("Could not fetch update information")
|
||
return json.dumps(status)
|
||
return ''
|
||
|
||
def _stable_updater_set_status(self, i, newer, status, parents, commit):
|
||
if i == -1 and newer is False:
|
||
status.update({
|
||
'update': True,
|
||
'success': True,
|
||
'message': _(
|
||
'Click on the button below to update to the latest stable version.'),
|
||
'history': parents
|
||
})
|
||
self.updateFile = commit[0]['zipball_url']
|
||
elif i == -1 and newer is True:
|
||
status.update({
|
||
'update': True,
|
||
'success': True,
|
||
'message': _('A new update is available. Click on the button below to '
|
||
'update to version: %(version)s', version=commit[0]['tag_name']),
|
||
'history': parents
|
||
})
|
||
self.updateFile = commit[0]['zipball_url']
|
||
return status
|
||
|
||
def _stable_updater_parse_major_version(self, commit, i, parents, current_version, status):
|
||
if int(commit[i + 1]['tag_name'].split('.')[1]) == current_version[1]:
|
||
parents.append([commit[i]['tag_name'],
|
||
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
|
||
status.update({
|
||
'update': True,
|
||
'success': True,
|
||
'message': _(u'A new update is available. Click on the button below to '
|
||
u'update to version: %(version)s', version=commit[i]['tag_name']),
|
||
'history': parents
|
||
})
|
||
self.updateFile = commit[i]['zipball_url']
|
||
else:
|
||
parents.append([commit[i + 1]['tag_name'],
|
||
commit[i + 1]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
|
||
status.update({
|
||
'update': True,
|
||
'success': True,
|
||
'message': _(u'A new update is available. Click on the button below to '
|
||
u'update to version: %(version)s', version=commit[i + 1]['tag_name']),
|
||
'history': parents
|
||
})
|
||
self.updateFile = commit[i + 1]['zipball_url']
|
||
return status, parents
|
||
|
||
def _stable_available_updates(self, request_method):
|
||
status = None
|
||
if request_method == "GET":
|
||
parents = []
|
||
# repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL
|
||
repository_url = _REPOSITORY_API_URL + '/releases?per_page=100'
|
||
status, commit = self._load_remote_data(repository_url)
|
||
if status['message'] != '':
|
||
return json.dumps(status)
|
||
if not commit:
|
||
status['success'] = True
|
||
status['message'] = _(u'No release information available')
|
||
return json.dumps(status)
|
||
version = status['current_commit_hash']
|
||
current_version = status['current_commit_hash'].split('.')
|
||
|
||
# we are already on newest version, no update available
|
||
if 'tag_name' not in commit[0]:
|
||
status['message'] = _(u'Unexpected data while reading update information')
|
||
log.error("Unexpected data while reading update information")
|
||
return json.dumps(status)
|
||
if commit[0]['tag_name'] == version:
|
||
status.update({
|
||
'update': False,
|
||
'success': True,
|
||
'message': _(u'No update available. You already have the latest version installed')
|
||
})
|
||
return json.dumps(status)
|
||
|
||
i = len(commit) - 1
|
||
newer = False
|
||
while i >= 0:
|
||
if 'tag_name' not in commit[i] or 'body' not in commit[i] or 'zipball_url' not in commit[i]:
|
||
status['message'] = _(u'Unexpected data while reading update information')
|
||
return json.dumps(status)
|
||
major_version_update = int(commit[i]['tag_name'].split('.')[0])
|
||
minor_version_update = int(commit[i]['tag_name'].split('.')[1])
|
||
patch_version_update = int(commit[i]['tag_name'].split('.')[2])
|
||
|
||
current_version[0] = int(current_version[0])
|
||
current_version[1] = int(current_version[1])
|
||
try:
|
||
current_version[2] = int(current_version[2])
|
||
except ValueError:
|
||
current_version[2] = int(current_version[2].replace("b", "").split(' ')[0])-1
|
||
|
||
# Check if major versions are identical search for newest non-equal commit and update to this one
|
||
if major_version_update == current_version[0]:
|
||
if (minor_version_update == current_version[1] and
|
||
patch_version_update > current_version[2]) or \
|
||
minor_version_update > current_version[1]:
|
||
parents.append([commit[i]['tag_name'], commit[i]['body'].replace('\r\n', '<p>')])
|
||
newer = True
|
||
i -= 1
|
||
continue
|
||
if major_version_update < current_version[0]:
|
||
i -= 1
|
||
continue
|
||
if major_version_update > current_version[0]:
|
||
# found update to last version before major update, unless current version is on last version
|
||
# before major update
|
||
if i == (len(commit) - 1):
|
||
i -= 1
|
||
status, parents = self._stable_updater_parse_major_version(commit,
|
||
i,
|
||
parents,
|
||
current_version,
|
||
status)
|
||
break
|
||
|
||
status = self._stable_updater_set_status(i, newer, status, parents, commit)
|
||
return json.dumps(status)
|
||
|
||
def _get_request_path(self):
|
||
if self.config.config_updatechannel == constants.UPDATE_STABLE:
|
||
return self.updateFile
|
||
return _REPOSITORY_API_URL + '/zipball/master'
|
||
|
||
def _load_remote_data(self, repository_url):
|
||
status = {
|
||
'update': False,
|
||
'success': False,
|
||
'message': '',
|
||
'current_commit_hash': ''
|
||
}
|
||
commit = None
|
||
version = self.get_current_version_info()
|
||
if version is False:
|
||
status['current_commit_hash'] = _(u'Unknown')
|
||
else:
|
||
status['current_commit_hash'] = version['version']
|
||
try:
|
||
headers = {'Accept': 'application/vnd.github.v3+json'}
|
||
r = requests.get(repository_url, headers=headers, timeout=10)
|
||
commit = r.json()
|
||
r.raise_for_status()
|
||
except requests.exceptions.HTTPError as e:
|
||
if commit:
|
||
if 'message' in commit:
|
||
status['message'] = _(u'HTTP Error') + ': ' + commit['message']
|
||
else:
|
||
status['message'] = _(u'HTTP Error') + ': ' + str(e)
|
||
except requests.exceptions.ConnectionError as e:
|
||
status['message'] = _(u'Connection error')
|
||
except requests.exceptions.Timeout:
|
||
status['message'] = _(u'Timeout while establishing connection')
|
||
except (requests.exceptions.RequestException, ValueError):
|
||
status['message'] = _(u'General error')
|
||
log.debug('Updater status: {}'.format(status['message'] or "OK"))
|
||
return status, commit
|