[system/kio-snapshot] /: Make filesnapshots work with Btrfs filesystems mounted at other than /, and make it work for directories

Bharadwaj Raju <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 941552b646035830af34483be8bacc611f4a8ea9 by Bharadwaj Raju.
Committed on 17/07/2026 at 03:59.
Pushed by bharadwaj-raju into branch 'master'.

Make filesnapshots work with Btrfs filesystems mounted at other than /, and make it work for directories

M  +1    -0    CMakeLists.txt
M  +32   -22   common/btrfssnapshots.cpp
M  +7    -5    common/btrfssnapshots.h
M  +1    -1    contextmenu/CMakeLists.txt
M  +19   -2    contextmenu/snapshotfileitemaction.cpp
M  +2    -2    kioworker/CMakeLists.txt
M  +16   -4    kioworker/filesnapshots.cpp

https://invent.kde.org/system/kio-snapshot/-/commit/941552b646035830af34483be8bacc611f4a8ea9

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 602ba91..c6c9416 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,6 +32,7 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS
 find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
     KIO
     I18n
+    Solid
 )
 
 find_package(PkgConfig REQUIRED)
diff --git a/common/btrfssnapshots.cpp b/common/btrfssnapshots.cpp
index 394621f..4ba4a0b 100644
--- a/common/btrfssnapshots.cpp
+++ b/common/btrfssnapshots.cpp
@@ -15,12 +15,17 @@
 
 using namespace Qt::StringLiterals;
 
-std::optional<qulonglong> BtrfsSnapshots::getSubvolumeForPath(const QString &path)
+const char *cstr(const QString &s)
+{
+    return s.toLocal8Bit().constData();
+}
+
+std::optional<qulonglong> BtrfsSnapshots::getSubvolumeForPath(const QString &path, const QString &fsRoot)
 {
     enum btrfs_util_error btrfs_err;
 
     struct btrfs_util_subvolume_iterator *iter;
-    btrfs_err = btrfs_util_subvolume_iter_create("/", 0, 0, &iter);
+    btrfs_err = btrfs_util_subvolume_iter_create(cstr(fsRoot), 0, 0, &iter);
     if (btrfs_err != 0) {
         return std::nullopt;
     }
@@ -28,7 +33,7 @@ std::optional<qulonglong> BtrfsSnapshots::getSubvolumeForPath(const QString &pat
     struct btrfs_util_subvolume_info iter_info;
     char *iter_path;
     while ((btrfs_err = btrfs_util_subvolume_iter_next_info(iter, &iter_path, &iter_info)) == 0) {
-        if (path == "/"_L1 + QString::fromUtf8(iter_path)) {
+        if (path == QDir::cleanPath(fsRoot + "/"_L1 + QString::fromUtf8(iter_path))) {
             free(iter_path);
             return static_cast<qulonglong>(iter_info.id);
         }
@@ -38,12 +43,12 @@ std::optional<qulonglong> BtrfsSnapshots::getSubvolumeForPath(const QString &pat
     return std::nullopt;
 }
 
-std::optional<QString> BtrfsSnapshots::getPathForSubvolume(qulonglong subvolume)
+std::optional<QString> BtrfsSnapshots::getPathForSubvolume(qulonglong subvolume, const QString &fsRoot)
 {
     enum btrfs_util_error btrfs_err;
 
     struct btrfs_util_subvolume_iterator *iter;
-    btrfs_err = btrfs_util_subvolume_iter_create("/", 0, 0, &iter);
+    btrfs_err = btrfs_util_subvolume_iter_create(cstr(fsRoot), 0, 0, &iter);
     if (btrfs_err != 0) {
         return std::nullopt;
     }
@@ -52,7 +57,7 @@ std::optional<QString> BtrfsSnapshots::getPathForSubvolume(qulonglong subvolume)
     char *iter_path;
     while ((btrfs_err = btrfs_util_subvolume_iter_next_info(iter, &iter_path, &iter_info)) == 0) {
         if (subvolume == static_cast<qulonglong>(iter_info.id)) {
-            QString path = "/"_L1 + QString::fromUtf8(iter_path);
+            QString path = QDir::cleanPath(fsRoot + "/"_L1 + QString::fromUtf8(iter_path));
             free(iter_path);
             return path;
         }
@@ -62,14 +67,19 @@ std::optional<QString> BtrfsSnapshots::getPathForSubvolume(qulonglong subvolume)
     return std::nullopt;
 }
 
-QList<BtrfsSnapshots::FileSnapshot> BtrfsSnapshots::getSnapshotsForFile(const QString &path)
+QList<BtrfsSnapshots::FileSnapshot> BtrfsSnapshots::getSnapshotsForFile(const QString &path, const QString &fsRoot)
 {
     QList<FileSnapshot> fileSnapshots;
-
-    QDir subvolumeRoot(QFileInfo(path).absoluteDir());
-    struct btrfs_util_subvolume_info info;
+    QDir subvolumeRoot;
+    QFileInfo fileInfo(path);
+    if (fileInfo.isDir()) {
+        subvolumeRoot = QDir(path);
+    } else {
+        QDir subvolumeRoot(QFileInfo(path).absoluteDir());
+    }
+    struct btrfs_util_subvolume_info subvolume_root_info;
     enum btrfs_util_error btrfs_err;
-    while (!subvolumeRoot.isRoot() && (btrfs_err = btrfs_util_subvolume_get_info(subvolumeRoot.absolutePath().toLocal8Bit().constData(), 0, &info)) != 0) {
+    while (!subvolumeRoot.isRoot() && (btrfs_err = btrfs_util_subvolume_get_info(cstr(subvolumeRoot.absolutePath()), 0, &subvolume_root_info)) != 0) {
         subvolumeRoot.cdUp();
     }
 
@@ -80,7 +90,7 @@ QList<BtrfsSnapshots::FileSnapshot> BtrfsSnapshots::getSnapshotsForFile(const QS
     QString pathRel = subvolumeRoot.relativeFilePath(path);
 
     struct btrfs_util_subvolume_iterator *iter;
-    btrfs_err = btrfs_util_subvolume_iter_create(subvolumeRoot.absolutePath().toLocal8Bit().constData(), 0, 0, &iter);
+    btrfs_err = btrfs_util_subvolume_iter_create(cstr(fsRoot), 0, 0, &iter);
     if (btrfs_err != 0) {
         return fileSnapshots;
     }
@@ -88,11 +98,11 @@ QList<BtrfsSnapshots::FileSnapshot> BtrfsSnapshots::getSnapshotsForFile(const QS
     struct btrfs_util_subvolume_info iter_info;
     char *iter_path;
     while ((btrfs_err = btrfs_util_subvolume_iter_next_info(iter, &iter_path, &iter_info)) == 0) {
-        if (QByteArrayView::fromArray(iter_info.parent_uuid) == QByteArrayView::fromArray(info.uuid)) {
-            QString snapshotSubvolPath = QDir::cleanPath(subvolumeRoot.absolutePath() + "/"_L1 + QString::fromUtf8(iter_path));
+        if (QByteArrayView::fromArray(iter_info.parent_uuid) == QByteArrayView::fromArray(subvolume_root_info.uuid)) {
+            QString snapshotSubvolPath = QDir::cleanPath(fsRoot + "/"_L1 + QString::fromUtf8(iter_path));
             QString filePath = QDir(snapshotSubvolPath).absoluteFilePath(pathRel);
             QFileInfo file(filePath);
-            if (file.exists() && file.isFile() && file.isReadable()) {
+            if (file.exists() && file.isReadable()) {
                 FileSnapshot snapshotInfo;
                 snapshotInfo.path = QDir(snapshotSubvolPath).absoluteFilePath(pathRel);
                 snapshotInfo.snapshotted = QDateTime::fromMSecsSinceEpoch(iter_info.otime.tv_sec * 1000 + iter_info.otime.tv_nsec / 1000000);
@@ -107,20 +117,20 @@ QList<BtrfsSnapshots::FileSnapshot> BtrfsSnapshots::getSnapshotsForFile(const QS
     return fileSnapshots;
 }
 
-QList<BtrfsSnapshots::SubvolumeSnapshot> BtrfsSnapshots::getSnapshotsForSubvolume(const QString &path)
+QList<BtrfsSnapshots::SubvolumeSnapshot> BtrfsSnapshots::getSnapshotsForSubvolume(const QString &path, const QString &fsRoot)
 {
     QList<SubvolumeSnapshot> subvolSnapshots;
 
     struct btrfs_util_subvolume_info info;
     enum btrfs_util_error btrfs_err;
 
-    btrfs_err = btrfs_util_subvolume_get_info(path.toLocal8Bit().constData(), 0, &info);
+    btrfs_err = btrfs_util_subvolume_get_info(cstr(path), 0, &info);
     if (btrfs_err != 0) {
         return subvolSnapshots;
     }
 
     struct btrfs_util_subvolume_iterator *iter;
-    btrfs_err = btrfs_util_subvolume_iter_create(path.toLocal8Bit().constData(), 0, 0, &iter);
+    btrfs_err = btrfs_util_subvolume_iter_create(cstr(fsRoot), 0, 0, &iter);
     if (btrfs_err != 0) {
         return subvolSnapshots;
     }
@@ -130,7 +140,7 @@ QList<BtrfsSnapshots::SubvolumeSnapshot> BtrfsSnapshots::getSnapshotsForSubvolum
     while ((btrfs_err = btrfs_util_subvolume_iter_next_info(iter, &iter_path, &iter_info)) == 0) {
         if (QByteArrayView::fromArray(iter_info.parent_uuid) == QByteArrayView::fromArray(info.uuid)) {
             SubvolumeSnapshot snapshotInfo;
-            snapshotInfo.path = QString::fromUtf8(iter_path);
+            snapshotInfo.path = QDir::cleanPath(fsRoot + "/"_L1 + QString::fromUtf8(iter_path));
             snapshotInfo.subvolumeId = static_cast<qulonglong>(iter_info.id);
             snapshotInfo.snapshotted = QDateTime::fromMSecsSinceEpoch(iter_info.otime.tv_sec * 1000 + iter_info.otime.tv_nsec / 1000000);
             subvolSnapshots << snapshotInfo;
@@ -141,14 +151,14 @@ QList<BtrfsSnapshots::SubvolumeSnapshot> BtrfsSnapshots::getSnapshotsForSubvolum
     return subvolSnapshots;
 }
 
-QMap<qulonglong, QString> BtrfsSnapshots::getNonSnapshotSubvolumes()
+QMap<qulonglong, QString> BtrfsSnapshots::getNonSnapshotSubvolumes(const QString &fsRoot)
 {
     QMap<qulonglong, QString> subvolumes;
 
     enum btrfs_util_error btrfs_err;
 
     struct btrfs_util_subvolume_iterator *iter;
-    btrfs_err = btrfs_util_subvolume_iter_create("/", 0, 0, &iter);
+    btrfs_err = btrfs_util_subvolume_iter_create(cstr(fsRoot), 0, 0, &iter);
     if (btrfs_err != 0) {
         return subvolumes;
     }
@@ -157,7 +167,7 @@ QMap<qulonglong, QString> BtrfsSnapshots::getNonSnapshotSubvolumes()
     char *iter_path;
     while ((btrfs_err = btrfs_util_subvolume_iter_next_info(iter, &iter_path, &iter_info)) == 0) {
         if (QUuid::fromBytes(iter_info.parent_uuid).isNull()) {
-            subvolumes[static_cast<qulonglong>(iter_info.id)] = "/"_L1 + QString::fromUtf8(iter_path);
+            subvolumes[static_cast<qulonglong>(iter_info.id)] = QDir::cleanPath(fsRoot + "/"_L1 + QString::fromUtf8(iter_path));
         }
         free(iter_path);
     }
diff --git a/common/btrfssnapshots.h b/common/btrfssnapshots.h
index 52caf09..7ea4500 100644
--- a/common/btrfssnapshots.h
+++ b/common/btrfssnapshots.h
@@ -12,6 +12,8 @@
 #ifndef BTRFSSNAPSHOTS_H
 #define BTRFSSNAPSHOTS_H
 
+using namespace Qt::StringLiterals;
+
 namespace BtrfsSnapshots
 {
 class FileSnapshot
@@ -31,11 +33,11 @@ public:
     QDateTime snapshotted;
 };
 
-std::optional<qulonglong> getSubvolumeForPath(const QString &path);
-std::optional<QString> getPathForSubvolume(qulonglong subvolume);
-QList<SubvolumeSnapshot> getSnapshotsForSubvolume(const QString &path);
-QList<FileSnapshot> getSnapshotsForFile(const QString &path);
-QMap<qulonglong, QString> getNonSnapshotSubvolumes();
+std::optional<qulonglong> getSubvolumeForPath(const QString &path, const QString &fsRoot = "/"_L1);
+std::optional<QString> getPathForSubvolume(qulonglong subvolume, const QString &fsRoot = "/"_L1);
+QList<SubvolumeSnapshot> getSnapshotsForSubvolume(const QString &path, const QString &fsRoot = "/"_L1);
+QList<FileSnapshot> getSnapshotsForFile(const QString &path, const QString &fsRoot = "/"_L1);
+QMap<qulonglong, QString> getNonSnapshotSubvolumes(const QString &fsRoot = "/"_L1);
 }
 
 #endif
diff --git a/contextmenu/CMakeLists.txt b/contextmenu/CMakeLists.txt
index 6944522..0d4a1ca 100644
--- a/contextmenu/CMakeLists.txt
+++ b/contextmenu/CMakeLists.txt
@@ -21,4 +21,4 @@ ecm_qt_declare_logging_category(snapshotfileitemaction
     DESCRIPTION "Snapshot file item action"
 )
 
-target_link_libraries(snapshotfileitemaction KF6::KIOCore KF6::KIOWidgets KF6::I18n btrfssnapshots_lib)
+target_link_libraries(snapshotfileitemaction KF6::KIOCore KF6::KIOWidgets KF6::I18n KF6::Solid btrfssnapshots_lib)
diff --git a/contextmenu/snapshotfileitemaction.cpp b/contextmenu/snapshotfileitemaction.cpp
index 91e10ea..c834638 100644
--- a/contextmenu/snapshotfileitemaction.cpp
+++ b/contextmenu/snapshotfileitemaction.cpp
@@ -12,6 +12,9 @@
 #include <KIO/JobUiDelegate>
 #include <KIO/OpenUrlJob>
 
+#include <Solid/Device>
+#include <Solid/StorageAccess>
+
 #include <KFileItem>
 #include <KLocalizedString>
 #include <KPluginFactory>
@@ -47,7 +50,14 @@ QList<QAction *> SnapshotFileItemAction::actions(const KFileItemListProperties &
     }
 
     if (item.isDir()) {
-        if (!BtrfsSnapshots::getSnapshotsForSubvolume(itemUrl.toLocalFile()).empty()) {
+        QString localPath = itemUrl.toLocalFile();
+        auto fsRoot = Solid::Device::storageAccessFromPath(localPath).as<Solid::StorageAccess>();
+        if (!fsRoot) {
+            qCCritical(SNAPSHOT_FILEITEMACTION()) << "could not determine fs root path for" << localPath;
+            return actions;
+        }
+        QString fsRootPath = fsRoot->filePath();
+        if (!BtrfsSnapshots::getSnapshotsForSubvolume(itemUrl.toLocalFile(), fsRootPath).empty()) {
             auto subvolumeIdOpt = BtrfsSnapshots::getSubvolumeForPath(itemUrl.toLocalFile());
             if (!subvolumeIdOpt.has_value()) {
                 qCCritical(SNAPSHOT_FILEITEMACTION()) << "found snapshots for dir" << itemUrl.toLocalFile() << "but it did not have a subvolume id";
@@ -62,7 +72,14 @@ QList<QAction *> SnapshotFileItemAction::actions(const KFileItemListProperties &
             actions << action;
         }
     } else if (item.isLocalFile()) {
-        if (!BtrfsSnapshots::getSnapshotsForFile(itemUrl.toLocalFile()).empty()) {
+        QString localPath = itemUrl.toLocalFile();
+        auto fsRoot = Solid::Device::storageAccessFromPath(localPath).as<Solid::StorageAccess>();
+        if (!fsRoot) {
+            qCCritical(SNAPSHOT_FILEITEMACTION()) << "could not determine fs root path for" << localPath;
+            return actions;
+        }
+        QString fsRootPath = fsRoot->filePath();
+        if (!BtrfsSnapshots::getSnapshotsForFile(itemUrl.toLocalFile(), fsRootPath).empty()) {
             QAction *action = new QAction(QIcon::fromTheme("view-history"_L1), i18nc("@action:inmenu", "View snapshots…"), parentWidget);
             connect(action, &QAction::triggered, this, [this, item]() {
                 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl("filesnapshots://%1"_L1.arg(item.localPath())), "inode/directory"_L1, this);
diff --git a/kioworker/CMakeLists.txt b/kioworker/CMakeLists.txt
index 663b7c0..78f2907 100644
--- a/kioworker/CMakeLists.txt
+++ b/kioworker/CMakeLists.txt
@@ -43,5 +43,5 @@ ecm_qt_declare_logging_category(kio_filesnapshots
     EXPORT KIO
 )
 
-target_link_libraries(kio_snapshot KF6::KIOCore KF6::I18n btrfssnapshots_lib)
-target_link_libraries(kio_filesnapshots KF6::KIOCore KF6::I18n btrfssnapshots_lib)
+target_link_libraries(kio_snapshot KF6::KIOCore KF6::I18n KF6::Solid btrfssnapshots_lib)
+target_link_libraries(kio_filesnapshots KF6::KIOCore KF6::I18n KF6::Solid btrfssnapshots_lib)
diff --git a/kioworker/filesnapshots.cpp b/kioworker/filesnapshots.cpp
index 1ec5fdd..9f034bc 100644
--- a/kioworker/filesnapshots.cpp
+++ b/kioworker/filesnapshots.cpp
@@ -14,6 +14,9 @@
 #include <KIO/UDSEntry>
 #include <KIO/WorkerBase>
 
+#include <Solid/Device>
+#include <Solid/StorageAccess>
+
 #include <KLocalizedString>
 
 #include <QCoreApplication>
@@ -58,7 +61,15 @@ KIO::WorkerResult FileSnapshotsProtocol::listDir(const QUrl &url)
 {
     qCDebug(KIO_FILESNAPSHOTS) << "url" << url << "url.host" << url.host() << "subvolume" << url.path();
 
-    QList<BtrfsSnapshots::FileSnapshot> snapshots = BtrfsSnapshots::getSnapshotsForFile(url.path());
+    QString localPath = url.path();
+    auto fsRoot = Solid::Device::storageAccessFromPath(localPath).as<Solid::StorageAccess>();
+    if (!fsRoot) {
+        qCCritical(KIO_FILESNAPSHOTS) << "could not determine fs root path for" << localPath;
+        return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST);
+    }
+    QString fsRootPath = fsRoot->filePath();
+
+    QList<BtrfsSnapshots::FileSnapshot> snapshots = BtrfsSnapshots::getSnapshotsForFile(url.path(), fsRootPath);
 
     std::sort(snapshots.begin(), snapshots.end(), [](const BtrfsSnapshots::FileSnapshot &a, const BtrfsSnapshots::FileSnapshot &b) {
         return a.snapshotted.toSecsSinceEpoch() > b.snapshotted.toSecsSinceEpoch();
@@ -67,14 +78,15 @@ KIO::WorkerResult FileSnapshotsProtocol::listDir(const QUrl &url)
     QFileInfo currentInfo(url.path());
     BtrfsSnapshots::FileSnapshot current;
     current.path = url.path();
-    current.snapshotted = currentInfo.lastModified();
+    current.snapshotted = QDateTime::currentDateTime();
     current.modified = currentInfo.lastModified();
+    current.subvolumeId = 0;
     snapshots.insert(0, current);
 
     QList<BtrfsSnapshots::FileSnapshot> snapshotsFiltered;
     for (qsizetype i = 0; i < snapshots.size(); i++) {
         const BtrfsSnapshots::FileSnapshot &info = snapshots.at(i);
-        if (i == 0 || snapshots.at(i - 1).modified != info.modified || snapshots.at(i - 1).modified != info.modified) {
+        if (true || i == 0 || snapshots.at(i - 1).modified != info.modified || snapshots.at(i - 1).modified != info.modified) {
             snapshotsFiltered << info;
         }
     }
@@ -93,7 +105,7 @@ KIO::WorkerResult FileSnapshotsProtocol::listDir(const QUrl &url)
         } else {
             continue;
         }
-        entry.replace(KIO::UDSEntry::UDS_NAME, "%1-%2"_L1.arg(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString::number(snapshot.subvolumeId)));
+        entry.replace(KIO::UDSEntry::UDS_NAME, "snapshot-%1-%2"_L1.arg(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString::number(snapshot.subvolumeId)));
         entry.replace(KIO::UDSEntry::UDS_ACCESS, S_IRUSR);
         if (snapshot.path == url.path()) {
             dateRepr = i18nc("denoting the present / most-recent version of the file", "Current");
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.