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