1/*
2 This file is part of the KDE Baloo Project
3 SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <me@vhanda.in>
4 SPDX-FileCopyrightText: 2017-2018 James D. Smith <smithjd15@gmail.com>
5 SPDX-FileCopyrightText: 2020 Stefan BrĂ¼ns <bruns@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
8*/
9
10#include "kio_tags.h"
11#include "kio_tags_debug.h"
12
13#include <QUrl>
14
15#include <KLocalizedString>
16#include <KUser>
17#include <KIO/Job>
18
19#include <QCoreApplication>
20#include <QDir>
21#include <QRegularExpression>
22
23#include "file.h"
24#include "taglistjob.h"
25#include "../common/udstools.h"
26
27#include "term.h"
28
29using namespace Baloo;
30
31// Pseudo plugin class to embed meta data
32class KIOPluginForMetaData : public QObject
33{
34 Q_OBJECT
35 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.tags" FILE "tags.json")
36};
37
38TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket)
39 : KIO::ForwardingWorkerBase("tags", pool_socket, app_socket)
40{
41}
42
43TagsProtocol::~TagsProtocol()
44{
45}
46
47KIO::WorkerResult TagsProtocol::listDir(const QUrl& url)
48{
49 ParseResult result = parseUrl(url);
50
51 switch(result.urlType) {
52 case InvalidUrl:
53 case FileUrl:
54 qCWarning(KIO_TAGS) << result.decodedUrl << "list() invalid url";
55 return KIO::WorkerResult::fail(error: KIO::ERR_CANNOT_ENTER_DIRECTORY, errorString: result.decodedUrl);
56 case TagUrl:
57 listEntries(entry: result.pathUDSResults);
58 }
59
60 return KIO::WorkerResult::pass();
61}
62
63KIO::WorkerResult TagsProtocol::stat(const QUrl& url)
64{
65 ParseResult result = parseUrl(url);
66
67 switch(result.urlType) {
68 case InvalidUrl:
69 qCWarning(KIO_TAGS) << result.decodedUrl << "stat() invalid url";
70 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: result.decodedUrl);
71 case FileUrl:
72 return ForwardingWorkerBase::stat(url: result.fileUrl);
73 case TagUrl:
74 for (const KIO::UDSEntry& entry : std::as_const(t&: result.pathUDSResults)) {
75 if (entry.stringValue(field: KIO::UDSEntry::UDS_EXTRA) == result.tag) {
76 statEntry(entry: entry);
77 break;
78 }
79 }
80 }
81
82 return KIO::WorkerResult::pass();
83}
84
85KIO::WorkerResult TagsProtocol::copy(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags)
86{
87 Q_UNUSED(permissions);
88 Q_UNUSED(flags);
89
90 ParseResult srcResult = parseUrl(url: src);
91 ParseResult dstResult = parseUrl(url: dest, flags: QList<ParseFlags>() << ChopLastSection << LazyValidation);
92
93 if (srcResult.urlType == InvalidUrl) {
94 qCWarning(KIO_TAGS) << srcResult.decodedUrl << "copy() invalid src url";
95 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: srcResult.decodedUrl);
96 } else if (dstResult.urlType == InvalidUrl) {
97 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url";
98 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: dstResult.decodedUrl);
99 }
100
101 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& newTag) {
102 qCDebug(KIO_TAGS) << md.filePath() << "adding tag" << newTag;
103 QStringList tags = md.tags();
104 tags.append(t: newTag);
105 md.setTags(tags);
106 };
107
108 if (srcResult.metaData.tags().contains(str: dstResult.tag)) {
109 qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
110 infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
111 } else if (dstResult.urlType == TagUrl) {
112 rewriteTags(srcResult.metaData, dstResult.tag);
113 }
114
115 return KIO::WorkerResult::pass();
116}
117
118KIO::WorkerResult TagsProtocol::get(const QUrl& url)
119{
120 ParseResult result = parseUrl(url);
121
122 switch(result.urlType) {
123 case InvalidUrl:
124 qCWarning(KIO_TAGS) << result.decodedUrl << "get() invalid url";
125 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: result.decodedUrl);
126 case FileUrl:
127 return ForwardingWorkerBase::get(url: result.fileUrl);
128 case TagUrl:
129 return KIO::WorkerResult::fail(error: KIO::ERR_UNSUPPORTED_ACTION, errorString: result.decodedUrl);
130 }
131 Q_UNREACHABLE();
132 return KIO::WorkerResult::pass();
133}
134
135KIO::WorkerResult TagsProtocol::rename(const QUrl& src, const QUrl& dest, KIO::JobFlags flags)
136{
137 Q_UNUSED(flags);
138
139 ParseResult srcResult = parseUrl(url: src);
140 ParseResult dstResult;
141
142 if (srcResult.urlType == FileUrl) {
143 dstResult = parseUrl(url: dest, flags: QList<ParseFlags>() << ChopLastSection);
144 } else if (srcResult.urlType == TagUrl) {
145 dstResult = parseUrl(url: dest, flags: QList<ParseFlags>() << LazyValidation);
146 }
147
148 if (srcResult.urlType == InvalidUrl) {
149 qCWarning(KIO_TAGS) << srcResult.decodedUrl << "rename() invalid src url";
150 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: srcResult.decodedUrl);
151 } else if (dstResult.urlType == InvalidUrl) {
152 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url";
153 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: dstResult.decodedUrl);
154 }
155
156 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& oldTag, const QString& newTag) {
157 qCDebug(KIO_TAGS) << md.filePath() << "swapping tag" << oldTag << "with" << newTag;
158 QStringList tags = md.tags();
159 tags.removeAll(t: oldTag);
160 tags.append(t: newTag);
161 md.setTags(tags);
162 };
163
164 if (srcResult.metaData.tags().contains(str: dstResult.tag)) {
165 qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag;
166 infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag));
167 } else if (srcResult.urlType == FileUrl) {
168 rewriteTags(srcResult.metaData, srcResult.tag, dstResult.tag);
169 } else if (srcResult.urlType == TagUrl) {
170 ResultIterator it = srcResult.query.exec();
171 while (it.next()) {
172 KFileMetaData::UserMetaData md(it.filePath());
173 if (it.filePath() == srcResult.fileUrl.toLocalFile()) {
174 rewriteTags(md, srcResult.tag, dstResult.tag);
175 } else if (srcResult.fileUrl.isEmpty()) {
176 const auto tags = md.tags();
177 for (const QString& tag : tags) {
178 if (tag == srcResult.tag || (tag.startsWith(s: srcResult.tag + QLatin1Char('/')))) {
179 QString newTag = tag;
180 newTag.replace(before: srcResult.tag, after: dstResult.tag, cs: Qt::CaseInsensitive);
181 rewriteTags(md, tag, newTag);
182 }
183 }
184 }
185 }
186 }
187
188 return KIO::WorkerResult::pass();
189}
190
191KIO::WorkerResult TagsProtocol::del(const QUrl& url, bool isfile)
192{
193 Q_UNUSED(isfile);
194
195 ParseResult result = parseUrl(url);
196
197 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& tag) {
198 qCDebug(KIO_TAGS) << md.filePath() << "removing tag" << tag;
199 QStringList tags = md.tags();
200 tags.removeAll(t: tag);
201 md.setTags(tags);
202 };
203
204 switch(result.urlType) {
205 case InvalidUrl:
206 qCWarning(KIO_TAGS) << result.decodedUrl << "del() invalid url";
207 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: result.decodedUrl);
208 case FileUrl:
209 case TagUrl:
210 ResultIterator it = result.query.exec();
211 while (it.next()) {
212 KFileMetaData::UserMetaData md(it.filePath());
213 if (it.filePath() == result.fileUrl.toLocalFile()) {
214 rewriteTags(md, result.tag);
215 } else if (result.fileUrl.isEmpty()) {
216 const auto tags = md.tags();
217 for (const QString &tag : tags) {
218 if ((tag == result.tag) || (tag.startsWith(s: result.tag + QLatin1Char('/'), cs: Qt::CaseInsensitive))) {
219 rewriteTags(md, tag);
220 }
221 }
222 }
223 }
224 }
225
226 return KIO::WorkerResult::pass();
227}
228
229KIO::WorkerResult TagsProtocol::mimetype(const QUrl& url)
230{
231 ParseResult result = parseUrl(url);
232
233 switch(result.urlType) {
234 case InvalidUrl:
235 qCWarning(KIO_TAGS) << result.decodedUrl << "mimetype() invalid url";
236 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: result.decodedUrl);
237 case FileUrl:
238 return ForwardingWorkerBase::mimetype(url: result.fileUrl);
239 case TagUrl:
240 mimeType(QStringLiteral("inode/directory"));
241 }
242
243 return KIO::WorkerResult::pass();
244}
245
246KIO::WorkerResult TagsProtocol::mkdir(const QUrl& url, int permissions)
247{
248 Q_UNUSED(permissions);
249
250 ParseResult result = parseUrl(url, flags: QList<ParseFlags>() << LazyValidation);
251
252 switch(result.urlType) {
253 case InvalidUrl:
254 case FileUrl:
255 qCWarning(KIO_TAGS) << result.decodedUrl << "mkdir() invalid url";
256 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: result.decodedUrl);
257 case TagUrl:
258 m_unassignedTags << result.tag;
259 }
260
261 return KIO::WorkerResult::pass();
262}
263
264bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL)
265{
266 Q_UNUSED(url);
267 Q_UNUSED(newURL);
268
269 return false;
270}
271
272TagsProtocol::ParseResult TagsProtocol::parseUrl(const QUrl& url, const QList<ParseFlags> &flags)
273{
274 TagsProtocol::ParseResult result;
275 result.decodedUrl = QUrl::fromPercentEncoding(url.toString().toUtf8());
276
277 if ((url.scheme() == QLatin1String("tags")) && result.decodedUrl.length()>6 && result.decodedUrl.at(i: 6) == QLatin1Char('/')) {
278 result.urlType = InvalidUrl;
279 return result;
280 }
281
282 auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) {
283 KIO::UDSEntry uds;
284 uds.reserve(size: 9);
285 uds.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: tagSection);
286 uds.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
287 uds.fastInsert(field: KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
288 uds.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, l: 0700);
289 uds.fastInsert(field: KIO::UDSEntry::UDS_USER, value: KUser().loginName());
290 uds.fastInsert(field: KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag"));
291 uds.fastInsert(field: KIO::UDSEntry::UDS_EXTRA, value: tag);
292
293 QString displayType;
294 QString displayName;
295
296 // a tag/folder
297 if (tagSection == tag) {
298 displayType = i18nc("This is a noun", "Tag");
299 displayName = tag.section(asep: QLatin1Char('/'), astart: -1);
300 }
301
302 // a tagged file
303 else if (!tag.isEmpty()) {
304 displayType = i18nc("This is a noun", "Tag Fragment");
305 if (tagSection == QStringLiteral("..")) {
306 displayName = tag.section(asep: QLatin1Char('/'), astart: -2);
307 } else if (tagSection == QStringLiteral(".")) {
308 displayName = tag.section(asep: QLatin1Char('/'), astart: -1);
309 } else {
310 displayName = tagSection;
311 }
312 }
313
314 // The root folder
315 else {
316 displayType = i18n("All Tags");
317 displayName = i18n("All Tags");
318 }
319
320 uds.fastInsert(field: KIO::UDSEntry::UDS_DISPLAY_TYPE, value: displayType);
321 uds.fastInsert(field: KIO::UDSEntry::UDS_DISPLAY_NAME, value: displayName);
322
323 return uds;
324 };
325
326 TagListJob* tagJob = new TagListJob();
327 if (!tagJob->exec()) {
328 qCWarning(KIO_TAGS) << "tag fetch failed:" << tagJob->errorString();
329 return result;
330 }
331
332 if (url.isLocalFile()) {
333 result.urlType = FileUrl;
334 result.fileUrl = url;
335 result.metaData = KFileMetaData::UserMetaData(url.toLocalFile());
336 } else if (url.scheme() == QLatin1String("tags")) {
337 bool validTag = flags.contains(t: LazyValidation);
338
339 // Determine the tag from the URL.
340 result.tag = result.decodedUrl;
341 result.tag.remove(s: url.scheme() + QLatin1Char(':'));
342 result.tag = QDir::cleanPath(path: result.tag);
343 while (result.tag.startsWith(c: QLatin1Char('/'))) {
344 result.tag.remove(i: 0, len: 1);
345 }
346
347 // Extract any local file path from the URL.
348 QString tag = result.tag.section(asep: QDir::separator(), astart: 0, aend: -2);
349 QString fileName = result.tag.section(asep: QDir::separator(), astart: -1, aend: -1);
350 int pos = 0;
351
352 // Extract and remove any multiple filename suffix from the file name.
353 QRegularExpression regexp(QStringLiteral("\\s\\((\\d+)\\)$"));
354 QRegularExpressionMatch regMatch = regexp.match(subject: fileName);
355 if (regMatch.hasMatch()) {
356 pos = regMatch.captured(nth: 1).toInt();
357
358 fileName.remove(re: regexp);
359 }
360
361 Query q;
362 q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(args&: tag, args&: fileName));
363 ResultIterator it = q.exec();
364
365 int i = 0;
366 while (it.next()) {
367 result.fileUrl = QUrl::fromLocalFile(localfile: it.filePath());
368 result.metaData = KFileMetaData::UserMetaData(it.filePath());
369
370 if (i == pos) {
371 break;
372 } else {
373 i++;
374 }
375 }
376
377 if (!result.fileUrl.isEmpty() || flags.contains(t: ChopLastSection)) {
378 result.tag = result.tag.section(asep: QDir::separator(), astart: 0, aend: -2);
379 }
380
381 validTag = validTag || result.tag.isEmpty();
382
383 if (!result.tag.isEmpty()) {
384 // Create a query to find files that may be in the operation's scope.
385 QString query = result.tag;
386 query.prepend(QStringLiteral("tag:"));
387 query.replace(c: QLatin1Char(' '), QStringLiteral(" AND tag:"));
388 query.replace(c: QLatin1Char('/'), QStringLiteral(" AND tag:"));
389 result.query.setSearchString(query);
390
391 qCDebug(KIO_TAGS) << result.decodedUrl << "url query:" << query;
392 }
393
394 // Create the tag directory entries.
395 int index = result.tag.count(c: QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1);
396 QStringList tagPaths;
397
398 const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags;
399 for (const QString& tag : tags) {
400 if (result.tag.isEmpty() || (tag.startsWith(s: result.tag, cs: Qt::CaseInsensitive))) {
401 QString tagSection = tag.section(asep: QLatin1Char('/'), astart: index, aend: index, aflags: QString::SectionSkipEmpty);
402 if (!tagPaths.contains(str: tagSection, cs: Qt::CaseInsensitive) && !tagSection.isEmpty()) {
403 result.pathUDSResults << createUDSEntryForTag(tagSection, tag);
404 tagPaths << tagSection;
405 }
406 }
407
408 validTag = validTag || tag.startsWith(s: result.tag, cs: Qt::CaseInsensitive);
409 }
410
411 if (validTag && result.fileUrl.isEmpty()) {
412 result.urlType = TagUrl;
413 } else if (validTag && !result.fileUrl.isEmpty()) {
414 result.urlType = FileUrl;
415 }
416 }
417
418 if (result.urlType == FileUrl) {
419 return result;
420 } else {
421 result.pathUDSResults << createUDSEntryForTag(QStringLiteral("."), result.tag);
422 }
423
424 // The root tag url has no file entries.
425 if (result.tag.isEmpty()) {
426 return result;
427 } else {
428 result.pathUDSResults << createUDSEntryForTag(QStringLiteral(".."), result.tag);
429 }
430
431 // Query for any files associated with the tag.
432 Query q;
433 q.setSearchString(QStringLiteral("tag=\"%1\"").arg(a: result.tag));
434 ResultIterator it = q.exec();
435 QList<QString> resultNames;
436 UdsFactory udsf;
437
438 while (it.next()) {
439 KIO::UDSEntry uds = udsf.createUdsEntry(filePath: it.filePath());
440 if (uds.count() == 0) {
441 continue;
442 }
443
444 const QUrl url(uds.stringValue(field: KIO::UDSEntry::UDS_URL));
445 auto dupCount = resultNames.count(t: url.fileName());
446 if (dupCount > 0) {
447 uds.replace(field: KIO::UDSEntry::UDS_NAME, value: url.fileName() + QStringLiteral(" (%1)").arg(a: dupCount));
448 }
449
450 qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(field: KIO::UDSEntry::UDS_NAME);
451
452 resultNames << url.fileName();
453 result.pathUDSResults << uds;
454 }
455
456 return result;
457}
458
459extern "C"
460{
461 Q_DECL_EXPORT int kdemain(int argc, char** argv)
462 {
463 QCoreApplication app(argc, argv);
464 app.setApplicationName(QStringLiteral("kio_tags"));
465 Baloo::TagsProtocol worker(argv[2], argv[3]);
466 worker.dispatchLoop();
467 return 0;
468 }
469}
470
471#include "kio_tags.moc"
472#include "moc_kio_tags.cpp"
473

source code of baloo/src/kioworkers/tags/kio_tags.cpp