1 | /* |
2 | SPDX-FileCopyrightText: 2012-2013 Vishesh Handa <me@vhanda.in> |
3 | |
4 | SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL |
5 | */ |
6 | |
7 | #include <algorithm> |
8 | |
9 | #include <QCoreApplication> |
10 | #include <QCommandLineParser> |
11 | #include <QCommandLineOption> |
12 | #include <QDateTime> |
13 | #include <QFile> |
14 | #include <QTextStream> |
15 | |
16 | #include <KAboutData> |
17 | #include <KLocalizedString> |
18 | |
19 | #include <QJsonDocument> |
20 | #include <QJsonObject> |
21 | |
22 | #include "global.h" |
23 | #include "idutils.h" |
24 | #include "database.h" |
25 | #include "transaction.h" |
26 | |
27 | #include <unistd.h> |
28 | #include <KFileMetaData/PropertyInfo> |
29 | |
30 | QString colorString(const QString& input, int color) |
31 | { |
32 | static bool isTty = isatty(fd: fileno(stdout)); |
33 | if(isTty) { |
34 | QString colorStart = QStringLiteral("\033[0;%1m" ).arg(a: color); |
35 | QLatin1String colorEnd("\033[0;0m" ); |
36 | |
37 | return colorStart + input + colorEnd; |
38 | } else { |
39 | return input; |
40 | } |
41 | } |
42 | |
43 | inline KFileMetaData::PropertyMultiMap variantToPropertyMultiMap(const QVariantMap &varMap) |
44 | { |
45 | KFileMetaData::PropertyMultiMap propMap; |
46 | QVariantMap::const_iterator it = varMap.constBegin(); |
47 | for (; it != varMap.constEnd(); ++it) { |
48 | int p = it.key().toInt(); |
49 | propMap.insert(key: static_cast<KFileMetaData::Property::Property>(p), value: it.value()); |
50 | } |
51 | return propMap; |
52 | } |
53 | |
54 | int main(int argc, char* argv[]) |
55 | { |
56 | QCoreApplication app(argc, argv); |
57 | |
58 | KAboutData aboutData(QStringLiteral("balooshow" ), |
59 | i18n("Baloo Show" ), |
60 | QStringLiteral(PROJECT_VERSION), |
61 | i18n("The Baloo data Viewer - A debugging tool" ), |
62 | KAboutLicense::GPL, |
63 | i18n("(c) 2012, Vishesh Handa" )); |
64 | |
65 | KAboutData::setApplicationData(aboutData); |
66 | |
67 | QCommandLineParser parser; |
68 | parser.addPositionalArgument(QStringLiteral("files" ), i18n("Urls, document ids or inodes of the files" ), QStringLiteral("[file|id|inode...]" )); |
69 | parser.addOption(commandLineOption: QCommandLineOption(QStringList() << QStringLiteral("x" ), |
70 | i18n("Print internal info" ))); |
71 | parser.addOption(commandLineOption: QCommandLineOption(QStringList() << QStringLiteral("i" ), |
72 | i18n("Arguments are interpreted as inode numbers (requires -d)" ))); |
73 | parser.addOption(commandLineOption: QCommandLineOption(QStringList() << QStringLiteral("d" ), |
74 | i18n("Device id for the files" ), QStringLiteral("deviceId" ), QString())); |
75 | parser.addHelpOption(); |
76 | parser.process(app); |
77 | |
78 | const QStringList args = parser.positionalArguments(); |
79 | |
80 | if (args.isEmpty()) { |
81 | parser.showHelp(exitCode: 1); |
82 | } |
83 | |
84 | QTextStream stream(stdout); |
85 | |
86 | bool useInodes = parser.isSet(QStringLiteral("i" )); |
87 | quint32 devId; |
88 | if (useInodes) { |
89 | bool ok; |
90 | devId = parser.value(QStringLiteral("d" )).toULong(ok: &ok, base: 10); |
91 | if (!ok) { |
92 | devId = parser.value(QStringLiteral("d" )).toULong(ok: &ok, base: 16); |
93 | } |
94 | } |
95 | |
96 | if (useInodes && devId == 0) { |
97 | stream << i18n("Error: -i requires specifying a device (-d <deviceId>)" ) << '\n'; |
98 | parser.showHelp(exitCode: 1); |
99 | } |
100 | |
101 | Baloo::Database *db = Baloo::globalDatabaseInstance(); |
102 | if (!db->open(mode: Baloo::Database::ReadOnlyDatabase)) { |
103 | stream << i18n("The Baloo index could not be opened. Please run \"%1\" to see if Baloo is enabled and working." , |
104 | QStringLiteral("balooctl status" )) << '\n'; |
105 | return 1; |
106 | } |
107 | |
108 | Baloo::Transaction tr(db, Baloo::Transaction::ReadOnly); |
109 | |
110 | for (QString url : args) { |
111 | quint64 fid = 0; |
112 | QString internalUrl; |
113 | if (!useInodes) { |
114 | if (QFile::exists(fileName: url)) { |
115 | quint64 fsFid = Baloo::filePathToId(filePath: QFile::encodeName(fileName: url)); |
116 | fid = tr.documentId(path: QFile::encodeName(fileName: url)); |
117 | internalUrl = QFile::decodeName(localFileName: tr.documentUrl(id: fsFid)); |
118 | |
119 | if (fid && fid != fsFid) { |
120 | stream << i18n("The document IDs of the Baloo DB and the filesystem are different:" ) << '\n'; |
121 | auto dbInode = Baloo::idToInode(id: fid); |
122 | auto fsInode = Baloo::idToInode(id: fsFid); |
123 | auto dbDevId = Baloo::idToDeviceId(id: fid); |
124 | auto fsDevId = Baloo::idToDeviceId(id: fsFid); |
125 | |
126 | stream << "Url: " << url << "\n" ; |
127 | stream << "ID: " << fid << " (DB) <-> " << fsFid << " (FS)\n" ; |
128 | stream << "Inode: " << dbInode << " (DB) " << (dbInode == fsInode ? "== " : "<-> " ) << fsInode << " (FS)\n" ; |
129 | stream << "DeviceID: " << dbDevId << " (DB) " << (dbDevId == fsDevId ? "== " : "<-> " ) << fsDevId << " (FS)\n" ; |
130 | } |
131 | fid = fsFid; |
132 | } else { |
133 | bool ok; |
134 | fid = url.toULongLong(ok: &ok, base: 10); |
135 | if (!ok) { |
136 | fid = url.toULongLong(ok: &ok, base: 16); |
137 | } |
138 | if (!ok) { |
139 | stream << i18n("%1: Not a valid url or document id" , url) << '\n'; |
140 | continue; |
141 | } |
142 | url = QFile::decodeName(localFileName: tr.documentUrl(id: fid)); |
143 | internalUrl = url; |
144 | } |
145 | |
146 | } else { |
147 | bool ok; |
148 | quint32 inode = url.toULong(ok: &ok, base: 10); |
149 | if (!ok) { |
150 | inode = url.toULong(ok: &ok, base: 16); |
151 | } |
152 | if (!ok) { |
153 | stream << i18n("%1: Failed to parse inode number" , url) << '\n'; |
154 | continue; |
155 | } |
156 | |
157 | fid = Baloo::devIdAndInodeToId(devId, inode); |
158 | url = QFile::decodeName(localFileName: tr.documentUrl(id: fid)); |
159 | internalUrl = url; |
160 | } |
161 | |
162 | if (fid) { |
163 | stream << colorString(input: QString::number(fid, base: 16), color: 31) << ' '; |
164 | stream << colorString(input: QString::number(Baloo::idToDeviceId(id: fid)), color: 28) << ' '; |
165 | stream << colorString(input: QString::number(Baloo::idToInode(id: fid)), color: 28) << ' '; |
166 | } |
167 | if (fid && tr.hasDocument(id: fid)) { |
168 | stream << colorString(input: url, color: 32); |
169 | if (!internalUrl.isEmpty() && internalUrl != url) { |
170 | // The document is know by a different name inside the DB, |
171 | // e.g. a hardlink, or untracked rename |
172 | stream << QLatin1String(" [" ) << internalUrl << QLatin1Char(']'); |
173 | } |
174 | stream << '\n'; |
175 | } |
176 | else { |
177 | stream << i18n("%1: No index information found" , url) << '\n'; |
178 | continue; |
179 | } |
180 | |
181 | Baloo::DocumentTimeDB::TimeInfo time = tr.documentTimeInfo(id: fid); |
182 | stream << QStringLiteral("\tMtime: %1 " ).arg(a: time.mTime) |
183 | << QDateTime::fromSecsSinceEpoch(secs: time.mTime).toString(format: Qt::ISODate) |
184 | << QStringLiteral("\n\tCtime: %1 " ).arg(a: time.cTime) |
185 | << QDateTime::fromSecsSinceEpoch(secs: time.cTime).toString(format: Qt::ISODate) |
186 | << '\n'; |
187 | |
188 | const QJsonDocument jdoc = QJsonDocument::fromJson(json: tr.documentData(id: fid)); |
189 | const QVariantMap varMap = jdoc.object().toVariantMap(); |
190 | KFileMetaData::PropertyMultiMap propMap = variantToPropertyMultiMap(varMap); |
191 | if (!propMap.isEmpty()) { |
192 | stream << "\tCached properties:" << '\n'; |
193 | } |
194 | for (auto it = propMap.constBegin(); it != propMap.constEnd(); ++it) { |
195 | QString str; |
196 | if (it.value().typeId() == QMetaType::QVariantList) { |
197 | QStringList list; |
198 | const auto vars = it.value().toList(); |
199 | for (const QVariant& var : vars) { |
200 | list << var.toString(); |
201 | } |
202 | str = list.join(sep: QLatin1String(", " )); |
203 | } else { |
204 | str = it.value().toString(); |
205 | } |
206 | |
207 | KFileMetaData::PropertyInfo pi(it.key()); |
208 | stream << "\t\t" << pi.displayName() << ": " << str << '\n'; |
209 | } |
210 | |
211 | if (parser.isSet(QStringLiteral("x" ))) { |
212 | QVector<QByteArray> terms = tr.documentTerms(docId: fid); |
213 | QVector<QByteArray> fileNameTerms = tr.documentFileNameTerms(docId: fid); |
214 | QVector<QByteArray> xAttrTerms = tr.documentXattrTerms(docId: fid); |
215 | |
216 | auto join = [](const QVector<QByteArray>& v) { |
217 | QByteArray ba; |
218 | for (const QByteArray& arr : v) { |
219 | ba.append(a: arr); |
220 | ba.append(c: ' '); |
221 | } |
222 | return QString::fromUtf8(ba); |
223 | }; |
224 | |
225 | auto propertiesBegin = std::stable_partition(first: terms.begin(), last: terms.end(), |
226 | pred: [](const auto & t) { return t.isEmpty() || t[0] < 'A' || t[0] > 'Z'; }); |
227 | const QVector<QByteArray> propertyTerms{propertiesBegin, terms.end()}; |
228 | terms.erase(abegin: propertiesBegin, aend: terms.end()); |
229 | |
230 | stream << "\n" << i18n("Internal Info" ) << "\n" ; |
231 | stream << i18n("File Name Terms: %1" , join(fileNameTerms)) << "\n" ; |
232 | stream << i18n("%1 Terms: %2" , QStringLiteral("XAttr" ), join(xAttrTerms)) << '\n'; |
233 | stream << i18n("Plain Text Terms: %1" , join(terms)) << "\n" ; |
234 | stream << i18n("Property Terms: %1" , join(propertyTerms)) << "\n" ; |
235 | |
236 | QHash<int, QStringList> propertyWords; |
237 | KLocalizedString errorPrefix = ki18nc("Prefix string for internal errors" , "Internal Error - %1" ); |
238 | |
239 | for (const QByteArray& arr : propertyTerms) { |
240 | auto arrAsPrintable = [arr]() { |
241 | return QString::fromLatin1(ba: arr.toPercentEncoding()); |
242 | }; |
243 | |
244 | if (arr.length() < 1) { |
245 | auto error = QStringLiteral("malformed term (short): '%1'\n" ).arg(a: arrAsPrintable()); |
246 | stream << errorPrefix.subs(a: error).toString(); |
247 | continue; |
248 | } |
249 | |
250 | const QString word = QString::fromUtf8(ba: arr); |
251 | |
252 | if (word[0] == QLatin1Char('X')) { |
253 | if (word.length() < 4) { |
254 | // 'X<num>-<value> |
255 | auto error = QStringLiteral("malformed property term (short): '%1' in '%2'\n" ).arg(args: word, args: arrAsPrintable()); |
256 | stream << errorPrefix.subs(a: error).toString(); |
257 | continue; |
258 | } |
259 | const int posOfNonNumeric = word.indexOf(c: QLatin1Char('-'), from: 2); |
260 | if ((posOfNonNumeric < 0) || ((posOfNonNumeric + 1) == word.length())) { |
261 | auto error = QStringLiteral("malformed property term (no data): '%1' in '%2'\n" ).arg(args: word, args: arrAsPrintable()); |
262 | stream << errorPrefix.subs(a: error).toString(); |
263 | continue; |
264 | } |
265 | |
266 | bool ok; |
267 | const QStringView prop = QStringView(word).mid(pos: 1, n: posOfNonNumeric - 1); |
268 | int propNum = prop.toInt(ok: &ok); |
269 | if (!ok) { |
270 | auto error = QStringLiteral("malformed property term (bad index): '%1' in '%2'\n" ).arg(args: prop, args: arrAsPrintable()); |
271 | stream << errorPrefix.subs(a: error).toString(); |
272 | continue; |
273 | } |
274 | |
275 | const QString value = word.mid(position: posOfNonNumeric + 1); |
276 | propertyWords[propNum].append(t: value); |
277 | } |
278 | } |
279 | |
280 | for (auto it = propertyWords.constBegin(); it != propertyWords.constEnd(); it++) { |
281 | auto prop = static_cast<KFileMetaData::Property::Property>(it.key()); |
282 | KFileMetaData::PropertyInfo pi(prop); |
283 | |
284 | stream << pi.name() << ": " << it.value().join(sep: QLatin1Char(' ')) << '\n'; |
285 | } |
286 | } |
287 | } |
288 | stream.flush(); |
289 | return 0; |
290 | } |
291 | |