File indexing completed on 2024-05-26 05:13:31

0001 /*
0002  * This file is part of the KDE Akonadi Search Project
0003  * SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  *
0007  */
0008 
0009 #include <xapian.h>
0010 
0011 #include "akonadi_search_pim_debug.h"
0012 #include "indexeditems.h"
0013 
0014 #include <Akonadi/ServerManager>
0015 #include <QDir>
0016 #include <QHash>
0017 #include <QStandardPaths>
0018 
0019 using namespace Akonadi::Search::PIM;
0020 
0021 class Akonadi::Search::PIM::IndexedItemsPrivate
0022 {
0023 public:
0024     IndexedItemsPrivate() = default;
0025 
0026     [[nodiscard]] QString dbPath(const QString &dbName) const;
0027     [[nodiscard]] QString emailIndexingPath() const;
0028     [[nodiscard]] QString collectionIndexingPath() const;
0029     [[nodiscard]] QString calendarIndexingPath() const;
0030     [[nodiscard]] QString akonotesIndexingPath() const;
0031     [[nodiscard]] QString emailContactsIndexingPath() const;
0032     [[nodiscard]] QString contactIndexingPath() const;
0033 
0034     mutable QHash<QString, QString> m_cachePath;
0035     QString m_overridePrefixPath;
0036     [[nodiscard]] qlonglong indexedItems(const qlonglong id);
0037     [[nodiscard]] qlonglong indexedItemsInDatabase(const std::string &term, const QString &dbPath) const;
0038     void findIndexedInDatabase(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId, const QString &dbPath);
0039     void findIndexed(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId);
0040 };
0041 
0042 QString IndexedItemsPrivate::dbPath(const QString &dbName) const
0043 {
0044     const QString cachedPath = m_cachePath.value(dbName);
0045     if (!cachedPath.isEmpty()) {
0046         return cachedPath;
0047     }
0048     if (!m_overridePrefixPath.isEmpty()) {
0049         const QString path = QStringLiteral("%1/%2/").arg(m_overridePrefixPath, dbName);
0050         m_cachePath.insert(dbName, path);
0051         return path;
0052     }
0053 
0054     // First look into the old location from Baloo times in ~/.local/share/baloo,
0055     // because we don't migrate the database files automatically.
0056     QString basePath;
0057     bool hasInstanceIdentifier = Akonadi::ServerManager::hasInstanceIdentifier();
0058     if (hasInstanceIdentifier) {
0059         basePath = QStringLiteral("baloo/instances/%1").arg(Akonadi::ServerManager::instanceIdentifier());
0060     } else {
0061         basePath = QStringLiteral("baloo");
0062     }
0063     QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/%1/%2/").arg(basePath, dbName);
0064     if (QDir(dbPath).exists()) {
0065         m_cachePath.insert(dbName, dbPath);
0066         return dbPath;
0067     }
0068 
0069     // If the database does not exist in old Baloo folders, than use the new
0070     // location in Akonadi's datadir in ~/.local/share/akonadi/search_db.
0071     if (hasInstanceIdentifier) {
0072         basePath = QStringLiteral("akonadi/instance/%1/search_db").arg(Akonadi::ServerManager::instanceIdentifier());
0073     } else {
0074         basePath = QStringLiteral("akonadi/search_db");
0075     }
0076     dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/%1/%2/").arg(basePath, dbName);
0077     QDir().mkpath(dbPath);
0078     m_cachePath.insert(dbName, dbPath);
0079     return dbPath;
0080 }
0081 
0082 QString IndexedItemsPrivate::emailIndexingPath() const
0083 {
0084     return dbPath(QStringLiteral("email"));
0085 }
0086 
0087 QString IndexedItemsPrivate::contactIndexingPath() const
0088 {
0089     return dbPath(QStringLiteral("contacts"));
0090 }
0091 
0092 QString IndexedItemsPrivate::emailContactsIndexingPath() const
0093 {
0094     return dbPath(QStringLiteral("emailContacts"));
0095 }
0096 
0097 QString IndexedItemsPrivate::akonotesIndexingPath() const
0098 {
0099     return dbPath(QStringLiteral("notes"));
0100 }
0101 
0102 QString IndexedItemsPrivate::calendarIndexingPath() const
0103 {
0104     return dbPath(QStringLiteral("calendars"));
0105 }
0106 
0107 QString IndexedItemsPrivate::collectionIndexingPath() const
0108 {
0109     return dbPath(QStringLiteral("collections"));
0110 }
0111 
0112 qlonglong IndexedItemsPrivate::indexedItemsInDatabase(const std::string &term, const QString &dbPath) const
0113 {
0114     Xapian::Database db;
0115     try {
0116         db = Xapian::Database(QFile::encodeName(dbPath).toStdString());
0117     } catch (const Xapian::DatabaseError &e) {
0118         qCCritical(AKONADI_SEARCH_PIM_LOG) << "Failed to open database" << dbPath << ":" << QString::fromStdString(e.get_msg());
0119         return 0;
0120     }
0121     return db.get_termfreq(term);
0122 }
0123 
0124 qlonglong IndexedItemsPrivate::indexedItems(const qlonglong id)
0125 {
0126     const std::string term = QStringLiteral("C%1").arg(id).toStdString();
0127     return indexedItemsInDatabase(term, emailIndexingPath()) + indexedItemsInDatabase(term, contactIndexingPath())
0128         + indexedItemsInDatabase(term, akonotesIndexingPath()) + indexedItemsInDatabase(term, calendarIndexingPath());
0129 }
0130 
0131 void IndexedItemsPrivate::findIndexedInDatabase(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId, const QString &dbPath)
0132 {
0133     Xapian::Database db;
0134     try {
0135         db = Xapian::Database(QFile::encodeName(dbPath).toStdString());
0136     } catch (const Xapian::DatabaseError &e) {
0137         qCCritical(AKONADI_SEARCH_PIM_LOG) << "Failed to open database" << dbPath << ":" << QString::fromStdString(e.get_msg());
0138         return;
0139     }
0140     const std::string term = QStringLiteral("C%1").arg(collectionId).toStdString();
0141     const Xapian::Query query(term);
0142     Xapian::Enquire enquire(db);
0143     enquire.set_query(query);
0144 
0145     auto getResults = [&enquire, &indexed]() {
0146         Xapian::MSet mset;
0147         mset = enquire.get_mset(0, UINT_MAX);
0148         Xapian::MSetIterator it = mset.begin();
0149         for (auto result : mset) {
0150             indexed << result;
0151         }
0152     };
0153 
0154     try {
0155         getResults();
0156     } catch (const Xapian::DatabaseModifiedError &e) {
0157         qCCritical(AKONADI_SEARCH_PIM_LOG) << "Failed to read database" << dbPath << ":" << QString::fromStdString(e.get_msg());
0158         qCCritical(AKONADI_SEARCH_PIM_LOG) << "Calling reopen() on database" << dbPath << "and trying again";
0159         if (db.reopen()) { // only try again once
0160             try {
0161                 getResults();
0162             } catch (const Xapian::DatabaseModifiedError &e) {
0163                 qCCritical(AKONADI_SEARCH_PIM_LOG) << "Failed to query database" << dbPath << "even after calling reopen()";
0164             }
0165         }
0166     } catch (const Xapian::DatabaseCorruptError &e) {
0167         qCCritical(AKONADI_SEARCH_PIM_LOG) << "Failed to query database" << dbPath << ":" << QString::fromStdString(e.get_msg());
0168     }
0169 }
0170 
0171 void IndexedItemsPrivate::findIndexed(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId)
0172 {
0173     findIndexedInDatabase(indexed, collectionId, emailIndexingPath());
0174     findIndexedInDatabase(indexed, collectionId, contactIndexingPath());
0175     findIndexedInDatabase(indexed, collectionId, akonotesIndexingPath());
0176     findIndexedInDatabase(indexed, collectionId, calendarIndexingPath());
0177 }
0178 
0179 IndexedItems::IndexedItems(QObject *parent)
0180     : QObject(parent)
0181     , d(new Akonadi::Search::PIM::IndexedItemsPrivate())
0182 {
0183 }
0184 
0185 IndexedItems::~IndexedItems() = default;
0186 
0187 void IndexedItems::setOverrideDbPrefixPath(const QString &path)
0188 {
0189     d->m_overridePrefixPath = path;
0190     d->m_cachePath.clear();
0191 }
0192 
0193 qlonglong IndexedItems::indexedItems(const qlonglong id)
0194 {
0195     return d->indexedItems(id);
0196 }
0197 
0198 void IndexedItems::findIndexedInDatabase(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId, const QString &dbPath)
0199 {
0200     d->findIndexedInDatabase(indexed, collectionId, dbPath);
0201 }
0202 
0203 void IndexedItems::findIndexed(QSet<Akonadi::Item::Id> &indexed, Akonadi::Collection::Id collectionId)
0204 {
0205     d->findIndexed(indexed, collectionId);
0206 }
0207 
0208 QString IndexedItems::emailIndexingPath() const
0209 {
0210     return d->emailIndexingPath();
0211 }
0212 
0213 QString IndexedItems::collectionIndexingPath() const
0214 {
0215     return d->collectionIndexingPath();
0216 }
0217 
0218 QString IndexedItems::calendarIndexingPath() const
0219 {
0220     return d->calendarIndexingPath();
0221 }
0222 
0223 QString IndexedItems::akonotesIndexingPath() const
0224 {
0225     return d->akonotesIndexingPath();
0226 }
0227 
0228 QString IndexedItems::emailContactsIndexingPath() const
0229 {
0230     return d->emailContactsIndexingPath();
0231 }
0232 
0233 QString IndexedItems::contactIndexingPath() const
0234 {
0235     return d->contactIndexingPath();
0236 }
0237 
0238 #include "moc_indexeditems.cpp"