From 449b95e1b6707ea9b7359b2ee9023f22476a5189 Mon Sep 17 00:00:00 2001 From: Shivansh Singh <89853707+shivansh02@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:08:02 +0530 Subject: [PATCH] Add compaction scheduling. By @shivansh02 (#1981) --- src/vorta/assets/UI/scheduletab.ui | 110 +++++++++++++++++++++++++++++ src/vorta/scheduler.py | 24 +++++++ src/vorta/store/connection.py | 2 +- src/vorta/store/migrations.py | 16 +++++ src/vorta/store/models.py | 2 + src/vorta/views/schedule_tab.py | 14 ++++ 6 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index f7cb7b34..062c0771 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -443,6 +443,116 @@ + + + + Compaction: + + + + + + + 4 + + + 4 + + + + + Compact repository + + + + + + + + 0 + 0 + + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Interval: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 4 + 1 + + + + + + + + 1 + + + 52 + + + 3 + + + + + + + weeks + + + + + + + + diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index 82f4f0a9..9ff19681 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -5,18 +5,21 @@ from datetime import timedelta from typing import Dict, NamedTuple, Optional, Tuple, Union +from packaging import version from PyQt6 import QtCore, QtDBus from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import QApplication from vorta import application from vorta.borg.check import BorgCheckJob +from vorta.borg.compact import BorgCompactJob from vorta.borg.create import BorgCreateJob from vorta.borg.list_repo import BorgListRepoJob from vorta.borg.prune import BorgPruneJob from vorta.i18n import translate from vorta.notifications import VortaNotifications from vorta.store.models import BackupProfileModel, EventLogModel +from vorta.utils import borg_compat logger = logging.getLogger(__name__) @@ -489,6 +492,27 @@ def post_backup_tasks(self, profile_id): job = BorgCheckJob(msg['cmd'], msg, profile.repo.id) self.app.jobs_manager.add_job(job) + compaction_cutoff = dt.now() - timedelta(days=7 * profile.compaction_weeks) + recent_compactions = ( + EventLogModel.select() + .where( + (EventLogModel.subcommand == '--info') + & (EventLogModel.start_time > compaction_cutoff) + & (EventLogModel.repo_url == profile.repo.url) + ) + .count() + ) + + if ( + profile.compaction_on + and recent_compactions == 0 + and version.parse(borg_compat.version) >= version.parse("1.2") + ): + msg = BorgCompactJob.prepare(profile) + if msg['ok']: + job = BorgCompactJob(msg['cmd'], msg, profile.repo.id) + self.app.jobs_manager.add_job(job) + logger.info('Finished background task for profile %s', profile.name) notifier.deliver( self.tr('Vorta Backup'), diff --git a/src/vorta/store/connection.py b/src/vorta/store/connection.py index e02efe96..a4401c98 100644 --- a/src/vorta/store/connection.py +++ b/src/vorta/store/connection.py @@ -24,7 +24,7 @@ ) from .settings import get_misc_settings -SCHEMA_VERSION = 22 +SCHEMA_VERSION = 23 @signals.post_save(sender=SettingsModel) diff --git a/src/vorta/store/migrations.py b/src/vorta/store/migrations.py index a2689259..76ebb6b2 100644 --- a/src/vorta/store/migrations.py +++ b/src/vorta/store/migrations.py @@ -250,6 +250,22 @@ def run_migrations(current_schema, db_connection): ), ) + if current_schema.version < 23: + _apply_schema_update( + current_schema, + 23, + migrator.add_column( + BackupProfileModel._meta.table_name, + 'compaction_on', + pw.BooleanField(default=True), + ), + migrator.add_column( + BackupProfileModel._meta.table_name, + 'compaction_weeks', + pw.IntegerField(default=3), + ), + ) + def _apply_schema_update(current_schema, version_after, *operations): with DB.atomic(): diff --git a/src/vorta/store/models.py b/src/vorta/store/models.py index d2117a10..e7d08f30 100644 --- a/src/vorta/store/models.py +++ b/src/vorta/store/models.py @@ -90,6 +90,8 @@ class BackupProfileModel(BaseModel): schedule_make_up_missed = pw.BooleanField(default=True) validation_on = pw.BooleanField(default=True) validation_weeks = pw.IntegerField(default=3) + compaction_on = pw.BooleanField(default=False) + compaction_weeks = pw.IntegerField(default=3) prune_on = pw.BooleanField(default=False) prune_hour = pw.IntegerField(default=2) prune_day = pw.IntegerField(default=7) diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index dc97cfa4..2eba60fc 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -63,10 +63,12 @@ def __init__(self, parent=None): self.framePeriodic.setEnabled(False) self.frameDaily.setEnabled(False) self.frameValidation.setEnabled(False) + self.frameCompaction.setEnabled(False) self.scheduleIntervalRadio.toggled.connect(self.framePeriodic.setEnabled) self.scheduleFixedRadio.toggled.connect(self.frameDaily.setEnabled) self.validationCheckBox.toggled.connect(self.frameValidation.setEnabled) + self.compactionCheckBox.toggled.connect(self.frameCompaction.setEnabled) # POPULATE with data self.populate_from_profile() @@ -105,6 +107,12 @@ def __init__(self, parent=None): self.validationWeeksCount.valueChanged.connect( lambda new_val, attr='validation_weeks': self.save_profile_attr(attr, new_val) ) + self.compactionCheckBox.stateChanged.connect( + lambda new_val, attr='compaction_on': self.save_profile_attr(attr, new_val) + ) + self.compactionWeeksCount.valueChanged.connect( + lambda new_val, attr='compaction_weeks': self.save_profile_attr(attr, new_val) + ) # Connect to schedule update self.app.scheduler.schedule_changed.connect(lambda pid: self.draw_next_scheduled_backup()) @@ -154,6 +162,12 @@ def populate_from_profile(self): ) self.validationWeeksCount.setValue(profile.validation_weeks) + # set borg compact options + self.compactionCheckBox.setCheckState( + QtCore.Qt.CheckState.Checked if profile.compaction_on else QtCore.Qt.CheckState.Unchecked + ) + self.compactionWeeksCount.setValue(profile.compaction_weeks) + # Other checkbox options self.pruneCheckBox.setCheckState( QtCore.Qt.CheckState.Checked if profile.prune_on else QtCore.Qt.CheckState.Unchecked