File indexing completed on 2025-03-09 03:52:41
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2007-03-21 0007 * Description : Collection scanning to database - scan operations. 0008 * 0009 * SPDX-FileCopyrightText: 2005-2006 by Tom Albers <tomalbers at kde dot nl> 0010 * SPDX-FileCopyrightText: 2007-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0011 * SPDX-FileCopyrightText: 2009-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0012 * 0013 * SPDX-License-Identifier: GPL-2.0-or-later 0014 * 0015 * ============================================================ */ 0016 0017 #include "collectionscanner_p.h" 0018 0019 namespace Digikam 0020 { 0021 0022 void CollectionScanner::completeScan() 0023 { 0024 QElapsedTimer timer; 0025 timer.start(); 0026 0027 Q_EMIT startCompleteScan(); 0028 0029 { 0030 // lock database 0031 0032 CoreDbTransaction transaction; 0033 0034 mainEntryPoint(true); 0035 d->resetRemovedItemsTime(); 0036 } 0037 0038 // TODO: Implement a mechanism to watch for album root changes while we keep this list 0039 0040 QList<CollectionLocation> allLocations = CollectionManager::instance()->allAvailableLocations(); 0041 0042 if (d->wantSignals && d->needTotalFiles) 0043 { 0044 // count for progress info 0045 0046 int count = 0; 0047 0048 Q_FOREACH (const CollectionLocation& location, allLocations) 0049 { 0050 // cppcheck-suppress useStlAlgorithm 0051 count += countItemsInFolder(location.albumRootPath()); 0052 } 0053 0054 Q_EMIT totalFilesToScan(count); 0055 } 0056 0057 if (!d->checkObserver()) 0058 { 0059 Q_EMIT cancelled(); 0060 0061 return; 0062 } 0063 0064 // if we have no hints to follow, clean up all stale albums 0065 0066 if (!d->hints || !d->hints->hasAlbumHints()) 0067 { 0068 CoreDbAccess().db()->deleteStaleAlbums(); 0069 } 0070 0071 scanForStaleAlbums(allLocations); 0072 0073 if (!d->checkObserver()) 0074 { 0075 Q_EMIT cancelled(); 0076 0077 return; 0078 } 0079 0080 if (d->wantSignals) 0081 { 0082 Q_EMIT startScanningAlbumRoots(); 0083 } 0084 0085 Q_FOREACH (const CollectionLocation& location, allLocations) 0086 { 0087 scanAlbumRoot(location); 0088 } 0089 0090 // do not continue to clean up without a complete scan! 0091 0092 if (!d->checkObserver()) 0093 { 0094 Q_EMIT cancelled(); 0095 0096 return; 0097 } 0098 0099 if (d->deferredFileScanning) 0100 { 0101 qCDebug(DIGIKAM_DATABASE_LOG) << "Complete scan (file scanning deferred) took:" << timer.elapsed() << "msecs."; 0102 0103 Q_EMIT finishedCompleteScan(); 0104 0105 return; 0106 } 0107 0108 CoreDbTransaction transaction; 0109 completeScanCleanupPart(); 0110 0111 qCDebug(DIGIKAM_DATABASE_LOG) << "Complete scan took:" << timer.elapsed() << "msecs."; 0112 } 0113 0114 void CollectionScanner::finishCompleteScan(const QStringList& albumPaths) 0115 { 0116 Q_EMIT startCompleteScan(); 0117 0118 { 0119 // lock database 0120 0121 CoreDbTransaction transaction; 0122 0123 mainEntryPoint(true); 0124 d->resetRemovedItemsTime(); 0125 } 0126 0127 if (!d->checkObserver()) 0128 { 0129 Q_EMIT cancelled(); 0130 0131 return; 0132 } 0133 0134 if (d->wantSignals) 0135 { 0136 Q_EMIT startScanningAlbumRoots(); 0137 } 0138 0139 // remove subalbums from list if parent album is already contained 0140 0141 QStringList sortedPaths = albumPaths; 0142 std::sort(sortedPaths.begin(), sortedPaths.end()); 0143 QStringList::iterator it, it2; 0144 0145 for (it = sortedPaths.begin() ; it != sortedPaths.end() ; ) 0146 { 0147 // remove all following entries as long as they have the same beginning (= are subalbums) 0148 0149 for (it2 = it + 1 ; it2 != sortedPaths.end() && it2->startsWith(*it) ; ) 0150 { 0151 it2 = sortedPaths.erase(it2); 0152 } 0153 0154 it = it2; 0155 } 0156 0157 if (d->wantSignals && d->needTotalFiles) 0158 { 0159 // count for progress info 0160 0161 int count = 0; 0162 0163 Q_FOREACH (const QString& path, sortedPaths) 0164 { 0165 // cppcheck-suppress useStlAlgorithm 0166 count += countItemsInFolder(path); 0167 } 0168 0169 Q_EMIT totalFilesToScan(count); 0170 } 0171 0172 Q_FOREACH (const QString& path, sortedPaths) 0173 { 0174 CollectionLocation location = CollectionManager::instance()->locationForPath(path); 0175 QString album = CollectionManager::instance()->album(path); 0176 0177 if (album == QLatin1String("/")) 0178 { 0179 scanAlbumRoot(location); 0180 } 0181 else 0182 { 0183 scanAlbum(location, album); 0184 } 0185 } 0186 0187 // do not continue to clean up without a complete scan! 0188 0189 if (!d->checkObserver()) 0190 { 0191 Q_EMIT cancelled(); 0192 0193 return; 0194 } 0195 0196 CoreDbTransaction transaction; 0197 completeScanCleanupPart(); 0198 } 0199 0200 void CollectionScanner::completeScanCleanupPart() 0201 { 0202 completeHistoryScanning(); 0203 0204 updateRemovedItemsTime(); 0205 0206 // Items may be set to status removed, without being definitely deleted. 0207 // This deletion shall be done after a certain time, as checked by checkedDeleteRemoved 0208 0209 if (checkDeleteRemoved()) 0210 { 0211 // Mark items that are old enough and have the status trashed as obsolete 0212 // Only do this in a complete scan! 0213 0214 CoreDbAccess access; 0215 QList<qlonglong> trashedItems = access.db()->getImageIds(DatabaseItem::Status::Trashed); 0216 0217 Q_FOREACH (const qlonglong& item, trashedItems) 0218 { 0219 access.db()->setItemStatus(item, DatabaseItem::Status::Obsolete); 0220 } 0221 0222 resetDeleteRemovedSettings(); 0223 } 0224 else 0225 { 0226 // increment the count of complete scans during which removed items were not deleted 0227 0228 incrementDeleteRemovedCompleteScanCount(); 0229 } 0230 0231 markDatabaseAsScanned(); 0232 0233 Q_EMIT finishedCompleteScan(); 0234 } 0235 0236 void CollectionScanner::partialScan(const QString& filePath) 0237 { 0238 QString albumRoot = CollectionManager::instance()->albumRootPath(filePath); 0239 QString album = CollectionManager::instance()->album(filePath); 0240 partialScan(albumRoot, album); 0241 } 0242 0243 void CollectionScanner::partialScan(const QString& albumRoot, const QString& album) 0244 { 0245 if (albumRoot.isNull() || album.isEmpty()) 0246 { 0247 // If you want to scan the album root, pass "/" 0248 0249 qCWarning(DIGIKAM_DATABASE_LOG) << "partialScan(QString, QString) called with invalid values"; 0250 0251 return; 0252 } 0253 0254 /* 0255 if (CoreDbAccess().backend()->isInTransaction()) 0256 { 0257 // Install ScanController::instance()->suspendCollectionScan around your CoreDbTransaction 0258 0259 qCDebug(DIGIKAM_DATABASE_LOG) << "Detected an active database transaction when starting a collection scan. " 0260 "Please report this error."; 0261 0262 return; 0263 } 0264 */ 0265 0266 mainEntryPoint(false); 0267 d->resetRemovedItemsTime(); 0268 0269 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); 0270 0271 if (location.isNull()) 0272 { 0273 qCWarning(DIGIKAM_DATABASE_LOG) << "Did not find a CollectionLocation for album root path " << albumRoot; 0274 0275 return; 0276 } 0277 0278 // if we have no hints to follow, clean up all stale albums 0279 // Hint: Rethink with next major db update 0280 0281 if (!d->hints || !d->hints->hasAlbumHints()) 0282 { 0283 CoreDbAccess().db()->deleteStaleAlbums(); 0284 } 0285 0286 // Usually, we can restrict stale album scanning to our own location. 0287 // But when there are album hints from a second location to this location, 0288 // also scan the second location 0289 0290 QSet<int> locationIdsToScan; 0291 locationIdsToScan << location.id(); 0292 0293 if (d->hints) 0294 { 0295 QReadLocker locker(&d->hints->lock); 0296 QHash<CollectionScannerHints::DstPath, CollectionScannerHints::Album>::const_iterator it; 0297 0298 for (it = d->hints->albumHints.constBegin() ; it != d->hints->albumHints.constEnd() ; ++it) 0299 { 0300 if (it.key().albumRootId == location.id()) 0301 { 0302 locationIdsToScan << it.key().albumRootId; 0303 } 0304 } 0305 } 0306 0307 scanForStaleAlbums(locationIdsToScan.values()); 0308 0309 if (!d->checkObserver()) 0310 { 0311 Q_EMIT cancelled(); 0312 0313 return; 0314 } 0315 0316 if (album == QLatin1String("/")) 0317 { 0318 scanAlbumRoot(location); 0319 } 0320 else 0321 { 0322 scanAlbum(location, album); 0323 } 0324 0325 finishHistoryScanning(); 0326 0327 if (!d->checkObserver()) 0328 { 0329 Q_EMIT cancelled(); 0330 0331 return; 0332 } 0333 0334 updateRemovedItemsTime(); 0335 } 0336 0337 qlonglong CollectionScanner::scanFile(const QString& filePath, FileScanMode mode) 0338 { 0339 QFileInfo info(filePath); 0340 QString dirPath = info.path(); // strip off filename 0341 QString albumRoot = CollectionManager::instance()->albumRootPath(dirPath); 0342 0343 if (albumRoot.isNull()) 0344 { 0345 return -1; 0346 } 0347 0348 QString album = CollectionManager::instance()->album(dirPath); 0349 0350 return scanFile(albumRoot, album, info.fileName(), mode); 0351 } 0352 0353 qlonglong CollectionScanner::scanFile(const QString& albumRoot, const QString& album, 0354 const QString& fileName, FileScanMode mode) 0355 { 0356 if (album.isEmpty() || fileName.isEmpty()) 0357 { 0358 qCWarning(DIGIKAM_DATABASE_LOG) << "scanFile(QString, QString, QString) called with empty album or empty filename"; 0359 0360 return -1; 0361 } 0362 0363 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); 0364 0365 if (location.isNull()) 0366 { 0367 qCWarning(DIGIKAM_DATABASE_LOG) << "Did not find a CollectionLocation for album root path " << albumRoot; 0368 0369 return -1; 0370 } 0371 0372 QDir dir(location.albumRootPath() + album); 0373 QFileInfo fi(dir, fileName); 0374 0375 if (!fi.exists()) 0376 { 0377 qCWarning(DIGIKAM_DATABASE_LOG) << "File given to scan does not exist" << albumRoot << album << fileName; 0378 0379 return -1; 0380 } 0381 0382 int albumId = checkAlbum(location, album); 0383 qlonglong imageId = CoreDbAccess().db()->getImageId(albumId, fileName); 0384 imageId = scanFile(fi, albumId, imageId, mode); 0385 0386 return imageId; 0387 } 0388 0389 void CollectionScanner::scanFile(const ItemInfo& info, FileScanMode mode) 0390 { 0391 if (info.isNull() || !info.isLocationAvailable()) 0392 { 0393 return; 0394 } 0395 0396 QFileInfo fi(info.filePath()); 0397 scanFile(fi, info.albumId(), info.id(), mode); 0398 } 0399 0400 qlonglong CollectionScanner::scanFile(const QFileInfo& fi, int albumId, qlonglong imageId, FileScanMode mode) 0401 { 0402 mainEntryPoint(false); 0403 0404 if (!d->nameFilters.contains(fi.suffix().toLower())) 0405 { 0406 return -1; 0407 } 0408 0409 if (imageId == -1) 0410 { 0411 switch (mode) 0412 { 0413 case NormalScan: 0414 case ModifiedScan: 0415 imageId = scanNewFile(fi, albumId); 0416 break; 0417 0418 case Rescan: 0419 case CleanScan: 0420 imageId = scanNewFileFullScan(fi, albumId); 0421 break; 0422 } 0423 } 0424 else 0425 { 0426 ItemScanInfo scanInfo = CoreDbAccess().db()->getItemScanInfo(imageId); 0427 0428 switch (mode) 0429 { 0430 case NormalScan: 0431 scanFileNormal(fi, scanInfo); 0432 break; 0433 0434 case ModifiedScan: 0435 scanModifiedFile(fi, scanInfo); 0436 break; 0437 0438 case Rescan: 0439 rescanFile(fi, scanInfo); 0440 break; 0441 0442 case CleanScan: 0443 cleanScanFile(fi, scanInfo); 0444 break; 0445 } 0446 } 0447 0448 finishHistoryScanning(); 0449 0450 return imageId; 0451 } 0452 0453 void CollectionScanner::scanAlbumRoot(const CollectionLocation& location) 0454 { 0455 if (d->wantSignals) 0456 { 0457 Q_EMIT startScanningAlbumRoot(location.albumRootPath()); 0458 } 0459 0460 QMap<QString, QDateTime>::const_iterator it; 0461 const QMap<QString, QDateTime>& pathDateMap = CoreDbAccess().db()-> 0462 getAlbumModificationMap(location.id()); 0463 bool useFastScan = MetaEngineSettings::instance()->settings().useFastScan; 0464 0465 if (!useFastScan || !d->performFastScan || pathDateMap.isEmpty()) 0466 { 0467 scanAlbum(location, QLatin1String("/")); 0468 } 0469 else 0470 { 0471 for (it = pathDateMap.constBegin() ; it != pathDateMap.constEnd() ; ++it) 0472 { 0473 QDateTime modified; 0474 QString folder(location.albumRootPath() + it.key()); 0475 0476 if (d->albumDateCache.contains(folder)) 0477 { 0478 modified = d->albumDateCache.value(folder); 0479 } 0480 else 0481 { 0482 modified = QFileInfo(folder).lastModified(); 0483 modified.setTimeSpec(Qt::UTC); 0484 } 0485 0486 if (s_modificationDateEquals(modified, it.value())) 0487 { 0488 int albumID = CoreDbAccess().db()->getAlbumForPath(location.id(), it.key(), false); 0489 int counter = CoreDbAccess().db()->getNumberOfItemsInAlbum(albumID); 0490 0491 d->scannedAlbums << albumID; 0492 0493 if (d->wantSignals) 0494 { 0495 Q_EMIT scannedFiles(counter + 1); 0496 } 0497 } 0498 else 0499 { 0500 scanAlbum(location, it.key(), true); 0501 } 0502 } 0503 } 0504 0505 if (d->wantSignals) 0506 { 0507 Q_EMIT finishedScanningAlbumRoot(location.albumRootPath()); 0508 } 0509 } 0510 0511 void CollectionScanner::scanForStaleAlbums(const QList<CollectionLocation>& locations) 0512 { 0513 QList<int> locationIdsToScan; 0514 0515 Q_FOREACH (const CollectionLocation& location, locations) 0516 { 0517 locationIdsToScan << location.id(); 0518 } 0519 0520 scanForStaleAlbums(locationIdsToScan); 0521 } 0522 0523 void CollectionScanner::scanForStaleAlbums(const QList<int>& locationIdsToScan) 0524 { 0525 if (d->wantSignals) 0526 { 0527 Q_EMIT startScanningForStaleAlbums(); 0528 } 0529 0530 QList<AlbumShortInfo> albumList = CoreDbAccess().db()->getAlbumShortInfos(); 0531 QList<int> toBeDeleted; 0532 int counter = 0; 0533 0534 if (d->wantSignals && d->needTotalFiles) 0535 { 0536 Q_EMIT totalFilesToScan(albumList.count()); 0537 } 0538 0539 QList<AlbumShortInfo>::const_iterator it3; 0540 0541 for (it3 = albumList.constBegin() ; it3 != albumList.constEnd() ; ++it3) 0542 { 0543 ++counter; 0544 0545 if (d->wantSignals && counter && (counter % 10 == 0)) 0546 { 0547 Q_EMIT scannedFiles(counter); 0548 counter = 0; 0549 } 0550 0551 if (!locationIdsToScan.contains((*it3).albumRootId) || toBeDeleted.contains((*it3).id)) 0552 { 0553 continue; 0554 } 0555 0556 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId((*it3).albumRootId); 0557 0558 // Only handle albums on available locations 0559 0560 if (location.isAvailable()) 0561 { 0562 QFileInfo fileInfo(location.albumRootPath() + (*it3).relativePath); 0563 bool dirExist = (fileInfo.exists() && fileInfo.isDir()); 0564 0565 if (location.asQtCaseSensitivity() == Qt::CaseInsensitive) 0566 { 0567 if (dirExist && !(*it3).relativePath.endsWith(QLatin1Char('/'))) 0568 { 0569 QDir dir(fileInfo.dir()); 0570 dirExist = dir.entryList(QDir::Dirs | 0571 QDir::NoDotAndDotDot) 0572 .contains(fileInfo.fileName()); 0573 } 0574 } 0575 0576 // let digikam think that ignored directories got deleted 0577 // (if they already exist in the database, this will delete them) 0578 0579 if (!dirExist || d->ignoreDirectory.contains(fileInfo.fileName())) 0580 { 0581 // We have an ignored album, all sub-albums have to be ignored 0582 0583 QList<int> subAlbums = CoreDbAccess().db()->getAlbumAndSubalbumsForPath((*it3).albumRootId, 0584 (*it3).relativePath); 0585 toBeDeleted << subAlbums; 0586 d->scannedAlbums << subAlbums; 0587 } 0588 else 0589 { 0590 QDateTime dateTime = fileInfo.lastModified(); 0591 dateTime.setTimeSpec(Qt::UTC); 0592 d->albumDateCache.insert(fileInfo.filePath(), dateTime); 0593 } 0594 } 0595 } 0596 0597 // At this point, it is important to handle album renames. 0598 // We can still copy over album attributes later, but we cannot identify 0599 // the former album of removed images. 0600 // Just renaming the album is also much cheaper than rescanning all files. 0601 0602 if (!toBeDeleted.isEmpty() && d->hints) 0603 { 0604 // shallow copy for reading without caring for locks 0605 0606 QHash<CollectionScannerHints::DstPath, CollectionScannerHints::Album> albumHints; 0607 { 0608 QReadLocker locker(&d->hints->lock); 0609 albumHints = d->hints->albumHints; 0610 } 0611 0612 // go through all album copy/move hints 0613 0614 QHash<CollectionScannerHints::DstPath, CollectionScannerHints::Album>::const_iterator it; 0615 int toBeDeletedIndex; 0616 0617 for (it = albumHints.constBegin() ; it != albumHints.constEnd() ; ++it) 0618 { 0619 // if the src entry of a hint is found in toBeDeleted, we have a move/rename, no copy. Handle these here. 0620 0621 toBeDeletedIndex = toBeDeleted.indexOf(it.value().albumId); 0622 0623 // We must double check that not, for some reason, the target album has already been scanned. 0624 0625 QList<AlbumShortInfo>::const_iterator it2; 0626 0627 for (it2 = albumList.constBegin() ; it2 != albumList.constEnd() ; ++it2) 0628 { 0629 if ((it2->albumRootId == it.key().albumRootId) && 0630 (it2->relativePath == it.key().relativePath)) 0631 { 0632 toBeDeletedIndex = -1; 0633 break; 0634 } 0635 } 0636 0637 if (toBeDeletedIndex != -1) 0638 { 0639 // check for existence of target 0640 0641 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId(it.key().albumRootId); 0642 0643 if (location.isAvailable()) 0644 { 0645 QFileInfo fileInfo(location.albumRootPath() + it.key().relativePath); 0646 bool dirExist = (fileInfo.exists() && fileInfo.isDir()); 0647 0648 if (location.asQtCaseSensitivity() == Qt::CaseInsensitive) 0649 { 0650 if (dirExist && !it.key().relativePath.endsWith(QLatin1Char('/'))) 0651 { 0652 QDir dir(fileInfo.dir()); 0653 dirExist = dir.entryList(QDir::Dirs | 0654 QDir::NoDotAndDotDot) 0655 .contains(fileInfo.fileName()); 0656 } 0657 } 0658 0659 if (dirExist) 0660 { 0661 // Just set a new root/relativePath to the album. Further scanning will care for all cases or error. 0662 0663 CoreDbAccess().db()->renameAlbum(it.value().albumId, it.key().albumRootId, it.key().relativePath); 0664 0665 // No need any more to delete the album 0666 0667 toBeDeleted.removeAt(toBeDeletedIndex); 0668 } 0669 } 0670 } 0671 } 0672 } 0673 0674 safelyRemoveAlbums(toBeDeleted); 0675 0676 if (d->wantSignals) 0677 { 0678 Q_EMIT finishedScanningForStaleAlbums(); 0679 } 0680 } 0681 0682 void CollectionScanner::scanAlbum(const CollectionLocation& location, const QString& album, bool checkDate) 0683 { 0684 // + Adds album if it does not yet exist in the db. 0685 // + Recursively scans subalbums of album. 0686 // + Adds files if they do not yet exist in the db. 0687 // + Marks stale files as removed 0688 0689 QDir dir(location.albumRootPath() + album); 0690 0691 if (!dir.exists() || !dir.isReadable()) 0692 { 0693 qCWarning(DIGIKAM_DATABASE_LOG) << "Folder does not exist or is not readable: " 0694 << dir.path(); 0695 return; 0696 } 0697 0698 if (d->wantSignals) 0699 { 0700 Q_EMIT startScanningAlbum(location.albumRootPath(), album); 0701 } 0702 0703 int albumID = checkAlbum(location, album); 0704 QDateTime albumDateTime = QFileInfo(dir.path()).lastModified(); 0705 albumDateTime.setTimeSpec(Qt::UTC); 0706 QDateTime albumModified = CoreDbAccess().db()->getAlbumModificationDate(albumID); 0707 0708 if (checkDate && s_modificationDateEquals(albumDateTime, albumModified)) 0709 { 0710 // mark album as scanned 0711 0712 d->scannedAlbums << albumID; 0713 0714 if (d->wantSignals) 0715 { 0716 Q_EMIT finishedScanningAlbum(location.albumRootPath(), album, 1); 0717 } 0718 0719 return; 0720 } 0721 0722 const QList<ItemScanInfo>& scanInfos = CoreDbAccess().db()->getItemScanInfos(albumID); 0723 MetaEngineSettingsContainer settings = MetaEngineSettings::instance()->settings(); 0724 QHash<QString, int> fileNameIndexHash; 0725 QSet<qlonglong> itemIdSet; 0726 0727 // create a QHash filename -> index in list 0728 0729 for (int i = 0 ; i < scanInfos.size() ; ++i) 0730 { 0731 fileNameIndexHash[scanInfos.at(i).itemName] = i; 0732 itemIdSet << scanInfos.at(i).id; 0733 } 0734 0735 const QFileInfoList& list = dir.entryInfoList(QDir::Dirs | 0736 QDir::Files | 0737 QDir::NoDotAndDotDot, 0738 QDir::Name | QDir::DirsLast); 0739 0740 int counter = 0; 0741 bool updateAlbumDate = false; 0742 QDate albumDateOld = albumDateTime.date(); 0743 QDate albumDateNew = albumDateTime.date(); 0744 const QString xmpExt(QLatin1String(".xmp")); 0745 0746 Q_FOREACH (const QFileInfo& info, list) 0747 { 0748 if (!d->checkObserver()) 0749 { 0750 return; // return directly, do not go to cleanup code after loop! 0751 } 0752 0753 if (info.isFile()) 0754 { 0755 // filter with name filter 0756 0757 if (!d->nameFilters.contains(info.suffix().toLower())) 0758 { 0759 continue; 0760 } 0761 0762 ++counter; 0763 0764 if (d->wantSignals && counter && (counter % 100 == 0)) 0765 { 0766 Q_EMIT scannedFiles(counter); 0767 counter = 0; 0768 } 0769 0770 int index = fileNameIndexHash.value(info.fileName(), -1); 0771 0772 if (index != -1) 0773 { 0774 // mark item as "seen" 0775 0776 itemIdSet.remove(scanInfos.at(index).id); 0777 0778 bool hasSidecar = false; 0779 const QFileInfo* sinfo = nullptr; 0780 0781 if (settings.useXMPSidecar4Reading) 0782 { 0783 QString sidecarName; 0784 0785 if (!settings.useCompatibleFileName) 0786 { 0787 sidecarName = info.fileName() + xmpExt; 0788 } 0789 else 0790 { 0791 sidecarName = info.completeBaseName() + xmpExt; 0792 } 0793 0794 for (int i = 0 ; i < list.size() ; ++i) 0795 { 0796 if (list.at(i).fileName() == sidecarName) 0797 { 0798 sinfo = &list.at(i); 0799 hasSidecar = true; 0800 0801 break; 0802 } 0803 } 0804 } 0805 0806 scanFileNormal(info, scanInfos.at(index), hasSidecar, sinfo); 0807 } 0808 else if (info.completeSuffix().contains(QLatin1String("digikamtempfile."))) 0809 { 0810 // ignore temp files we created ourselves 0811 0812 continue; 0813 } 0814 else 0815 { 0816 // Read the creation date of each image to determine the oldest one 0817 0818 qlonglong imageId = scanNewFile(info, albumID); 0819 0820 if (imageId > 0) 0821 { 0822 ItemInfo itemInfo(imageId); 0823 QDate itemDate = itemInfo.dateTime().date(); 0824 0825 if (itemDate.isValid()) 0826 { 0827 if ((settings.albumDateFrom == MetaEngineSettingsContainer::NewestItemDate) || 0828 (settings.albumDateFrom == MetaEngineSettingsContainer::AverageDate)) 0829 { 0830 // Change album date only if the item date is newer. 0831 0832 if (itemDate > albumDateNew) 0833 { 0834 albumDateNew = itemDate; 0835 updateAlbumDate = true; 0836 } 0837 } 0838 0839 if ((settings.albumDateFrom == MetaEngineSettingsContainer::OldestItemDate) || 0840 (settings.albumDateFrom == MetaEngineSettingsContainer::AverageDate)) 0841 { 0842 // Change album date only if the item date is older. 0843 0844 if (itemDate < albumDateOld) 0845 { 0846 albumDateOld = itemDate; 0847 updateAlbumDate = true; 0848 } 0849 } 0850 } 0851 } 0852 0853 // Q_EMIT signals for scanned files with much higher granularity 0854 0855 if (d->wantSignals && counter && (counter % 2 == 0)) 0856 { 0857 Q_EMIT scannedFiles(counter); 0858 counter = 0; 0859 } 0860 } 0861 } 0862 else if (info.isDir()) 0863 { 0864 0865 #ifdef Q_OS_WIN 0866 0867 // Hide album that starts with a dot, as under Linux. 0868 0869 if (info.fileName().startsWith(QLatin1Char('.'))) 0870 { 0871 continue; 0872 } 0873 0874 #endif 0875 0876 if (d->ignoreDirectory.contains(info.fileName())) 0877 { 0878 continue; 0879 } 0880 0881 ++counter; 0882 0883 QString subAlbum = album; 0884 0885 if (subAlbum != QLatin1String("/")) 0886 { 0887 subAlbum += QLatin1Char('/'); 0888 } 0889 0890 scanAlbum(location, subAlbum + info.fileName(), checkDate); 0891 } 0892 } 0893 0894 if (!d->deferredFileScanning && !s_modificationDateEquals(albumDateTime, albumModified)) 0895 { 0896 CoreDbAccess().db()->setAlbumModificationDate(albumID, albumDateTime); 0897 } 0898 0899 if (updateAlbumDate) 0900 { 0901 // Write the new album date from the image information 0902 0903 if (settings.albumDateFrom == MetaEngineSettingsContainer::OldestItemDate) 0904 { 0905 CoreDbAccess().db()->setAlbumDate(albumID, albumDateOld); 0906 } 0907 else if (settings.albumDateFrom == MetaEngineSettingsContainer::NewestItemDate) 0908 { 0909 CoreDbAccess().db()->setAlbumDate(albumID, albumDateNew); 0910 } 0911 else if (settings.albumDateFrom == MetaEngineSettingsContainer::AverageDate) 0912 { 0913 qint64 julianDayCount = albumDateOld.toJulianDay(); 0914 julianDayCount += albumDateNew.toJulianDay(); 0915 0916 CoreDbAccess().db()->setAlbumDate(albumID, QDate::fromJulianDay(julianDayCount / 2)); 0917 } 0918 else if (settings.albumDateFrom == MetaEngineSettingsContainer::FolderDate) 0919 { 0920 CoreDbAccess().db()->setAlbumDate(albumID, albumDateTime.date()); 0921 } 0922 } 0923 0924 if (d->wantSignals && counter) 0925 { 0926 Q_EMIT scannedFiles(counter); 0927 } 0928 0929 // Mark items in the db which we did not see on disk. 0930 0931 if (!itemIdSet.isEmpty()) 0932 { 0933 QList<qlonglong> ids = itemIdSet.values(); 0934 CoreDbOperationGroup group; 0935 CoreDbAccess().db()->removeItems(ids, QList<int>() << albumID); 0936 itemsWereRemoved(ids); 0937 } 0938 0939 // mark album as scanned 0940 0941 d->scannedAlbums << albumID; 0942 0943 if (d->wantSignals) 0944 { 0945 Q_EMIT finishedScanningAlbum(location.albumRootPath(), album, list.count()); 0946 } 0947 } 0948 0949 void CollectionScanner::scanFileNormal(const QFileInfo& fi, const ItemScanInfo& scanInfo, 0950 bool checkSidecar, const QFileInfo* const sidecarInfo) 0951 { 0952 bool hasAnyHint = d->hints && d->hints->hasAnyNormalHint(scanInfo.id); 0953 QDateTime modificationDate = fi.lastModified(); 0954 modificationDate.setTimeSpec(Qt::UTC); 0955 0956 // if the date is null, this signals a full rescan 0957 0958 if (scanInfo.modificationDate.isNull() || 0959 (hasAnyHint && d->hints->hasRescanHint(scanInfo.id))) 0960 { 0961 if (hasAnyHint) 0962 { 0963 QWriteLocker locker(&d->hints->lock); 0964 d->hints->rescanItemHints.remove(scanInfo.id); 0965 } 0966 0967 rescanFile(fi, scanInfo); 0968 0969 return; 0970 } 0971 else if (hasAnyHint && d->hints->hasModificationHint(scanInfo.id)) 0972 { 0973 { 0974 QWriteLocker locker(&d->hints->lock); 0975 d->hints->modifiedItemHints.remove(scanInfo.id); 0976 } 0977 0978 scanModifiedFile(fi, scanInfo); 0979 0980 return; 0981 } 0982 else if (hasAnyHint) // metadata adjustment hints 0983 { 0984 if (d->hints->hasMetadataAboutToAdjustHint(scanInfo.id)) 0985 { 0986 // postpone scan 0987 0988 return; 0989 } 0990 else // hasMetadataAdjustedHint 0991 { 0992 { 0993 QWriteLocker locker(&d->hints->lock); 0994 d->hints->metadataAdjustedHints.remove(scanInfo.id); 0995 } 0996 0997 scanFileUpdateHashReuseThumbnail(fi, scanInfo, true); 0998 0999 return; 1000 } 1001 } 1002 else if (d->updatingHashHint) 1003 { 1004 // if the file need not be scanned because of modification, update the hash 1005 1006 if (s_modificationDateEquals(modificationDate, scanInfo.modificationDate) && 1007 (fi.size() == scanInfo.fileSize)) 1008 { 1009 scanFileUpdateHashReuseThumbnail(fi, scanInfo, false); 1010 1011 return; 1012 } 1013 } 1014 1015 MetaEngineSettingsContainer settings = MetaEngineSettings::instance()->settings(); 1016 1017 if (checkSidecar && settings.useXMPSidecar4Reading) 1018 { 1019 if (sidecarInfo) 1020 { 1021 QDateTime sidecarDate = sidecarInfo->lastModified(); 1022 sidecarDate.setTimeSpec(Qt::UTC); 1023 1024 if (sidecarDate > modificationDate) 1025 { 1026 modificationDate = sidecarDate; 1027 } 1028 } 1029 else if (DMetadata::hasSidecar(fi.filePath())) 1030 { 1031 QString filePath = DMetadata::sidecarPath(fi.filePath()); 1032 QDateTime sidecarDate = QFileInfo(filePath).lastModified(); 1033 sidecarDate.setTimeSpec(Qt::UTC); 1034 1035 if (sidecarDate > modificationDate) 1036 { 1037 modificationDate = sidecarDate; 1038 } 1039 } 1040 } 1041 1042 if (!s_modificationDateEquals(modificationDate, scanInfo.modificationDate) || 1043 (fi.size() != scanInfo.fileSize)) 1044 { 1045 if (settings.rescanImageIfModified) 1046 { 1047 cleanScanFile(fi, scanInfo); 1048 } 1049 else 1050 { 1051 scanModifiedFile(fi, scanInfo); 1052 } 1053 } 1054 } 1055 1056 qlonglong CollectionScanner::scanNewFile(const QFileInfo& info, int albumId) 1057 { 1058 if (d->checkDeferred(info)) 1059 { 1060 return -1; 1061 } 1062 1063 ItemScanner scanner(info); 1064 scanner.setCategory(category(info)); 1065 1066 // Check copy/move hints for single items 1067 1068 qlonglong srcId = 0; 1069 1070 if (d->hints) 1071 { 1072 QReadLocker locker(&d->hints->lock); 1073 srcId = d->hints->itemHints.value(NewlyAppearedFile(albumId, info.fileName())); 1074 } 1075 1076 if (srcId > 0) 1077 { 1078 scanner.copiedFrom(albumId, srcId); 1079 } 1080 else 1081 { 1082 // Check copy/move hints for whole albums 1083 1084 int srcAlbum = d->establishedSourceAlbums.value(albumId); 1085 1086 if (srcAlbum) 1087 { 1088 // if we have one source album, find out if there is a file with the same name 1089 1090 srcId = CoreDbAccess().db()->getImageId(srcAlbum, info.fileName()); 1091 } 1092 1093 if (srcId > 0) 1094 { 1095 scanner.copiedFrom(albumId, srcId); 1096 } 1097 else 1098 { 1099 // Establishing identity with the unique hash 1100 1101 scanner.newFile(albumId); 1102 } 1103 } 1104 1105 d->finishScanner(scanner); 1106 d->newIdsList << scanner.id(); 1107 1108 return scanner.id(); 1109 } 1110 1111 qlonglong CollectionScanner::scanNewFileFullScan(const QFileInfo& info, int albumId) 1112 { 1113 if (d->checkDeferred(info)) 1114 { 1115 return -1; 1116 } 1117 1118 ItemScanner scanner(info); 1119 scanner.setCategory(category(info)); 1120 scanner.newFileFullScan(albumId); 1121 d->finishScanner(scanner); 1122 1123 return scanner.id(); 1124 } 1125 1126 void CollectionScanner::scanModifiedFile(const QFileInfo& info, const ItemScanInfo& scanInfo) 1127 { 1128 if (d->checkDeferred(info)) 1129 { 1130 return; 1131 } 1132 1133 ItemScanner scanner(info, scanInfo); 1134 scanner.setCategory(category(info)); 1135 scanner.fileModified(); 1136 d->finishScanner(scanner); 1137 } 1138 1139 void CollectionScanner::scanFileUpdateHashReuseThumbnail(const QFileInfo& info, const ItemScanInfo& scanInfo, 1140 bool fileWasEdited) 1141 { 1142 QString oldHash = scanInfo.uniqueHash; 1143 qlonglong oldSize = scanInfo.fileSize; 1144 1145 // same code as scanModifiedFile 1146 1147 ItemScanner scanner(info, scanInfo); 1148 scanner.setCategory(category(info)); 1149 scanner.fileModified(); 1150 1151 QString newHash = scanner.itemScanInfo().uniqueHash; 1152 qlonglong newSize = scanner.itemScanInfo().fileSize; 1153 1154 if (ThumbsDbAccess::isInitialized()) 1155 { 1156 if (fileWasEdited) 1157 { 1158 // The file was edited in such a way that we know that the pixel content did not change, so we can reuse the thumbnail. 1159 // We need to add a link to the thumbnail data with the new hash/file size _and_ adjust 1160 // the file modification date in the data table. 1161 1162 ThumbsDbInfo thumbDbInfo = ThumbsDbAccess().db()->findByHash(oldHash, oldSize); 1163 1164 if (thumbDbInfo.id != -1) 1165 { 1166 ThumbsDbAccess().db()->insertUniqueHash(newHash, newSize, thumbDbInfo.id); 1167 ThumbsDbAccess().db()->updateModificationDate(thumbDbInfo.id, scanner.itemScanInfo().modificationDate); 1168 1169 // TODO: also update details thumbnails (by file path and URL scheme) 1170 } 1171 } 1172 else 1173 { 1174 ThumbsDbAccess().db()->replaceUniqueHash(oldHash, oldSize, newHash, newSize); 1175 } 1176 } 1177 1178 d->finishScanner(scanner); 1179 } 1180 1181 void CollectionScanner::cleanScanFile(const QFileInfo& info, const ItemScanInfo& scanInfo) 1182 { 1183 if (d->checkDeferred(info)) 1184 { 1185 return; 1186 } 1187 1188 ItemScanner scanner(info, scanInfo); 1189 scanner.setCategory(category(info)); 1190 scanner.cleanScan(); 1191 d->finishScanner(scanner); 1192 } 1193 1194 void CollectionScanner::rescanFile(const QFileInfo& info, const ItemScanInfo& scanInfo) 1195 { 1196 if (d->checkDeferred(info)) 1197 { 1198 return; 1199 } 1200 1201 ItemScanner scanner(info, scanInfo); 1202 scanner.setCategory(category(info)); 1203 scanner.rescan(); 1204 d->finishScanner(scanner); 1205 } 1206 1207 void CollectionScanner::completeHistoryScanning() 1208 { 1209 // scan tagged images 1210 1211 int needResolvingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needResolvingHistory()); 1212 int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); 1213 1214 QList<qlonglong> ids = CoreDbAccess().db()->getItemIDsInTag(needResolvingTag); 1215 historyScanningStage2(ids); 1216 1217 ids = CoreDbAccess().db()->getItemIDsInTag(needTaggingTag); 1218 qCDebug(DIGIKAM_DATABASE_LOG) << "items to tag" << ids; 1219 historyScanningStage3(ids); 1220 } 1221 1222 void CollectionScanner::finishHistoryScanning() 1223 { 1224 // scan recorded ids 1225 1226 QList<qlonglong> ids; 1227 1228 // stage 2 1229 1230 ids = d->needResolveHistorySet.values(); 1231 d->needResolveHistorySet.clear(); 1232 historyScanningStage2(ids); 1233 1234 if (!d->checkObserver()) 1235 { 1236 return; 1237 } 1238 1239 // stage 3 1240 1241 ids = d->needTaggingHistorySet.values(); 1242 d->needTaggingHistorySet.clear(); 1243 historyScanningStage3(ids); 1244 } 1245 1246 void CollectionScanner::historyScanningStage2(const QList<qlonglong>& ids) 1247 { 1248 Q_FOREACH (const qlonglong& id, ids) 1249 { 1250 if (!d->checkObserver()) 1251 { 1252 return; 1253 } 1254 1255 CoreDbOperationGroup group; 1256 1257 if (d->recordHistoryIds) 1258 { 1259 QList<qlonglong> needTaggingIds; 1260 ItemScanner::resolveImageHistory(id, &needTaggingIds); 1261 1262 Q_FOREACH (const qlonglong& needTag, needTaggingIds) 1263 { 1264 d->needTaggingHistorySet << needTag; 1265 } 1266 } 1267 else 1268 { 1269 ItemScanner::resolveImageHistory(id); 1270 } 1271 } 1272 } 1273 1274 void CollectionScanner::historyScanningStage3(const QList<qlonglong>& ids) 1275 { 1276 Q_FOREACH (const qlonglong& id, ids) 1277 { 1278 if (!d->checkObserver()) 1279 { 1280 return; 1281 } 1282 1283 CoreDbOperationGroup group; 1284 ItemScanner::tagItemHistoryGraph(id); 1285 } 1286 } 1287 1288 bool CollectionScanner::databaseInitialScanDone() 1289 { 1290 CoreDbAccess access; 1291 1292 return !access.db()->getSetting(QLatin1String("Scanned")).isEmpty(); 1293 } 1294 1295 } // namespace Digikam