bazarr/bazarr/backup.py

208 lines
7.1 KiB
Python

# coding=utf-8
import os
import io
import sqlite3
import shutil
import logging
from datetime import datetime, timedelta
from zipfile import ZipFile, BadZipFile
from glob import glob
from get_args import args
from config import settings
def get_backup_path():
backup_dir = settings.backup.folder
if not os.path.isdir(backup_dir):
os.makedirs(backup_dir)
logging.debug(f'Backup directory path is: {backup_dir}')
return backup_dir
def get_restore_path():
restore_dir = os.path.join(args.config_dir, 'restore')
if not os.path.isdir(restore_dir):
os.makedirs(restore_dir)
logging.debug(f'Restore directory path is: {restore_dir}')
return restore_dir
def get_backup_files(fullpath=True):
backup_file_pattern = os.path.join(get_backup_path(), 'bazarr_backup_v*.zip')
file_list = glob(backup_file_pattern)
file_list.sort(key=os.path.getmtime)
if fullpath:
return file_list
else:
return [{
'type': 'backup',
'filename': os.path.basename(x),
'size': sizeof_fmt(os.path.getsize(x)),
'date': datetime.fromtimestamp(os.path.getmtime(x)).strftime("%b %d %Y")
} for x in file_list]
def backup_to_zip():
now = datetime.now()
now_string = now.strftime("%Y.%m.%d_%H.%M.%S")
backup_filename = f"bazarr_backup_v{os.environ['BAZARR_VERSION']}_{now_string}.zip"
logging.debug(f'Backup filename will be: {backup_filename}')
database_src_file = os.path.join(args.config_dir, 'db', 'bazarr.db')
logging.debug(f'Database file path to backup is: {database_src_file}')
try:
database_src_con = sqlite3.connect(database_src_file)
database_backup_file = os.path.join(get_backup_path(), 'bazarr_temp.db')
database_backup_con = sqlite3.connect(database_backup_file)
with database_backup_con:
database_src_con.backup(database_backup_con)
database_backup_con.close()
database_src_con.close()
except Exception:
database_backup_file = None
logging.exception('Unable to backup database file.')
config_file = os.path.join(args.config_dir, 'config', 'config.ini')
logging.debug(f'Config file path to backup is: {config_file}')
with ZipFile(os.path.join(get_backup_path(), backup_filename), 'w') as backupZip:
if database_backup_file:
backupZip.write(database_backup_file, 'bazarr.db')
else:
logging.debug('Database file is not included in backup. See previous exception')
backupZip.write(config_file, 'config.ini')
try:
os.remove(database_backup_file)
except OSError:
logging.exception(f'Unable to delete temporary database backup file: {database_backup_file}')
def restore_from_backup():
restore_config_path = os.path.join(get_restore_path(), 'config.ini')
dest_config_path = os.path.join(args.config_dir, 'config', 'config.ini')
restore_database_path = os.path.join(get_restore_path(), 'bazarr.db')
dest_database_path = os.path.join(args.config_dir, 'db', 'bazarr.db')
if os.path.isfile(restore_config_path) and os.path.isfile(restore_database_path):
try:
shutil.copy(restore_config_path, dest_config_path)
os.remove(restore_config_path)
except OSError:
logging.exception(f'Unable to restore or delete config.ini to {dest_config_path}')
try:
shutil.copy(restore_database_path, dest_database_path)
os.remove(restore_database_path)
except OSError:
logging.exception(f'Unable to restore or delete bazarr.db to {dest_database_path}')
else:
try:
if os.path.isfile(dest_database_path + '-shm'):
os.remove(dest_database_path + '-shm')
if os.path.isfile(dest_database_path + '-wal'):
os.remove(dest_database_path + '-wal')
except OSError:
logging.exception('Unable to delete SHM and WAL file.')
logging.info('Backup restored successfully. Bazarr will restart.')
try:
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e:
logging.error('BAZARR Cannot create bazarr.restart file: ' + repr(e))
else:
logging.info('Bazarr is being restarted...')
restart_file.write(str(''))
restart_file.close()
os._exit(0)
elif os.path.isfile(restore_config_path) or os.path.isfile(restore_database_path):
logging.debug('Cannot restore a partial backup. You must have both config and database.')
else:
logging.debug('No backup to restore.')
return
try:
os.remove(restore_config_path)
except OSError:
logging.exception(f'Unable to delete {dest_config_path}')
try:
os.remove(restore_database_path)
except OSError:
logging.exception(f'Unable to delete {dest_database_path}')
def prepare_restore(filename):
src_zip_file_path = os.path.join(get_backup_path(), filename)
dest_zip_file_path = os.path.join(get_restore_path(), filename)
success = False
try:
shutil.copy(src_zip_file_path, dest_zip_file_path)
except OSError:
logging.exception(f'Unable to copy backup archive to {dest_zip_file_path}')
else:
try:
with ZipFile(dest_zip_file_path, 'r') as zipObj:
zipObj.extractall(path=get_restore_path())
except BadZipFile:
logging.exception(f'Unable to extract files from backup archive {dest_zip_file_path}')
success = True
finally:
try:
os.remove(dest_zip_file_path)
except OSError:
logging.exception(f'Unable to delete backup archive {dest_zip_file_path}')
if success:
logging.debug('time to restart')
from server import webserver
webserver.restart()
def backup_rotation():
backup_retention = settings.backup.retention
try:
int(backup_retention)
except ValueError:
logging.error('Backup retention time must be a valid integer. Please fix this in your settings.')
return
backup_files = get_backup_files()
logging.debug(f'Cleaning up backup files older than {backup_retention} days')
for file in backup_files:
if datetime.fromtimestamp(os.path.getmtime(file)) + timedelta(days=int(backup_retention)) < datetime.utcnow():
logging.debug(f'Deleting old backup file {file}')
try:
os.remove(file)
except OSError:
logging.debug(f'Unable to delete backup file {file}')
logging.debug('Finished cleaning up old backup files')
def delete_backup_file(filename):
backup_file_path = os.path.join(get_backup_path(), filename)
try:
os.remove(backup_file_path)
return True
except OSError:
logging.debug(f'Unable to delete backup file {backup_file_path}')
return False
def sizeof_fmt(num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1000.0:
return f"{num:3.1f} {unit}{suffix}"
num /= 1000.0
return f"{num:.1f} Y{suffix}"