7#include "storagejanitor.h"
8#include "agentmanagerinterface.h"
10#include "akonadiserver_debug.h"
13#include "resourcemanager.h"
14#include "search/searchmanager.h"
15#include "search/searchrequest.h"
16#include "storage/collectionstatistics.h"
17#include "storage/datastore.h"
18#include "storage/dbtype.h"
19#include "storage/query.h"
20#include "storage/selectquerybuilder.h"
21#include "storage/transaction.h"
23#include "private/dbus_p.h"
24#include "private/externalpartstorage_p.h"
25#include "private/standarddirs_p.h"
29#include <QDirIterator>
32#include <QStringBuilder>
36#include <qregularexpression.h>
39using namespace Akonadi::Server;
40using namespace AkRanges;
42class StorageJanitorDataStore :
public DataStore
45 StorageJanitorDataStore(AkonadiServer *server,
DbConfig *config)
52 : AkThread(QStringLiteral(
"StorageJanitor"),
QThread::IdlePriority)
53 , m_lostFoundCollectionId(-1)
55 , m_dbConfig(dbConfig)
60 : AkThread(QStringLiteral(
"StorageJanitor"), AkThread::NoThread)
61 , m_lostFoundCollectionId(-1)
63 , m_dbConfig(dbConfig)
65 StorageJanitor::init();
68StorageJanitor::~StorageJanitor()
73void StorageJanitor::init()
79 m_dataStore = std::make_unique<StorageJanitorDataStore>(m_akonadi, m_dbConfig);
84 conn.
registerObject(QStringLiteral(AKONADI_DBUS_STORAGEJANITOR_PATH),
89void StorageJanitor::quit()
103void StorageJanitor::registerTasks()
105 m_tasks = {{QStringLiteral(
"Looking for collections not belonging to a valid resource..."), &StorageJanitor::findOrphanedCollections},
106 {QStringLiteral(
"Checking collection tree consistency..."), &StorageJanitor::checkCollectionTreeConsistency},
107 {QStringLiteral(
"Looking for items not belonging to a valid collection..."), &StorageJanitor::findOrphanedItems},
108 {QStringLiteral(
"Looking for item parts not belonging to a valid item..."), &StorageJanitor::findOrphanedParts},
109 {QStringLiteral(
"Looking for item flags not belonging to a valid item..."), &StorageJanitor::findOrphanedPimItemFlags},
110 {QStringLiteral(
"Looking for duplicate item flags..."), &StorageJanitor::findDuplicateFlags},
111 {QStringLiteral(
"Looking for duplicate mime types..."), &StorageJanitor::findDuplicateMimeTypes},
112 {QStringLiteral(
"Looking for duplicate part types..."), &StorageJanitor::findDuplicatePartTypes},
113 {QStringLiteral(
"Looking for duplicate tag types..."), &StorageJanitor::findDuplicateTagTypes},
114 {QStringLiteral(
"Looking for overlapping external parts..."), &StorageJanitor::findOverlappingParts},
115 {QStringLiteral(
"Verifying external parts..."), &StorageJanitor::verifyExternalParts},
116 {QStringLiteral(
"Checking size threshold changes..."), &StorageJanitor::checkSizeTreshold},
117 {QStringLiteral(
"Looking for dirty objects..."), &StorageJanitor::findDirtyObjects},
118 {QStringLiteral(
"Looking for rid-duplicates not matching the content mime-type of the parent collection"), &StorageJanitor::findRIDDuplicates},
119 {QStringLiteral(
"Migrating parts to new cache hierarchy..."), &StorageJanitor::migrateToLevelledCacheHierarchy},
120 {QStringLiteral(
"Making sure virtual search resource and collections exist"), &StorageJanitor::ensureSearchCollection}};
124 m_tasks += {{QStringLiteral(
"Looking for resources in the DB not matching a configured resource..."), &StorageJanitor::findOrphanedResources},
125 {QStringLiteral(
"Checking search index consistency..."), &StorageJanitor::findOrphanSearchIndexEntries},
126 {QStringLiteral(
"Flushing collection statistics memory cache..."), &StorageJanitor::expireCollectionStatisticsCache}};
141 m_lostFoundCollectionId = -1;
143 for (
const auto &[idx, task] : m_tasks | Views::enumerate(1)) {
144 inform(QStringLiteral(
"%1/%2 %3").arg(idx, 2).arg(m_tasks.
size()).arg(task.name));
145 std::invoke(task.func,
this);
148 inform(
"Consistency check done.");
153qint64 StorageJanitor::lostAndFoundCollection()
155 if (m_lostFoundCollectionId > 0) {
156 return m_lostFoundCollectionId;
159 Transaction transaction(m_dataStore.get(), QStringLiteral(
"JANITOR LOST+FOUND"));
160 Resource lfRes = Resource::retrieveByName(m_dataStore.get(), QStringLiteral(
"akonadi_lost+found_resource"));
161 if (!lfRes.isValid()) {
162 lfRes.
setName(QStringLiteral(
"akonadi_lost+found_resource"));
163 if (!lfRes.insert(m_dataStore.get())) {
164 qCCritical(AKONADISERVER_LOG) <<
"Failed to create lost+found resource!";
170 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, lfRes.id());
171 qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is,
QVariant());
173 qCCritical(AKONADISERVER_LOG) <<
"Failed to query top level collections";
177 if (cols.
size() > 1) {
178 qCCritical(AKONADISERVER_LOG) <<
"More than one top-level lost+found collection!?";
179 }
else if (cols.
size() == 1) {
180 lfRoot = cols.
first();
182 lfRoot.
setName(QStringLiteral(
"lost+found"));
183 lfRoot.setResourceId(lfRes.id());
184 lfRoot.setCachePolicyLocalParts(QStringLiteral(
"ALL"));
185 lfRoot.setCachePolicyCacheTimeout(-1);
186 lfRoot.setCachePolicyInherit(
false);
187 if (!lfRoot.insert(m_dataStore.get())) {
188 qCCritical(AKONADISERVER_LOG) <<
"Failed to create lost+found root.";
191 m_dataStore->notificationCollector()->collectionAdded(lfRoot, lfRes.
name().
toUtf8());
197 lfCol.setResourceId(lfRes.id());
198 lfCol.setParentId(lfRoot.id());
199 if (!lfCol.insert(m_dataStore.get())) {
200 qCCritical(AKONADISERVER_LOG) <<
"Failed to create lost+found collection!";
203 const auto retrieveAll = MimeType::retrieveAll(m_dataStore.get());
204 for (
const MimeType &mt : retrieveAll) {
205 lfCol.addMimeType(m_dataStore.get(), mt);
209 m_dataStore->notificationCollector()->collectionAdded(lfCol, lfRes.
name().
toUtf8());
212 transaction.commit();
213 m_lostFoundCollectionId = lfCol.id();
214 return m_lostFoundCollectionId;
217void StorageJanitor::findOrphanedResources()
220 OrgFreedesktopAkonadiAgentManagerInterface iface(DBus::serviceName(DBus::Control), QStringLiteral(
"/AgentManager"),
QDBusConnection::sessionBus(),
this);
221 if (!iface.isValid()) {
222 inform(QStringLiteral(
"ERROR: Couldn't talk to %1").arg(DBus::Control));
225 const QStringList knownResources = iface.agentInstances();
226 if (knownResources.
isEmpty()) {
227 inform(QStringLiteral(
"ERROR: no known resources. This must be a mistake?"));
230 qbres.addValueCondition(Resource::nameFullColumnName(), Query::NotIn,
QVariant(knownResources));
231 qbres.addValueCondition(Resource::idFullColumnName(), Query::NotEquals, 1);
233 inform(
"Failed to query known resources, skipping test");
237 const Resource::List orphanResources = qbres.result();
238 const int orphanResourcesSize(orphanResources.size());
239 if (orphanResourcesSize > 0) {
241 resourceNames.
reserve(orphanResourcesSize);
242 for (
const Resource &resource : orphanResources) {
243 resourceNames.
append(resource.name());
245 inform(QStringLiteral(
"Found %1 orphan resources: %2").arg(orphanResourcesSize).arg(resourceNames.
join(
QLatin1Char(
','))));
246 for (
const QString &resourceName : std::as_const(resourceNames)) {
247 inform(QStringLiteral(
"Removing resource %1").arg(resourceName));
248 m_akonadi->resourceManager().removeResourceInstance(resourceName);
253void StorageJanitor::findOrphanedCollections()
256 qb.addJoin(
QueryBuilder::LeftJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName());
257 qb.addValueCondition(Resource::idFullColumnName(), Query::Is,
QVariant());
260 inform(
"Failed to query orphaned collections, skipping test");
270void StorageJanitor::checkCollectionTreeConsistency()
274 checkPathToRoot(col);
278void StorageJanitor::checkPathToRoot(
const Collection &col)
280 if (col.parentId() == 0) {
291 if (col.resourceId() !=
parent.resourceId()) {
300void StorageJanitor::findOrphanedItems()
303 qb.addJoin(
QueryBuilder::LeftJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName());
304 qb.addValueCondition(Collection::idFullColumnName(), Query::Is,
QVariant());
306 inform(
"Failed to query orphaned items, skipping test");
309 const PimItem::List orphans = qb.result();
310 if (!orphans.isEmpty()) {
313 Transaction transaction(m_dataStore.get(), QStringLiteral(
"JANITOR ORPHANS"));
314 QueryBuilder qb(m_dataStore.get(), PimItem::tableName(), QueryBuilder::Update);
315 qint64 col = lostAndFoundCollection();
319 qb.setColumnValue(PimItem::collectionIdColumn(), col);
320 qb.addValueCondition(PimItem::idFullColumnName(),
322 orphans | Views::transform([](
const auto &item) {
324 }) | Actions::toQList);
325 if (qb.exec() && transaction.commit()) {
329 + qb.query().lastError().text());
334void StorageJanitor::findOrphanedParts()
337 qb.addJoin(
QueryBuilder::LeftJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName());
338 qb.addValueCondition(PimItem::idFullColumnName(), Query::Is,
QVariant());
340 inform(
"Failed to query orphaned parts, skipping test");
343 const Part::List orphans = qb.result();
344 if (!orphans.isEmpty()) {
350void StorageJanitor::findOrphanedPimItemFlags()
352 QueryBuilder sqb(m_dataStore.get(), PimItemFlagRelation::tableName(), QueryBuilder::Select);
353 sqb.addColumn(PimItemFlagRelation::leftFullColumnName());
354 sqb.addJoin(
QueryBuilder::LeftJoin, PimItem::tableName(), PimItemFlagRelation::leftFullColumnName(), PimItem::idFullColumnName());
355 sqb.addValueCondition(PimItem::idFullColumnName(), Query::Is,
QVariant());
357 inform(
"Failed to query orphaned item flags, skipping test");
362 while (sqb.query().next()) {
363 ids.
append(sqb.query().value(0).toInt());
365 sqb.query().finish();
367 QueryBuilder qb(m_dataStore.get(), PimItemFlagRelation::tableName(), QueryBuilder::Delete);
368 qb.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, ids);
370 qCCritical(AKONADISERVER_LOG) <<
"Error:" << qb.query().lastError().text();
380 QString deduplEntityIdColumnName;
383template<
typename DeduplEntity>
384std::optional<int> findDuplicatesImpl(
DataStore *dataStore,
const QString &nameColumn,
const RelationDesc &relation)
386 QueryBuilder sqb(dataStore, DeduplEntity::tableName(), QueryBuilder::Select);
387 sqb.addColumns({DeduplEntity::idColumn(), nameColumn});
388 sqb.addSortColumn(DeduplEntity::idColumn());
394 while (sqb.query().next()) {
395 const auto id = sqb.query().value(0).toLongLong();
396 const auto name = sqb.query().value(1).toString();
399 if (it == duplicates.
end()) {
406 for (
const auto &[duplicateName, duplicateIds] : duplicates.
asKeyValueRange()) {
407 if (duplicateIds.size() <= 1) {
412 Transaction transaction(dataStore, QStringLiteral(
"StorageJanitor deduplicate %1 %2").arg(DeduplEntity::tableName(), duplicateName));
416 const auto firstId = duplicateIds.takeFirst();
418 QueryBuilder updateQb(dataStore, relation.tableName, QueryBuilder::Update);
419 updateQb.setColumnValue(relation.deduplEntityIdColumnName, firstId);
420 updateQb.addValueCondition(relation.deduplEntityIdColumnName, Query::In, duplicateIds);
421 if (!updateQb.exec()) {
426 QueryBuilder removeQb(dataStore, DeduplEntity::tableName(), QueryBuilder::Delete);
427 removeQb.addValueCondition(DeduplEntity::idColumn(), Query::In, duplicateIds);
428 if (!removeQb.exec()) {
434 transaction.commit();
440void StorageJanitor::findDuplicateFlags()
443 findDuplicatesImpl<Flag>(m_dataStore.get(), Flag::nameFullColumnName(), {PimItemFlagRelation::tableName(), PimItemFlagRelation::rightFullColumnName()});
445 inform(u
"Removed " %
QString::number(*removed) % u
" duplicate item flags");
447 inform(
"Error while trying to remove duplicate Flags");
451void StorageJanitor::findDuplicateMimeTypes()
454 findDuplicatesImpl<MimeType>(m_dataStore.get(), MimeType::nameFullColumnName(), {PimItem::tableName(), PimItem::mimeTypeIdFullColumnName()});
456 inform(u
"Removed " %
QString::number(*removed) % u
" duplicate mime types");
458 inform(
"Error while trying to remove duplicate MimeTypes");
462void StorageJanitor::findDuplicatePartTypes()
466 if (
DbType::type(m_dataStore->database()) == DbType::MySQL) {
467 nameColumn = QStringLiteral(
"CONCAT_WS(':', %1, %2) AS name");
469 nameColumn = QStringLiteral(
"(%1 || ':' || %2) AS name");
472 const auto removed = findDuplicatesImpl<PartType>(m_dataStore.get(),
473 nameColumn.
arg(PartType::nsFullColumnName(), PartType::nameFullColumnName()),
474 {Part::tableName(), Part::partTypeIdFullColumnName()});
476 inform(u
"Removed " %
QString::number(*removed) % u
" duplicate part types");
478 inform(
"Error while trying to remove duplicate PartTypes");
482void StorageJanitor::findDuplicateTagTypes()
484 const auto removed = findDuplicatesImpl<TagType>(m_dataStore.get(), TagType::nameFullColumnName(), {Tag::tableName(), Tag::typeIdFullColumnName()});
486 inform(u
"Removed " %
QString::number(*removed) % u
" duplicate tag types");
488 inform(
"Error while trying to remove duplicate TagTypes");
492void StorageJanitor::findOverlappingParts()
494 QueryBuilder qb(m_dataStore.get(), Part::tableName(), QueryBuilder::Select);
495 qb.addColumn(Part::dataColumn());
497 qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External);
498 qb.addValueCondition(Part::dataColumn(), Query::IsNot,
QVariant());
499 qb.addGroupColumn(Part::dataColumn());
502 inform(
"Failed to query overlapping parts, skipping test");
507 while (qb.query().next()) {
519void StorageJanitor::verifyExternalParts()
525 const QString dataDir = StandardDirs::saveDir(
"data", QStringLiteral(
"file_db_data"));
527 while (it.hasNext()) {
528 existingFiles.
insert(it.next());
535 QueryBuilder qb(m_dataStore.get(), Part::tableName(), QueryBuilder::Select);
536 qb.addColumn(Part::dataColumn());
537 qb.addColumn(Part::pimItemIdColumn());
538 qb.addColumn(Part::idColumn());
539 qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External);
540 qb.addValueCondition(Part::dataColumn(), Query::IsNot,
QVariant());
542 inform(
"Failed to query existing parts, skipping test");
545 while (qb.query().next()) {
546 const auto filename = qb.query().value(0).toByteArray();
547 const auto pimItemId = qb.query().value(1).value<Entity::Id>();
548 const auto partId = qb.query().value(2).value<Entity::Id>();
550 if (!filename.isEmpty()) {
551 partPath = ExternalPartStorage::resolveAbsolutePath(filename);
553 partPath = ExternalPartStorage::resolveAbsolutePath(ExternalPartStorage::nameForPartId(partId));
555 if (existingFiles.
contains(partPath)) {
556 usedFiles.
insert(partPath);
563 part.setPimItemId(pimItemId);
566 part.setStorage(Part::Internal);
567 part.update(m_dataStore.get());
574 const QSet<QString> unreferencedFiles = existingFiles - usedFiles;
575 if (!unreferencedFiles.
isEmpty()) {
576 const QString lfDir = StandardDirs::saveDir(
"data", QStringLiteral(
"file_lost+found"));
577 for (
const QString &file : unreferencedFiles) {
582 inform(QStringLiteral(
"Moved %1 unreferenced files to lost+found.").arg(unreferencedFiles.size()));
584 inform(
"Found no unreferenced external files.");
588void StorageJanitor::findDirtyObjects()
591 cqb.setSubQueryMode(Query::Or);
592 cqb.addValueCondition(Collection::remoteIdColumn(), Query::Is,
QVariant());
593 cqb.addValueCondition(Collection::remoteIdColumn(), Query::Equals,
QString());
595 inform(
"Failed to query collections without RID, skipping test");
606 iqb1.setSubQueryMode(Query::Or);
607 iqb1.addValueCondition(PimItem::remoteIdColumn(), Query::Is,
QVariant());
608 iqb1.addValueCondition(PimItem::remoteIdColumn(), Query::Equals,
QString());
610 inform(
"Failed to query items without RID, skipping test");
613 const PimItem::List ridLessItems = iqb1.result();
614 for (
const PimItem &item : ridLessItems) {
621 iqb2.addValueCondition(PimItem::dirtyColumn(), Query::Equals,
true);
622 iqb2.addValueCondition(PimItem::remoteIdColumn(), Query::IsNot,
QVariant());
623 iqb2.addSortColumn(PimItem::idFullColumnName());
625 inform(
"Failed to query dirty items, skipping test");
628 const PimItem::List dirtyItems = iqb2.result();
629 for (
const PimItem &item : dirtyItems) {
635void StorageJanitor::findRIDDuplicates()
637 QueryBuilder qb(m_dataStore.get(), Collection::tableName(), QueryBuilder::Select);
638 qb.addColumn(Collection::idColumn());
639 qb.addColumn(Collection::nameColumn());
642 while (qb.query().next()) {
644 const QString name = qb.query().value(1).toString();
645 inform(QStringLiteral(
"Checking ") + name);
647 QueryBuilder duplicates(m_dataStore.get(), PimItem::tableName(), QueryBuilder::Select);
648 duplicates.addColumn(PimItem::remoteIdColumn());
649 duplicates.addColumn(QStringLiteral(
"count(") + PimItem::idColumn() + QStringLiteral(
") as cnt"));
650 duplicates.addValueCondition(PimItem::remoteIdColumn(), Query::IsNot,
QVariant());
651 duplicates.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId);
652 duplicates.addGroupColumn(PimItem::remoteIdColumn());
656 Akonadi::Server::Collection col = Akonadi::Server::Collection::retrieveById(m_dataStore.get(), colId);
658 QVariantList contentMimeTypesVariantList;
659 contentMimeTypesVariantList.
reserve(contentMimeTypes.
count());
660 for (
const Akonadi::Server::MimeType &mimeType : contentMimeTypes) {
661 contentMimeTypesVariantList <<
mimeType.id();
663 while (duplicates.query().next()) {
664 const QString rid = duplicates.query().
value(0).toString();
667 condition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, rid);
668 condition.addValueCondition(PimItem::mimeTypeIdColumn(), Query::NotIn, contentMimeTypesVariantList);
669 condition.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId);
671 QueryBuilder items(m_dataStore.get(), PimItem::tableName(), QueryBuilder::Select);
672 items.addColumn(PimItem::idColumn());
673 items.addCondition(condition);
675 inform(QStringLiteral(
"Error while deleting duplicates: ") + items.query().lastError().text());
678 QVariantList itemsIds;
679 while (items.query().next()) {
680 itemsIds.push_back(items.query().value(0));
682 items.query().finish();
683 if (itemsIds.isEmpty()) {
689 inform(QStringLiteral(
"Found duplicates ") + rid);
692 parts.addValueCondition(Part::pimItemIdFullColumnName(), Query::In,
QVariant::fromValue(itemsIds));
693 parts.addValueCondition(Part::storageFullColumnName(), Query::Equals,
static_cast<int>(Part::External));
695 const auto partsList = parts.result();
696 for (
const auto &part : partsList) {
698 const auto filename = ExternalPartStorage::resolveAbsolutePath(part.data(), &exists);
705 items =
QueryBuilder(m_dataStore.get(), PimItem::tableName(), QueryBuilder::Delete);
706 items.addCondition(condition);
708 inform(QStringLiteral(
"Error while deleting duplicates ") + items.query().lastError().text());
711 duplicates.query().finish();
719 if (dbType == DbType::MySQL || dbType == DbType::PostgreSQL) {
720 inform(
"vacuuming database, that'll take some time and require a lot of temporary disk space...");
721 const auto tables = allDatabaseTables();
722 for (
const QString &table : tables) {
723 inform(QStringLiteral(
"optimizing table %1...").arg(table));
726 if (dbType == DbType::MySQL) {
728 }
else if (dbType == DbType::PostgreSQL) {
734 if (!q.
exec(queryStr)) {
735 qCCritical(AKONADISERVER_LOG) <<
"failed to optimize table" << table <<
":" << q.
lastError().
text();
738 inform(
"vacuum done");
740 inform(
"Vacuum not supported for this database backend. (Sqlite backend)");
746void StorageJanitor::checkSizeTreshold()
749 QueryBuilder qb(m_dataStore.get(), Part::tableName(), QueryBuilder::Select);
750 qb.addColumn(Part::idFullColumnName());
751 qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::Internal);
752 qb.addValueCondition(Part::datasizeFullColumnName(), Query::Greater, m_dbConfig->
sizeThreshold());
754 inform(
"Failed to query parts larger than threshold, skipping test");
758 auto &query = qb.query();
759 inform(QStringLiteral(
"Found %1 parts to be moved to external files").arg(query.
size()));
761 while (query.next()) {
762 Transaction transaction(m_dataStore.get(), QStringLiteral(
"JANITOR CHECK SIZE THRESHOLD"));
763 Part part = Part::retrieveById(m_dataStore.get(), query.
value(0).toLongLong());
764 const QByteArray name = ExternalPartStorage::nameForPartId(part.id());
765 const QString partPath = ExternalPartStorage::resolveAbsolutePath(name);
768 qCDebug(AKONADISERVER_LOG) <<
"External payload file" << name <<
"already exists";
773 qCCritical(AKONADISERVER_LOG) <<
"Failed to open file" << name <<
"for writing";
776 if (f.write(part.data()) != part.datasize()) {
777 qCCritical(AKONADISERVER_LOG) <<
"Failed to write data to payload file" <<
name;
783 part.setStorage(Part::External);
784 if (!part.update(m_dataStore.get()) || !transaction.commit()) {
785 qCCritical(AKONADISERVER_LOG) <<
"Failed to update database entry of part" << part.id();
790 inform(QStringLiteral(
"Moved part %1 from database into external file %2").arg(part.id()).arg(
QString::fromLatin1(name)));
795 QueryBuilder qb(m_dataStore.get(), Part::tableName(), QueryBuilder::Select);
796 qb.addColumn(Part::idFullColumnName());
797 qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External);
800 inform(
"Failed to query parts smaller than threshold, skipping test");
804 auto &
query = qb.query();
805 inform(QStringLiteral(
"Found %1 parts to be moved to database").arg(
query.
size()));
807 while (
query.next()) {
808 Transaction transaction(m_dataStore.get(), QStringLiteral(
"JANITOR CHECK SIZE THRESHOLD 2"));
809 Part part = Part::retrieveById(m_dataStore.get(),
query.
value(0).toLongLong());
810 const QString partPath = ExternalPartStorage::resolveAbsolutePath(part.data());
813 qCCritical(AKONADISERVER_LOG) <<
"Part file" << part.data() <<
"does not exist";
817 qCCritical(AKONADISERVER_LOG) <<
"Failed to open part file" << part.data() <<
"for reading";
821 part.setStorage(Part::Internal);
822 part.setData(f.readAll());
823 if (part.data().size() != part.datasize()) {
824 qCCritical(AKONADISERVER_LOG) <<
"Sizes of" << part.id() <<
"data don't match";
827 if (!part.update(m_dataStore.get()) || !transaction.commit()) {
828 qCCritical(AKONADISERVER_LOG) <<
"Failed to update database entry of part" << part.id();
834 inform(QStringLiteral(
"Moved part %1 from external file into database").arg(part.id()));
839void StorageJanitor::migrateToLevelledCacheHierarchy()
842 const QString db_data = StandardDirs::saveDir(
"data", QStringLiteral(
"file_db_data"));
844 if (entries.isEmpty()) {
845 inform(
"No external parts in legacy location, skipping migration");
849 QueryBuilder qb(m_dataStore.get(), Part::tableName(), QueryBuilder::Select);
850 qb.addColumn(Part::idColumn());
851 qb.addColumn(Part::dataColumn());
852 qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External);
854 inform(
"Failed to query external payload parts, skipping test");
858 auto &
query = qb.query();
859 while (
query.next()) {
860 const qint64
id =
query.
value(0).toLongLong();
863 bool oldExists =
false;
864 bool newExists =
false;
866 const QString currentPath = ExternalPartStorage::resolveAbsolutePath(fileName, &oldExists);
869 const QString newPath = ExternalPartStorage::resolveAbsolutePath(fileName, &newExists,
false);
871 qCCritical(AKONADISERVER_LOG) <<
"Old payload part does not exist, skipping part" << fileName;
874 if (currentPath != newPath) {
876 qCCritical(AKONADISERVER_LOG) <<
"Part is in legacy location, but the destination file already exists, skipping part" << fileName;
880 QFile f(currentPath);
881 if (!f.rename(newPath)) {
882 qCCritical(AKONADISERVER_LOG) <<
"Failed to move part from" << currentPath <<
" to " << newPath <<
":" << f.errorString();
885 inform(QStringLiteral(
"Migrated part %1 to new levelled cache").arg(
id));
890void StorageJanitor::findOrphanSearchIndexEntries()
892 QueryBuilder qb(m_dataStore.get(), Collection::tableName(), QueryBuilder::Select);
893 qb.addSortColumn(Collection::idColumn(), Query::Ascending);
894 qb.addColumn(Collection::idColumn());
895 qb.addColumn(Collection::isVirtualColumn());
897 inform(
"Failed to query collections, skipping test");
901 QDBusInterface iface(DBus::agentServiceName(QStringLiteral(
"akonadi_indexing_agent"), DBus::Agent),
903 QStringLiteral(
"org.freedesktop.Akonadi.Indexer"),
905 if (!iface.isValid()) {
906 inform(
"Akonadi Indexing Agent is not running, skipping test");
910 auto &
query = qb.query();
911 while (
query.next()) {
912 const qint64 colId =
query.
value(0).toLongLong();
915 inform(QStringLiteral(
"Skipping virtual Collection %1").arg(colId));
919 inform(QStringLiteral(
"Checking Collection %1 search index...").arg(colId));
920 SearchRequest req(
"StorageJanitor", m_akonadi->searchManager(), m_akonadi->agentSearchManager());
921 req.setStoreResults(
true);
922 req.setCollections({colId});
923 req.setRemoteSearch(
false);
924 req.setQuery(QStringLiteral(
"{ }"));
928 const auto colMts = col.mimeTypes();
929 if (colMts.isEmpty()) {
935 for (
const auto &mt : colMts) {
938 req.setMimeTypes(mts);
940 auto searchResults = req.results();
942 QueryBuilder iqb(m_dataStore.get(), PimItem::tableName(), QueryBuilder::Select);
943 iqb.addColumn(PimItem::idColumn());
944 iqb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId);
946 inform(QStringLiteral(
"Failed to query items in collection %1").arg(colId));
950 auto &itemQuery = iqb.query();
951 while (itemQuery.next()) {
952 searchResults.remove(itemQuery.value(0).toLongLong());
955 if (!searchResults.isEmpty()) {
956 inform(QStringLiteral(
"Collection %1 search index contains %2 orphan items. Scheduling reindexing").arg(colId).arg(searchResults.count()));
957 iface.call(
QDBus::NoBlock, QStringLiteral(
"reindexCollection"), colId);
962void StorageJanitor::ensureSearchCollection()
964 static const auto searchResourceName = QStringLiteral(
"akonadi_search_resource");
966 auto searchResource = Resource::retrieveByName(m_dataStore.get(), searchResourceName);
967 if (!searchResource.isValid()) {
968 searchResource.setName(searchResourceName);
969 searchResource.setIsVirtual(
true);
970 if (!searchResource.insert(m_dataStore.get())) {
971 inform(QStringLiteral(
"Failed to create Search resource."));
976 auto searchCols = Collection::retrieveFiltered(m_dataStore.get(), Collection::resourceIdColumn(), searchResource.id());
977 if (searchCols.isEmpty()) {
980 searchCol.
setName(QStringLiteral(
"Search"));
982 searchCol.setIndexPref(Collection::False);
983 searchCol.setIsVirtual(
true);
984 if (!searchCol.insert(m_dataStore.get())) {
985 inform(QStringLiteral(
"Failed to create Search Collection"));
991void StorageJanitor::expireCollectionStatisticsCache()
993 m_akonadi->collectionStatistics().expireCache();
996void StorageJanitor::inform(
const char *msg)
1001void StorageJanitor::inform(
const QString &msg)
1003 qCDebug(AKONADISERVER_LOG) << msg;
1007#include "moc_storagejanitor.cpp"
Represents a collection of PIM items.
qint64 Id
Describes the unique id type.
void setName(const QString &name)
Sets the i18n'ed name of the collection.
void setResource(const QString &identifier)
Sets the identifier of the resource owning the collection.
void setId(Id identifier)
Sets the unique identifier of the collection.
This class handles all the database access.
A base class that provides an unique access layer to configuration and initialization of different da...
virtual qint64 sizeThreshold() const
Payload data bigger than this value will be stored in separate files, instead of the database.
static DbConfig * configuredDatabase()
Returns the DbConfig instance for the database the user has configured.
Helper class to construct arbitrary SQL queries.
@ HavingCondition
add condition to HAVING part of the query NOTE: only supported for SELECT queries
@ LeftJoin
NOTE: only supported for SELECT queries.
Represents a WHERE condition tree.
Helper class for creating and executing database SELECT queries.
Q_SCRIPTABLE Q_NOREPLY void vacuum()
Triggers a vacuuming of the database, that is compacting of unused space.
StorageJanitor(AkonadiServer *mAkonadi, DbConfig *config=DbConfig::configuredDatabase())
Use AkThread::create() to create and start a new StorageJanitor thread.
Q_SCRIPTABLE Q_NOREPLY void check()
Triggers a consistency check of the internal storage.
Q_SCRIPTABLE void information(const QString &msg)
Sends informational messages to a possible UI for this.
Helper class for DataStore transaction handling.
void setName(QString value)
Type
Supported database types.
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Helper integration between Akonadi and Qt.
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KCALUTILS_EXPORT QString mimeType()
QString name(StandardAction id)
QDateTime currentDateTime()
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
bool registerService(const QString &serviceName)
QDBusConnection sessionBus()
void unregisterObject(const QString &path, UnregisterMode mode)
bool unregisterService(const QString &serviceName)
QFileInfoList entryInfoList(Filters filters, SortFlags sort) const const
bool rename(const QString &newName)
QString toString() const const
void append(QList< T > &&value)
qsizetype count() const const
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
T value(qsizetype i) const const
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
T value(const Key &key, const T &defaultValue) const const
const QObjectList & children() const const
QObject * parent() const const
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
bool remove(const T &value)
qsizetype size() const const
QString text() const const
QSqlError lastError() const const
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QByteArray toUtf8() const const
QString trimmed() const const
QString join(QChar separator) const const
QVariant fromValue(T &&value)