import os import sys from PyQt5 import uic from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import (QMainWindow, QFileSystemModel, QFileDialog, QMessageBox) from borg_qt.config import Config from borg_qt.helper import (BorgException, show_error, convert_size, open_path, create_path, remove_path, check_path) from borg_qt.help import Help import borg_qt.borg_interface as borg from borg_qt.progress import ProgressDialog class MainWindow(QMainWindow): """The main window of the application. It provides the various functions to control BorgBackup.""" def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) QCoreApplication.setApplicationName("borg-qt") # Load the UI file to get the dialogs layout. dir_path = os.path.dirname(os.path.realpath(__file__)) ui_path = os.path.join(dir_path + '/static/UI/MainWindow.ui') uic.loadUi(ui_path, self) # Set the window title after the UI has been loaded. Otherwise it gets # overwritten. self.setWindowTitle("Borg-Qt") # Create a Config object for storing the configuration. self.config = Config() # list of mounted archives self.mount_paths = [] # File tree model = QFileSystemModel() # model.setRootPath('/') model.setRootPath(os.getenv('HOME')) self.treeview_files.setModel(model) self.treeview_files.expandAll() self.treeview_files.setIndentation(20) self.treeview_files.setColumnHidden(1, True) self.treeview_files.setColumnHidden(2, True) self.treeview_files.setColumnHidden(3, True) # return the clicking on an item in the tree self.treeview_files.clicked.connect(self.get_selected_path) self.list_archive.setSortingEnabled(True) # Connecting actions and buttons. self.action_settings.triggered.connect(self.show_settings) self.action_backup.triggered.connect(self.create_backup) self.action_restore.triggered.connect(self.restore_backup) self.action_delete.triggered.connect(self.delete_backup) self.action_mount.triggered.connect(self.mount_backup) self.action_help.triggered.connect(self.show_help) def start(self): """This method is intendet to be used only once at the application start. It reads the configuration file and sets the required environment variables.""" try: self.config.read() # show the help window if needed and save it's answer if not self.config.hide_help: self.config.config['borgqt']['hide_help'] = ( str(self.show_help())) self.config.write() self.config._set_environment_variables() self._update_archives() self._update_repository_stats() except BorgException as e: show_error(e) sys.exit(1) def closeEvent(self, *args, **kwargs): super(QMainWindow, self).closeEvent(*args, **kwargs) # When the application gets close unmount all the archives and remove # their paths. self._umount_archives() def _umount_archives(self): if self.mount_paths: for path in self.mount_paths: if os.path.exists(path): os.system('borg umount ' + path) remove_path(path) self.mount_paths = [] def show_settings(self): """Display the settings dialog.""" self.config.set_form_values() self.config.exec_() def background_backup(self): self.config.read() self.config._set_environment_variables() backup_thread = borg.BackupThread(self.config.includes, excludes=self.config.excludes, prefix=self.config.prefix) backup_thread.run() if self.config.retention_policy_enabled: prune_thread = borg.PruneThread(self.config.retention_policy) prune_thread.run() def get_selected_path(self, signal): """returns the path of the item selected in the file tree.""" self.src_path = self.treeview_files.model().filePath(signal) def _check_path(self): """Makes sure that the user selected a path to backup.""" message = ("Please select a file or directory " "before taking a backup.") if not hasattr(self, 'src_path'): raise BorgException(message) def create_backup(self): """Creates a backup of the selected item in the treeview.""" if not self._check_mounts(): return try: self._check_path() backup_thread = borg.BackupThread([self.src_path], excludes=self.config.excludes, prefix=self.config.prefix) backup_dialog = ProgressDialog(backup_thread) backup_dialog.label_info.setText("Borg-Qt is currently creating an" " archive.") backup_dialog.exec_() if self.config.retention_policy_enabled: prune_thread = borg.PruneThread(self.config.retention_policy) prune_dialog = ProgressDialog(prune_thread) prune_dialog.label_info.setText("Borg-Qt is currently pruning " "the repository.") prune_dialog.exec_() self.update_ui() except BorgException as e: show_error(e) def _get_target_path(self): """Opens a file dialog and returns the opened path.""" dlg = QFileDialog dlg.DirectoryOnly folder_name = str(dlg.getExistingDirectory( self, "Select Directory", os.getenv('HOME'))) return folder_name def show_help(self): """Diplays the help dialog with some informations about the application.""" help_window = Help() help_window.exec_() return help_window.check_hide_enabled.isChecked() @property def selected_archive(self): return self.list_archive.currentItem().text() def restore_backup(self): """Restores a selected backup to the given path.""" try: archive_name = self.selected_archive target_path = self._get_target_path() except AttributeError: error = BorgException("Please create or select an archive first.") archive_name = None target_path = None show_error(error) # Only restore the backup if the target is writeable and the archive # was selected. if check_path(target_path) and archive_name: try: restore_path = os.path.join(target_path, archive_name) create_path(restore_path) thread = borg.RestoreThread(archive_name, restore_path) dialog = ProgressDialog(thread) dialog.label_info.setText( "Borg-Qt is currently restoring a backup.") dialog.exec_() open_path(restore_path) except BorgException as e: show_error(e) remove_path(restore_path) def delete_backup(self): """Deletes the selected archive from the repository.""" if not self._check_mounts(): return try: archive_name = self.selected_archive except AttributeError: error = BorgException("Please create or select an archive first.") archive_name = None show_error(error) # Only continue if an archive was selected. if archive_name: # Prompt the user before continuing. if self.yes_no("Do you want to delete this archive?"): try: thread = borg.DeleteThread(archive_name) dialog = ProgressDialog(thread) dialog.label_info.setText( "Borg-Qt is currently deleting an archive.") dialog.exec_() self.update_ui() except BorgException as e: show_error(e) def _update_archives(self): """Lists all the archive names in the UI.""" thread = borg.ListThread() self.list_archive.clear() archive_names = [] for archive in thread.run(): archive_names.append(archive['name']) self.list_archive.addItems(archive_names) def update_ui(self): """Updates the archive list and repository stats in the UI.""" try: self._update_archives() self._update_repository_stats() except BorgException as e: show_error(e) def _update_repository_stats(self): """Update the repository stats and display them in a human readable format.""" thread = borg.InfoThread() stats = thread.run() self.label_repo_original_size.setText( "Original Size: " + convert_size(stats['total_size'])) self.label_repo_compressed_size.setText( "Compressed Size: " + convert_size(stats['total_csize'])) self.label_repo_deduplicated_size.setText( "Deduplicated Size: " + convert_size(stats['unique_csize'])) def mount_backup(self): """Mount the selected archive in the tmp directory. If it succeeds the mount_path gets written to a property of the main_window.""" try: archive_name = self.selected_archive except AttributeError: error = BorgException("Please create or select an archive first.") archive_name = None show_error(error) # only continue if the user selected an archive if archive_name: mount_path = os.path.join('/tmp/', archive_name) create_path(mount_path) # only continue if the mount_path is writeable if os.access(mount_path, os.W_OK): thread = borg.MountThread(archive_name, mount_path) try: thread.run() self.mount_paths.append(mount_path) open_path(mount_path) except BorgException as e: show_error(e) remove_path(mount_path) else: # Opens the path in a file manager open_path(mount_path) def _check_mounts(self): if self.mount_paths: if self.yes_no("To proceed you need to unmout all " "archives. Do you want to continue?"): self._umount_archives() return True else: return False def yes_no(self, question): """Simple yes/no dialog. Args: question (str) The question to display to the user.""" button_reply = QMessageBox.question(self, 'Borg-Qt', question, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if button_reply == QMessageBox.Yes: return True else: return False