From ca6e399ae9ea6097380be32c4e34a2c2cdbf010b Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Wed, 28 Jun 2023 14:49:39 +0100 Subject: [PATCH] Add config.ini backup try to validate saved config.ini try to use backed up config.ini file if saving fails check config.ini copy before compressing it to backup --- sickgear/__init__.py | 67 ++++++++++++++++++++++++++++++++++++++-- sickgear/config.py | 54 ++++++++++++++++++++++++++++++++ sickgear/show_updater.py | 7 ++++- 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/sickgear/__init__.py b/sickgear/__init__.py index c88e2a4c..24901649 100644 --- a/sickgear/__init__.py +++ b/sickgear/__init__.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . - from collections import OrderedDict from threading import Lock @@ -23,6 +22,7 @@ import os import re import signal import socket +import time import webbrowser # apparently py2exe won't build these unless they're imported somewhere @@ -54,6 +54,7 @@ from configobj import ConfigObj from api_trakt import TraktAPI from _23 import b64encodestring, decode_bytes, scandir +from sg_helpers import remove_file_perm from six import iteritems, string_types import sg_helpers @@ -2423,7 +2424,69 @@ def save_config(): new_config['ANIME'] = {} new_config['ANIME']['anime_treat_as_hdtv'] = int(ANIME_TREAT_AS_HDTV) - new_config.write() + from sg_helpers import copy_file + backup_config = re.sub(r'\.ini$', '.bak', CONFIG_FILE) + from .config import check_valid_config + try: + copy_file(CONFIG_FILE, backup_config) + if not check_valid_config(backup_config): + logger.error('config file seams to be invalid, not backing up.') + remove_file_perm(backup_config) + backup_config = None + except (BaseException, Exception): + backup_config = None + + for _ in range(0, 3): + new_config.write() + if check_valid_config(CONFIG_FILE): + return + logger.warning('saving config file failed, retrying...') + remove_file_perm(CONFIG_FILE) + time.sleep(3) + + # we only get here if the config saving failed multiple times + if None is not backup_config and os.path.isfile(backup_config): + logger.error('saving config file failed, using backup file') + try: + copy_file(backup_config, CONFIG_FILE) + logger.log('using old backup config file') + return + except (BaseException, Exception): + logger.error('failed to use backup config file') + + from sg_helpers import scantree + try: + target_base = os.path.join(BACKUP_DB_PATH or os.path.join(DATA_DIR, 'backup')) + file_list = [f for f in scantree(target_base, include='config', filter_kind=False)] + if file_list: + logger.log('trying to use latest config.ini backup') + # sort newest to oldest backup + file_list.sort(key=lambda _f: _f.stat(follow_symlinks=False).st_mtime) + import zipfile + try: + with zipfile.ZipFile(file_list[0].path, mode='r') as zf: + zf.extractall(target_base) + backup_config_file = os.path.join(target_base, 'config.ini') + if os.path.isfile(backup_config_file): + os.replace(backup_config_file, CONFIG_FILE) + if check_valid_config(CONFIG_FILE): + logger.log(f'used latest config.ini backup file: {file_list[0].name}') + return + else: + logger.error(f'failed to use latest config.ini backup file: {file_list[0].name}') + remove_file_perm(CONFIG_FILE) + except (BaseException, Exception): + pass + finally: + try: + remove_file_perm(backup_config_file) + except (BaseException, Exception): + pass + logger.error('failed to use latest config.ini') + except (BaseException, Exception): + pass + + logger.error('saving config file failed and no backup available') def launch_browser(start_port=None): diff --git a/sickgear/config.py b/sickgear/config.py index 92ed1db8..262219db 100644 --- a/sickgear/config.py +++ b/sickgear/config.py @@ -24,6 +24,7 @@ from . import db, helpers, logger, naming from lib.api_trakt import TraktAPI from _23 import urlsplit, urlunsplit +from sg_helpers import compress_file, copy_file, remove_file_perm, scantree, try_int from six import string_types @@ -455,6 +456,58 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True): return (my_val, def_val)['None' == my_val] +def check_valid_config(filename): + # type: (str) -> bool + """ + check if file appears to be a vaild config file + :param filename: full path config file name + """ + from configobj import ConfigObj + try: + conf_obj = ConfigObj(filename) + if not (all(section in conf_obj for section in ('General', 'GUI')) and 'config_version' in conf_obj['General'] + and isinstance(try_int(conf_obj['General']['config_version'], None), int)): + return False + return True + except (BaseException, Exception): + return False + finally: + try: + del conf_obj + except (BaseException, Exception): + pass + +def backup_config(): + """ + backup config.ini + """ + logger.log('backing up config.ini') + try: + if not check_valid_config(sickgear.CONFIG_FILE): + logger.error('config file seams to be invalid, not backing up.') + return + now = datetime.datetime.now() + d = datetime.datetime.strftime(now, '%Y-%m-%d') + t = datetime.datetime.strftime(now, '%H-%M') + target_base = os.path.join(sickgear.BACKUP_DB_PATH or os.path.join(sickgear.DATA_DIR, 'backup')) + target = os.path.join(target_base, 'config.ini') + copy_file(sickgear.CONFIG_FILE, target) + if not check_valid_config(target): + logger.error('config file seams to be invalid, not backing up.') + remove_file_perm(target) + return + compress_file(target, 'config.ini') + os.rename(re.sub(r'\.ini$', '.zip', target), os.path.join(target_base, f'config_{d}_{t}.zip')) + # remove old files + use_count = (1, sickgear.BACKUP_DB_MAX_COUNT)[not sickgear.BACKUP_DB_ONEDAY] + file_list = [f for f in scantree(target_base, include='config', filter_kind=False)] + if use_count < len(file_list): + file_list.sort(key=lambda _f: _f.stat(follow_symlinks=False).st_mtime, reverse=True) + for direntry in file_list[use_count:]: + remove_file_perm(direntry.path) + except (BaseException, Exception): + logger.error('backup config.ini error') + class ConfigMigrator(object): def __init__(self, config_obj): @@ -926,3 +979,4 @@ class ConfigMigrator(object): def _migrate_v22(self): self.deprecate_anon_service() + diff --git a/sickgear/show_updater.py b/sickgear/show_updater.py index abf20fe6..5872e028 100644 --- a/sickgear/show_updater.py +++ b/sickgear/show_updater.py @@ -24,6 +24,7 @@ from exceptions_helper import ex import sickgear from . import db, logger, network_timezones, properFinder, ui from .scheduler import Job +from .config import backup_config # noinspection PyUnreachableCode if False: @@ -70,7 +71,11 @@ class ShowUpdater(Job): if sickgear.db.db_supports_backup and 0 < sickgear.BACKUP_DB_MAX_COUNT: logger.log('backing up all db\'s') try: - sickgear.db.backup_all_dbs(sickgear.BACKUP_DB_PATH or os.path.join(sickgear.DATA_DIR, 'backup')) + backup_success = sickgear.db.backup_all_dbs( + sickgear.BACKUP_DB_PATH or os.path.join(sickgear.DATA_DIR, 'backup')) + if isinstance(backup_success, tuple) and backup_success[0]: + # backup config.ini + backup_config() except (BaseException, Exception): logger.error('backup db error')