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