File indexing completed on 2024-05-12 15:55:37

0001 // SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
0002 // SPDX-FileCopyrightText: 2021-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "ThumbnailCache.h"
0007 
0008 #include <kpabase/Logging.h>
0009 #include <kpabase/SettingsData.h>
0010 
0011 #include <QBuffer>
0012 #include <QCache>
0013 #include <QDir>
0014 #include <QElapsedTimer>
0015 #include <QFile>
0016 #include <QMutexLocker>
0017 #include <QPixmap>
0018 #include <QTemporaryFile>
0019 #include <QTimer>
0020 
0021 namespace
0022 {
0023 
0024 // We split the thumbnails into chunks to avoid a huge file changing over and over again, with a bad hit for backups
0025 constexpr int MAX_FILE_SIZE = 32 * 1024 * 1024;
0026 constexpr int THUMBNAIL_FILE_VERSION_MIN = 4;
0027 // We map some thumbnail files into memory and manage them in a least-recently-used fashion
0028 constexpr size_t LRU_SIZE = 2;
0029 
0030 constexpr int THUMBNAIL_CACHE_SAVE_INTERNAL_MS = (5 * 1000);
0031 
0032 constexpr auto INDEXFILE_NAME = "thumbnailindex";
0033 constexpr QFileDevice::Permissions FILE_PERMISSIONS { QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther };
0034 }
0035 
0036 namespace ImageManager
0037 {
0038 /**
0039  * The ThumbnailMapping wraps the memory-mapped data of a QFile.
0040  * Upon initialization with a file name, the corresponding file is opened
0041  * and its contents mapped into memory (as a QByteArray).
0042  *
0043  * Deleting the ThumbnailMapping unmaps the memory and closes the file.
0044  */
0045 class ThumbnailMapping
0046 {
0047 public:
0048     explicit ThumbnailMapping(const QString &filename)
0049         : file(filename)
0050         , map(nullptr)
0051     {
0052         if (!file.open(QIODevice::ReadOnly))
0053             qCWarning(ImageManagerLog, "Failed to open thumbnail file");
0054 
0055         uchar *data = file.map(0, file.size());
0056         if (!data || QFile::NoError != file.error()) {
0057             qCWarning(ImageManagerLog, "Failed to map thumbnail file");
0058         } else {
0059             map = QByteArray::fromRawData(reinterpret_cast<const char *>(data), file.size());
0060         }
0061     }
0062     bool isValid()
0063     {
0064         return !map.isEmpty();
0065     }
0066     // we need to keep the file around to keep the data mapped:
0067     QFile file;
0068     QByteArray map;
0069 };
0070 
0071 QString defaultThumbnailDirectory()
0072 {
0073     return QString::fromLatin1(".thumbnails/");
0074 }
0075 }
0076 
0077 ImageManager::ThumbnailCache::ThumbnailCache(const QString &baseDirectory)
0078     : m_baseDir(baseDirectory)
0079     , m_currentFile(0)
0080     , m_currentOffset(0)
0081     , m_timer(new QTimer)
0082     , m_needsFullSave(true)
0083     , m_isDirty(false)
0084     , m_memcache(new QCache<int, ThumbnailMapping>(LRU_SIZE))
0085     , m_currentWriter(nullptr)
0086 {
0087     if (!m_baseDir.exists()) {
0088         if (!QDir().mkpath(m_baseDir.path())) {
0089             qCWarning(ImageManagerLog, "Failed to create thumbnail cache directory!");
0090         }
0091     }
0092 
0093     // set a default value for version 4 files and new databases:
0094     m_thumbnailSize = Settings::SettingsData::instance()->thumbnailSize();
0095 
0096     load();
0097     connect(this, &ImageManager::ThumbnailCache::doSave, this, &ImageManager::ThumbnailCache::saveImpl);
0098     connect(m_timer, &QTimer::timeout, this, &ImageManager::ThumbnailCache::saveImpl);
0099     m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
0100     m_timer->setSingleShot(true);
0101     m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
0102 }
0103 
0104 ImageManager::ThumbnailCache::~ThumbnailCache()
0105 {
0106     m_needsFullSave = true;
0107     saveInternal();
0108     delete m_memcache;
0109     delete m_timer;
0110     if (m_currentWriter)
0111         delete m_currentWriter;
0112 }
0113 
0114 void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QImage &image)
0115 {
0116     if (image.isNull()) {
0117         qCWarning(ImageManagerLog) << "Thumbnail for file" << name.relative() << "is invalid!";
0118         return;
0119     }
0120 
0121     QByteArray data;
0122     QBuffer buffer(&data);
0123     bool OK = buffer.open(QIODevice::WriteOnly);
0124     Q_ASSERT(OK);
0125     Q_UNUSED(OK);
0126 
0127     OK = image.save(&buffer, "JPG");
0128     Q_ASSERT(OK);
0129 
0130     insert(name, data);
0131 }
0132 
0133 void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QByteArray &thumbnailData)
0134 {
0135     if (thumbnailData.isNull()) {
0136         qCWarning(ImageManagerLog) << "Thumbnail data for file" << name.relative() << "is invalid!";
0137         return;
0138     }
0139     QMutexLocker thumbnailLocker(&m_thumbnailWriterLock);
0140     if (!m_currentWriter) {
0141         m_currentWriter = new QFile(fileNameForIndex(m_currentFile));
0142         if (!m_currentWriter->open(QIODevice::ReadWrite)) {
0143             qCWarning(ImageManagerLog, "Failed to open thumbnail file for inserting");
0144             return;
0145         }
0146         if (!m_currentWriter->setPermissions(FILE_PERMISSIONS)) {
0147             qCWarning(ImageManagerLog) << "Could not set permissions on thumbnail file" << m_currentWriter->fileName();
0148         }
0149     }
0150     if (!m_currentWriter->seek(m_currentOffset)) {
0151         qCWarning(ImageManagerLog, "Failed to seek in thumbnail file");
0152         return;
0153     }
0154 
0155     QMutexLocker dataLocker(&m_dataLock);
0156     // purge in-memory cache for the current file:
0157     m_memcache->remove(m_currentFile);
0158 
0159     const int sizeBytes = thumbnailData.size();
0160     if (!(m_currentWriter->write(thumbnailData.data(), sizeBytes) == sizeBytes && m_currentWriter->flush())) {
0161         qCWarning(ImageManagerLog, "Failed to write image data to thumbnail file");
0162         return;
0163     }
0164 
0165     if (m_currentOffset + sizeBytes > MAX_FILE_SIZE) {
0166         delete m_currentWriter;
0167         m_currentWriter = nullptr;
0168     }
0169     thumbnailLocker.unlock();
0170 
0171     if (m_hash.contains(name)) {
0172         CacheFileInfo info = m_hash[name];
0173         if (info.fileIndex == m_currentFile && info.offset == m_currentOffset && info.size == sizeBytes) {
0174             qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << "but no change in information";
0175             dataLocker.unlock();
0176             return;
0177         } else {
0178             // File has moved; incremental save does no good.
0179             // Either the image file has changed and with it the thumbnail, or
0180             // this is a video file and a different frame has been selected as thumbnail
0181             qCDebug(ImageManagerLog) << "Setting new thumbnail for image " << name.relative() << ", need full save! ";
0182             QMutexLocker saveLocker(&m_saveLock);
0183             m_needsFullSave = true;
0184         }
0185     }
0186 
0187     m_hash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, sizeBytes));
0188     m_isDirty = true;
0189 
0190     m_unsavedHash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, sizeBytes));
0191 
0192     // Update offset
0193     m_currentOffset += sizeBytes;
0194     if (m_currentOffset > MAX_FILE_SIZE) {
0195         m_currentFile++;
0196         m_currentOffset = 0;
0197     }
0198     int unsaved = m_unsavedHash.count();
0199     dataLocker.unlock();
0200 
0201     // Thumbnail building is a lot faster now.  Even on an HDD this corresponds to less
0202     // than 1 minute of work.
0203     //
0204     // We need to call the internal version that does not interact with the timer.
0205     // We can't simply signal from here because if we're in the middle of loading new
0206     // images the signal won't get invoked until we return to the main application loop.
0207     if (unsaved >= 100) {
0208         saveInternal();
0209     }
0210 
0211     Q_EMIT thumbnailUpdated(name);
0212 }
0213 
0214 QString ImageManager::ThumbnailCache::fileNameForIndex(int index) const
0215 {
0216     return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index));
0217 }
0218 
0219 QPixmap ImageManager::ThumbnailCache::lookup(const DB::FileName &name) const
0220 {
0221     auto array = lookupRawData(name);
0222     if (array.isNull())
0223         return QPixmap();
0224 
0225     QBuffer buffer(&array);
0226     buffer.open(QIODevice::ReadOnly);
0227     QImage image;
0228     image.load(&buffer, "JPG");
0229 
0230     // Notice the above image is sharing the bits with the file, so I can't just return it as it then will be invalid when the file goes out of scope.
0231     // PENDING(blackie) Is that still true?
0232     return QPixmap::fromImage(image);
0233 }
0234 
0235 QByteArray ImageManager::ThumbnailCache::lookupRawData(const DB::FileName &name) const
0236 {
0237     m_dataLock.lock();
0238     CacheFileInfo info = m_hash[name];
0239     m_dataLock.unlock();
0240 
0241     ThumbnailMapping *t = m_memcache->object(info.fileIndex);
0242     if (!t || !t->isValid()) {
0243         t = new ThumbnailMapping(fileNameForIndex(info.fileIndex));
0244         if (!t->isValid()) {
0245             delete t;
0246             qCWarning(ImageManagerLog, "Failed to map thumbnail file");
0247             return QByteArray();
0248         }
0249         m_memcache->insert(info.fileIndex, t);
0250     }
0251     QByteArray array(t->map.mid(info.offset, info.size));
0252     return array;
0253 }
0254 
0255 void ImageManager::ThumbnailCache::saveFull()
0256 {
0257     QElapsedTimer timer;
0258     timer.start();
0259     // First ensure that any dirty thumbnails are written to disk
0260     QMutexLocker thumbnailLocker(&m_thumbnailWriterLock);
0261     if (m_currentWriter) {
0262         delete m_currentWriter;
0263         m_currentWriter = nullptr;
0264     }
0265     thumbnailLocker.unlock();
0266 
0267     QMutexLocker dataLocker(&m_dataLock);
0268     if (!m_isDirty) {
0269         qCDebug(ImageManagerLog) << "ThumbnailCache::saveFull(): cache not dirty.";
0270         return;
0271     }
0272     QTemporaryFile file;
0273     if (!file.open()) {
0274         qCWarning(ImageManagerLog, "Failed to create temporary file");
0275         return;
0276     }
0277     QHash<DB::FileName, CacheFileInfo> tempHash = m_hash;
0278 
0279     m_unsavedHash.clear();
0280     m_needsFullSave = false;
0281     // Clear the dirty flag early so that we can allow further work to proceed.
0282     // If the save fails, we'll set the dirty flag again.
0283     m_isDirty = false;
0284     m_fileVersion = preferredFileVersion();
0285     dataLocker.unlock();
0286 
0287     QDataStream stream(&file);
0288     stream << preferredFileVersion()
0289            << m_thumbnailSize
0290            << m_currentFile
0291            << m_currentOffset
0292            << m_hash.count();
0293 
0294     for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) {
0295         const CacheFileInfo &cacheInfo = it.value();
0296         stream << it.key().relative()
0297                << cacheInfo.fileIndex
0298                << cacheInfo.offset
0299                << cacheInfo.size;
0300     }
0301     file.close();
0302 
0303     const QString realFileName = thumbnailPath(INDEXFILE_NAME);
0304     QFile::remove(realFileName);
0305     bool success = false;
0306     if (!file.copy(realFileName)) {
0307         qCWarning(ImageManagerLog, "Failed to copy the temporary file %s to %s", qPrintable(file.fileName()), qPrintable(realFileName));
0308     } else {
0309         QFile realFile(realFileName);
0310         if (!realFile.open(QIODevice::ReadOnly)) {
0311             qCWarning(ImageManagerLog, "Could not open the file %s for reading!", qPrintable(realFileName));
0312         } else {
0313             if (!realFile.setPermissions(FILE_PERMISSIONS)) {
0314                 qCWarning(ImageManagerLog, "Could not set permissions on file %s!", qPrintable(realFileName));
0315             } else {
0316                 realFile.close();
0317                 qCDebug(ImageManagerLog) << "ThumbnailCache::saveFull(): cache saved.";
0318                 qCDebug(TimingLog, "Saved thumbnail cache with %d images in %f seconds", size(), timer.elapsed() / 1000.0);
0319                 Q_EMIT saveComplete();
0320                 success = true;
0321             }
0322         }
0323     }
0324     if (!success) {
0325         dataLocker.relock();
0326         m_isDirty = true;
0327         m_needsFullSave = true;
0328     }
0329 }
0330 
0331 // Incremental save does *not* clear the dirty flag.  We always want to do a full
0332 // save eventually.
0333 void ImageManager::ThumbnailCache::saveIncremental()
0334 {
0335     QMutexLocker thumbnailLocker(&m_thumbnailWriterLock);
0336     if (m_currentWriter) {
0337         delete m_currentWriter;
0338         m_currentWriter = nullptr;
0339     }
0340     thumbnailLocker.unlock();
0341 
0342     QMutexLocker dataLocker(&m_dataLock);
0343     if (m_unsavedHash.count() == 0) {
0344         return;
0345     }
0346     QHash<DB::FileName, CacheFileInfo> tempUnsavedHash = m_unsavedHash;
0347     m_unsavedHash.clear();
0348     m_isDirty = true;
0349 
0350     const QString realFileName = thumbnailPath(INDEXFILE_NAME);
0351     QFile file(realFileName);
0352     if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) {
0353         qCWarning(ImageManagerLog, "Failed to open thumbnail cache for appending");
0354         m_needsFullSave = true;
0355         return;
0356     }
0357     QDataStream stream(&file);
0358     for (auto it = tempUnsavedHash.constBegin(); it != tempUnsavedHash.constEnd(); ++it) {
0359         const CacheFileInfo &cacheInfo = it.value();
0360         stream << it.key().relative()
0361                << cacheInfo.fileIndex
0362                << cacheInfo.offset
0363                << cacheInfo.size;
0364     }
0365     file.close();
0366 }
0367 
0368 void ImageManager::ThumbnailCache::saveInternal()
0369 {
0370     QMutexLocker saveLocker(&m_saveLock);
0371     const QString realFileName = thumbnailPath(INDEXFILE_NAME);
0372     // If something has asked for a full save, do it!
0373     if (m_needsFullSave || !QFile(realFileName).exists()) {
0374         saveFull();
0375     } else {
0376         saveIncremental();
0377     }
0378 }
0379 
0380 void ImageManager::ThumbnailCache::saveImpl()
0381 {
0382     m_timer->stop();
0383     saveInternal();
0384     m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
0385     m_timer->setSingleShot(true);
0386     m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS);
0387 }
0388 
0389 void ImageManager::ThumbnailCache::save()
0390 {
0391     QMutexLocker saveLocker(&m_saveLock);
0392     m_needsFullSave = true;
0393     saveLocker.unlock();
0394     Q_EMIT doSave();
0395 }
0396 
0397 void ImageManager::ThumbnailCache::load()
0398 {
0399     QFile file(thumbnailPath(INDEXFILE_NAME));
0400     if (!file.exists()) {
0401         qCWarning(ImageManagerLog) << "Thumbnail index file" << file.fileName() << "not found!";
0402         return;
0403     }
0404 
0405     QElapsedTimer timer;
0406     timer.start();
0407     if (!file.open(QIODevice::ReadOnly)) {
0408         qCWarning(ImageManagerLog) << "Could not open thumbnail index file" << file.fileName() << "!";
0409         return;
0410     }
0411     QDataStream stream(&file);
0412     stream >> m_fileVersion;
0413 
0414     if (m_fileVersion != preferredFileVersion() && m_fileVersion != THUMBNAIL_FILE_VERSION_MIN) {
0415         qCWarning(ImageManagerLog) << "Thumbnail index version" << m_fileVersion << "can not be used. Discarding...";
0416         return; // Discard cache
0417     }
0418 
0419     // We can't allow anything to modify the structure while we're doing this.
0420     QMutexLocker dataLocker(&m_dataLock);
0421 
0422     if (m_fileVersion == THUMBNAIL_FILE_VERSION_MIN) {
0423         qCInfo(ImageManagerLog) << "Loading thumbnail index version " << m_fileVersion
0424                                 << "- assuming thumbnail size" << m_thumbnailSize << "px";
0425     } else {
0426         stream >> m_thumbnailSize;
0427         qCDebug(ImageManagerLog) << "Thumbnail cache has thumbnail size" << m_thumbnailSize << "px";
0428     }
0429 
0430     int expectedCount = 0;
0431     stream >> m_currentFile
0432         >> m_currentOffset
0433         >> expectedCount;
0434     int count = 0;
0435 
0436     while (!stream.atEnd()) {
0437         QString name;
0438         int fileIndex;
0439         int offset;
0440         int size;
0441         stream >> name
0442             >> fileIndex
0443             >> offset
0444             >> size;
0445 
0446         // qCDebug(ImageManagerLog) << "Adding file to index:" << name
0447         //                          << "(index/offset/size:" << fileIndex << "/" << offset << "/" << size << ")";
0448         m_hash.insert(DB::FileName::fromRelativePath(name), CacheFileInfo(fileIndex, offset, size));
0449         if (fileIndex > m_currentFile) {
0450             m_currentFile = fileIndex;
0451             m_currentOffset = offset + size;
0452         } else if (fileIndex == m_currentFile && offset + size > m_currentOffset) {
0453             m_currentOffset = offset + size;
0454         }
0455         if (m_currentOffset > MAX_FILE_SIZE) {
0456             m_currentFile++;
0457             m_currentOffset = 0;
0458         }
0459         count++;
0460     }
0461     qCDebug(TimingLog, "Loaded %d (expected: %d) thumbnails in %f seconds", count, expectedCount, timer.elapsed() / 1000.0);
0462 }
0463 
0464 bool ImageManager::ThumbnailCache::contains(const DB::FileName &name) const
0465 {
0466     QMutexLocker dataLocker(&m_dataLock);
0467     bool answer = m_hash.contains(name);
0468     return answer;
0469 }
0470 
0471 QString ImageManager::ThumbnailCache::thumbnailPath(const char *utf8FileName) const
0472 {
0473     return m_baseDir.filePath(QString::fromUtf8(utf8FileName));
0474 }
0475 
0476 QString ImageManager::ThumbnailCache::thumbnailPath(const QString &file) const
0477 {
0478     return m_baseDir.filePath(file);
0479 }
0480 
0481 int ImageManager::ThumbnailCache::thumbnailSize() const
0482 {
0483     return m_thumbnailSize;
0484 }
0485 
0486 int ImageManager::ThumbnailCache::actualFileVersion() const
0487 {
0488     return m_fileVersion;
0489 }
0490 
0491 int ImageManager::ThumbnailCache::preferredFileVersion()
0492 {
0493     return 5;
0494 }
0495 
0496 DB::FileNameList ImageManager::ThumbnailCache::findIncorrectlySizedThumbnails() const
0497 {
0498     QMutexLocker dataLocker(&m_dataLock);
0499     const QHash<DB::FileName, CacheFileInfo> tempHash = m_hash;
0500     dataLocker.unlock();
0501 
0502     // accessing the data directly instead of using the lookupRawData() method
0503     // may be more efficient, but this method should be called rarely
0504     // and readability therefore trumps performance
0505     DB::FileNameList resultList;
0506     for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) {
0507         const auto filename = it.key();
0508         auto jpegData = lookupRawData(filename);
0509         Q_ASSERT(!jpegData.isNull());
0510 
0511         QBuffer buffer(&jpegData);
0512         buffer.open(QIODevice::ReadOnly);
0513         QImage image;
0514         image.load(&buffer, "JPG");
0515         const auto size = image.size();
0516         if (size.width() != m_thumbnailSize && size.height() != m_thumbnailSize) {
0517             qCDebug(ImageManagerLog) << "Thumbnail for file " << filename.relative() << "has incorrect size:" << size;
0518             resultList.append(filename);
0519         }
0520     }
0521 
0522     return resultList;
0523 }
0524 
0525 int ImageManager::ThumbnailCache::size() const
0526 {
0527     QMutexLocker dataLocker(&m_dataLock);
0528     return m_hash.size();
0529 }
0530 
0531 void ImageManager::ThumbnailCache::vacuum()
0532 {
0533     QMutexLocker dataLocker(&m_dataLock);
0534     while (m_isDirty) {
0535         dataLocker.unlock();
0536         saveFull();
0537         dataLocker.relock();
0538     }
0539     QElapsedTimer timer;
0540     timer.start();
0541 
0542     long oldStorageSize = 0;
0543     const auto backupSuffix = QChar::fromLatin1('~');
0544     // save what we need
0545     for (int i = 0; i <= m_currentFile; ++i) {
0546         const auto cacheFile = fileNameForIndex(i);
0547         oldStorageSize += QFileInfo(cacheFile).size();
0548         QFile::rename(cacheFile, cacheFile + backupSuffix);
0549     }
0550 
0551     const int maxFileIndex = m_currentFile;
0552     // we need to store the filename besides the cache file info so that we can reinsert it later
0553     struct RichCacheFileInfo {
0554         CacheFileInfo info;
0555         DB::FileName name;
0556     };
0557     QList<RichCacheFileInfo> cacheEntries;
0558     for (auto it = m_hash.constKeyValueBegin(); it != m_hash.constKeyValueEnd(); ++it) {
0559         cacheEntries.append(RichCacheFileInfo { (*it).second, (*it).first });
0560     }
0561     // sort for sequential I/O:
0562     std::sort(cacheEntries.begin(), cacheEntries.end(), [](RichCacheFileInfo a, RichCacheFileInfo b) { return a.info.fileIndex < b.info.fileIndex || (a.info.fileIndex == b.info.fileIndex && a.info.offset < b.info.offset); });
0563 
0564     // flush the cache manually (cache files have been moved already)
0565     m_currentFile = 0;
0566     m_currentOffset = 0;
0567     m_isDirty = true;
0568     m_hash.clear();
0569     m_unsavedHash.clear();
0570     m_memcache->clear();
0571     dataLocker.unlock();
0572 
0573     // rebuild
0574     int currentFileIndex { -1 };
0575     ThumbnailMapping *currentFile { nullptr };
0576     for (const auto &entry : qAsConst(cacheEntries)) {
0577         Q_ASSERT(entry.info.fileIndex != -1);
0578         if (entry.info.fileIndex != currentFileIndex) {
0579             currentFileIndex = entry.info.fileIndex;
0580             if (currentFile)
0581                 delete currentFile;
0582             currentFile = new ThumbnailMapping(fileNameForIndex(currentFileIndex) + backupSuffix);
0583         }
0584 
0585         const QByteArray imageData(currentFile->map.mid(entry.info.offset, entry.info.size));
0586         insert(entry.name, imageData);
0587     }
0588     if (currentFile)
0589         delete currentFile;
0590 
0591     qCDebug(TimingLog, "Rewrote %d thumbnails in %f seconds", size(), timer.elapsed() / 1000.0);
0592     long newStorageSize = 0;
0593     for (int i = 0; i <= m_currentFile; ++i) {
0594         const auto cacheFile = fileNameForIndex(i);
0595         newStorageSize += QFileInfo(cacheFile).size();
0596     }
0597     qCDebug(ImageManagerLog, "Thumbnail storage used %ld bytes in %d files before and %ld bytes in %d files after operation.", oldStorageSize, maxFileIndex, newStorageSize, m_currentFile);
0598     qCDebug(ImageManagerLog, "Size reduction: %.2f%%", 100.0 * (oldStorageSize - newStorageSize) / oldStorageSize);
0599     for (int i = 0; i <= maxFileIndex; ++i) {
0600         const auto cacheFile = fileNameForIndex(i);
0601         QFile::remove(cacheFile + backupSuffix);
0602     }
0603     save();
0604 }
0605 
0606 void ImageManager::ThumbnailCache::flush()
0607 {
0608     QMutexLocker dataLocker(&m_dataLock);
0609     for (int i = 0; i <= m_currentFile; ++i)
0610         QFile::remove(fileNameForIndex(i));
0611     m_currentFile = 0;
0612     m_currentOffset = 0;
0613     m_isDirty = true;
0614     m_hash.clear();
0615     m_unsavedHash.clear();
0616     m_memcache->clear();
0617     dataLocker.unlock();
0618     save();
0619     Q_EMIT cacheFlushed();
0620 }
0621 
0622 void ImageManager::ThumbnailCache::removeThumbnail(const DB::FileName &fileName)
0623 {
0624     QMutexLocker dataLocker(&m_dataLock);
0625     m_isDirty = true;
0626     m_hash.remove(fileName);
0627     dataLocker.unlock();
0628     save();
0629 }
0630 void ImageManager::ThumbnailCache::removeThumbnails(const DB::FileNameList &files)
0631 {
0632     QMutexLocker dataLocker(&m_dataLock);
0633     m_isDirty = true;
0634     for (const DB::FileName &fileName : files) {
0635         m_hash.remove(fileName);
0636     }
0637     dataLocker.unlock();
0638     save();
0639 }
0640 
0641 void ImageManager::ThumbnailCache::setThumbnailSize(int thumbSize)
0642 {
0643     if (thumbSize < 0)
0644         return;
0645 
0646     if (thumbSize != m_thumbnailSize) {
0647         m_thumbnailSize = thumbSize;
0648         flush();
0649         Q_EMIT cacheInvalidated();
0650     }
0651 }
0652 // vi:expandtab:tabstop=4 shiftwidth=4:
0653 
0654 #include "moc_ThumbnailCache.cpp"