File indexing completed on 2024-03-24 04:42:32
0001 /* 0002 SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz@gmx.at> 0003 SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in> 0004 SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "filemetadataprovider.h" 0010 #include "filemetadatautil_p.h" 0011 #include "filefetchjob.h" 0012 0013 #include <Baloo/IndexerConfig> 0014 #include <KFileMetaData/PropertyInfo> 0015 #include <KFormat> 0016 #include <KLocalizedString> 0017 #include <KProtocolInfo> 0018 #include <KShell> 0019 0020 #include <QPair> 0021 0022 // Required includes for subDirectoriesCount(): 0023 #ifdef Q_OS_WIN 0024 #include <QDir> 0025 #else 0026 #include <QFile> 0027 #include <dirent.h> 0028 #endif 0029 0030 using namespace Baloo; 0031 0032 namespace 0033 { 0034 /** 0035 * The standard QMap::unite will contain the key multiple times if both \p v1 and \p v2 0036 * contain the same key. 0037 * 0038 * This will only take the key from \p v2 into account 0039 */ 0040 QVariantMap unite(const QVariantMap &v1, const QVariantMap &v2) 0041 { 0042 QVariantMap v(v1); 0043 QMapIterator<QString, QVariant> it(v2); 0044 while (it.hasNext()) { 0045 it.next(); 0046 0047 v[it.key()] = it.value(); 0048 } 0049 0050 return v; 0051 } 0052 0053 /** 0054 * @return The number of files and hidden files for the directory path. 0055 */ 0056 QPair<int, int> subDirectoriesCount(const QString &path) 0057 { 0058 #ifdef Q_OS_WIN 0059 QDir dir(path); 0060 int count = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System).count(); 0061 int hiddenCount = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden).count(); 0062 return QPair<int, int>(count, hiddenCount); 0063 #else 0064 // Taken from kdelibs/kio/kio/kdirmodel.cpp 0065 // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org> 0066 0067 int count = -1; 0068 int hiddenCount = -1; 0069 DIR *dir = ::opendir(QFile::encodeName(path).constData()); 0070 if (dir) { 0071 count = 0; 0072 hiddenCount = 0; 0073 struct dirent *dirEntry = nullptr; 0074 while ((dirEntry = ::readdir(dir))) { // krazy:exclude=syscalls 0075 if (dirEntry->d_name[0] == '.') { 0076 if (dirEntry->d_name[1] == '\0') { 0077 // Skip "." 0078 continue; 0079 } 0080 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { 0081 // Skip ".." 0082 continue; 0083 } 0084 // hidden files 0085 hiddenCount++; 0086 } else { 0087 ++count; 0088 } 0089 } 0090 ::closedir(dir); 0091 } 0092 return QPair<int, int>(count, hiddenCount); 0093 #endif 0094 } 0095 0096 /** 0097 * Fill \p data with properties can be derived from others 0098 */ 0099 void extractDerivedProperties(QVariantMap &data) 0100 { 0101 const auto width = data.value(QStringLiteral("width")); 0102 const auto height = data.value(QStringLiteral("height")); 0103 if (!width.isNull() && !height.isNull()) { 0104 data.insert(QStringLiteral("dimensions"), i18nc("width × height", "%1 × %2", width.toInt(), height.toInt())); 0105 } 0106 0107 bool okLatitude; 0108 const auto gpsLatitude = data.value(QStringLiteral("photoGpsLatitude")).toFloat(&okLatitude); 0109 bool okLongitude; 0110 const auto gpsLongitude = data.value(QStringLiteral("photoGpsLongitude")).toFloat(&okLongitude); 0111 0112 if (okLatitude && okLongitude) { 0113 data.insert(QStringLiteral("gpsLocation"), QVariant::fromValue(QPair<float, float>(gpsLatitude, gpsLongitude))); 0114 } 0115 } 0116 } // anonymous namespace 0117 0118 class Q_DECL_HIDDEN Baloo::FileMetaDataProviderPrivate : public QObject 0119 { 0120 public: 0121 FileMetaDataProviderPrivate(FileMetaDataProvider *parent) 0122 : QObject(parent) 0123 , m_parent(parent) 0124 , m_readOnly(false) 0125 { 0126 } 0127 0128 ~FileMetaDataProviderPrivate() 0129 { 0130 } 0131 0132 void insertEditableData(); 0133 0134 void processFileItems(); 0135 0136 void setFileItem(); 0137 void setFileItems(); 0138 0139 /** 0140 * Insert basic data of a single file 0141 */ 0142 void insertSingleFileBasicData(); 0143 0144 /** 0145 * Insert basic data of a list of files 0146 */ 0147 void insertFilesListBasicData(); 0148 0149 FileMetaDataProvider *m_parent; 0150 0151 bool m_readOnly; 0152 0153 QList<KFileItem> m_fileItems; 0154 0155 QVariantMap m_data; 0156 Baloo::IndexerConfig m_config; 0157 0158 public Q_SLOTS: 0159 void slotFileFetchFinished(KJob *job); 0160 }; 0161 0162 void FileMetaDataProviderPrivate::slotFileFetchFinished(KJob *job) 0163 { 0164 auto fetchJob = static_cast<FileFetchJob *>(job); 0165 QList<QVariantMap> files = fetchJob->data(); 0166 0167 Q_ASSERT(!files.isEmpty()); 0168 0169 if (files.size() > 1) { 0170 Baloo::Private::mergeCommonData(m_data, files); 0171 } else { 0172 m_data = unite(m_data, files.first()); 0173 } 0174 extractDerivedProperties(m_data); 0175 m_readOnly = !fetchJob->canEditAll(); 0176 0177 insertEditableData(); 0178 Q_EMIT m_parent->loadingFinished(); 0179 } 0180 0181 void FileMetaDataProviderPrivate::insertSingleFileBasicData() 0182 { 0183 // TODO: Handle case if remote URLs are used properly. isDir() does 0184 // not work, the modification date needs also to be adjusted... 0185 Q_ASSERT(m_fileItems.count() == 1); 0186 { 0187 const KFileItem &item = m_fileItems.first(); 0188 0189 KFormat format; 0190 if (item.isDir()) { 0191 if (item.isLocalFile() && !item.isSlow()) { 0192 const QPair<int, int> counts = subDirectoriesCount(item.url().path()); 0193 const int count = counts.first; 0194 if (count != -1) { 0195 QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count); 0196 m_data.insert(QStringLiteral("kfileitem#size"), itemCountString); 0197 0198 const int hiddenCount = counts.second; 0199 if (hiddenCount > 0) { 0200 // add hidden items count 0201 QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount); 0202 m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString); 0203 } 0204 } 0205 } else if (item.entry().contains(KIO::UDSEntry::UDS_SIZE)) { 0206 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size())); 0207 } 0208 if (item.entry().contains(KIO::UDSEntry::UDS_RECURSIVE_SIZE)) { 0209 m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(item.recursiveSize())); 0210 } 0211 } else { 0212 if (item.entry().contains(KIO::UDSEntry::UDS_SIZE)) { 0213 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size())); 0214 } 0215 } 0216 0217 m_data.insert(QStringLiteral("kfileitem#type"), item.mimeComment()); 0218 if (item.isLink()) { 0219 m_data.insert(QStringLiteral("kfileitem#linkDest"), item.linkDest()); 0220 } 0221 if (item.entry().contains(KIO::UDSEntry::UDS_TARGET_URL)) { 0222 m_data.insert(QStringLiteral("kfileitem#targetUrl"), KShell::tildeCollapse(item.targetUrl().toDisplayString(QUrl::PreferLocalFile))); 0223 } 0224 QDateTime modificationTime = item.time(KFileItem::ModificationTime); 0225 if (modificationTime.isValid()) { 0226 m_data.insert(QStringLiteral("kfileitem#modified"), modificationTime); 0227 } 0228 QDateTime creationTime = item.time(KFileItem::CreationTime); 0229 if (creationTime.isValid()) { 0230 m_data.insert(QStringLiteral("kfileitem#created"), creationTime); 0231 } 0232 QDateTime accessTime = item.time(KFileItem::AccessTime); 0233 if (accessTime.isValid()) { 0234 m_data.insert(QStringLiteral("kfileitem#accessed"), accessTime); 0235 } 0236 0237 m_data.insert(QStringLiteral("kfileitem#owner"), item.user()); 0238 m_data.insert(QStringLiteral("kfileitem#group"), item.group()); 0239 m_data.insert(QStringLiteral("kfileitem#permissions"), item.permissionsString()); 0240 0241 const auto extraFields = KProtocolInfo::extraFields(item.url()); 0242 for (int i = 0; i < extraFields.count(); ++i) { 0243 const auto &field = extraFields.at(i); 0244 if (field.type == KProtocolInfo::ExtraField::Invalid) { 0245 continue; 0246 } 0247 0248 const QString text = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + i); 0249 if (text.isEmpty()) { 0250 continue; 0251 } 0252 0253 const QString key = QStringLiteral("kfileitem#extra_%1_%2").arg(item.url().scheme(), QString::number(i + 1)); 0254 0255 if (field.type == KProtocolInfo::ExtraField::DateTime) { 0256 const QDateTime date = QDateTime::fromString(text, Qt::ISODate); 0257 if (!date.isValid()) { 0258 continue; 0259 } 0260 0261 m_data.insert(key, date); 0262 } else { 0263 m_data.insert(key, text); 0264 } 0265 } 0266 } 0267 } 0268 0269 void FileMetaDataProviderPrivate::insertFilesListBasicData() 0270 { 0271 // If all directories 0272 Q_ASSERT(m_fileItems.count() > 1); 0273 bool allDirectories = true; 0274 for (const KFileItem &item : std::as_const(m_fileItems)) { 0275 allDirectories &= item.isDir(); 0276 if (!allDirectories) { 0277 break; 0278 } 0279 } 0280 0281 if (allDirectories) { 0282 int count = 0; 0283 int hiddenCount = 0; 0284 for (const KFileItem &item : std::as_const(m_fileItems)) { 0285 if (!item.isLocalFile() || item.isSlow()) { 0286 return; 0287 } 0288 const QPair<int, int> counts = subDirectoriesCount(item.url().path()); 0289 const int subcount = counts.first; 0290 if (subcount == -1) { 0291 return; 0292 } 0293 count += subcount; 0294 hiddenCount += counts.second; 0295 } 0296 QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count); 0297 if (hiddenCount > 0) { 0298 // add hidden items count 0299 QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount); 0300 m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString); 0301 } 0302 m_data.insert(QStringLiteral("kfileitem#totalSize"), itemCountString); 0303 0304 } else { 0305 // Calculate the size of all items 0306 quint64 totalSize = 0; 0307 for (const KFileItem &item : std::as_const(m_fileItems)) { 0308 if (!item.isDir() && !item.isLink()) { 0309 totalSize += item.size(); 0310 } 0311 } 0312 KFormat format; 0313 m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(totalSize)); 0314 } 0315 } 0316 0317 void FileMetaDataProviderPrivate::insertEditableData() 0318 { 0319 if (!m_readOnly) { 0320 if (!m_data.contains(QStringLiteral("tags"))) { 0321 m_data.insert(QStringLiteral("tags"), QVariant()); 0322 } 0323 if (!m_data.contains(QStringLiteral("rating"))) { 0324 m_data.insert(QStringLiteral("rating"), 0); 0325 } 0326 if (!m_data.contains(QStringLiteral("userComment"))) { 0327 m_data.insert(QStringLiteral("userComment"), QVariant()); 0328 } 0329 } 0330 } 0331 0332 FileMetaDataProvider::FileMetaDataProvider(QObject *parent) 0333 : QObject(parent) 0334 , d(new FileMetaDataProviderPrivate(this)) 0335 { 0336 } 0337 0338 FileMetaDataProvider::~FileMetaDataProvider() = default; 0339 0340 void FileMetaDataProviderPrivate::processFileItems() 0341 { 0342 // There are several code paths - 0343 // Remote file 0344 // Single local file - 0345 // * Not Indexed 0346 // * Indexed 0347 // 0348 // Multiple Files - 0349 // * Not Indexed 0350 // * Indexed 0351 0352 bool singleFileMode = m_fileItems.size() <= 1; 0353 0354 QStringList urls; 0355 urls.reserve(m_fileItems.size()); 0356 // Only extract data from indexed files, 0357 // it would be too expensive otherwise. 0358 for (const KFileItem &item : std::as_const(m_fileItems)) { 0359 const QUrl url = item.targetUrl(); 0360 if (url.isLocalFile() && !item.isSlow()) { 0361 urls << url.toLocalFile(); 0362 } 0363 } 0364 0365 if (singleFileMode) { 0366 insertSingleFileBasicData(); 0367 } else { 0368 insertFilesListBasicData(); 0369 } 0370 0371 if (!urls.isEmpty()) { 0372 // Editing only if all URLs are local 0373 bool canEdit = (urls.size() == m_fileItems.size()); 0374 0375 // Don't use indexing when we have multiple files 0376 auto indexingMode = FileFetchJob::UseRealtimeIndexing::Disabled; 0377 0378 if (singleFileMode) { 0379 // Fully indexed by Baloo 0380 indexingMode = FileFetchJob::UseRealtimeIndexing::Fallback; 0381 0382 if (!m_config.fileIndexingEnabled() || !m_config.shouldBeIndexed(urls.first()) || m_config.onlyBasicIndexing()) { 0383 // Not indexed or only basic file indexing (no content) 0384 indexingMode = FileFetchJob::UseRealtimeIndexing::Only; 0385 } 0386 } 0387 0388 auto job = new FileFetchJob(urls, canEdit, indexingMode, this); 0389 connect(job, &FileFetchJob::finished, this, &FileMetaDataProviderPrivate::slotFileFetchFinished); 0390 job->start(); 0391 0392 } else { 0393 // FIXME - are extended attributes supported for remote files? 0394 m_readOnly = true; 0395 Q_EMIT m_parent->loadingFinished(); 0396 } 0397 } 0398 0399 void FileMetaDataProvider::setItems(const KFileItemList &items) 0400 { 0401 d->m_fileItems = items; 0402 d->m_data.clear(); 0403 0404 if (items.isEmpty()) { 0405 Q_EMIT loadingFinished(); 0406 } else { 0407 d->processFileItems(); 0408 } 0409 } 0410 0411 QString FileMetaDataProvider::label(const QString &metaDataLabel) const 0412 { 0413 static QHash<QString, QString> hash = { 0414 {QStringLiteral("kfileitem#comment"), i18nc("@label", "Comment")}, 0415 {QStringLiteral("kfileitem#created"), i18nc("@label", "Created")}, 0416 {QStringLiteral("kfileitem#accessed"), i18nc("@label", "Accessed")}, 0417 {QStringLiteral("kfileitem#modified"), i18nc("@label", "Modified")}, 0418 {QStringLiteral("kfileitem#owner"), i18nc("@label", "Owner")}, 0419 {QStringLiteral("kfileitem#group"), i18nc("@label", "Group")}, 0420 {QStringLiteral("kfileitem#permissions"), i18nc("@label", "Permissions")}, 0421 {QStringLiteral("kfileitem#rating"), i18nc("@label", "Rating")}, 0422 {QStringLiteral("kfileitem#size"), i18nc("@label", "Size")}, 0423 {QStringLiteral("kfileitem#tags"), i18nc("@label", "Tags")}, 0424 {QStringLiteral("kfileitem#totalSize"), i18nc("@label", "Total Size")}, 0425 {QStringLiteral("kfileitem#hiddenItems"), i18nc("@label", "Hidden items")}, 0426 {QStringLiteral("kfileitem#type"), i18nc("@label", "Type")}, 0427 {QStringLiteral("kfileitem#linkDest"), i18nc("@label", "Link to")}, 0428 {QStringLiteral("kfileitem#targetUrl"), i18nc("@label", "Points to")}, 0429 {QStringLiteral("tags"), i18nc("@label", "Tags")}, 0430 {QStringLiteral("rating"), i18nc("@label", "Rating")}, 0431 {QStringLiteral("userComment"), i18nc("@label", "Comment")}, 0432 {QStringLiteral("originUrl"), i18nc("@label", "Downloaded From")}, 0433 {QStringLiteral("dimensions"), i18nc("@label", "Dimensions")}, 0434 {QStringLiteral("gpsLocation"), i18nc("@label", "GPS Location")}, 0435 }; 0436 0437 QString value = hash.value(metaDataLabel); 0438 if (value.isEmpty()) { 0439 static const auto extraPrefix = QStringLiteral("kfileitem#extra_"); 0440 if (metaDataLabel.startsWith(extraPrefix)) { 0441 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0442 const auto parts = metaDataLabel.splitRef(QLatin1Char('_')); 0443 #else 0444 const auto parts = metaDataLabel.split(QLatin1Char('_')); 0445 #endif 0446 Q_ASSERT(parts.count() == 3); 0447 const auto protocol = parts.at(1); 0448 const int extraNumber = parts.at(2).toInt() - 1; 0449 0450 // Have to construct a dummy URL for KProtocolInfo::extraFields... 0451 QUrl url; 0452 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0453 url.setScheme(protocol.toString()); 0454 #else 0455 url.setScheme(protocol); 0456 #endif 0457 const auto extraFields = KProtocolInfo::extraFields(url); 0458 auto field = extraFields.value(extraNumber); 0459 if (field.type != KProtocolInfo::ExtraField::Invalid) { 0460 value = field.name; 0461 } 0462 } 0463 } 0464 0465 if (value.isEmpty()) { 0466 value = KFileMetaData::PropertyInfo::fromName(metaDataLabel).displayName(); 0467 } 0468 0469 return value; 0470 } 0471 0472 QString FileMetaDataProvider::group(const QString &label) const 0473 { 0474 static QHash<QString, QString> uriGrouper = { 0475 0476 // KFileItem Data 0477 {QStringLiteral("kfileitem#type"), QStringLiteral("0FileItemA")}, 0478 {QStringLiteral("kfileitem#linkDest"), QStringLiteral("0FileItemB")}, 0479 {QStringLiteral("kfileitem#size"), QStringLiteral("0FileItemC")}, 0480 {QStringLiteral("kfileitem#totalSize"), QStringLiteral("0FileItemC")}, 0481 {QStringLiteral("kfileitem#hiddenItems"), QStringLiteral("0FileItemD")}, 0482 {QStringLiteral("kfileitem#modified"), QStringLiteral("0FileItemE")}, 0483 {QStringLiteral("kfileitem#accessed"), QStringLiteral("0FileItemF")}, 0484 {QStringLiteral("kfileitem#created"), QStringLiteral("0FileItemG")}, 0485 {QStringLiteral("kfileitem#owner"), QStringLiteral("0FileItemH")}, 0486 {QStringLiteral("kfileitem#group"), QStringLiteral("0FileItemI")}, 0487 {QStringLiteral("kfileitem#permissions"), QStringLiteral("0FileItemJ")}, 0488 0489 // Editable Data 0490 {QStringLiteral("tags"), QStringLiteral("1EditableDataA")}, 0491 {QStringLiteral("rating"), QStringLiteral("1EditableDataB")}, 0492 {QStringLiteral("userComment"), QStringLiteral("1EditableDataC")}, 0493 0494 // Image Data 0495 {QStringLiteral("width"), QStringLiteral("2ImageA")}, 0496 {QStringLiteral("height"), QStringLiteral("2ImageB")}, 0497 {QStringLiteral("dimensions"), QStringLiteral("2ImageCA")}, 0498 {QStringLiteral("photoFNumber"), QStringLiteral("2ImageC")}, 0499 {QStringLiteral("photoExposureTime"), QStringLiteral("2ImageD")}, 0500 {QStringLiteral("photoExposureBiasValue"), QStringLiteral("2ImageE")}, 0501 {QStringLiteral("photoISOSpeedRatings"), QStringLiteral("2ImageF")}, 0502 {QStringLiteral("photoFocalLength"), QStringLiteral("2ImageG")}, 0503 {QStringLiteral("photoFocalLengthIn35mmFilm"), QStringLiteral("2ImageH")}, 0504 {QStringLiteral("photoFlash"), QStringLiteral("2ImageI")}, 0505 {QStringLiteral("imageOrientation"), QStringLiteral("2ImageJ")}, 0506 {QStringLiteral("photoGpsLocation"), QStringLiteral("2ImageK")}, 0507 {QStringLiteral("photoGpsLatitude"), QStringLiteral("2ImageL")}, 0508 {QStringLiteral("photoGpsLongitude"), QStringLiteral("2ImageM")}, 0509 {QStringLiteral("photoGpsAltitude"), QStringLiteral("2ImageN")}, 0510 {QStringLiteral("manufacturer"), QStringLiteral("2ImageO")}, 0511 {QStringLiteral("model"), QStringLiteral("2ImageP")}, 0512 0513 // Media Data 0514 {QStringLiteral("title"), QStringLiteral("3MediaA")}, 0515 {QStringLiteral("artist"), QStringLiteral("3MediaB")}, 0516 {QStringLiteral("album"), QStringLiteral("3MediaC")}, 0517 {QStringLiteral("albumArtist"), QStringLiteral("3MediaD")}, 0518 {QStringLiteral("genre"), QStringLiteral("3MediaE")}, 0519 {QStringLiteral("trackNumber"), QStringLiteral("3MediaF")}, 0520 {QStringLiteral("discNumber"), QStringLiteral("3MediaG")}, 0521 {QStringLiteral("releaseYear"), QStringLiteral("3MediaH")}, 0522 {QStringLiteral("duration"), QStringLiteral("3MediaI")}, 0523 {QStringLiteral("sampleRate"), QStringLiteral("3MediaJ")}, 0524 {QStringLiteral("bitRate"), QStringLiteral("3MediaK")}, 0525 0526 // Miscellaneous Data 0527 {QStringLiteral("originUrl"), QStringLiteral("4MiscA")}, 0528 }; 0529 0530 const QString val = uriGrouper.value(label); 0531 if (val.isEmpty()) { 0532 return QStringLiteral("lastGroup"); 0533 } 0534 return val; 0535 } 0536 0537 KFileItemList FileMetaDataProvider::items() const 0538 { 0539 return d->m_fileItems; 0540 } 0541 0542 void FileMetaDataProvider::setReadOnly(bool readOnly) 0543 { 0544 d->m_readOnly = readOnly; 0545 } 0546 0547 bool FileMetaDataProvider::isReadOnly() const 0548 { 0549 return d->m_readOnly; 0550 } 0551 0552 QVariantMap FileMetaDataProvider::data() const 0553 { 0554 return d->m_data; 0555 } 0556 0557 #include "moc_filemetadataprovider.cpp"