def __init__(self, parent=None): super().__init__() self.setupUi(self) self.setWindowTitle('Vorta for Borg Backup') self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.app = parent self.current_profile = BackupProfileModel.select().order_by('id').first() self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint) self.tests_running = False # Load tab models self.repoTab = RepoTab(self.repoTabSlot) self.sourceTab = SourceTab(self.sourceTabSlot) self.archiveTab = ArchiveTab(self.archiveTabSlot) self.scheduleTab = ScheduleTab(self.scheduleTabSlot) self.miscTab = MiscTab(self.miscTabSlot) self.miscTab.set_borg_details(borg_compat.version, borg_compat.path) self.tabWidget.setCurrentIndex(0) self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile) self.repoTab.repo_added.connect(self.archiveTab.list_action) self.tabWidget.currentChanged.connect(self.scheduleTab._draw_next_scheduled_backup) self.createStartBtn.clicked.connect(self.app.create_backup_action) self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit) QShortcut(QKeySequence("Ctrl+W"), self).activated.connect(self.on_close_window) QShortcut(QKeySequence("Ctrl+Q"), self).activated.connect(self.on_close_window) self.app.backup_started_event.connect(self.backup_started_event) self.app.backup_finished_event.connect(self.backup_finished_event) self.app.backup_log_event.connect(self.set_status) self.app.backup_cancelled_event.connect(self.backup_cancelled_event) # Init profile list for profile in BackupProfileModel.select(): self.profileSelector.addItem(profile.name, profile.id) self.profileSelector.setCurrentIndex(0) self.profileSelector.currentIndexChanged.connect(self.profile_select_action) self.profileRenameButton.clicked.connect(self.profile_rename_action) self.profileDeleteButton.clicked.connect(self.profile_delete_action) self.profileAddButton.clicked.connect(self.profile_add_action) # OS-specific startup options: if sys.platform != 'darwin': # Hide Wifi-rule section in schedule tab. self.scheduleTab.wifiListLabel.hide() self.scheduleTab.wifiListWidget.hide() self.scheduleTab.page_2.hide() self.scheduleTab.toolBox.removeItem(1) # Connect to existing thread. if BorgThread.is_running(): self.createStartBtn.setEnabled(False) self.cancelButton.setEnabled(True) self.set_status(self.tr('Backup in progress.'), progress_max=0)
def test_profile_edit(qapp, qtbot): main = qapp.main_window edit_profile_window = EditProfileWindow( main, rename_existing_id=main.profileSelector.currentData()) qtbot.addWidget(edit_profile_window) # Edit profile name edit_profile_window.profileNameField.setText("") qtbot.keyClicks(edit_profile_window.profileNameField, 'Test Profile') qtbot.mouseClick( edit_profile_window.buttonBox.button(QDialogButtonBox.Save), QtCore.Qt.LeftButton) assert BackupProfileModel.get_or_none(name='Default') is None assert BackupProfileModel.get_or_none(name='Test Profile') is not None
def app(tmpdir, qtbot, mocker): tmp_db = tmpdir.join('settings.sqlite') mock_db = peewee.SqliteDatabase(str(tmp_db)) vorta.models.init_db(mock_db) mocker.patch.object(vorta.application.VortaApp, 'set_borg_details_action', return_value=None) new_repo = RepoModel(url='[email protected]:repo') new_repo.save() profile = BackupProfileModel.get(id=1) profile.repo = new_repo.id profile.save() test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo) source_dir.save() app = VortaApp([]) app.open_main_window_action() qtbot.addWidget(app.main_window) app.main_window.tests_running = True return app
def on_user_click(self): """Build system tray menu based on current state.""" menu = self.contextMenu() menu.clear() open_action = menu.addAction(self.tr('Vorta for Borg Backup')) open_action.triggered.connect(self.app.open_main_window_action) menu.addSeparator() status = menu.addAction(self.app.scheduler.next_job) status.setEnabled(False) if BorgThread.is_running(): status.setText(self.tr('Backup in Progress')) cancel_action = menu.addAction(self.tr('Cancel Backup')) cancel_action.triggered.connect(self.app.backup_cancelled_event.emit) else: status.setText(self.tr('Next Task: %s') % self.app.scheduler.next_job) profiles = BackupProfileModel.select() if profiles.count() > 1: profile_menu = menu.addMenu(self.tr('Backup Now')) for profile in profiles: new_item = profile_menu.addAction(profile.name) new_item.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i)) else: profile = profiles.first() profile_menu = menu.addAction(self.tr('Backup Now')) profile_menu.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i)) menu.addSeparator() exit_action = menu.addAction(self.tr('Quit')) exit_action.triggered.connect(self.app.quit)
def app(tmpdir, qtbot): tmp_db = tmpdir.join('settings.sqlite') mock_db = peewee.SqliteDatabase(str(tmp_db)) vorta.models.init_db(mock_db) new_repo = RepoModel(url='[email protected]:repo') new_repo.save() profile = BackupProfileModel.get(id=1) profile.repo = new_repo.id profile.save() test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo) source_dir.save() app = VortaApp([]) app.main_window.show() qtbot.addWidget(app.main_window) return app
def init_db(qapp): vorta.models.db.drop_tables(models) vorta.models.init_db() new_repo = RepoModel(url='[email protected]:repo') new_repo.encryption = 'none' new_repo.save() profile = BackupProfileModel.get(id=1) profile.repo = new_repo.id profile.dont_run_on_metered_networks = False profile.save() test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() test_archive1 = ArchiveModel(snapshot_id='99998', name='test-archive1', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive1.save() source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo, dir_size=100, dir_files_count=18, path_isdir=True) source_dir.save() qapp.main_window = MainWindow( qapp) # Re-open main window to apply mock data in UI
def create_backup(self, profile_id): notifier = VortaNotifications.pick() profile = BackupProfileModel.get(id=profile_id) logger.info('Starting background backup for %s', profile.name) notifier.deliver(self.tr('Vorta Backup'), self.tr('Starting background backup for %s.') % profile.name, level='info') msg = BorgCreateThread.prepare(profile) if msg['ok']: logger.info('Preparation for backup successful.') thread = BorgCreateThread(msg['cmd'], msg) thread.start() thread.wait() if thread.process.returncode in [0, 1]: notifier.deliver(self.tr('Vorta Backup'), self.tr('Backup successful for %s.') % profile.name, level='info') logger.info('Backup creation successful.') self.post_backup_tasks(profile_id) else: notifier.deliver(self.tr('Vorta Backup'), self.tr('Error during backup creation.'), level='error') logger.error('Error during backup creation.') else: logger.error('Conditions for backup not met. Aborting.') logger.error(msg['message']) notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error')
def profile_delete_action(self): if self.profileSelector.count() > 1: to_delete = BackupProfileModel.get( id=self.profileSelector.currentData()) # Remove pending background jobs to_delete_id = str(to_delete.id) msg = self.tr( "Are you sure you want to delete profile '{}'?".format( to_delete.name)) reply = QMessageBox.question(self, self.tr("Confirm deletion"), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: if self.app.scheduler.get_job(to_delete_id): self.app.scheduler.remove_job(to_delete_id) to_delete.delete_instance(recursive=True) self.profileSelector.removeItem( self.profileSelector.currentIndex()) self.profile_select_action(0) else: warn = self.tr("Can't delete the last profile.") point = QPoint(0, self.profileDeleteButton.size().height() / 2) QToolTip.showText(self.profileDeleteButton.mapToGlobal(point), warn)
def init_db(qapp): vorta.models.db.drop_tables(models) vorta.models.init_db() new_repo = RepoModel(url='[email protected]:repo') new_repo.save() profile = BackupProfileModel.get(id=1) profile.repo = new_repo.id profile.save() test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() test_archive1 = ArchiveModel(snapshot_id='99998', name='test-archive1', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive1.save() source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo) source_dir.save() qapp.open_main_window_action()
def profile_select_action(self, index): self.current_profile = BackupProfileModel.get( id=self.profileSelector.currentData()) self.archiveTab.populate_from_profile() self.repoTab.populate_from_profile() self.sourceTab.populate_from_profile() self.scheduleTab.populate_from_profile()
def populate_profile_selector(self): self.profileSelector.clear() for profile in BackupProfileModel.select().order_by( BackupProfileModel.name): self.profileSelector.addItem(profile.name, profile.id) current_profile_index = self.profileSelector.findData( self.current_profile.id) self.profileSelector.setCurrentIndex(current_profile_index)
def test_import_bootstrap_success(qapp, mocker): mocked_unlink = mocker.MagicMock() mocker.patch.object(Path, 'unlink', mocked_unlink) qapp.bootstrap_profile(Path(VALID_IMPORT_FILE)) assert mocked_unlink.called restored_profile = BackupProfileModel.get_or_none( name="Test Profile Restoration") assert restored_profile is not None restored_repo = restored_profile.repo assert restored_repo is not None assert len(SourceFileModel.select().where( SourceFileModel.profile == restored_profile)) == 3 assert BackupProfileModel.select().count() == 2
def profile_select_action(self, index): self.current_profile = BackupProfileModel.get(id=self.profileSelector.currentData()) self.archiveTab.populate_from_profile() self.repoTab.populate_from_profile() self.sourceTab.populate_from_profile() self.scheduleTab.populate_from_profile() SettingsModel.update({SettingsModel.str_value: self.current_profile.id})\ .where(SettingsModel.key == 'previous_profile_id')\ .execute()
def test_profile_edit(qapp, qtbot): main = qapp.main_window qtbot.mouseClick(main.profileRenameButton, QtCore.Qt.LeftButton) edit_profile_window = main.window qtbot.addWidget(edit_profile_window) qtbot.waitUntil(lambda: edit_profile_window == qapp.activeWindow(), timeout=5000) edit_profile_window.profileNameField.setText("") qtbot.keyClicks(edit_profile_window.profileNameField, 'Test Profile') qtbot.mouseClick( edit_profile_window.buttonBox.button(QDialogButtonBox.Save), QtCore.Qt.LeftButton) assert BackupProfileModel.get_or_none(name='Default') is None assert BackupProfileModel.get_or_none(name='Test Profile') is not None assert main.profileSelector.currentText() == 'Test Profile'
def delete_current_profile(qapp): ''' Delete current profile for cleanup ''' main = qapp.main_window target = BackupProfileModel.get(id=main.profileSelector.currentData()) if qapp.scheduler.get_job(target.id): qapp.scheduler.remove_job(target.id) target.delete_instance(recursive=True) main.profileSelector.removeItem(main.profileSelector.currentIndex()) main.profile_select_action(0)
def bootstrap_profile(self, bootstrap_file=PROFILE_BOOTSTRAP_FILE): """ Make sure there is at least one profile when first starting Vorta. Will either import a profile placed in ~/.vorta-init.json or add an empty "Default" profile. """ if bootstrap_file.is_file(): profile_export = ProfileExport.from_json(bootstrap_file) profile = profile_export.to_db(overwrite_profile=True, overwrite_settings=True) bootstrap_file.unlink() notifier = VortaNotifications.pick() notifier.deliver(self.tr('Profile import successful!'), self.tr('Profile {} imported.').format( profile.name), level='info') logger.info('Profile {} imported.'.format(profile.name)) if BackupProfileModel.select().count() == 0: default_profile = BackupProfileModel(name='Default') default_profile.save()
def init_overwrite_profile_checkbox(self): """Disable the overwrite profile checkbox if no profile with that name currently exists.""" existing_backup_profile = BackupProfileModel.get_or_none( BackupProfileModel.name == self.profile_export.name) if not existing_backup_profile: self.overwriteExistingProfile.setChecked(False) self.overwriteExistingProfile.setEnabled(False) self.overwriteExistingProfile.setToolTip( self. tr('A profile with the name {} does not exist. Nothing to overwrite.' .format(self.profile_export.name)))
def create_backups_cmdline(self, profile_name): profile = BackupProfileModel.get_or_none(name=profile_name) if profile is not None: if profile.repo is None: logger.warning(f"Add a repository to {profile_name}") # Wait a bit in case something is running while BorgThread.is_running(): time.sleep(0.1) self.create_backup_action(profile_id=profile.id) else: logger.warning(f"Invalid profile name {profile_name}")
def test_prune_intervals(qapp, qtbot): prune_intervals = ['hour', 'day', 'week', 'month', 'year'] main = qapp.main_window tab = main.archiveTab profile = BackupProfileModel.get(id=1) for i in prune_intervals: getattr(tab, f'prune_{i}').setValue(9) tab.save_prune_setting(None) profile = profile.refresh() assert getattr(profile, f'prune_{i}') == 9
def test_profile_add(qapp, qtbot): main = qapp.main_window add_profile_window = AddProfileWindow(main) qtbot.addWidget(add_profile_window) # Add a new profile qtbot.keyClicks(add_profile_window.profileNameField, 'Test Profile') qtbot.mouseClick( add_profile_window.buttonBox.button(QDialogButtonBox.Save), QtCore.Qt.LeftButton) assert BackupProfileModel.get_or_none(name='Test Profile') is not None
def profile_delete_action(self): if self.profileSelector.count() > 1: to_delete = BackupProfileModel.get(id=self.profileSelector.currentData()) # Remove pending background jobs to_delete_id = str(to_delete.id) if self.app.scheduler.get_job(to_delete_id): self.app.scheduler.remove_job(to_delete_id) to_delete.delete_instance(recursive=True) self.profileSelector.removeItem(self.profileSelector.currentIndex()) self.profile_select_action(0)
def create_backup_action(self, profile_id=None): if not profile_id: profile_id = self.main_window.current_profile.id profile = BackupProfileModel.get(id=profile_id) msg = BorgCreateThread.prepare(profile) if msg['ok']: thread = BorgCreateThread(msg['cmd'], msg, parent=self) thread.start() else: notifier = VortaNotifications.pick() notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error') self.backup_log_event.emit(translate('messages', msg['message']))
def next_job(self): self.wakeup() self._process_jobs() jobs = [] for job in self.get_jobs(): jobs.append((job.next_run_time, job.id)) if jobs: jobs.sort(key=lambda job: job[0]) profile = BackupProfileModel.get(id=int(jobs[0][1])) return f"{jobs[0][0].strftime('%H:%M')} ({profile.name})" else: return self.tr('None scheduled')
def init_db(qapp, qtbot, tmpdir_factory): tmp_db = tmpdir_factory.mktemp('Vorta').join('settings.sqlite') mock_db = peewee.SqliteDatabase(str(tmp_db), pragmas={ 'journal_mode': 'wal', }) vorta.models.init_db(mock_db) default_profile = BackupProfileModel(name='Default') default_profile.save() new_repo = RepoModel(url='[email protected]:repo') new_repo.encryption = 'none' new_repo.save() default_profile.repo = new_repo.id default_profile.dont_run_on_metered_networks = False default_profile.save() test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive.save() test_archive1 = ArchiveModel(snapshot_id='99998', name='test-archive1', time=dt(2000, 1, 1, 0, 0), repo=1) test_archive1.save() source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo, dir_size=100, dir_files_count=18, path_isdir=True) source_dir.save() qapp.main_window.deleteLater() del qapp.main_window qapp.main_window = MainWindow( qapp) # Re-open main window to apply mock data in UI yield qapp.backup_cancelled_event.emit() qtbot.waitUntil(lambda: not vorta.borg.borg_thread.BorgThread.is_running()) mock_db.close()
def profile_delete_action(self): if self.profileSelector.count() > 1: to_delete = BackupProfileModel.get(id=self.profileSelector.currentData()) # Remove pending background jobs to_delete_id = str(to_delete.id) msg = self.tr("Are you sure you want to delete profile '{}'?".format(to_delete.name)) reply = QMessageBox.question(self, self.tr("Confirm deletion"), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: if self.app.scheduler.get_job(to_delete_id): self.app.scheduler.remove_job(to_delete_id) to_delete.delete_instance(recursive=True) self.profileSelector.removeItem(self.profileSelector.currentIndex()) self.profile_select_action(0)
def test_profile_add(qapp, qtbot): main = qapp.main_window qtbot.mouseClick(main.profileAddButton, QtCore.Qt.LeftButton) add_profile_window = main.window qtbot.addWidget(add_profile_window) qtbot.waitUntil(lambda: add_profile_window == qapp.activeWindow(), **pytest._wait_defaults) qtbot.keyClicks(add_profile_window.profileNameField, 'Test Profile') qtbot.mouseClick( add_profile_window.buttonBox.button(QDialogButtonBox.Save), QtCore.Qt.LeftButton) assert BackupProfileModel.get_or_none(name='Test Profile') is not None assert main.profileSelector.currentText() == 'Test Profile'
def reload(self): for profile in BackupProfileModel.select(): trigger = None job_id = f'{profile.id}' if profile.schedule_mode == 'interval': if profile.schedule_interval_hours >= 24: days = profile.schedule_interval_hours // 24 leftover_hours = profile.schedule_interval_hours % 24 if leftover_hours == 0: cron_hours = '1' else: cron_hours = f'*/{leftover_hours}' trigger = cron.CronTrigger(day=f'*/{days}', hour=cron_hours, minute=profile.schedule_interval_minutes) else: trigger = cron.CronTrigger(hour=f'*/{profile.schedule_interval_hours}', minute=profile.schedule_interval_minutes) elif profile.schedule_mode == 'fixed': trigger = cron.CronTrigger(hour=profile.schedule_fixed_hour, minute=profile.schedule_fixed_minute) if self.get_job(job_id) is not None and trigger is not None: self.reschedule_job(job_id, trigger=trigger) notifier = VortaNotifications.pick() notifier.deliver(self.tr('Vorta Scheduler'), self.tr('Background scheduler was changed.')) logger.debug('Job for profile %s was rescheduled.', profile.name) elif trigger is not None: self.add_job( func=self.create_backup, args=[profile.id], trigger=trigger, id=job_id, misfire_grace_time=180 ) logger.debug('New job for profile %s was added.', profile.name) elif self.get_job(job_id) is not None and trigger is None: self.remove_job(job_id) logger.debug('Job for profile %s was removed.', profile.name)
def test_import_success(qapp, qtbot, rootdir, monkeypatch): monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args: [VALID_IMPORT_FILE]) monkeypatch.setattr(QMessageBox, 'information', lambda *args: None) main = qapp.main_window main.profile_import_action() import_dialog: ImportWindow = main.window import_dialog.overwriteExistingSettings.setChecked(True) qtbot.mouseClick(import_dialog.buttonBox.button(QDialogButtonBox.Ok), QtCore.Qt.LeftButton) qtbot.waitSignal(import_dialog.profile_imported, **pytest._wait_defaults) restored_profile = BackupProfileModel.get_or_none( name="Test Profile Restoration") assert restored_profile is not None restored_repo = restored_profile.repo assert restored_repo is not None assert len(SourceFileModel.select().where( SourceFileModel.profile == restored_profile)) == 3
def post_backup_tasks(self, profile_id): """ Pruning and checking after successful backup. """ profile = BackupProfileModel.get(id=profile_id) logger.info('Doing post-backup jobs for %s', profile.name) if profile.prune_on: msg = BorgPruneThread.prepare(profile) if msg['ok']: prune_thread = BorgPruneThread(msg['cmd'], msg) prune_thread.start() prune_thread.wait() # Refresh archives msg = BorgListRepoThread.prepare(profile) if msg['ok']: list_thread = BorgListRepoThread(msg['cmd'], msg) list_thread.start() list_thread.wait() validation_cutoff = date.today() - timedelta(days=7 * profile.validation_weeks) recent_validations = EventLogModel.select().where( ( EventLogModel.subcommand == 'check' ) & ( EventLogModel.start_time > validation_cutoff ) & ( EventLogModel.repo_url == profile.repo.url ) ).count() if profile.validation_on and recent_validations == 0: msg = BorgCheckThread.prepare(profile) if msg['ok']: check_thread = BorgCheckThread(msg['cmd'], msg) check_thread.start() check_thread.wait() logger.info('Finished background task for profile %s', profile.name)
def react_to_log(self, mgs, context): """ Trigger Vorta actions based on Borg logs. E.g. repo lock. """ msgid = context.get('msgid') if msgid == 'LockTimeout': profile = BackupProfileModel.get(name=context['profile_name']) repo_url = context.get('repo_url') msg = QMessageBox() msg.setWindowTitle(self.tr("Repository In Use")) msg.setIcon(QMessageBox.Critical) abortButton = msg.addButton(self.tr("Abort"), QMessageBox.RejectRole) msg.addButton(self.tr("Continue"), QMessageBox.AcceptRole) msg.setDefaultButton(abortButton) msg.setText( self. tr(f"The repository at {repo_url} might be in use elsewhere.")) msg.setInformativeText( self. tr("Only break the lock if you are certain no other Borg process " "on any machine is accessing the repository. Abort or break the lock?" )) msg.accepted.connect(lambda: self.break_lock(profile)) self._msg = msg msg.show() elif msgid == 'LockFailed': repo_url = context.get('repo_url') msg = QMessageBox() msg.setText( self. tr(f"You do not have permission to access the repository at {repo_url}. Gain access and try again." )) # noqa: E501 msg.setWindowTitle(self.tr("No Repository Permissions")) self._msg = msg msg.show()