File indexing completed on 2024-06-16 05:01:12

0001 /*
0002  *   Copyright (C) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0003  *
0004  *   This program is free software; you can redistribute it and/or modify
0005  *   it under the terms of the GNU General Public License as published by
0006  *   the Free Software Foundation; either version 2 of the License, or
0007  *   (at your option) any later version.
0008  *
0009  *   This program is distributed in the hope that it will be useful,
0010  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *   GNU General Public License for more details.
0013  *
0014  *   You should have received a copy of the GNU General Public License
0015  *   along with this program; if not, write to the
0016  *   Free Software Foundation, Inc.,
0017  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
0018  */
0019 
0020 #include <QDebug>
0021 #include <QObject> // tr()
0022 #include <QFile>
0023 
0024 #include "common/resource.h"
0025 #include "common/storage.h"
0026 #include "common/resourceconfig.h"
0027 #include "common/log.h"
0028 #include "common/storage.h"
0029 #include "common/definitions.h"
0030 #include "common/entitybuffer.h"
0031 #include "common/metadata_generated.h"
0032 #include "common/bufferutils.h"
0033 #include "common/fulltextindex.h"
0034 
0035 #include "storage/key.h"
0036 
0037 #include "sinksh_utils.h"
0038 #include "state.h"
0039 #include "syntaxtree.h"
0040 
0041 namespace SinkInspect
0042 {
0043 
0044 using Sink::Storage::Key;
0045 using Sink::Storage::Identifier;
0046 using Sink::Storage::Revision;
0047 
0048 QString parse(const QByteArray &bytes)
0049 {
0050     if (Revision::isValidInternal(bytes)) {
0051         return Revision::fromInternalByteArray(bytes).toDisplayString();
0052     } else if (Key::isValidInternal(bytes)) {
0053         return Key::fromInternalByteArray(bytes).toDisplayString();
0054     } else if (Identifier::isValidInternal(bytes)) {
0055         return Identifier::fromInternalByteArray(bytes).toDisplayString();
0056     } else {
0057         return QString::fromUtf8(bytes);
0058     }
0059 }
0060 
0061 static QString operationName(int operation)
0062 {
0063     switch (operation) {
0064         case 1: return "Create";
0065         case 2: return "Modify";
0066         case 3: return "Delete";
0067     }
0068     return {};
0069 }
0070 
0071 Syntax::List syntax();
0072 
0073 bool inspect(QStringList args, State &state)
0074 {
0075     if (args.isEmpty()) {
0076         state.printError(syntax()[0].usage());
0077         return false;
0078     }
0079 
0080     const auto resource = SinkshUtils::parseUid(args.takeFirst().toLatin1());
0081     auto options = SyntaxTree::parseOptions(args);
0082 
0083     Sink::Storage::DataStore storage(Sink::storageLocation(), resource, Sink::Storage::DataStore::ReadOnly);
0084     auto transaction = storage.createTransaction(Sink::Storage::DataStore::ReadOnly);
0085 
0086     if (options.options.contains("validaterids")) {
0087         if (options.options.value("validaterids").isEmpty()) {
0088             state.printError(QObject::tr("Specify a type to validate."));
0089             return false;
0090         }
0091         auto type = options.options.value("validaterids").first().toUtf8();
0092         /*
0093          * Try to find all rid's for all uid's.
0094          * If we have entities without rid's that either means we have only created it locally or that we have a problem.
0095          */
0096         Sink::Storage::DataStore syncStore(Sink::storageLocation(), resource + ".synchronization", Sink::Storage::DataStore::ReadOnly);
0097         auto syncTransaction = syncStore.createTransaction(Sink::Storage::DataStore::ReadOnly);
0098 
0099         auto db = transaction.openDatabase(type + ".main",
0100                 [&] (const Sink::Storage::DataStore::Error &e) {
0101                     Q_ASSERT(false);
0102                     state.printError(e.message);
0103                 }, Sink::Storage::IntegerKeys);
0104 
0105         auto ridMap = syncTransaction.openDatabase("localid.mapping." + type,
0106                 [&] (const Sink::Storage::DataStore::Error &e) {
0107                     Q_ASSERT(false);
0108                     state.printError(e.message);
0109                 });
0110 
0111         QHash<QByteArray, QByteArray> hash;
0112 
0113         ridMap.scan("", [&] (const QByteArray &key, const QByteArray &data) {
0114                     hash.insert(key, data);
0115                     return true;
0116                 },
0117                 [&](const Sink::Storage::DataStore::Error &e) {
0118                     state.printError(e.message);
0119                 },
0120                 false);
0121 
0122         QSet<QByteArray> uids;
0123         db.scan("", [&] (const QByteArray &key, const QByteArray &data) {
0124                     size_t revision = Sink::byteArrayToSizeT(key);
0125                     uids.insert(Sink::Storage::DataStore::getUidFromRevision(transaction, revision).toDisplayByteArray());
0126                     return true;
0127                 },
0128                 [&](const Sink::Storage::DataStore::Error &e) {
0129                     state.printError(e.message);
0130                 },
0131                 false);
0132 
0133         int missing = 0;
0134         for (const auto &uid : uids) {
0135             if (!hash.remove(uid)) {
0136                 missing++;
0137                 qWarning() << "Failed to find RID for " << uid;
0138             }
0139         }
0140         if (missing) {
0141             qWarning() << "Found a total of " << missing << " missing rids";
0142         }
0143 
0144         //If we still have items in the hash it means we have rid mappings for entities
0145         //that no longer exist.
0146         if (!hash.isEmpty()) {
0147             qWarning() << "Have rids left: " << hash.size();
0148         } else if (!missing) {
0149             qWarning() << "Everything is in order.";
0150         }
0151 
0152         return false;
0153     }
0154     if (options.options.contains("fulltext")) {
0155         FulltextIndex index(resource, Sink::Storage::DataStore::ReadOnly);
0156         if (options.options.value("fulltext").isEmpty()) {
0157             state.printLine(QString("Total document count: ") + QString::number(index.getDoccount()));
0158         } else {
0159             const auto entityId = SinkshUtils::parseUid(options.options.value("fulltext").first().toUtf8());
0160             const auto content = index.getIndexContent(entityId);
0161             if (!content.found) {
0162                 state.printLine(QString("Failed to find the document with the id: ") + entityId);
0163             } else {
0164                 state.printLine(QString("Found document with terms: ") + content.terms.join(", "), 1);
0165             }
0166 
0167         }
0168         return false;
0169     }
0170 
0171     if (options.options.contains("history")) {
0172         FulltextIndex index(resource, Sink::Storage::DataStore::ReadOnly);
0173         if (options.options.value("history").isEmpty()) {
0174             state.printLine(QString("Provide an entity uid to search for."));
0175         } else {
0176             const auto entityId = SinkshUtils::parseUid(options.options.value("history").first().toUtf8());
0177 
0178             auto db = transaction.openDatabase("mail.main",
0179                 [&] (const Sink::Storage::DataStore::Error &e) {
0180                     Q_ASSERT(false);
0181                     state.printError(e.message);
0182                 });
0183             //Print a by revision history for the given uid.
0184             for (size_t revision : Sink::Storage::DataStore::getRevisionsFromUid(transaction, Sink::Storage::Identifier::fromDisplayByteArray(entityId))) {
0185                 db.scan(revision, [&] (const size_t &key, const QByteArray &data) {
0186                     Sink::EntityBuffer buffer(const_cast<const char *>(data.data()), data.size());
0187                     if (!buffer.isValid()) {
0188                         state.printError("Read invalid buffer from disk.");
0189                     } else {
0190                         const auto metadata = flatbuffers::GetRoot<Sink::Metadata>(buffer.metadataBuffer());
0191                         state.printLine(" Operation: " + operationName(metadata->operation())
0192                                         + " Replay: " + (metadata->replayToSource() ? "true" : "false")
0193                                         + ((metadata->modifiedProperties() && metadata->modifiedProperties()->size() != 0) ? (" [" + Sink::BufferUtils::fromVector(*metadata->modifiedProperties()).join(", ")) + "]": "")
0194                                         + " Value size: " + QString::number(data.size())
0195                                         );
0196                     }
0197                     return true;
0198                 },
0199                 [&](const Sink::Storage::DataStore::Error &e) {
0200                     state.printError(e.message);
0201                 });
0202             }
0203 
0204         }
0205         return false;
0206     }
0207 
0208     auto dbs = options.options.value("db");
0209     auto idFilter = options.options.value("filter");
0210 
0211     state.printLine(QString("Current revision: %1").arg(Sink::Storage::DataStore::maxRevision(transaction)));
0212     state.printLine(QString("Last clean revision: %1").arg(Sink::Storage::DataStore::cleanedUpRevision(transaction)));
0213 
0214     auto databases = transaction.getDatabaseNames();
0215     if (dbs.isEmpty()) {
0216         state.printLine("Available databases: ");
0217         for (const auto &db : databases) {
0218             state.printLine(db, 1);
0219         }
0220         return false;
0221     }
0222     auto dbName = dbs.value(0).toUtf8();
0223     auto isMainDb = dbName.contains(".main");
0224     if (!databases.contains(dbName)) {
0225         state.printError(QString("Database not available: ") + dbName);
0226     }
0227 
0228     state.printLine(QString("Opening: ") + dbName);
0229     auto db = transaction.openDatabase(dbName,
0230             [&] (const Sink::Storage::DataStore::Error &e) {
0231                 Q_ASSERT(false);
0232                 state.printError(e.message);
0233             });
0234 
0235     QByteArray filter;
0236     if (!idFilter.isEmpty()) {
0237         filter = idFilter.first().toUtf8();
0238     }
0239 
0240     //Print rest of db
0241     bool findSubstringKeys = !filter.isEmpty();
0242     int keySizeTotal = 0;
0243     int valueSizeTotal = 0;
0244     auto count = db.scan(filter, [&] (const QByteArray &key, const QByteArray &data) {
0245                 keySizeTotal += key.size();
0246                 valueSizeTotal += data.size();
0247 
0248                 const auto parsedKey = parse(key);
0249 
0250                 if (isMainDb) {
0251                     Sink::EntityBuffer buffer(const_cast<const char *>(data.data()), data.size());
0252                     if (!buffer.isValid()) {
0253                         state.printError("Read invalid buffer from disk: " + parsedKey);
0254                     } else {
0255                         const auto metadata = flatbuffers::GetRoot<Sink::Metadata>(buffer.metadataBuffer());
0256                         state.printLine("Key: " + parsedKey
0257                                         + " Operation: " + operationName(metadata->operation())
0258                                         + " Replay: " + (metadata->replayToSource() ? "true" : "false")
0259                                         + ((metadata->modifiedProperties() && metadata->modifiedProperties()->size() != 0) ? (" [" + Sink::BufferUtils::fromVector(*metadata->modifiedProperties()).join(", ")) + "]": "")
0260                                         + " Value size: " + QString::number(data.size())
0261                                         );
0262                     }
0263                 } else {
0264                     state.printLine("Key: " + parsedKey + "\tValue: " + parse(data));
0265                 }
0266                 return true;
0267             },
0268             [&](const Sink::Storage::DataStore::Error &e) {
0269                 state.printError(e.message);
0270             },
0271             findSubstringKeys);
0272 
0273     state.printLine("Found " + QString::number(count) + " entries");
0274     state.printLine("Keys take up " + QString::number(keySizeTotal) + " bytes => " + QString::number(keySizeTotal/1024) + " kb");
0275     state.printLine("Values take up " + QString::number(valueSizeTotal) + " bytes => " + QString::number(valueSizeTotal/1024) + " kb");
0276     return false;
0277 }
0278 
0279 Syntax::List syntax()
0280 {
0281     Syntax state("inspect", QObject::tr("Inspect database for the resource requested"),
0282         &SinkInspect::inspect, Syntax::NotInteractive);
0283 
0284     state.addPositionalArgument({"resource", "Resource to inspect."});
0285     state.addParameter("db", {"database", "Which database to inspect"});
0286     state.addParameter("filter", {"id", "A specific id to filter the results by (currently not working)"});
0287     state.addParameter("validaterids", {"type", "Validate remote Ids of the given type"});
0288     state.addParameter("fulltext", {"id", "If 'id' is not given, count the number of fulltext documents. Else, print the terms of the document with the given id"});
0289     state.addParameter("history", {"id", "Print all revisions for the entity with the given id"});
0290 
0291     state.completer = &SinkshUtils::resourceCompleter;
0292 
0293     return Syntax::List() << state;
0294 }
0295 
0296 REGISTER_SYNTAX(SinkInspect)
0297 
0298 }