[system/kup] /: Better exclusions system
Bharadwaj Raju <[email protected]> Tue, 4 Aug 2026 12:23:27 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 86ce12daf4916c4bccabb28640cd3d990ffec26b by Bharadwaj Raju.
Committed on 04/08/2026 at 12:23.
Pushed by bharadwaj-raju into branch 'master'.
Better exclusions system
(see #30)
Implements a dynamically-resolved exclusions system, with the following categories:
1. Caches (now including Flatpak app caches which are stored in `~/.var/app/$appname/cache`)
2. Trash
3. App states (i.e. `~/.local/state`)
4. Containers (i.e. Docker, Podman, Incus, GNOME Boxes, libvirt)
5. Encrypted folders (encrypted mounts, and Plasma Vaults mountpoints)
6. Snapshots (currently just `~/.snapshots`, but with either an optional dependency on libbtrfsutil or maybe integrating that into Solid, we could be more thorough here)
The KCM Sources UI allows configuring these, and takes them into account when displaying the tree and when looking for problems.
Part of https://invent.kde.org/kde-linux/kde-linux/-/work_items/666
M +1 -0 daemon/CMakeLists.txt
M +6 -1 daemon/bupjob.cpp
M +6 -2 daemon/rsyncjob.cpp
M +1 -0 kcm/CMakeLists.txt
M +140 -59 kcm/backupplanwidget.cpp
M +2 -1 kcm/backupplanwidget.h
M +56 -3 kcm/folderselectionmodel.cpp
M +15 -1 kcm/folderselectionmodel.h
M +15 -8 settings/backupplan.cpp
M +6 -0 settings/backupplan.h
A +103 -0 settings/dynamicexclusions.cpp [License: GPL(3+eV) GPL(v3.0)]
A +27 -0 settings/dynamicexclusions.h [License: GPL(3+eV) GPL(v3.0)]
https://invent.kde.org/system/kup/-/commit/86ce12daf4916c4bccabb28640cd3d990ffec26b
diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt
index e8df78b..e5d628d 100644
--- a/daemon/CMakeLists.txt
+++ b/daemon/CMakeLists.txt
@@ -18,6 +18,7 @@ rsyncjob.cpp
../settings/backupplan.cpp
../settings/kupsettings.cpp
../settings/kuputils.cpp
+../settings/dynamicexclusions.cpp
)
ecm_qt_declare_logging_category(kupdaemon_SRCS
diff --git a/daemon/bupjob.cpp b/daemon/bupjob.cpp
index ebb91c7..c18faa9 100644
--- a/daemon/bupjob.cpp
+++ b/daemon/bupjob.cpp
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "bupjob.h"
+#include "dynamicexclusions.h"
#include "kupdaemon_debug.h"
#include <KLocalizedString>
@@ -135,10 +136,14 @@ void BupJob::startIndexing()
mIndexProcess << QStringLiteral("-d") << mDestinationPath;
mIndexProcess << QStringLiteral("index") << QStringLiteral("-u");
- foreach (const QString &lExclude, mBackupPlan.mPathsExcluded) {
+ DynamicExclusions lDynExclusions;
+ lDynExclusions.setFromPlan(mBackupPlan);
+
+ foreach (const QString &lExclude, mBackupPlan.mPathsExcluded + lDynExclusions.pathsExcluded(mBackupPlan.mPathsIncluded)) {
mIndexProcess << QStringLiteral("--exclude");
mIndexProcess << lExclude;
}
+
QString lExcludesPath = mBackupPlan.absoluteExcludesFilePath();
if (mBackupPlan.mExcludePatterns && QFileInfo::exists(lExcludesPath)) {
mIndexProcess << QStringLiteral("--exclude-rx-from") << lExcludesPath;
diff --git a/daemon/rsyncjob.cpp b/daemon/rsyncjob.cpp
index d27f9c3..052247d 100644
--- a/daemon/rsyncjob.cpp
+++ b/daemon/rsyncjob.cpp
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "rsyncjob.h"
+#include "dynamicexclusions.h"
#include "kuputils.h"
#include <csignal>
@@ -60,6 +61,9 @@ void RsyncJob::performJob()
mRsyncProcess << QStringLiteral("--delete-excluded") << QStringLiteral("--delete-before") << QStringLiteral("--verbose")
<< QStringLiteral("--info=progress2");
+ DynamicExclusions lDynExclusions;
+ lDynExclusions.setFromPlan(mBackupPlan);
+
QStringList lIncludeNames;
foreach (const QString &lInclude, mBackupPlan.mPathsIncluded) {
lIncludeNames << lastPartOfPath(lInclude);
@@ -67,13 +71,13 @@ void RsyncJob::performJob()
if (lIncludeNames.removeDuplicates() > 0) {
// There would be a naming conflict in the destination folder, instead use full paths.
mRsyncProcess << QStringLiteral("-R");
- foreach (const QString &lExclude, mBackupPlan.mPathsExcluded) {
+ foreach (const QString &lExclude, mBackupPlan.mPathsExcluded + lDynExclusions.pathsExcluded(mBackupPlan.mPathsIncluded)) {
mRsyncProcess << QStringLiteral("--exclude") << lExclude;
}
} else {
// when NOT using -R, need to then strip parent paths from excludes, everything above the
// include. Leave the leading slash!
- foreach (QString lExclude, mBackupPlan.mPathsExcluded) {
+ foreach (QString lExclude, mBackupPlan.mPathsExcluded + lDynExclusions.pathsExcluded(mBackupPlan.mPathsIncluded)) {
for (int i = 0; i < mBackupPlan.mPathsIncluded.length(); ++i) {
const QString &lInclude = mBackupPlan.mPathsIncluded.at(i);
QString lIncludeWithSlash = lInclude;
diff --git a/kcm/CMakeLists.txt b/kcm/CMakeLists.txt
index 4a40505..310b34d 100644
--- a/kcm/CMakeLists.txt
+++ b/kcm/CMakeLists.txt
@@ -17,6 +17,7 @@ kbuttongroup.cpp
../settings/backupplan.cpp
../settings/kupsettings.cpp
../settings/kuputils.cpp
+../settings/dynamicexclusions.cpp
)
kcoreaddons_add_plugin(kcm_kup SOURCES ${kcm_kup_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings_qwidgets")
diff --git a/kcm/backupplanwidget.cpp b/kcm/backupplanwidget.cpp
index 2c0f5e8..a3592d5 100644
--- a/kcm/backupplanwidget.cpp
+++ b/kcm/backupplanwidget.cpp
@@ -336,7 +336,7 @@ void FolderSelectionWidget::setHiddenFoldersVisible(bool pVisible)
void FolderSelectionWidget::expandToShowSelections()
{
- foreach (const QString &lFolder, mModel->includedPaths() + mModel->excludedPaths()) {
+ foreach (const QString &lFolder, mModel->includedPaths() + mModel->excludedPaths() + mModel->dynamicallyExcludedPaths()) {
QFileInfo lFolderInfo(lFolder);
bool lShouldBeShown = true;
while (lFolderInfo.absoluteFilePath() != QStringLiteral("/")) {
@@ -491,7 +491,8 @@ BackupPlanWidget::BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupV
mConfigPages->addPage(mSourcePage);
mConfigPages->addPage(createDestinationPage());
mConfigPages->addPage(createSchedulePage());
- mConfigPages->addPage(createAdvancedPage(pPar2Available));
+ mAdvancedPage = createAdvancedPage(pPar2Available);
+ mConfigPages->addPage(mAdvancedPage);
auto lHLayout1 = new QHBoxLayout;
lHLayout1->addWidget(mConfigureButton);
@@ -595,8 +596,26 @@ KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, co
KPageWidgetItem *BackupPlanWidget::createSourcePage()
{
- mSourceSelectionWidget = new FolderSelectionWidget(new FolderSelectionModel(mBackupPlan->mShowHiddenFolders), this);
- auto lPage = new KPageWidgetItem(mSourceSelectionWidget);
+ auto lSourceSelectionModel = new FolderSelectionModel(mBackupPlan->mShowHiddenFolders, this);
+ mSourceSelectionWidget = new FolderSelectionWidget(lSourceSelectionModel, this);
+
+ auto lWidget = new QWidget(this);
+ auto lGridLayout = new QGridLayout(lWidget);
+ lGridLayout->addWidget(mSourceSelectionWidget);
+
+ auto lExclusionInfoLabel = new QLabel(lWidget);
+ lExclusionInfoLabel->setText(xi18nc("@info",
+ "Some files and folders (such as caches or encrypted folders) are automatically excluded. "
+ "You can configure this in the <interface><link>Advanced</link></interface> settings."));
+ lExclusionInfoLabel->setWordWrap(true);
+ connect(lExclusionInfoLabel, &QLabel::linkActivated, mConfigPages, [this]() {
+ mConfigPages->setCurrentPage(mAdvancedPage);
+ });
+ lGridLayout->addWidget(lExclusionInfoLabel);
+
+ lWidget->setLayout(lGridLayout);
+
+ auto lPage = new KPageWidgetItem(lWidget);
lPage->setName(xi18nc("@title", "Sources"));
lPage->setHeader(xi18nc("@label", "Select which folders to include in backup"));
lPage->setIcon(QIcon::fromTheme(QStringLiteral("cloud-upload")));
@@ -826,6 +845,30 @@ KPageWidgetItem *BackupPlanWidget::createSchedulePage()
return lPage;
}
+std::pair<QWidget *, QCheckBox *> createCheckBoxWithDescription(QWidget *pParent, QString pLabel, QString pExplanation, QString pObjName)
+{
+ auto lWidget = new QWidget(pParent);
+
+ auto lExplanationLabel = new QLabel(pExplanation, lWidget);
+ lExplanationLabel->setWordWrap(true);
+
+ int lIndentation = lWidget->style()->pixelMetric(QStyle::PM_IndicatorWidth) + lWidget->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing);
+ auto lLayout = new QGridLayout(lWidget);
+ lLayout->setAlignment(Qt::AlignmentFlag::AlignTop);
+ lLayout->setContentsMargins(0, 0, 0, 0);
+ lLayout->setSpacing(0);
+ lLayout->setColumnMinimumWidth(0, lIndentation);
+
+ auto lCheckbox = new QCheckBox(pLabel, lWidget);
+ lCheckbox->setObjectName(pObjName);
+
+ lLayout->addWidget(lCheckbox, 0, 0, 1, 2);
+ lLayout->addWidget(lExplanationLabel, 1, 1);
+ lWidget->setLayout(lLayout);
+
+ return {lWidget, lCheckbox};
+}
+
KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available)
{
auto lAdvancedWidget = new QWidget(this);
@@ -834,37 +877,28 @@ KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available)
int lIndentation =
lAdvancedWidget->style()->pixelMetric(QStyle::PM_IndicatorWidth) + lAdvancedWidget->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing);
- auto lShowHiddenWidget = new QWidget;
- auto lShowHiddenCheckBox = new QCheckBox(xi18nc("@option:check", "Show hidden folders in source selection"));
- lShowHiddenCheckBox->setObjectName(QStringLiteral("kcfg_Show hidden folders"));
+ auto [lShowHiddenWidget, lShowHiddenCheckBox] = createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Show hidden folders in source selection"),
+ xi18nc("@info",
+ "This makes it possible to explicitly include or "
+ "exclude hidden folders in the backup source "
+ "selection. Hidden folders have a name that starts "
+ "with a dot. They are typically located in your home "
+ "folder and are used to store settings and temporary "
+ "files for your applications."),
+ QStringLiteral("kcfg_Show hidden folders"));
connect(lShowHiddenCheckBox, SIGNAL(toggled(bool)), mSourceSelectionWidget, SLOT(setHiddenFoldersVisible(bool)));
- auto lShowHiddenLabel = new QLabel(xi18nc("@info",
- "This makes it possible to explicitly include or "
- "exclude hidden folders in the backup source "
- "selection. Hidden folders have a name that starts "
- "with a dot. They are typically located in your home "
- "folder and are used to store settings and temporary "
- "files for your applications."));
- lShowHiddenLabel->setWordWrap(true);
- auto lShowHiddenLayout = new QGridLayout;
- lShowHiddenLayout->setContentsMargins(0, 0, 0, 0);
- lShowHiddenLayout->setSpacing(0);
- lShowHiddenLayout->setColumnMinimumWidth(0, lIndentation);
- lShowHiddenLayout->addWidget(lShowHiddenCheckBox, 0, 0, 1, 2);
- lShowHiddenLayout->addWidget(lShowHiddenLabel, 1, 1);
- lShowHiddenWidget->setLayout(lShowHiddenLayout);
-
- auto lRecoveryWidget = new QWidget;
+
+ auto [lRecoveryWidget, lRecoveryCheckBox] = createCheckBoxWithDescription(lAdvancedWidget,
+ QString{}, // will be set below
+ xi18nc("@info",
+ "This will make your backups use around 10% more storage "
+ "space and saving backups will take slightly longer time. In "
+ "return it will be possible to recover from a partially corrupted "
+ "backup."),
+ QStringLiteral("kcfg_Generate recovery info"));
+
lRecoveryWidget->setVisible(false);
- auto lRecoveryCheckBox = new QCheckBox;
- lRecoveryCheckBox->setObjectName(QStringLiteral("kcfg_Generate recovery info"));
-
- auto lRecoveryLabel = new QLabel(xi18nc("@info",
- "This will make your backups use around 10% more storage "
- "space and saving backups will take slightly longer time. In "
- "return it will be possible to recover from a partially corrupted "
- "backup."));
- lRecoveryLabel->setWordWrap(true);
if (pPar2Available) {
lRecoveryCheckBox->setText(xi18nc("@option:check", "Generate recovery information"));
} else {
@@ -872,36 +906,20 @@ KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available)
"Generate recovery information (not available "
"because <application>par2</application> is not installed)"));
lRecoveryCheckBox->setEnabled(false);
- lRecoveryLabel->setEnabled(false);
}
- auto lRecoveryLayout = new QGridLayout;
- lRecoveryLayout->setContentsMargins(0, 0, 0, 0);
- lRecoveryLayout->setSpacing(0);
- lRecoveryLayout->setColumnMinimumWidth(0, lIndentation);
- lRecoveryLayout->addWidget(lRecoveryCheckBox, 0, 0, 1, 2);
- lRecoveryLayout->addWidget(lRecoveryLabel, 1, 1);
- lRecoveryWidget->setLayout(lRecoveryLayout);
connect(mVersionedRadio, SIGNAL(toggled(bool)), lRecoveryWidget, SLOT(setVisible(bool)));
- auto lVerificationWidget = new QWidget;
+ auto [lVerificationWidget, lVerificationCheckBox] = createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Verify integrity of backups"),
+ xi18nc("@info",
+ "Checks the whole backup archive for corruption "
+ "every time you save new data. Saving backups will take a "
+ "little bit longer time but it allows you to catch corruption "
+ "problems sooner than at the time you need to use a backup, "
+ "at that time it could be too late."),
+ QStringLiteral("kcfg_Check backups"));
lVerificationWidget->setVisible(false);
- auto lVerificationCheckBox = new QCheckBox(xi18nc("@option:check", "Verify integrity of backups"));
- lVerificationCheckBox->setObjectName(QStringLiteral("kcfg_Check backups"));
-
- auto lVerificationLabel = new QLabel(xi18nc("@info",
- "Checks the whole backup archive for corruption "
- "every time you save new data. Saving backups will take a "
- "little bit longer time but it allows you to catch corruption "
- "problems sooner than at the time you need to use a backup, "
- "at that time it could be too late."));
- lVerificationLabel->setWordWrap(true);
- auto lVerificationLayout = new QGridLayout;
- lVerificationLayout->setContentsMargins(0, 0, 0, 0);
- lVerificationLayout->setSpacing(0);
- lVerificationLayout->setColumnMinimumWidth(0, lIndentation);
- lVerificationLayout->addWidget(lVerificationCheckBox, 0, 0, 1, 2);
- lVerificationLayout->addWidget(lVerificationLabel, 1, 1);
- lVerificationWidget->setLayout(lVerificationLayout);
+
connect(mVersionedRadio, SIGNAL(toggled(bool)), lVerificationWidget, SLOT(setVisible(bool)));
auto lExcludesWidget = new QWidget;
@@ -961,10 +979,73 @@ KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available)
lExcludesLayout->addWidget(lExcludesButton, 2, 2);
lExcludesWidget->setLayout(lExcludesLayout);
+ auto [lExcludeCachesWidget, lExcludeCachesCheckbox] = createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude caches"),
+ xi18nc("@info", "Temporary data that can be recreated"),
+ QStringLiteral("kcfg_Exclude caches"));
+ connect(lExcludeCachesCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeCaches = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
+ auto [lExcludeEncryptedWidget, lExcludeEncryptedCheckbox] =
+ createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude encrypted folders"),
+ xi18nc("@info", "Contents of unlocked encrypted folders, such as those created with Plasma Vault"),
+ QStringLiteral("kcfg_Exclude encrypted folders"));
+ connect(lExcludeEncryptedCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeEncryptedMounts = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
+ auto [lExcludeTrashWidget, lExcludeTrashCheckbox] = createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude trash"),
+ xi18nc("@info", "Files in trash that have not been permanently deleted"),
+ QStringLiteral("kcfg_Exclude trash"));
+ connect(lExcludeTrashCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeTrash = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
+ auto [lExcludeAppStatesWidget, lExcludeAppStatesCheckbox] =
+ createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude app states"),
+ xi18nc("@info", "App-specific state data, such as window position, history, and open files"),
+ QStringLiteral("kcfg_Exclude app states"));
+ connect(lExcludeAppStatesCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeAppStates = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
+ auto [lExcludeContainersWidget, lExcludeContainersCheckbox] =
+ createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude containers and virtual machine images"),
+ xi18nc("@info", "Containers or virtual machine images, such as those created by GNOME Boxes, Docker, Podman, and Incus"),
+ QStringLiteral("kcfg_Exclude containers"));
+ connect(lExcludeContainersCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeContainers = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
+ auto [lExcludeSnapshotsWidget, lExcludeSnapshotsCheckbox] = createCheckBoxWithDescription(lAdvancedWidget,
+ xi18nc("@option:check", "Exclude snapshots"),
+ xi18nc("@info", "Btrfs filesystem snapshots"),
+ QStringLiteral("kcfg_Exclude snapshots"));
+ connect(lExcludeSnapshotsCheckbox, &QCheckBox::toggled, mSourceSelectionWidget, [this](bool state) {
+ mSourceSelectionWidget->mModel->dynamicExclusions.mExcludeSnapshots = state;
+ mSourceSelectionWidget->mModel->recalculateDynamicExclusions();
+ });
+
lAdvancedLayout->addWidget(lShowHiddenWidget);
lAdvancedLayout->addWidget(lVerificationWidget);
lAdvancedLayout->addWidget(lRecoveryWidget);
lAdvancedLayout->addWidget(lExcludesWidget);
+ lAdvancedLayout->addWidget(lExcludeCachesWidget);
+ lAdvancedLayout->addWidget(lExcludeEncryptedWidget);
+ lAdvancedLayout->addWidget(lExcludeTrashWidget);
+ lAdvancedLayout->addWidget(lExcludeAppStatesWidget);
+ lAdvancedLayout->addWidget(lExcludeContainersWidget);
+ lAdvancedLayout->addWidget(lExcludeSnapshotsWidget);
lAdvancedLayout->addStretch();
lAdvancedLayout->setSpacing(lIndentation);
lAdvancedWidget->setLayout(lAdvancedLayout);
diff --git a/kcm/backupplanwidget.h b/kcm/backupplanwidget.h
index 4655ece..1a50b8b 100644
--- a/kcm/backupplanwidget.h
+++ b/kcm/backupplanwidget.h
@@ -69,6 +69,7 @@ class FolderSelectionWidget : public QWidget
public:
explicit FolderSelectionWidget(FolderSelectionModel *pModel, QWidget *pParent = nullptr);
virtual ~FolderSelectionWidget();
+ FolderSelectionModel *mModel;
public slots:
void setHiddenFoldersVisible(bool pVisible);
@@ -81,7 +82,6 @@ public slots:
protected:
QTreeView *mTreeView;
- FolderSelectionModel *mModel;
KMessageWidget *mMessageWidget;
QThread *mWorkerThread;
QStringList mUnreadableFolders;
@@ -160,6 +160,7 @@ protected:
QRadioButton *mSyncedRadio{};
FolderSelectionWidget *mSourceSelectionWidget{};
KPageWidgetItem *mSourcePage;
+ KPageWidgetItem *mAdvancedPage;
KMessageWidget *mLocalMessage;
KMessageWidget *mExistMessage;
diff --git a/kcm/folderselectionmodel.cpp b/kcm/folderselectionmodel.cpp
index 5266f86..66f1a26 100644
--- a/kcm/folderselectionmodel.cpp
+++ b/kcm/folderselectionmodel.cpp
@@ -36,6 +36,7 @@ bool setContainsSubdir(const QSet<QString> &pSet, const QString &pParentDir)
FolderSelectionModel::FolderSelectionModel(bool pHiddenFoldersVisible, QObject *pParent)
: QFileSystemModel(pParent)
+ , dynamicExclusions(DynamicExclusions{})
{
setHiddenFoldersVisible(pHiddenFoldersVisible);
}
@@ -44,6 +45,9 @@ Qt::ItemFlags FolderSelectionModel::flags(const QModelIndex &pIndex) const
{
Qt::ItemFlags lFlags = QFileSystemModel::flags(pIndex);
lFlags |= Qt::ItemIsUserCheckable;
+ if (pIndex.isValid() && mDynamicallyExcludedPaths.contains(filePath(pIndex))) {
+ lFlags &= ~Qt::ItemIsEnabled;
+ }
return lFlags;
}
@@ -59,7 +63,7 @@ QVariant FolderSelectionModel::data(const QModelIndex &pIndex, int pRole) const
switch (lState) {
case StateIncluded:
case StateIncludeInherited:
- if (setContainsSubdir(mExcludedPaths, lPath)) {
+ if (setContainsSubdir(mExcludedPaths | mDynamicallyExcludedPaths, lPath)) {
return Qt::PartiallyChecked;
}
return Qt::Checked;
@@ -85,7 +89,7 @@ QVariant FolderSelectionModel::data(const QModelIndex &pIndex, int pRole) const
switch (lState) {
case StateIncluded:
case StateIncludeInherited:
- if (setContainsSubdir(mExcludedPaths, lPath)) {
+ if (setContainsSubdir(mExcludedPaths | mDynamicallyExcludedPaths, lPath)) {
return xi18nc("@info:tooltip %1 is the path of the folder in a listview",
"<filename>%1</filename><nl/>will be included in the backup, except "
"for unchecked subfolders",
@@ -94,6 +98,11 @@ QVariant FolderSelectionModel::data(const QModelIndex &pIndex, int pRole) const
return xi18nc("@info:tooltip %1 is the path of the folder in a listview",
"<filename>%1</filename><nl/>will be included in the backup",
filePath(pIndex));
+ case StateExcludedAutomatic:
+ return xi18nc("@info:tooltip %1 is the path of the folder in a listview",
+ "<filename>%1</filename><nl/>is <emphasis>automatically excluded</emphasis> from the backup."
+ "<nl/>You can control this in the <interface>Advanced</interface> settings.",
+ filePath(pIndex));
default:
if (setContainsSubdir(mIncludedPaths, lPath)) {
return xi18nc("@info:tooltip %1 is the path of the folder in a listview",
@@ -160,7 +169,7 @@ void FolderSelectionModel::includePath(const QString &pPath)
void FolderSelectionModel::excludePath(const QString &pPath)
{
const InclusionState lState = inclusionState(pPath);
- if (lState == StateExcluded) {
+ if (lState == StateExcluded || lState == StateExcludedAutomatic) {
return;
}
removeSubDirs(pPath);
@@ -217,6 +226,11 @@ QSet<QString> FolderSelectionModel::excludedPaths() const
return mExcludedPaths;
}
+QSet<QString> FolderSelectionModel::dynamicallyExcludedPaths() const
+{
+ return mDynamicallyExcludedPaths;
+}
+
FolderSelectionModel::InclusionState FolderSelectionModel::inclusionState(const QModelIndex &pIndex) const
{
return inclusionState(filePath(pIndex));
@@ -227,6 +241,9 @@ FolderSelectionModel::InclusionState FolderSelectionModel::inclusionState(const
if (mIncludedPaths.contains(pPath)) {
return StateIncluded;
}
+ if (mDynamicallyExcludedPaths.contains(pPath)) {
+ return StateExcludedAutomatic;
+ }
if (mExcludedPaths.contains(pPath)) {
return StateExcluded;
}
@@ -295,3 +312,39 @@ void FolderSelectionModel::removeSubDirs(const QString &pPath)
}
}
}
+
+void FolderSelectionModel::recalculateDynamicExclusions()
+{
+ QStringList lNewDynamicPathList = dynamicExclusions.pathsExcluded(mIncludedPaths.values());
+ QSet<QString> lNewDynamicPathSet = QSet(lNewDynamicPathList.begin(), lNewDynamicPathList.end());
+
+ QSet<QString> lRemoved = mDynamicallyExcludedPaths - lNewDynamicPathSet;
+ QSet<QString> lAdded = lNewDynamicPathSet - mDynamicallyExcludedPaths;
+ if (lRemoved.count() + lAdded.count() == 0)
+ return;
+
+ mDynamicallyExcludedPaths = lNewDynamicPathSet;
+
+ foreach (const QString &lRemovedPath, lRemoved) {
+ removeSubDirs(lRemovedPath);
+ emit excludedPathRemoved(lRemovedPath);
+ const QModelIndex lRemovedIndex = index(lRemovedPath);
+ emit dataChanged(lRemovedIndex, findLastLeaf(lRemovedIndex));
+ QModelIndex lRecurseIndex = lRemovedIndex.parent();
+ while (lRecurseIndex.isValid()) {
+ emit dataChanged(lRecurseIndex, lRecurseIndex);
+ lRecurseIndex = lRecurseIndex.parent();
+ }
+ }
+ foreach (const QString &lAddedPath, lAdded) {
+ removeSubDirs(lAddedPath);
+ emit excludedPathAdded(lAddedPath);
+ const QModelIndex lAddedIndex = index(lAddedPath);
+ emit dataChanged(lAddedIndex, findLastLeaf(lAddedIndex));
+ QModelIndex lRecurseIndex = lAddedIndex.parent();
+ while (lRecurseIndex.isValid()) {
+ emit dataChanged(lRecurseIndex, lRecurseIndex);
+ lRecurseIndex = lRecurseIndex.parent();
+ }
+ }
+}
diff --git a/kcm/folderselectionmodel.h b/kcm/folderselectionmodel.h
index bf2455c..52f8a20 100644
--- a/kcm/folderselectionmodel.h
+++ b/kcm/folderselectionmodel.h
@@ -10,6 +10,8 @@
#ifndef FOLDER_SELECTION_MODEL_H
#define FOLDER_SELECTION_MODEL_H
+#include "../settings/dynamicexclusions.h"
+
#include <QFileSystemModel>
#include <QSet>
@@ -20,7 +22,14 @@ class FolderSelectionModel : public QFileSystemModel
public:
explicit FolderSelectionModel(bool pHiddenFoldersVisible = false, QObject *pParent = nullptr);
- enum InclusionState { StateNone, StateIncluded, StateExcluded, StateIncludeInherited, StateExcludeInherited };
+ enum InclusionState {
+ StateNone,
+ StateIncluded,
+ StateExcluded,
+ StateIncludeInherited,
+ StateExcludeInherited,
+ StateExcludedAutomatic
+ };
enum CustomRoles { IncludeStateRole = 7777 };
@@ -32,6 +41,7 @@ public:
void setExcludedPaths(const QSet<QString> &pExcludedPaths);
QSet<QString> includedPaths() const;
QSet<QString> excludedPaths() const;
+ QSet<QString> dynamicallyExcludedPaths() const;
/**
* Include the specified path. All subdirs will be reset.
@@ -53,8 +63,11 @@ public:
bool hiddenFoldersVisible() const;
+ DynamicExclusions dynamicExclusions;
+
public slots:
void setHiddenFoldersVisible(bool pVisible);
+ void recalculateDynamicExclusions();
signals:
void includedPathAdded(const QString &pPath);
@@ -68,6 +81,7 @@ private:
QSet<QString> mIncludedPaths;
QSet<QString> mExcludedPaths;
+ QSet<QString> mDynamicallyExcludedPaths;
};
#endif
diff --git a/settings/backupplan.cpp b/settings/backupplan.cpp
index 2e7e444..0bb86c2 100644
--- a/settings/backupplan.cpp
+++ b/settings/backupplan.cpp
@@ -6,6 +6,7 @@
#include "kuputils.h"
#include <QDir>
+#include <QDirIterator>
#include <QStandardPaths>
#include <QString>
#include <QTimeZone>
@@ -28,15 +29,8 @@ BackupPlan::BackupPlan(int pPlanNumber, KSharedConfigPtr pConfig, QObject *pPare
lDefaultIncludeList << QDir::homePath();
addItemStringList(QStringLiteral("Paths included"), mPathsIncluded, lDefaultIncludeList);
QStringList lDefaultExcludeList;
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.cache");
lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.bup");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.thumbnails");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/Trash");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/baloo");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/container");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.local/share/TelegramDesktop/tdata/temp");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/.config/Riot/Cache");
- lDefaultExcludeList << QDir::homePath() + QStringLiteral("/Vaults"); // might be mounted; don't leak data
+
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
lDefaultExcludeList << QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation);
#endif
@@ -45,6 +39,13 @@ BackupPlan::BackupPlan(int pPlanNumber, KSharedConfigPtr pConfig, QObject *pPare
ensureNoTrailingSlash(i.next());
}
+ addItemBool(QStringLiteral("Exclude trash"), mExcludeTrash, true);
+ addItemBool(QStringLiteral("Exclude app states"), mExcludeAppStates, true);
+ addItemBool(QStringLiteral("Exclude caches"), mExcludeCaches, true);
+ addItemBool(QStringLiteral("Exclude encrypted folders"), mExcludeEncryptedMounts, true);
+ addItemBool(QStringLiteral("Exclude snapshots"), mExcludeSnapshots, true);
+ addItemBool(QStringLiteral("Exclude containers"), mExcludeContainers, true);
+
addItemStringList(QStringLiteral("Paths excluded"), mPathsExcluded, lDefaultExcludeList);
addItemInt(QStringLiteral("Backup type"), mBackupType);
addItemInt(QStringLiteral("Backup version"), mBackupVersion);
@@ -113,6 +114,12 @@ void BackupPlan::copyFrom(const BackupPlan &pPlan)
mShowHiddenFolders = pPlan.mShowHiddenFolders;
mGenerateRecoveryInfo = pPlan.mGenerateRecoveryInfo;
mCheckBackups = pPlan.mCheckBackups;
+ mExcludeTrash = pPlan.mExcludeTrash;
+ mExcludeAppStates = pPlan.mExcludeAppStates;
+ mExcludeCaches = pPlan.mExcludeCaches;
+ mExcludeEncryptedMounts = pPlan.mExcludeEncryptedMounts;
+ mExcludeSnapshots = pPlan.mExcludeSnapshots;
+ mExcludeContainers = pPlan.mExcludeContainers;
}
QDateTime BackupPlan::nextScheduledTime()
diff --git a/settings/backupplan.h b/settings/backupplan.h
index f63b8bc..63cb0a6 100644
--- a/settings/backupplan.h
+++ b/settings/backupplan.h
@@ -24,6 +24,12 @@ public:
QString mDescription;
QStringList mPathsIncluded;
QStringList mPathsExcluded;
+ bool mExcludeTrash{};
+ bool mExcludeAppStates{};
+ bool mExcludeCaches{};
+ bool mExcludeEncryptedMounts{};
+ bool mExcludeSnapshots{};
+ bool mExcludeContainers{};
enum BackupType { BupType = 0, RsyncType };
qint32 mBackupType{};
qint32 mBackupVersion{};
diff --git a/settings/dynamicexclusions.cpp b/settings/dynamicexclusions.cpp
new file mode 100644
index 0000000..1f274b3
--- /dev/null
+++ b/settings/dynamicexclusions.cpp
@@ -0,0 +1,103 @@
+// SPDX-FileCopyrightText: 2026 Bharadwaj Raju <[email protected]>
+//
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+
+#include "dynamicexclusions.h"
+#include "kuputils.h"
+
+#include <QDir>
+#include <QDirIterator>
+#include <QStandardPaths>
+
+#include <KConfig>
+#include <Solid/Device>
+#include <Solid/StorageAccess>
+
+void DynamicExclusions::setFromPlan(const BackupPlan &pPlan)
+{
+ mExcludeTrash = pPlan.mExcludeTrash;
+ mExcludeAppStates = pPlan.mExcludeAppStates;
+ mExcludeCaches = pPlan.mExcludeCaches;
+ mExcludeEncryptedMounts = pPlan.mExcludeEncryptedMounts;
+ mExcludeSnapshots = pPlan.mExcludeSnapshots;
+ mExcludeContainers = pPlan.mExcludeContainers;
+}
+
+QStringList DynamicExclusions::pathsExcluded(const QStringList &pPathsIncluded)
+{
+ QStringList lExclusions;
+
+ if (mExcludeEncryptedMounts) {
+ const auto lDevices = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.encrypted == true"));
+ for (const auto &lDev : lDevices) {
+ auto lStorageAccess = lDev.as<Solid::StorageAccess>();
+ if (!lStorageAccess || !lStorageAccess->isAccessible()) {
+ continue;
+ }
+ if (pPathsIncluded.contains(lStorageAccess->filePath())) {
+ continue;
+ }
+ lExclusions << lStorageAccess->filePath();
+ }
+
+ const KConfig lPlasmaVaultConfig(QStringLiteral("plasmavaultrc"));
+ const auto lDeviceKeys = lPlasmaVaultConfig.group(QStringLiteral("EncryptedDevices")).keyList();
+ for (const QString &lDeviceKey : lDeviceKeys) {
+ const auto lDevice = lPlasmaVaultConfig.group(lDeviceKey);
+ if (lDevice.exists()) {
+ lExclusions << lDevice.readEntry(QStringLiteral("mountPoint"));
+ }
+ }
+ }
+
+ if (mExcludeCaches) {
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
+ lExclusions << QDir::homePath() + QStringLiteral("/.thumbnails");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/baloo");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/TelegramDesktop/tdata/temp");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/Riot/cache");
+
+ // Flatpak
+ QDirIterator lVarAppIter(QDir::homePath() + QStringLiteral("/.var/app"), QDir::Dirs | QDir::NoDotAndDotDot);
+ while (lVarAppIter.hasNext()) {
+ lExclusions << lVarAppIter.next() + "/cache";
+ }
+ }
+
+ if (mExcludeContainers) {
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/containers");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/gnome-boxes");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/libvirt");
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/docker");
+ lExclusions << QDir::homePath() + QStringLiteral("/.var/app/org.gnome.Boxes");
+ }
+
+ if (mExcludeAppStates) {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation);
+#else
+ lExclusions << QDir::homePath() + QStringLiteral("/.local/state");
+#endif
+ }
+
+ if (mExcludeTrash) {
+ lExclusions << QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash");
+ }
+
+ if (mExcludeSnapshots) {
+ // this catches snapshots of the home dir taken by Snapper, which stores them under the respective subvolume
+ lExclusions << QDir::homePath() + QStringLiteral("/.snapshots");
+ }
+
+ QMutableStringListIterator lExclusionsIter(lExclusions);
+ while (lExclusionsIter.hasNext()) {
+ QString &lNext = lExclusionsIter.next();
+ lNext = QDir::cleanPath(lNext);
+ ensureNoTrailingSlash(lNext);
+ if (!QFileInfo::exists(lNext)) {
+ lExclusionsIter.remove();
+ }
+ }
+
+ return lExclusions;
+}
diff --git a/settings/dynamicexclusions.h b/settings/dynamicexclusions.h
new file mode 100644
index 0000000..966c4f6
--- /dev/null
+++ b/settings/dynamicexclusions.h
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2026 Bharadwaj Raju <[email protected]>
+//
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+
+#ifndef DYNAMICEXCLUSIONS_H
+#define DYNAMICEXCLUSIONS_H
+
+#include "backupplan.h"
+
+#include <QStringList>
+
+class DynamicExclusions
+{
+public:
+ void setFromPlan(const BackupPlan &plan);
+
+ bool mExcludeTrash{};
+ bool mExcludeAppStates{};
+ bool mExcludeCaches{};
+ bool mExcludeEncryptedMounts{};
+ bool mExcludeSnapshots{};
+ bool mExcludeContainers{};
+
+ QStringList pathsExcluded(const QStringList &pPathsIncluded);
+};
+
+#endif // DYNAMICEXCLUSIONS_H