Baloo

kio_tags.cpp
1 /*
2  This file is part of the KDE Baloo Project
3  SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <[email protected]>
4  SPDX-FileCopyrightText: 2017-2018 James D. Smith <[email protected]>
5  SPDX-FileCopyrightText: 2020 Stefan BrĂ¼ns <[email protected]>
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 
29 using namespace Baloo;
30 
31 // Pseudo plugin class to embed meta data
32 class KIOPluginForMetaData : public QObject
33 {
34  Q_OBJECT
35  Q_PLUGIN_METADATA(IID "org.kde.kio.worker.tags" FILE "tags.json")
36 };
37 
38 TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket)
39  : KIO::ForwardingWorkerBase("tags", pool_socket, app_socket)
40 {
41 }
42 
43 TagsProtocol::~TagsProtocol()
44 {
45 }
46 
47 KIO::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 
60  return KIO::WorkerResult::pass();
61 }
62 
63 KIO::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 
82  return KIO::WorkerResult::pass();
83 }
84 
85 KIO::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 
115  return KIO::WorkerResult::pass();
116 }
117 
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  }
131  Q_UNREACHABLE();
132  return KIO::WorkerResult::pass();
133 }
134 
135 KIO::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()) {
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(srcResult.tag + QLatin1Char('/')))) {
179  QString newTag = tag;
180  newTag.replace(srcResult.tag, dstResult.tag, Qt::CaseInsensitive);
181  rewriteTags(md, tag, newTag);
182  }
183  }
184  }
185  }
186  }
187 
188  return KIO::WorkerResult::pass();
189 }
190 
191 KIO::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(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 
226  return KIO::WorkerResult::pass();
227 }
228 
229 KIO::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 
243  return KIO::WorkerResult::pass();
244 }
245 
246 KIO::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 
261  return KIO::WorkerResult::pass();
262 }
263 
264 bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL)
265 {
266  Q_UNUSED(url);
267  Q_UNUSED(newURL);
268 
269  return false;
270 }
271 
272 TagsProtocol::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) {
283  KIO::UDSEntry uds;
284  uds.reserve(9);
285  uds.fastInsert(KIO::UDSEntry::UDS_NAME, tagSection);
287  uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
290  uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag"));
292 
293  QString displayType;
295 
296  // a tag/folder
297  if (tagSection == tag) {
298  displayType = i18n("Tag");
299  displayName = tag.section(QLatin1Char('/'), -1);
300  }
301 
302  // a tagged file
303  else if (!tag.isEmpty()) {
304  displayType = i18n("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 {
310  displayName = tagSection;
311  }
312  }
313 
314  // The root folder
315  else {
316  displayType = i18n("All Tags");
317  displayName = i18n("All Tags");
318  }
319 
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);
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(result.tag, Qt::CaseInsensitive))) {
401  QString tagSection = tag.section(QLatin1Char('/'), index, index, QString::SectionSkipEmpty);
402  if (!tagPaths.contains(tagSection, Qt::CaseInsensitive) && !tagSection.isEmpty()) {
403  result.pathUDSResults << createUDSEntryForTag(tagSection, tag);
404  tagPaths << tagSection;
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();
435  QList<QString> resultNames;
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 
459 extern "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"
void append(const T &value)
int count() const
static WorkerResult pass()
std::optional< QSqlQuery > query(const QString &queryStatement)
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
CaseInsensitive
int removeAll(const T &value)
QString scheme() const const
int count(const T &value) const const
QChar separator()
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QString fromPercentEncoding(const QByteArray &input)
KCALUTILS_EXPORT QString mimeType()
bool contains(const T &value) const const
QString stringValue(uint field) const
QString toString(QUrl::FormattingOptions options) const const
QString i18n(const char *text, const TYPE &arg...)
static WorkerResult fail(int _error=KIO::ERR_UNKNOWN, const QString &_errorString=QString())
Implements storage for docIds without any associated data Instantiated for:
Definition: coding.cpp:11
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
QString fileName(QUrl::ComponentFormattingOptions options) const const
QByteArray toUtf8() const const
void reserve(int size)
int toInt(bool *ok, int base) const const
QString toLocalFile() const const
bool hasMatch() const const
QString loginName() const
QString & replace(int position, int n, QChar after)
QString & remove(int position, int n)
AKONADI_CALENDAR_EXPORT QString displayName(Akonadi::ETMCalendar *calendar, const Akonadi::Collection &collection)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString cleanPath(const QString &path)
void fastInsert(uint field, const QString &value)
void replace(uint field, const QString &value)
bool isLocalFile() const const
QString captured(int nth) const const
virtual QVariant get(ScriptableExtension *callerPrincipal, quint64 objId, const QString &propName)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Wed Nov 29 2023 03:56:26 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.