Baloo

kio_tags.cpp
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{
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(KIO::ERR_CANNOT_ENTER_DIRECTORY, result.decodedUrl);
56 case TagUrl:
57 listEntries(result.pathUDSResults);
58 }
59
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(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
71 case FileUrl:
72 return ForwardingWorkerBase::stat(result.fileUrl);
73 case TagUrl:
74 for (const KIO::UDSEntry& entry : std::as_const(result.pathUDSResults)) {
75 if (entry.stringValue(KIO::UDSEntry::UDS_EXTRA) == result.tag) {
76 statEntry(entry);
77 break;
78 }
79 }
80 }
81
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(src);
91 ParseResult dstResult = parseUrl(dest, 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(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
96 } else if (dstResult.urlType == InvalidUrl) {
97 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url";
98 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, 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(newTag);
105 md.setTags(tags);
106 };
107
108 if (srcResult.metaData.tags().contains(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
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(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
126 case FileUrl:
127 return ForwardingWorkerBase::get(result.fileUrl);
128 case TagUrl:
129 return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, result.decodedUrl);
130 }
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(src);
140 ParseResult dstResult;
141
142 if (srcResult.urlType == FileUrl) {
143 dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection);
144 } else if (srcResult.urlType == TagUrl) {
145 dstResult = parseUrl(dest, 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(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl);
151 } else if (dstResult.urlType == InvalidUrl) {
152 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url";
153 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, 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(oldTag);
160 tags.append(newTag);
161 md.setTags(tags);
162 };
163
164 if (srcResult.metaData.tags().contains(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()) {
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(srcResult.tag + QLatin1Char('/')))) {
179 QString newTag = tag;
181 rewriteTags(md, tag, newTag);
182 }
183 }
184 }
185 }
186 }
187
189}
190
191KIO::WorkerResult TagsProtocol::del(const QUrl& url, bool isfile)
192{
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(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(KIO::ERR_DOES_NOT_EXIST, 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(result.tag + QLatin1Char('/'), Qt::CaseInsensitive))) {
219 rewriteTags(md, tag);
220 }
221 }
222 }
223 }
224 }
225
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(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
237 case FileUrl:
238 return ForwardingWorkerBase::mimetype(result.fileUrl);
239 case TagUrl:
240 mimeType(QStringLiteral("inode/directory"));
241 }
242
244}
245
246KIO::WorkerResult TagsProtocol::mkdir(const QUrl& url, int permissions)
247{
248 Q_UNUSED(permissions);
249
250 ParseResult result = parseUrl(url, 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(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl);
257 case TagUrl:
258 m_unassignedTags << result.tag;
259 }
260
262}
263
264bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL)
265{
266 Q_UNUSED(url);
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(6) == QLatin1Char('/')) {
278 result.urlType = InvalidUrl;
279 return result;
280 }
281
282 auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) {
284 uds.reserve(9);
287 uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
288 uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
290 uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag"));
291 uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, tag);
292
295
296 // a tag/folder
297 if (tagSection == tag) {
298 displayType = i18nc("This is a noun", "Tag");
299 displayName = tag.section(QLatin1Char('/'), -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(QLatin1Char('/'), -2);
307 } else if (tagSection == QStringLiteral(".")) {
308 displayName = tag.section(QLatin1Char('/'), -1);
309 } else {
311 }
312 }
313
314 // The root folder
315 else {
316 displayType = i18n("All Tags");
317 displayName = i18n("All Tags");
318 }
319
321 uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, 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(LazyValidation);
338
339 // Determine the tag from the URL.
340 result.tag = result.decodedUrl;
341 result.tag.remove(url.scheme() + QLatin1Char(':'));
342 result.tag = QDir::cleanPath(result.tag);
343 while (result.tag.startsWith(QLatin1Char('/'))) {
344 result.tag.remove(0, 1);
345 }
346
347 // Extract any local file path from the URL.
348 QString tag = result.tag.section(QDir::separator(), 0, -2);
349 QString fileName = result.tag.section(QDir::separator(), -1, -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(fileName);
355 if (regMatch.hasMatch()) {
356 pos = regMatch.captured(1).toInt();
357
358 fileName.remove(regexp);
359 }
360
361 Query q;
362 q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(tag, fileName));
363 ResultIterator it = q.exec();
364
365 int i = 0;
366 while (it.next()) {
367 result.fileUrl = QUrl::fromLocalFile(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(ChopLastSection)) {
378 result.tag = result.tag.section(QDir::separator(), 0, -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(QLatin1Char(' '), QStringLiteral(" AND tag:"));
388 query.replace(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(QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1);
397
398 const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags;
399 for (const QString& tag : tags) {
400 if (result.tag.isEmpty() || (tag.startsWith(result.tag, Qt::CaseInsensitive))) {
403 result.pathUDSResults << createUDSEntryForTag(tagSection, tag);
405 }
406 }
407
408 validTag = validTag || tag.startsWith(result.tag, 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(result.tag));
434 ResultIterator it = q.exec();
436 UdsFactory udsf;
437
438 while (it.next()) {
439 KIO::UDSEntry uds = udsf.createUdsEntry(it.filePath());
440 if (uds.count() == 0) {
441 continue;
442 }
443
444 const QUrl url(uds.stringValue(KIO::UDSEntry::UDS_URL));
445 auto dupCount = resultNames.count(url.fileName());
446 if (dupCount > 0) {
447 uds.replace(KIO::UDSEntry::UDS_NAME, url.fileName() + QStringLiteral(" (%1)").arg(dupCount));
448 }
449
450 qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(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 {
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"
The Query class is the central class to query to search for files from the Index.
Definition query.h:54
void setSearchString(const QString &str)
Set some text which should be used to search for Items.
Definition query.cpp:87
void statEntry(const UDSEntry &_entry)
void infoMessage(const QString &msg)
void listEntries(const UDSEntryList &_entry)
void mimeType(const QString &_type)
static WorkerResult pass()
static WorkerResult fail(int _error=KIO::ERR_UNKNOWN, const QString &_errorString=QString())
QString loginName() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT QString displayName(Akonadi::ETMCalendar *calendar, const Akonadi::Collection &collection)
Implements storage for docIds without any associated data Instantiated for:
Definition coding.cpp:11
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QString cleanPath(const QString &path)
QChar separator()
void append(QList< T > &&value)
bool contains(const AT &value) const const
qsizetype count() const const
bool isEmpty() const const
void prepend(parameter_type value)
qsizetype removeAll(const AT &t)
void replace(qsizetype i, parameter_type value)
void reserve(qsizetype size)
Q_OBJECTQ_OBJECT
qsizetype count() const const
const QChar at(qsizetype position) const const
bool isEmpty() const const
qsizetype length() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString section(QChar sep, qsizetype start, qsizetype end, SectionFlags flags) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
CaseInsensitive
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QString fromPercentEncoding(const QByteArray &input)
bool isEmpty() const const
bool isLocalFile() const const
QString scheme() const const
QString toLocalFile() const const
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:16 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.