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"