Merge pull request #2087 from rdswift/backup_and_restore_ini_file

PICARD-2282: Add maintenance options to backup and restore configuration INI files
This commit is contained in:
Philipp Wolfer
2022-04-09 17:01:03 +02:00
committed by GitHub
5 changed files with 211 additions and 17 deletions

View File

@@ -12,7 +12,7 @@
# Copyright (C) 2017 Sophist-UK
# Copyright (C) 2018 Vishal Choudhary
# Copyright (C) 2020-2021 Gabriel Ferreira
# Copyright (C) 2021 Bob Swift
# Copyright (C) 2021-2022 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -241,7 +241,8 @@ class Config(QtCore.QSettings):
self.setting = SettingConfigSection(self, "setting")
self.persist = ConfigSection(self, "persist")
TextOption("application", "version", '0.0.0dev0')
if 'version' not in self.application or not self.application['version']:
TextOption("application", "version", '0.0.0dev0')
self._version = Version.from_string(self.application["version"])
self._upgrade_hooks = dict()
@@ -368,11 +369,16 @@ class Config(QtCore.QSettings):
def _backup_settings(self):
if Version(0, 0, 0) < self._version < PICARD_VERSION:
backup_path = self._versioned_config_filename()
log.info('Backing up config file to %s', backup_path)
try:
shutil.copyfile(self.fileName(), backup_path)
except OSError:
log.error('Failed backing up config file to %s', backup_path)
self._save_backup(backup_path)
def _save_backup(self, backup_path):
log.info('Backing up config file to %s', backup_path)
try:
shutil.copyfile(self.fileName(), backup_path)
except OSError:
log.error('Failed backing up config file to %s', backup_path)
return False
return True
def _write_version(self):
self.application["version"] = self._version.to_string()
@@ -384,6 +390,12 @@ class Config(QtCore.QSettings):
return os.path.join(os.path.dirname(self.fileName()), '%s-%s.ini' % (
self.applicationName(), version.to_string(short=True)))
def save_user_backup(self, backup_path):
if backup_path == self.fileName():
log.warning("Attempt to backup configuration file to the same path.")
return False
return self._save_backup(backup_path)
class Option(QtCore.QObject):
@@ -479,3 +491,14 @@ def get_config():
Config objects for threads are created on demand and cached for later use.
"""
return config
def load_new_config(filename=None):
config_file = get_config().fileName()
try:
shutil.copy(filename, config_file)
except OSError:
log.error('Failed restoring config file from %s', filename)
return False
setup_config(QtCore.QObject.tagger, config_file)
return True

View File

@@ -13,7 +13,7 @@
# Copyright (C) 2016-2017 Sambhav Kothari
# Copyright (C) 2017 Suhas
# Copyright (C) 2018 Vishal Choudhary
# Copyright (C) 2021 Bob Swift
# Copyright (C) 2021-2022 Bob Swift
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
@@ -174,17 +174,15 @@ class OptionsDialog(PicardDialog, SingletonDialog):
self.restoreWindowState()
self.finished.connect(self.saveWindowState)
for page in self.pages:
try:
page.load()
except Exception:
log.exception('Failed loading options page %r', page)
self.disable_page(page.NAME)
self.load_all_pages()
self.ui.pages_tree.setCurrentItem(self.default_item)
self.profile_page = self.get_page('profiles')
self.profile_page.signal_refresh.connect(self.update_from_profile_changes)
self.maintenance_page = self.get_page('maintenance')
self.maintenance_page.signal_reload.connect(self.load_all_pages)
self.first_enter = True
self.installEventFilter(self)
@@ -192,6 +190,14 @@ class OptionsDialog(PicardDialog, SingletonDialog):
current_page = self.item_to_page[self.ui.pages_tree.currentItem()]
self.set_profiles_button_and_highlight(current_page)
def load_all_pages(self):
for page in self.pages:
try:
page.load()
except Exception:
log.exception('Failed loading options page %r', page)
self.disable_page(page.NAME)
def page_has_profile_options(self, page):
try:
name = page.PARENT if page.PARENT in UserProfileGroups.SETTINGS_GROUPS else page.NAME

View File

@@ -2,7 +2,7 @@
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2021 Bob Swift
# Copyright (C) 2021-2022 Bob Swift
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2021-2022 Philipp Wolfer
#
@@ -21,6 +21,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import datetime
from os import path
from PyQt5 import (
@@ -33,7 +34,9 @@ from picard import log
from picard.config import (
Option,
get_config,
load_new_config,
)
from picard.config_upgrade import upgrade_config
from picard.ui.options import (
OptionsPage,
@@ -65,6 +68,8 @@ class MaintenanceOptionsPage(OptionsPage):
options = []
signal_reload = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MaintenanceOptionsPage()
@@ -88,6 +93,8 @@ class MaintenanceOptionsPage(OptionsPage):
self.ui.select_all.stateChanged.connect(self.select_all_changed)
self.ui.enable_cleanup.stateChanged.connect(self.enable_cleanup_changed)
self.ui.open_folder_button.clicked.connect(self.open_config_dir)
self.ui.save_backup_button.clicked.connect(self.save_backup)
self.ui.load_backup_button.clicked.connect(self.load_backup)
# Set the palette of the config file QLineEdit widget to inactive.
palette_normal = self.ui.config_file.palette()
@@ -154,6 +161,116 @@ class MaintenanceOptionsPage(OptionsPage):
config_dir = path.split(config.fileName())[0]
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(config_dir))
def _get_dialog_filetypes(self, _ext='.ini'):
return ";;".join((
_("Configuration files") + " (*{0})".format(_ext,),
_("All files") + " (*)",
))
def _make_backup_filename(self, auto=False):
config = get_config()
_filename = path.split(config.fileName())[1]
_root, _ext = path.splitext(_filename)
return "{0}_{1}_Backup_{2}{3}".format(
_root,
'Auto' if auto else 'User',
datetime.datetime.now().strftime("%Y%m%d_%H%M"),
_ext,
)
def _backup_error(self, dialog_title=None):
if not dialog_title:
dialog_title = _("Backup Configuration File")
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Critical,
dialog_title,
_("There was a problem backing up the configuration file. Please see the logs for more details."),
QtWidgets.QMessageBox.StandardButton.Ok,
self
)
dialog.exec_()
def save_backup(self):
config = get_config()
directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
filename = self._make_backup_filename()
ext = path.splitext(filename)[1]
default_path = path.normpath(path.join(directory, filename))
dialog_title = _("Backup Configuration File")
dialog_file_types = self._get_dialog_filetypes(ext)
options = QtWidgets.QFileDialog.Options()
filename, file_type = QtWidgets.QFileDialog.getSaveFileName(self, dialog_title, default_path, dialog_file_types, options=options)
if not filename:
return
# Fix issue where Qt may set the extension twice
(name, ext) = path.splitext(filename)
if ext and str(name).endswith('.' + ext):
filename = name
if config.save_user_backup(filename):
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
dialog_title,
_("Configuration successfully backed up to %s") % filename,
QtWidgets.QMessageBox.StandardButton.Ok,
self
)
dialog.exec_()
else:
self._backup_error(dialog_title)
def load_backup(self):
dialog_title = _("Load Backup Configuration File")
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Warning,
dialog_title,
_("Loading a backup configuration file will replace the current configuration settings. "
"A backup copy of the current file will be saved automatically.\n\nDo you want to continue?"),
QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel,
self
)
dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Cancel)
if not dialog.exec_() == QtWidgets.QMessageBox.StandardButton.Ok:
return
config = get_config()
directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
filename = path.join(directory, self._make_backup_filename(auto=True))
if not config.save_user_backup(filename):
self._backup_error()
return
ext = path.splitext(filename)[1]
dialog_file_types = self._get_dialog_filetypes(ext)
directory = path.normpath(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.StandardLocation.DocumentsLocation))
options = QtWidgets.QFileDialog.Options()
filename, file_type = QtWidgets.QFileDialog.getOpenFileName(self, dialog_title, directory, dialog_file_types, options=options)
if not filename:
return
log.warning('Loading configuration from %s' % filename)
if load_new_config(filename):
config = get_config()
upgrade_config(config)
QtCore.QObject.config = get_config()
self.signal_reload.emit()
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
dialog_title,
_("Configuration successfully loaded from %s") % filename,
QtWidgets.QMessageBox.StandardButton.Ok,
self
)
else:
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Critical,
dialog_title,
_("There was a problem restoring the configuration file. Please see the logs for more details."),
QtWidgets.QMessageBox.StandardButton.Ok,
self
)
dialog.exec_()
def column_items(self, column):
for idx in range(self.ui.tableWidget.rowCount()):
yield self.ui.tableWidget.item(idx, column)

View File

@@ -27,6 +27,18 @@ class Ui_MaintenanceOptionsPage(object):
self.open_folder_button.setObjectName("open_folder_button")
self.horizontalLayout_3.addWidget(self.open_folder_button)
self.vboxlayout.addLayout(self.horizontalLayout_3)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setContentsMargins(-1, -1, -1, 0)
self.horizontalLayout.setObjectName("horizontalLayout")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.load_backup_button = QtWidgets.QToolButton(MaintenanceOptionsPage)
self.load_backup_button.setObjectName("load_backup_button")
self.horizontalLayout.addWidget(self.load_backup_button)
self.save_backup_button = QtWidgets.QToolButton(MaintenanceOptionsPage)
self.save_backup_button.setObjectName("save_backup_button")
self.horizontalLayout.addWidget(self.save_backup_button)
self.vboxlayout.addLayout(self.horizontalLayout)
self.option_counts = QtWidgets.QLabel(MaintenanceOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@@ -46,8 +58,8 @@ class Ui_MaintenanceOptionsPage(object):
self.description.setIndent(0)
self.description.setObjectName("description")
self.vboxlayout.addWidget(self.description)
spacerItem = QtWidgets.QSpacerItem(20, 8, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.vboxlayout.addItem(spacerItem)
spacerItem1 = QtWidgets.QSpacerItem(20, 8, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.vboxlayout.addItem(spacerItem1)
self.line = QtWidgets.QFrame(MaintenanceOptionsPage)
self.line.setFrameShape(QtWidgets.QFrame.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
@@ -73,5 +85,7 @@ class Ui_MaintenanceOptionsPage(object):
_translate = QtCore.QCoreApplication.translate
self.label.setText(_("Configuration File:"))
self.open_folder_button.setText(_("Open folder"))
self.load_backup_button.setText(_("Load Backup"))
self.save_backup_button.setText(_("Save Backup"))
self.enable_cleanup.setText(_("Remove selected options"))
self.select_all.setText(_("Select all"))

View File

@@ -39,6 +39,40 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="load_backup_button">
<property name="text">
<string>Load Backup</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="save_backup_button">
<property name="text">
<string>Save Backup</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="option_counts">
<property name="sizePolicy">