1 /*
2     This file is part of the KDE Baloo project.
3     SPDX-FileCopyrightText: 2015 Vishesh Handa <vhanda@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.1-or-later
6 */
7 
8 #include "statuscommand.h"
9 #include "indexerconfig.h"
10 
11 #include "global.h"
12 #include "database.h"
13 #include "transaction.h"
14 #include "idutils.h"
15 
16 #include "fileindexerinterface.h"
17 #include "schedulerinterface.h"
18 #include "maininterface.h"
19 #include "indexerstate.h"
20 
21 #include <KLocalizedString>
22 #include <KFormat>
23 
24 using namespace Baloo;
25 
command()26 QString StatusCommand::command()
27 {
28     return QStringLiteral("status");
29 }
30 
description()31 QString StatusCommand::description()
32 {
33     return i18n("Print the status of the Indexer");
34 }
35 
36 class FileIndexStatus
37 {
38 public:
39     enum class FileStatus : uint8_t {
40         NonExisting,
41         Directory,
42         RegularFile,
43         SymLink,
44         Other,
45     };
46     enum class IndexStateReason : uint8_t {
47         NoFileOrDirectory,
48         ExcludedByPath, // FIXME - be more specific, requires changes to shouldBeIndexed(path)
49         WaitingForIndexingBoth,
50         WaitingForBasicIndexing,
51         BasicIndexingDone,
52         WaitingForContentIndexing,
53         FailedToIndex,
54         Done,
55     };
56     const QString m_filePath;
57     FileStatus m_fileStatus;
58     IndexStateReason m_indexState;
59     uint32_t m_dataSize;
60 };
61 
collectFileStatus(Transaction & tr,IndexerConfig & cfg,const QString & file)62 FileIndexStatus collectFileStatus(Transaction& tr, IndexerConfig&  cfg, const QString& file)
63 {
64     using FileStatus = FileIndexStatus::FileStatus;
65     using IndexStateReason = FileIndexStatus::IndexStateReason;
66 
67     bool onlyBasicIndexing = cfg.onlyBasicIndexing();
68 
69     const QFileInfo fileInfo = QFileInfo(file);
70     const QString filePath = fileInfo.absoluteFilePath();
71     quint64 id = filePathToId(QFile::encodeName(filePath));
72     if (id == 0) {
73         return FileIndexStatus{filePath, FileStatus::NonExisting, IndexStateReason::NoFileOrDirectory, 0};
74     }
75 
76     FileStatus fileStatus = fileInfo.isSymLink() ? FileStatus::SymLink :
77                             fileInfo.isFile() ? FileStatus::RegularFile :
78                             fileInfo.isDir() ? FileStatus::Directory : FileStatus::Other;
79 
80     if (fileStatus == FileStatus::Other || fileStatus == FileStatus::SymLink) {
81         return FileIndexStatus{filePath, fileStatus, IndexStateReason::NoFileOrDirectory, 0};
82     }
83 
84     if (!cfg.shouldBeIndexed(filePath)) {
85         return FileIndexStatus{filePath, fileStatus, IndexStateReason::ExcludedByPath, 0};
86     }
87 
88     if (onlyBasicIndexing || fileStatus == FileStatus::Directory) {
89         if (!tr.hasDocument(id)) {
90             return FileIndexStatus{filePath, fileStatus, IndexStateReason::WaitingForBasicIndexing, 0};
91         } else {
92             return FileIndexStatus{filePath, fileStatus, IndexStateReason::BasicIndexingDone, 0};
93         }
94     }
95 
96     // File && shouldBeIndexed && contentIndexing
97     if (!tr.hasDocument(id)) {
98         return FileIndexStatus{filePath, fileStatus, IndexStateReason::WaitingForIndexingBoth, 0};
99     } else if (tr.inPhaseOne(id)) {
100         return FileIndexStatus{filePath, fileStatus, IndexStateReason::WaitingForContentIndexing, 0};
101     } else if (tr.hasFailed(id)) {
102         return FileIndexStatus{filePath, fileStatus, IndexStateReason::FailedToIndex, 0};
103     } else {
104         uint32_t size = tr.documentData(id).size();
105         return FileIndexStatus{filePath, fileStatus, IndexStateReason::Done, size};
106     }
107 }
108 
printMultiLine(Transaction & tr,IndexerConfig & cfg,const QStringList & args)109 void printMultiLine(Transaction& tr, IndexerConfig&  cfg, const QStringList& args) {
110     using FileStatus = FileIndexStatus::FileStatus;
111     using IndexStateReason = FileIndexStatus::IndexStateReason;
112 
113     QTextStream out(stdout);
114     QTextStream err(stderr);
115 
116     const QMap<IndexStateReason, QString> basicIndexStateValue = {
117         { IndexStateReason::NoFileOrDirectory,         i18n("File ignored") },
118         { IndexStateReason::ExcludedByPath,            i18n("Basic Indexing: Disabled") },
119         { IndexStateReason::WaitingForIndexingBoth,    i18n("Basic Indexing: Scheduled") },
120         { IndexStateReason::WaitingForBasicIndexing,   i18n("Basic Indexing: Scheduled") },
121         { IndexStateReason::BasicIndexingDone,         i18n("Basic Indexing: Done") },
122         { IndexStateReason::WaitingForContentIndexing, i18n("Basic Indexing: Done") },
123         { IndexStateReason::FailedToIndex,             i18n("Basic Indexing: Done") },
124         { IndexStateReason::Done,                      i18n("Basic Indexing: Done") },
125     };
126 
127     const QMap<IndexStateReason, QString> contentIndexStateValue = {
128         { IndexStateReason::NoFileOrDirectory,         QString() },
129         { IndexStateReason::ExcludedByPath,            QString() },
130         { IndexStateReason::WaitingForIndexingBoth,    i18n("Content Indexing: Scheduled") },
131         { IndexStateReason::WaitingForBasicIndexing,   QString() },
132         { IndexStateReason::BasicIndexingDone,         QString() },
133         { IndexStateReason::WaitingForContentIndexing, i18n("Content Indexing: Scheduled") },
134         { IndexStateReason::FailedToIndex,             i18n("Content Indexing: Failed") },
135         { IndexStateReason::Done,                      i18n("Content Indexing: Done") },
136     };
137 
138     for (const auto& fileName : args) {
139         const auto file = collectFileStatus(tr, cfg, fileName);
140 
141         if (file.m_fileStatus == FileStatus::NonExisting) {
142             err << i18n("Ignoring non-existent file %1", file.m_filePath) << '\n';
143             continue;
144         }
145 
146         if (file.m_fileStatus == FileStatus::SymLink || file.m_fileStatus == FileStatus::Other) {
147             err << i18n("Ignoring symlink/special file %1", file.m_filePath) << '\n';
148             continue;
149         }
150 
151         out << i18n("File: %1", file.m_filePath) << '\n';
152         out << basicIndexStateValue[file.m_indexState] << '\n';
153         const QString contentState = contentIndexStateValue[file.m_indexState];
154         if (!contentState.isEmpty()) {
155             out << contentState << '\n';
156         }
157     }
158 }
159 
printSimpleFormat(Transaction & tr,IndexerConfig & cfg,const QStringList & args)160 void printSimpleFormat(Transaction& tr, IndexerConfig&  cfg, const QStringList& args) {
161     using FileStatus = FileIndexStatus::FileStatus;
162     using IndexStateReason = FileIndexStatus::IndexStateReason;
163 
164     QTextStream out(stdout);
165     QTextStream err(stderr);
166 
167     const QMap<IndexStateReason, QString> simpleIndexStateValue = {
168         { IndexStateReason::NoFileOrDirectory,         QStringLiteral("No regular file or directory") },
169         { IndexStateReason::ExcludedByPath,            QStringLiteral("Indexing disabled") },
170         { IndexStateReason::WaitingForIndexingBoth,    QStringLiteral("Basic and Content indexing scheduled") },
171         { IndexStateReason::WaitingForBasicIndexing,   QStringLiteral("Basic indexing scheduled") },
172         { IndexStateReason::BasicIndexingDone,         QStringLiteral("Basic indexing done") },
173         { IndexStateReason::WaitingForContentIndexing, QStringLiteral("Content indexing scheduled") },
174         { IndexStateReason::FailedToIndex,             QStringLiteral("Content indexing failed") },
175         { IndexStateReason::Done,                      QStringLiteral("Content indexing done") },
176     };
177 
178     for (const auto& fileName : args) {
179         const auto file = collectFileStatus(tr, cfg, fileName);
180 
181         if (file.m_fileStatus == FileStatus::NonExisting) {
182             err << i18n("Ignoring non-existent file %1", file.m_filePath) << '\n';
183             continue;
184         }
185 
186         if (file.m_fileStatus == FileStatus::SymLink || file.m_fileStatus == FileStatus::Other) {
187             err << i18n("Ignoring symlink/special file %1", file.m_filePath) << '\n';
188             continue;
189         }
190 
191         out << simpleIndexStateValue[file.m_indexState];
192         out << ": " << file.m_filePath << '\n';
193     }
194 }
195 
printJSON(Transaction & tr,IndexerConfig & cfg,const QStringList & args)196 void printJSON(Transaction& tr, IndexerConfig&  cfg, const QStringList& args)
197 {
198 
199     using FileStatus = FileIndexStatus::FileStatus;
200     using IndexStateReason = FileIndexStatus::IndexStateReason;
201 
202     QJsonArray filesInfo;
203     QTextStream err(stderr);
204 
205     const QMap<IndexStateReason, QString> jsonIndexStateValue = {
206         { IndexStateReason::NoFileOrDirectory,         QStringLiteral("nofile") },
207         { IndexStateReason::ExcludedByPath,            QStringLiteral("disabled") },
208         { IndexStateReason::WaitingForIndexingBoth,    QStringLiteral("scheduled") },
209         { IndexStateReason::WaitingForBasicIndexing,   QStringLiteral("scheduled") },
210         { IndexStateReason::BasicIndexingDone,         QStringLiteral("done") },
211         { IndexStateReason::WaitingForContentIndexing, QStringLiteral("scheduled") },
212         { IndexStateReason::FailedToIndex,             QStringLiteral("failed") },
213         { IndexStateReason::Done,                      QStringLiteral("done") },
214     };
215 
216     const QMap<IndexStateReason, QString> jsonIndexLevelValue = {
217         { IndexStateReason::NoFileOrDirectory,         QStringLiteral("nofile") },
218         { IndexStateReason::ExcludedByPath,            QStringLiteral("none") },
219         { IndexStateReason::WaitingForIndexingBoth,    QStringLiteral("content") },
220         { IndexStateReason::WaitingForBasicIndexing,   QStringLiteral("basic") },
221         { IndexStateReason::BasicIndexingDone,         QStringLiteral("basic") },
222         { IndexStateReason::WaitingForContentIndexing, QStringLiteral("content") },
223         { IndexStateReason::FailedToIndex,             QStringLiteral("content") },
224         { IndexStateReason::Done,                      QStringLiteral("content") },
225     };
226 
227     for (const auto& fileName : args) {
228         const auto file = collectFileStatus(tr, cfg, fileName);
229 
230         if (file.m_fileStatus == FileStatus::NonExisting) {
231             err << i18n("Ignoring non-existent file %1", file.m_filePath) << '\n';
232             continue;
233         }
234 
235         if (file.m_fileStatus == FileStatus::SymLink || file.m_fileStatus == FileStatus::Other) {
236             err << i18n("Ignoring symlink/special file %1", file.m_filePath) << '\n';
237             continue;
238         }
239 
240         QJsonObject fileInfo;
241         fileInfo[QStringLiteral("file")] = file.m_filePath;
242         fileInfo[QStringLiteral("indexing")] = jsonIndexLevelValue[file.m_indexState];
243         fileInfo[QStringLiteral("status")] = jsonIndexStateValue[file.m_indexState];
244 
245         filesInfo.append(fileInfo);
246     }
247 
248     QJsonDocument json;
249     json.setArray(filesInfo);
250     QTextStream out(stdout);
251     out << json.toJson(QJsonDocument::Indented);
252 }
253 
exec(const QCommandLineParser & parser)254 int StatusCommand::exec(const QCommandLineParser& parser)
255 {
256     QTextStream out(stdout);
257     QTextStream err(stderr);
258 
259     const QStringList allowedFormats({QStringLiteral("simple"), QStringLiteral("json"), QStringLiteral("multiline")});
260     const QString format = parser.value(QStringLiteral("format"));
261 
262     if (!allowedFormats.contains(format)) {
263         err << i18n("Output format \"%1\" is invalid, use one of:\n", format);
264         for (const auto& format : allowedFormats) {
265             err << i18nc("bullet list item with output format", "- %1\n", format);
266         }
267         return 1;
268     }
269 
270     IndexerConfig cfg;
271     if (!cfg.fileIndexingEnabled()) {
272         err << i18n("Baloo is currently disabled. To enable, please run %1\n", QStringLiteral("balooctl enable"));
273         return 1;
274     }
275 
276     Database *db = globalDatabaseInstance();
277     if (!db->open(Database::ReadOnlyDatabase)) {
278         err << i18n("Baloo Index could not be opened\n");
279         return 1;
280     }
281 
282     Transaction tr(db, Transaction::ReadOnly);
283 
284     QStringList args = parser.positionalArguments();
285     args.pop_front();
286 
287     if (args.isEmpty()) {
288         org::kde::baloo::main mainInterface(QStringLiteral("org.kde.baloo"),
289                                                     QStringLiteral("/"),
290                                                     QDBusConnection::sessionBus());
291 
292         org::kde::baloo::scheduler schedulerinterface(QStringLiteral("org.kde.baloo"),
293                                             QStringLiteral("/scheduler"),
294                                             QDBusConnection::sessionBus());
295 
296         bool running = mainInterface.isValid();
297 
298         if (running) {
299             org::kde::baloo::fileindexer indexerInterface(QStringLiteral("org.kde.baloo"),
300             QStringLiteral("/fileindexer"),
301             QDBusConnection::sessionBus());
302 
303             const QString currentFile = indexerInterface.currentFile();
304 
305             out << i18n("Baloo File Indexer is running\n");
306             if (!currentFile.isEmpty()) {
307                 out << i18n("Indexer state: %1", stateString(IndexerState::ContentIndexing)) << '\n';
308                 out << i18nc("currently indexed file", "Indexing: %1", currentFile) << '\n';
309             } else {
310                 out << i18n("Indexer state: %1", stateString(schedulerinterface.state())) << '\n';
311             }
312         }
313         else {
314             out << i18n("Baloo File Indexer is not running\n");
315         }
316 
317         uint phaseOne = tr.phaseOneSize();
318         uint total = tr.size();
319         uint failed = tr.failedIds(100).size();
320 
321         out << i18n("Total files indexed: %1", total) << '\n';
322         out << i18n("Files waiting for content indexing: %1", phaseOne) << '\n';
323         out << i18n("Files failed to index: %1", failed) << '\n';
324 
325         const QString path = fileIndexDbPath();
326 
327         const QFileInfo indexInfo(path + QLatin1String("/index"));
328         const auto size = indexInfo.size();
329         KFormat format(QLocale::system());
330         if (size) {
331             out << i18n("Current size of index is %1", format.formatByteSize(size, 2)) << '\n';
332         } else {
333             out << i18n("Index does not exist yet\n");
334         }
335     } else if (format == allowedFormats[0]){
336         printSimpleFormat(tr, cfg, args);
337     } else if (format == allowedFormats[1]){
338         printJSON(tr, cfg, args);
339     } else {
340         printMultiLine(tr, cfg, args);
341     }
342 
343     return 0;
344 }
345