File indexing completed on 2024-05-12 15:54:49

0001 /*
0002  * SPDX-FileCopyrightText: (C) 2014 Vishesh Handa <vhanda@kde.org>
0003  * SPDX-FileCopyrightText: (C) 2017 Atul Sharma <atulsharma406@gmail.com>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-or-later
0006  */
0007 
0008 #include "filesystemtracker.h"
0009 
0010 #include "filesystemimagefetcher.h"
0011 
0012 #include <QSqlDatabase>
0013 #include <QSqlError>
0014 #include <QSqlQuery>
0015 
0016 #include <QDBusConnection>
0017 
0018 #include <QDebug>
0019 #include <QDir>
0020 #include <QMimeDatabase>
0021 #include <QStandardPaths>
0022 
0023 #include <KDirNotify>
0024 #include <kdirwatch.h>
0025 
0026 FileSystemTracker::FileSystemTracker(QObject *parent)
0027     : QObject(parent)
0028 {
0029     QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &FileSystemTracker::setSubFolder);
0030 
0031     org::kde::KDirNotify *kdirnotify = new org::kde::KDirNotify(QString(), QString(), QDBusConnection::sessionBus(), this);
0032 
0033     connect(kdirnotify, &org::kde::KDirNotify::FilesRemoved, this, [this](const QStringList &files) {
0034         for (const QString &filePath : files) {
0035             removeFile(filePath);
0036         }
0037     });
0038     connect(kdirnotify, &org::kde::KDirNotify::FilesAdded, this, &FileSystemTracker::setSubFolder);
0039     connect(kdirnotify, &org::kde::KDirNotify::FileRenamedWithLocalPath, this, [this](const QString &src, const QString &dst, const QString &) {
0040         removeFile(src);
0041         slotNewFiles({dst});
0042     });
0043     connect(kdirnotify, &org::kde::KDirNotify::FilesChanged, this, [this](const QStringList &files) {
0044         for (const QString &filePath : files) {
0045             removeFile(filePath);
0046         }
0047         slotNewFiles(files);
0048     });
0049 
0050     connect(this, &FileSystemTracker::subFolderChanged, this, &FileSystemTracker::reindexSubFolder);
0051 }
0052 
0053 void FileSystemTracker::setupDb()
0054 {
0055     static QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/koko/";
0056     QDir().mkpath(dir);
0057 
0058     QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("fstracker"));
0059     db.setDatabaseName(dir + "/fstracker.sqlite3");
0060     if (!db.open()) {
0061         qWarning() << "Failed to open db" << db.lastError().text();
0062         return;
0063     }
0064 
0065     if (db.tables().contains("files")) {
0066         QSqlQuery query(db);
0067         query.prepare("PRAGMA table_info(files)");
0068         bool metadataChangeTime_present = false;
0069         if (!query.exec()) {
0070             qDebug() << "Failed to read db" << query.lastError();
0071             return;
0072         }
0073         while (query.next()) {
0074             if (query.value(1).toString() == "metadataChangeTime") {
0075                 metadataChangeTime_present = true;
0076             }
0077         }
0078         if (metadataChangeTime_present) {
0079             return;
0080         } else {
0081             // reindex everything
0082             qDebug() << "REINDEXING files";
0083             query.exec("DROP TABLE files");
0084         }
0085     }
0086 
0087     QSqlQuery query(db);
0088     bool ret =
0089         query.exec(QLatin1String("CREATE TABLE files("
0090                                  "id INTEGER PRIMARY KEY, "
0091                                  "metadataChangeTime STRING NOT NULL,"
0092                                  "url TEXT NOT NULL UNIQUE)"));
0093     if (!ret) {
0094         qWarning() << "Could not create files table" << query.lastError().text();
0095         return;
0096     }
0097 
0098     ret = query.exec(QLatin1String("CREATE INDEX fileUrl_index ON files (url)"));
0099     if (!ret) {
0100         qWarning() << "Could not create tags index" << query.lastError().text();
0101         return;
0102     }
0103 
0104     //
0105     // WAL Journaling mode has much lower io writes than the traditional journal
0106     // based indexing.
0107     //
0108     ret = query.exec(QLatin1String("PRAGMA journal_mode = WAL"));
0109     if (!ret) {
0110         qWarning() << "Could not set WAL journaling mode" << query.lastError().text();
0111         return;
0112     }
0113 }
0114 
0115 FileSystemTracker::~FileSystemTracker()
0116 {
0117     QSqlDatabase::removeDatabase(QStringLiteral("fstracker"));
0118 }
0119 
0120 void FileSystemTracker::slotImageResult(const QString &file)
0121 {
0122     QString filePath = file;
0123     filePath.replace("file://", "");
0124     QSqlQuery query(QSqlDatabase::database("fstracker"));
0125     query.prepare("SELECT id, metadataChangeTime from files where url = ?");
0126     query.addBindValue(filePath);
0127     if (!query.exec()) {
0128         qDebug() << query.lastError();
0129         return;
0130     }
0131 
0132     bool indexed = query.next();
0133 
0134     if (indexed && query.value(1).toString() != QFileInfo(filePath).metadataChangeTime().toString(Qt::ISODate)) {
0135         // reindex if metadata has changed
0136         removeFile(filePath);
0137         indexed = false;
0138         qDebug() << "REINDEXING" << filePath;
0139     }
0140 
0141     if (!indexed) {
0142         QSqlQuery query(QSqlDatabase::database("fstracker"));
0143         query.prepare("INSERT into files(url, metadataChangeTime) VALUES (?, ?)");
0144         query.addBindValue(filePath);
0145         query.addBindValue(QFileInfo(filePath).metadataChangeTime().toString(Qt::ISODate));
0146         if (!query.exec()) {
0147             qDebug() << "slotImageResult: " << query.lastError();
0148             return;
0149         }
0150         qDebug() << "ADDED" << filePath;
0151         emit imageAdded(filePath);
0152     }
0153 
0154     m_filePaths << filePath;
0155 }
0156 
0157 void FileSystemTracker::slotFetchFinished()
0158 {
0159     QSqlQuery query(QSqlDatabase::database("fstracker"));
0160     query.prepare("SELECT url from files");
0161     if (!query.exec()) {
0162         qDebug() << query.lastError();
0163         return;
0164     }
0165 
0166     while (query.next()) {
0167         QString filePath = query.value(0).toString();
0168 
0169         if (filePath.contains(m_subFolder) && !m_filePaths.contains(filePath)) {
0170             removeFile(filePath);
0171         }
0172     }
0173 
0174     QSqlDatabase::database("fstracker").commit();
0175 
0176     m_filePaths.clear();
0177     emit initialScanComplete();
0178 }
0179 
0180 void FileSystemTracker::removeFile(const QString &file)
0181 {
0182     QString filePath = file;
0183     filePath.replace("file://", "");
0184     qDebug() << "REMOVED" << filePath;
0185     emit imageRemoved(filePath);
0186     QSqlQuery query(QSqlDatabase::database("fstracker"));
0187     query.prepare("DELETE from files where url = ?");
0188     query.addBindValue(filePath);
0189     if (!query.exec()) {
0190         qWarning() << query.lastError();
0191     }
0192 }
0193 
0194 void FileSystemTracker::slotNewFiles(const QStringList &files)
0195 {
0196     if (!m_filePaths.isEmpty()) {
0197         // A scan is already going on. No point interrupting it.
0198         return;
0199     }
0200 
0201     QMimeDatabase db;
0202     for (const QString &file : files) {
0203         QMimeType mimetype = db.mimeTypeForFile(file);
0204         if (mimetype.name().startsWith("image/") || mimetype.name().startsWith("video/")) {
0205             slotImageResult(file);
0206         }
0207     }
0208 
0209     m_filePaths.clear();
0210 }
0211 
0212 void FileSystemTracker::setFolder(const QString &folder)
0213 {
0214     if (m_folder == folder) {
0215         return;
0216     }
0217 
0218     KDirWatch::self()->removeDir(m_folder);
0219     m_folder = folder;
0220     KDirWatch::self()->addDir(m_folder, KDirWatch::WatchSubDirs);
0221 }
0222 
0223 QString FileSystemTracker::folder() const
0224 {
0225     return m_folder;
0226 }
0227 
0228 void FileSystemTracker::setSubFolder(const QString &folder)
0229 {
0230     if (QFileInfo(folder).isDir()) {
0231         m_subFolder = folder;
0232         emit subFolderChanged();
0233     }
0234 }
0235 
0236 void FileSystemTracker::reindexSubFolder()
0237 {
0238     FileSystemImageFetcher *fetcher = new FileSystemImageFetcher(m_subFolder);
0239     connect(fetcher, &FileSystemImageFetcher::imageResult, this, &FileSystemTracker::slotImageResult, Qt::QueuedConnection);
0240     connect(
0241         fetcher,
0242         &FileSystemImageFetcher::finished,
0243         this,
0244         [this, fetcher] {
0245             slotFetchFinished();
0246             fetcher->deleteLater();
0247         },
0248         Qt::QueuedConnection);
0249 
0250     fetcher->fetch();
0251 
0252     QSqlDatabase::database("fstracker").transaction();
0253 }