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
This commit is contained in:
Prinz23 2023-06-28 14:49:39 +01:00 committed by JackDandy
parent e7d06c541a
commit ca6e399ae9
3 changed files with 125 additions and 3 deletions

View file

@ -13,7 +13,6 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>. # along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from collections import OrderedDict from collections import OrderedDict
from threading import Lock from threading import Lock
@ -23,6 +22,7 @@ import os
import re import re
import signal import signal
import socket import socket
import time
import webbrowser import webbrowser
# apparently py2exe won't build these unless they're imported somewhere # 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 api_trakt import TraktAPI
from _23 import b64encodestring, decode_bytes, scandir from _23 import b64encodestring, decode_bytes, scandir
from sg_helpers import remove_file_perm
from six import iteritems, string_types from six import iteritems, string_types
import sg_helpers import sg_helpers
@ -2423,7 +2424,69 @@ def save_config():
new_config['ANIME'] = {} new_config['ANIME'] = {}
new_config['ANIME']['anime_treat_as_hdtv'] = int(ANIME_TREAT_AS_HDTV) new_config['ANIME']['anime_treat_as_hdtv'] = int(ANIME_TREAT_AS_HDTV)
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() 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): def launch_browser(start_port=None):

View file

@ -24,6 +24,7 @@ from . import db, helpers, logger, naming
from lib.api_trakt import TraktAPI from lib.api_trakt import TraktAPI
from _23 import urlsplit, urlunsplit from _23 import urlsplit, urlunsplit
from sg_helpers import compress_file, copy_file, remove_file_perm, scantree, try_int
from six import string_types 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] 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): class ConfigMigrator(object):
def __init__(self, config_obj): def __init__(self, config_obj):
@ -926,3 +979,4 @@ class ConfigMigrator(object):
def _migrate_v22(self): def _migrate_v22(self):
self.deprecate_anon_service() self.deprecate_anon_service()

View file

@ -24,6 +24,7 @@ from exceptions_helper import ex
import sickgear import sickgear
from . import db, logger, network_timezones, properFinder, ui from . import db, logger, network_timezones, properFinder, ui
from .scheduler import Job from .scheduler import Job
from .config import backup_config
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
if False: if False:
@ -70,7 +71,11 @@ class ShowUpdater(Job):
if sickgear.db.db_supports_backup and 0 < sickgear.BACKUP_DB_MAX_COUNT: if sickgear.db.db_supports_backup and 0 < sickgear.BACKUP_DB_MAX_COUNT:
logger.log('backing up all db\'s') logger.log('backing up all db\'s')
try: 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): except (BaseException, Exception):
logger.error('backup db error') logger.error('backup db error')