File indexing completed on 2025-01-05 03:59:36

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2009 Bastian Holst <bastianholst@gmx.de>
0004 //
0005 
0006 // Own
0007 #include "FileStorageWatcher.h"
0008 
0009 // Qt
0010 #include <QDir>
0011 #include <QDirIterator>
0012 #include <QFileInfo>
0013 #include <QTimer>
0014 
0015 // Marble
0016 #include "MarbleGlobal.h"
0017 #include "MarbleDirs.h"
0018 
0019 #include "digikam_debug.h"
0020 
0021 using namespace Marble;
0022 
0023 // Only remove 20 files without checking
0024 // changed cacheLimits and changed themes etc.
0025 static const int maxFilesDelete = 20;
0026 static const int softLimitPercent = 5;
0027 
0028 
0029 // Methods of FileStorageWatcherThread
0030 FileStorageWatcherThread::FileStorageWatcherThread( const QString &dataDirectory, QObject *parent )
0031     : QObject( parent ),
0032       m_dataDirectory( dataDirectory ),
0033       m_deleting( false ),
0034       m_willQuit( false )
0035 {
0036     // For now setting cache limit to 0. This won't delete anything
0037     setCacheLimit( 0 );
0038 
0039     connect( this, SIGNAL(variableChanged()),
0040          this, SLOT(ensureCacheSize()),
0041          Qt::QueuedConnection );
0042 }
0043 
0044 FileStorageWatcherThread::~FileStorageWatcherThread()
0045 {
0046 }
0047 
0048 quint64 FileStorageWatcherThread::cacheLimit()
0049 {
0050     return m_cacheLimit;
0051 }
0052 
0053 void FileStorageWatcherThread::setCacheLimit( quint64 bytes )
0054 {
0055     m_limitMutex.lock();
0056     m_cacheLimit = bytes;
0057     m_cacheSoftLimit = bytes * ( 100 - softLimitPercent ) / 100;
0058     m_limitMutex.unlock();
0059     Q_EMIT variableChanged();
0060 }
0061 
0062 void FileStorageWatcherThread::addToCurrentSize( qint64 bytes )
0063 {
0064 //     qCDebug(DIGIKAM_MARBLE_LOG) << "Current cache size changed by " << bytes;
0065     qint64 changedSize = bytes + m_currentCacheSize;
0066     if( changedSize >= 0 )
0067     m_currentCacheSize = changedSize;
0068     else
0069     m_currentCacheSize = 0;
0070     Q_EMIT variableChanged();
0071 }
0072 
0073 void FileStorageWatcherThread::resetCurrentSize()
0074 {
0075     m_currentCacheSize = 0;
0076     Q_EMIT variableChanged();
0077 }
0078 
0079 void FileStorageWatcherThread::prepareQuit()
0080 {
0081     m_willQuit = true;
0082 }
0083 
0084 void FileStorageWatcherThread::getCurrentCacheSize()
0085 {
0086     qCDebug(DIGIKAM_MARBLE_LOG) << "FileStorageWatcher: Creating cache size";
0087     quint64 dataSize = 0;
0088     const QString basePath = m_dataDirectory + QLatin1String("/maps");
0089     QDirIterator it( basePath,
0090                      QDir::Files | QDir::Writable,
0091                      QDirIterator::Subdirectories );
0092 
0093     const int basePathDepth = basePath.split(QLatin1Char('/')).size();
0094     while( it.hasNext() && !m_willQuit ) {
0095         it.next();
0096         QFileInfo file = it.fileInfo();
0097         // We try to be very careful and just delete images
0098         QString suffix = file.suffix().toLower();
0099         const QStringList path = file.path().split(QLatin1Char('/'));
0100 
0101         // planet/theme/tilelevel should be deeper than 4
0102         if ( path.size() > basePathDepth + 3 ) {
0103             bool ok = false;
0104             int tileLevel = path[basePathDepth + 2].toInt(&ok);
0105             // internal theme layer case
0106             // (e.g. "earth/openseamap/seamarks/4")
0107             if (!ok) tileLevel = path[basePathDepth + 3].toInt(&ok);
0108             if ((ok && tileLevel >= maxBaseTileLevel ) &&
0109               (suffix == QLatin1String("jpg") ||
0110                suffix == QLatin1String("png") ||
0111                suffix == QLatin1String("gif") ||
0112                suffix == QLatin1String("svg") ||
0113                suffix == QLatin1String("o5m"))) {
0114                 dataSize += file.size();
0115                 m_filesCache.insert(file.lastModified(), file.absoluteFilePath());
0116             }
0117         }
0118     }
0119     m_currentCacheSize = dataSize;
0120 }
0121 
0122 void FileStorageWatcherThread::ensureCacheSize()
0123 {
0124 //     qCDebug(DIGIKAM_MARBLE_LOG) << "Size of tile cache: " << m_currentCacheSize;
0125     // We start deleting files if m_currentCacheSize is larger than
0126     // the hard cache limit. Then we delete files until our cache size
0127     // is smaller than the cache (soft) limit.
0128     // m_cacheLimit = 0 means no limit.
0129     if( ( ( m_currentCacheSize > m_cacheLimit )
0130          || ( m_deleting && ( m_currentCacheSize > m_cacheSoftLimit ) ) )
0131     && ( m_cacheLimit != 0 )
0132     && ( m_cacheSoftLimit != 0 )
0133     && !m_willQuit ) {
0134 
0135         qCDebug(DIGIKAM_MARBLE_LOG) << "Deleting extra cached tiles";
0136         // The counter for deleted files
0137         m_filesDeleted = 0;
0138         // We have not reached our soft limit, yet.
0139         m_deleting = true;
0140 
0141         // We iterate over the m_filesCache which is sorted by lastModified
0142         // and remove a chunk of the oldest 20 (maxFilesDelete) files.
0143         QMultiMap<QDateTime, QString>::iterator it= m_filesCache.begin();
0144         while ( it != m_filesCache.end() &&
0145                 keepDeleting() ) {
0146             QString filePath = it.value();
0147             QFileInfo info( filePath );
0148 
0149             ++m_filesDeleted;
0150             m_currentCacheSize -= info.size();
0151             it = m_filesCache.erase(it);
0152             bool success = QFile::remove( filePath );
0153             if (!success) {
0154                 qCDebug(DIGIKAM_MARBLE_LOG) << "Failed to remove:" << filePath;
0155             }
0156         }
0157         // There might be more chunks left for deletion which we
0158         // process with a delay to account for for load-reduction.
0159         if( m_filesDeleted >= maxFilesDelete ) {
0160             QTimer::singleShot( 1000, this, SLOT(ensureCacheSize()) );
0161             return;
0162         }
0163         else {
0164             // A partial chunk is reached at the end of m_filesCache.
0165             // At this point deletion is done.
0166             m_deleting = false;
0167         }
0168 
0169         // If the current Cache Size is still larger than the cacheSoftLimit
0170         // then our requested cacheSoftLimit is unreachable.
0171         if( m_currentCacheSize > m_cacheSoftLimit ) {
0172             qCDebug(DIGIKAM_MARBLE_LOG) << "FileStorageWatcher: Requested Cache Limit could not be reached!";
0173             qCDebug(DIGIKAM_MARBLE_LOG) << "Increasing Cache Limit to prevent further futile attempts.";
0174             // Softlimit is now exactly on the current cache size.
0175             setCacheLimit( m_currentCacheSize / ( 100 - softLimitPercent ) * 100 );
0176         }
0177     }
0178 }
0179 
0180 bool FileStorageWatcherThread::keepDeleting() const
0181 {
0182     return ( ( m_currentCacheSize > m_cacheSoftLimit ) &&
0183          ( m_filesDeleted < maxFilesDelete ) &&
0184               !m_willQuit );
0185 }
0186 // End of methods of our Thread
0187 
0188 
0189 // Beginning of Methods of the main class
0190 FileStorageWatcher::FileStorageWatcher( const QString &dataDirectory, QObject * parent )
0191     : QThread( parent ),
0192       m_dataDirectory( dataDirectory )
0193 {
0194     if ( m_dataDirectory.isEmpty() )
0195         m_dataDirectory = MarbleDirs::localPath() + QLatin1String("/cache/");
0196 
0197     if ( ! QDir( m_dataDirectory ).exists() )
0198         QDir::root().mkpath( m_dataDirectory );
0199 
0200     m_started = false;
0201     m_limitMutex = new QMutex();
0202 
0203     m_thread = nullptr;
0204     m_quitting = false;
0205 }
0206 
0207 FileStorageWatcher::~FileStorageWatcher()
0208 {
0209     qCDebug(DIGIKAM_MARBLE_LOG) << "Deleting FileStorageWatcher";
0210 
0211     // Making sure that Thread is stopped.
0212     m_quitting = true;
0213 
0214     if( m_thread )
0215     m_thread->prepareQuit();
0216     quit();
0217     if( !wait( 5000 ) ) {
0218     qCDebug(DIGIKAM_MARBLE_LOG) << "Failed to stop FileStorageWatcher-Thread, terminating!";
0219     terminate();
0220     }
0221 
0222     delete m_thread;
0223 
0224     delete m_limitMutex;
0225 }
0226 
0227 void FileStorageWatcher::setCacheLimit( quint64 bytes )
0228 {
0229     QMutexLocker locker( m_limitMutex );
0230     if( m_started )
0231     // This is done directly to ensure that a running ensureCacheSize()
0232     // recognizes the new size.
0233     m_thread->setCacheLimit( bytes );
0234     // Save the limit, thread has to be initialized with the right one.
0235     m_limit = bytes;
0236 }
0237 
0238 quint64 FileStorageWatcher::cacheLimit()
0239 {
0240     if( m_started )
0241     return m_thread->cacheLimit();
0242     else
0243     return m_limit;
0244 }
0245 
0246 void FileStorageWatcher::addToCurrentSize( qint64 bytes )
0247 {
0248     Q_EMIT sizeChanged( bytes );
0249 }
0250 
0251 void FileStorageWatcher::resetCurrentSize()
0252 {
0253     Q_EMIT cleared();
0254 }
0255 
0256 void FileStorageWatcher::run()
0257 {
0258     m_thread = new FileStorageWatcherThread( m_dataDirectory );
0259     if( !m_quitting ) {
0260         m_limitMutex->lock();
0261         m_thread->setCacheLimit( m_limit );
0262         m_started = true;
0263         m_limitMutex->unlock();
0264 
0265         m_thread->getCurrentCacheSize();
0266 
0267         connect( this, SIGNAL(sizeChanged(qint64)),
0268                  m_thread, SLOT(addToCurrentSize(qint64)) );
0269         connect( this, SIGNAL(cleared()),
0270                  m_thread, SLOT(resetCurrentSize()) );
0271 
0272         // Make sure that we don't want to stop process.
0273         // The thread wouldn't exit from event loop.
0274         if( !m_quitting )
0275             exec();
0276 
0277         m_started = false;
0278     }
0279     delete m_thread;
0280     m_thread = nullptr;
0281 }
0282 // End of all methods
0283 
0284 #include "moc_FileStorageWatcher.cpp"