Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qnetworkdiskcache.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5//#define QNETWORKDISKCACHE_DEBUG
6
7
10
11#include <qfile.h>
12#include <qdir.h>
13#include <qdatastream.h>
14#include <qdatetime.h>
15#include <qdirlisting.h>
16#include <qurl.h>
17#include <qcryptographichash.h>
18#include <qdebug.h>
19
20#include <memory>
21
22#define CACHE_POSTFIX ".d"_L1
23#define CACHE_VERSION 8
24#define DATA_DIR "data"_L1
25
26#define MAX_COMPRESSION_SIZE (1024 * 1024 * 3)
27
29
30using namespace Qt::StringLiterals;
31
32/*!
33 \class QNetworkDiskCachePrivate
34 \internal
35*/
36
37/*!
38 \class QNetworkDiskCache
39 \since 4.5
40 \inmodule QtNetwork
41
42 \brief The QNetworkDiskCache class provides a very basic disk cache.
43
44 QNetworkDiskCache stores each url in its own file inside of the
45 cacheDirectory using QDataStream. Files with a text MimeType
46 are compressed using qCompress. Data is written to disk only in insert()
47 and updateMetaData().
48
49 Currently you cannot share the same cache files with more than
50 one disk cache.
51
52 QNetworkDiskCache by default limits the amount of space that the cache will
53 use on the system to 50MB.
54
55 Note you have to set the cache directory before it will work.
56
57 A network disk cache can be enabled by:
58
59 \snippet code/src_network_access_qnetworkdiskcache.cpp 0
60
61 When sending requests, to control the preference of when to use the cache
62 and when to use the network, consider the following:
63
64 \snippet code/src_network_access_qnetworkdiskcache.cpp 1
65
66 To check whether the response came from the cache or from the network, the
67 following can be applied:
68
69 \snippet code/src_network_access_qnetworkdiskcache.cpp 2
70*/
71
72/*!
73 Creates a new disk cache. The \a parent argument is passed to
74 QAbstractNetworkCache's constructor.
75 */
76QNetworkDiskCache::QNetworkDiskCache(QObject *parent)
77 : QAbstractNetworkCache(*new QNetworkDiskCachePrivate, parent)
78{
79}
80
81/*!
82 Destroys the cache object. This does not clear the disk cache.
83 */
84QNetworkDiskCache::~QNetworkDiskCache()
85{
86 Q_D(QNetworkDiskCache);
87 qDeleteAll(d->inserting);
88}
89
90/*!
91 Returns the location where cached files will be stored.
92*/
93QString QNetworkDiskCache::cacheDirectory() const
94{
95 Q_D(const QNetworkDiskCache);
96 return d->cacheDirectory;
97}
98
99/*!
100 Sets the directory where cached files will be stored to \a cacheDir
101
102 QNetworkDiskCache will create this directory if it does not exists.
103
104 Prepared cache items will be stored in the new cache directory when
105 they are inserted.
106
107 \sa QStandardPaths::CacheLocation
108*/
109void QNetworkDiskCache::setCacheDirectory(const QString &cacheDir)
110{
111#if defined(QNETWORKDISKCACHE_DEBUG)
112 qDebug() << "QNetworkDiskCache::setCacheDirectory()" << cacheDir;
113#endif
114 Q_D(QNetworkDiskCache);
115 if (cacheDir.isEmpty())
116 return;
117 d->cacheDirectory = cacheDir;
118 QDir dir(d->cacheDirectory);
119 d->cacheDirectory = dir.absolutePath();
120 if (!d->cacheDirectory.endsWith(u'/'))
121 d->cacheDirectory += u'/';
122
123 d->dataDirectory = d->cacheDirectory + DATA_DIR + QString::number(CACHE_VERSION) + u'/';
124 d->prepareLayout();
125}
126
127/*!
128 \reimp
129*/
130qint64 QNetworkDiskCache::cacheSize() const
131{
132#if defined(QNETWORKDISKCACHE_DEBUG)
133 qDebug("QNetworkDiskCache::cacheSize()");
134#endif
135 Q_D(const QNetworkDiskCache);
136 if (d->cacheDirectory.isEmpty())
137 return 0;
138 if (d->currentCacheSize < 0) {
139 QNetworkDiskCache *that = const_cast<QNetworkDiskCache*>(this);
140 that->d_func()->currentCacheSize = that->expire();
141 }
142 return d->currentCacheSize;
143}
144
145/*!
146 \reimp
147*/
148QIODevice *QNetworkDiskCache::prepare(const QNetworkCacheMetaData &metaData)
149{
150#if defined(QNETWORKDISKCACHE_DEBUG)
151 qDebug() << "QNetworkDiskCache::prepare()" << metaData.url();
152#endif
153 Q_D(QNetworkDiskCache);
154 if (!metaData.isValid() || !metaData.url().isValid() || !metaData.saveToDisk())
155 return nullptr;
156
157 if (d->cacheDirectory.isEmpty()) {
158 qWarning("QNetworkDiskCache::prepare() The cache directory is not set");
159 return nullptr;
160 }
161
162 const auto sizeValue = metaData.headers().value(QHttpHeaders::WellKnownHeader::ContentLength);
163 const qint64 size = sizeValue.toLongLong();
164 if (size > (maximumCacheSize() * 3)/4)
165 return nullptr;
166
167 std::unique_ptr<QCacheItem> cacheItem = std::make_unique<QCacheItem>();
168 cacheItem->metaData = metaData;
169
170 QIODevice *device = nullptr;
171 if (cacheItem->canCompress()) {
172 cacheItem->data.open(QBuffer::ReadWrite);
173 device = &(cacheItem->data);
174 } else {
175 QString fileName = d->cacheFileName(cacheItem->metaData.url());
176 cacheItem->file = new(std::nothrow) QSaveFile(fileName, &cacheItem->data);
177 if (!cacheItem->file || !cacheItem->file->open(QFileDevice::WriteOnly)) {
178 qWarning("QNetworkDiskCache::prepare() unable to open temporary file");
179 cacheItem.reset();
180 return nullptr;
181 }
182 cacheItem->writeHeader(cacheItem->file);
183 device = cacheItem->file;
184 }
185 d->inserting[device] = cacheItem.release();
186 return device;
187}
188
189/*!
190 \reimp
191*/
192void QNetworkDiskCache::insert(QIODevice *device)
193{
194#if defined(QNETWORKDISKCACHE_DEBUG)
195 qDebug() << "QNetworkDiskCache::insert()" << device;
196#endif
197 Q_D(QNetworkDiskCache);
198 const auto it = d->inserting.constFind(device);
199 if (Q_UNLIKELY(it == d->inserting.cend())) {
200 qWarning() << "QNetworkDiskCache::insert() called on a device we don't know about" << device;
201 return;
202 }
203
204 d->storeItem(it.value());
205 delete it.value();
206 d->inserting.erase(it);
207}
208
209
210/*!
211 Create subdirectories and other housekeeping on the filesystem.
212 Prevents too many files from being present in any single directory.
213*/
215{
216 QDir helper;
217
218 //Create directory and subdirectories 0-F
219 helper.mkpath(dataDirectory);
220 for (uint i = 0; i < 16 ; i++) {
221 QString str = QString::number(i, 16);
222 QString subdir = dataDirectory + str;
223 helper.mkdir(subdir);
224 }
225}
226
227
228void QNetworkDiskCachePrivate::storeItem(QCacheItem *cacheItem)
229{
230 Q_Q(QNetworkDiskCache);
231 Q_ASSERT(cacheItem->metaData.saveToDisk());
232
233 QString fileName = cacheFileName(cacheItem->metaData.url());
234 Q_ASSERT(!fileName.isEmpty());
235
236 if (QFile::exists(fileName)) {
237 if (!removeFile(fileName)) {
238 qWarning() << "QNetworkDiskCache: couldn't remove the cache file " << fileName;
239 return;
240 }
241 }
242
243 currentCacheSize = q->expire();
244 if (!cacheItem->file) {
245 cacheItem->file = new QSaveFile(fileName, &cacheItem->data);
246 if (cacheItem->file->open(QFileDevice::WriteOnly)) {
247 cacheItem->writeHeader(cacheItem->file);
248 cacheItem->writeCompressedData(cacheItem->file);
249 }
250 }
251
252 if (cacheItem->file
253 && cacheItem->file->isOpen()
254 && cacheItem->file->error() == QFileDevice::NoError) {
255 // We have to call size() here instead of inside the if-body because
256 // commit() invalidates the file-engine, and size() will create a new
257 // one, pointing at an empty filename.
258 qint64 size = cacheItem->file->size();
259 if (cacheItem->file->commit())
260 currentCacheSize += size;
261 // Delete and unset the QSaveFile, it's invalid now.
262 delete std::exchange(cacheItem->file, nullptr);
263 }
264 if (cacheItem->metaData.url() == lastItem.metaData.url())
265 lastItem.reset();
266}
267
268/*!
269 \reimp
270*/
271bool QNetworkDiskCache::remove(const QUrl &url)
272{
273#if defined(QNETWORKDISKCACHE_DEBUG)
274 qDebug() << "QNetworkDiskCache::remove()" << url;
275#endif
276 Q_D(QNetworkDiskCache);
277
278 // remove is also used to cancel insertions, not a common operation
279 for (auto it = d->inserting.cbegin(), end = d->inserting.cend(); it != end; ++it) {
280 QCacheItem *item = it.value();
281 if (item && item->metaData.url() == url) {
282 delete item;
283 d->inserting.erase(it);
284 return true;
285 }
286 }
287
288 if (d->lastItem.metaData.url() == url)
289 d->lastItem.reset();
290 return d->removeFile(d->cacheFileName(url));
291}
292
293/*!
294 Put all of the misc file removing into one function to be extra safe
295 */
296bool QNetworkDiskCachePrivate::removeFile(const QString &file)
297{
298#if defined(QNETWORKDISKCACHE_DEBUG)
299 qDebug() << "QNetworkDiskCache::removFile()" << file;
300#endif
301 if (file.isEmpty())
302 return false;
303 QFileInfo info(file);
304 QString fileName = info.fileName();
305 if (!fileName.endsWith(CACHE_POSTFIX))
306 return false;
307 qint64 size = info.size();
308 if (QFile::remove(file)) {
309 currentCacheSize -= size;
310 return true;
311 }
312 return false;
313}
314
315/*!
316 \reimp
317*/
318QNetworkCacheMetaData QNetworkDiskCache::metaData(const QUrl &url)
319{
320#if defined(QNETWORKDISKCACHE_DEBUG)
321 qDebug() << "QNetworkDiskCache::metaData()" << url;
322#endif
323 Q_D(QNetworkDiskCache);
324 if (d->lastItem.metaData.url() == url)
325 return d->lastItem.metaData;
326 return fileMetaData(d->cacheFileName(url));
327}
328
329/*!
330 Returns the QNetworkCacheMetaData for the cache file \a fileName.
331
332 If \a fileName is not a cache file QNetworkCacheMetaData will be invalid.
333 */
334QNetworkCacheMetaData QNetworkDiskCache::fileMetaData(const QString &fileName) const
335{
336#if defined(QNETWORKDISKCACHE_DEBUG)
337 qDebug() << "QNetworkDiskCache::fileMetaData()" << fileName;
338#endif
339 Q_D(const QNetworkDiskCache);
340 QFile file(fileName);
341 if (!file.open(QFile::ReadOnly))
342 return QNetworkCacheMetaData();
343 if (!d->lastItem.read(&file, false)) {
344 file.close();
345 QNetworkDiskCachePrivate *that = const_cast<QNetworkDiskCachePrivate*>(d);
346 that->removeFile(fileName);
347 }
348 return d->lastItem.metaData;
349}
350
351/*!
352 \reimp
353*/
354QIODevice *QNetworkDiskCache::data(const QUrl &url)
355{
356#if defined(QNETWORKDISKCACHE_DEBUG)
357 qDebug() << "QNetworkDiskCache::data()" << url;
358#endif
359 Q_D(QNetworkDiskCache);
360 std::unique_ptr<QBuffer> buffer;
361 if (!url.isValid())
362 return nullptr;
363 if (d->lastItem.metaData.url() == url && d->lastItem.data.isOpen()) {
364 buffer.reset(new QBuffer);
365 buffer->setData(d->lastItem.data.data());
366 } else {
367 QFile file(d->cacheFileName(url));
368 if (!file.open(QFile::ReadOnly | QIODevice::Unbuffered))
369 return nullptr;
370
371 if (!d->lastItem.read(&file, true)) {
372 file.close(); // On Windows the file can't be removed if it's open
373 remove(url);
374 return nullptr;
375 }
376 if (d->lastItem.data.isOpen()) {
377 // compressed
378 buffer.reset(new QBuffer);
379 buffer->setData(d->lastItem.data.data());
380 } else {
381 buffer.reset(new QBuffer);
382 buffer->setData(file.readAll());
383 }
384 }
385 buffer->open(QBuffer::ReadOnly);
386 return buffer.release();
387}
388
389/*!
390 \reimp
391*/
392void QNetworkDiskCache::updateMetaData(const QNetworkCacheMetaData &metaData)
393{
394#if defined(QNETWORKDISKCACHE_DEBUG)
395 qDebug() << "QNetworkDiskCache::updateMetaData()" << metaData.url();
396#endif
397 QUrl url = metaData.url();
398 QIODevice *oldDevice = data(url);
399 if (!oldDevice) {
400#if defined(QNETWORKDISKCACHE_DEBUG)
401 qDebug("QNetworkDiskCache::updateMetaData(), no device!");
402#endif
403 return;
404 }
405
406 QIODevice *newDevice = prepare(metaData);
407 if (!newDevice) {
408#if defined(QNETWORKDISKCACHE_DEBUG)
409 qDebug() << "QNetworkDiskCache::updateMetaData(), no new device!" << url;
410#endif
411 return;
412 }
413 char data[1024];
414 while (!oldDevice->atEnd()) {
415 qint64 s = oldDevice->read(data, 1024);
416 newDevice->write(data, s);
417 }
418 delete oldDevice;
419 insert(newDevice);
420}
421
422/*!
423 Returns the current maximum size for the disk cache.
424
425 \sa setMaximumCacheSize()
426 */
427qint64 QNetworkDiskCache::maximumCacheSize() const
428{
429 Q_D(const QNetworkDiskCache);
430 return d->maximumCacheSize;
431}
432
433/*!
434 Sets the maximum size of the disk cache to be \a size.
435
436 If the new size is smaller then the current cache size then the cache will call expire().
437
438 \sa maximumCacheSize()
439 */
440void QNetworkDiskCache::setMaximumCacheSize(qint64 size)
441{
442 Q_D(QNetworkDiskCache);
443 bool expireCache = (size < d->maximumCacheSize);
444 d->maximumCacheSize = size;
445 if (expireCache)
446 d->currentCacheSize = expire();
447}
448
449/*!
450 Cleans the cache so that its size is under the maximum cache size.
451 Returns the current size of the cache.
452
453 When the current size of the cache is greater than the maximumCacheSize()
454 older cache files are removed until the total size is less then 90% of
455 maximumCacheSize() starting with the oldest ones first using the file
456 creation date to determine how old a cache file is.
457
458 Subclasses can reimplement this function to change the order that cache
459 files are removed taking into account information in the application
460 knows about that QNetworkDiskCache does not, for example the number of times
461 a cache is accessed.
462
463 \note cacheSize() calls expire if the current cache size is unknown.
464
465 \sa maximumCacheSize(), fileMetaData()
466 */
467qint64 QNetworkDiskCache::expire()
468{
469 Q_D(QNetworkDiskCache);
470 if (d->currentCacheSize >= 0 && d->currentCacheSize < maximumCacheSize())
471 return d->currentCacheSize;
472
473 if (cacheDirectory().isEmpty()) {
474 qWarning("QNetworkDiskCache::expire() The cache directory is not set");
475 return 0;
476 }
477
478 // close file handle to prevent "in use" error when QFile::remove() is called
479 d->lastItem.reset();
480
481 struct CacheItem
482 {
483 std::chrono::milliseconds msecs;
484 QString path;
485 qint64 size = 0;
486 };
487 std::vector<CacheItem> cacheItems;
488 qint64 totalSize = 0;
489 using F = QDirListing::IteratorFlag;
490 for (const auto &dirEntry : QDirListing(cacheDirectory(), F::FilesOnly | F::Recursive)) {
491 if (!dirEntry.fileName().endsWith(CACHE_POSTFIX))
492 continue;
493
494 const QFileInfo &info = dirEntry.fileInfo();
495 QDateTime fileTime = info.birthTime(QTimeZone::UTC);
496 if (!fileTime.isValid())
497 fileTime = info.metadataChangeTime(QTimeZone::UTC);
498 const std::chrono::milliseconds msecs{fileTime.toMSecsSinceEpoch()};
499 const qint64 size = info.size();
500 cacheItems.push_back(CacheItem{msecs, info.filePath(), size});
501 totalSize += size;
502 }
503
504 const qint64 goal = (maximumCacheSize() * 9) / 10;
505 if (totalSize < goal)
506 return totalSize; // Nothing to do
507
508 auto byFileTime = [&](const auto &a, const auto &b) { return a.msecs < b.msecs; };
509 std::sort(cacheItems.begin(), cacheItems.end(), byFileTime);
510
511 [[maybe_unused]] int removedFiles = 0; // used under QNETWORKDISKCACHE_DEBUG
512 for (const CacheItem &cached : cacheItems) {
513 QFile::remove(cached.path);
514 ++removedFiles;
515 totalSize -= cached.size;
516 if (totalSize < goal)
517 break;
518 }
519#if defined(QNETWORKDISKCACHE_DEBUG)
520 if (removedFiles > 0) {
521 qDebug() << "QNetworkDiskCache::expire()"
522 << "Removed:" << removedFiles
523 << "Kept:" << cacheItems.count() - removedFiles;
524 }
525#endif
526 return totalSize;
527}
528
529/*!
530 \reimp
531*/
532void QNetworkDiskCache::clear()
533{
534#if defined(QNETWORKDISKCACHE_DEBUG)
535 qDebug("QNetworkDiskCache::clear()");
536#endif
537 Q_D(QNetworkDiskCache);
538 qint64 size = d->maximumCacheSize;
539 d->maximumCacheSize = 0;
540 d->currentCacheSize = expire();
541 d->maximumCacheSize = size;
542}
543
544/*!
545 Given a URL, generates a unique enough filename (and subdirectory)
546 */
547QString QNetworkDiskCachePrivate::uniqueFileName(const QUrl &url)
548{
549 QUrl cleanUrl = url;
550 cleanUrl.setPassword(QString());
551 cleanUrl.setFragment(QString());
552
553 const QByteArray hash = QCryptographicHash::hash(cleanUrl.toEncoded(), QCryptographicHash::Sha1);
554 // convert sha1 to base36 form and return first 8 bytes for use as string
555 const QByteArray id = QByteArray::number(*(qlonglong*)hash.data(), 36).left(8);
556 // generates <one-char subdir>/<8-char filename.d>
557 uint code = (uint)id.at(id.size()-1) % 16;
558 QString pathFragment = QString::number(code, 16) + u'/' + QLatin1StringView(id) + CACHE_POSTFIX;
559
560 return pathFragment;
561}
562
563/*!
564 Generates fully qualified path of cached resource from a URL.
565 */
566QString QNetworkDiskCachePrivate::cacheFileName(const QUrl &url) const
567{
568 if (!url.isValid())
569 return QString();
570
571 QString fullpath = dataDirectory + uniqueFileName(url);
572 return fullpath;
573}
574
575/*!
576 \class QCacheItem
577 \internal
578 */
579
580/*!
581 We compress small text and JavaScript files.
582 */
583bool QCacheItem::canCompress() const
584{
585 const auto h = metaData.headers();
586
587 const auto sizeValue = h.value(QHttpHeaders::WellKnownHeader::ContentLength);
588 if (sizeValue.empty())
589 return false;
590
591 qint64 size = sizeValue.toLongLong();
592 if (size > MAX_COMPRESSION_SIZE)
593 return false;
594
595 const auto type = h.value(QHttpHeaders::WellKnownHeader::ContentType);
596 if (!type.empty())
597 return false;
598
599 if (!type.startsWith("text/")
600 && !(type.startsWith("application/")
601 && (type.endsWith("javascript") || type.endsWith("ecmascript")))) {
602 return false;
603 }
604
605 return true;
606}
607
608enum
609{
612};
613
614void QCacheItem::writeHeader(QFileDevice *device) const
615{
616 QDataStream out(device);
617
618 out << qint32(CacheMagic);
619 out << qint32(CurrentCacheVersion);
620 out << static_cast<qint32>(out.version());
621 out << metaData;
622 bool compressed = canCompress();
623 out << compressed;
624}
625
626void QCacheItem::writeCompressedData(QFileDevice *device) const
627{
628 QDataStream out(device);
629
630 out << qCompress(data.data());
631}
632
633/*!
634 Returns \c false if the file is a cache file,
635 but is an older version and should be removed otherwise true.
636 */
637bool QCacheItem::read(QFileDevice *device, bool readData)
638{
639 reset();
640
641 QDataStream in(device);
642
643 qint32 marker;
644 qint32 v;
645 in >> marker;
646 in >> v;
647 if (marker != CacheMagic)
648 return true;
649
650 // If the cache magic is correct, but the version is not we should remove it
651 if (v != CurrentCacheVersion)
652 return false;
653
654 qint32 streamVersion;
655 in >> streamVersion;
656 // Default stream version is also the highest we can handle
657 if (streamVersion > in.version())
658 return false;
659 in.setVersion(streamVersion);
660
661 bool compressed;
662 QByteArray dataBA;
663 in >> metaData;
664 in >> compressed;
665 if (readData && compressed) {
666 in >> dataBA;
667 data.setData(qUncompress(dataBA));
668 data.open(QBuffer::ReadOnly);
669 }
670
671 // quick and dirty check if metadata's URL field and the file's name are in synch
672 QString expectedFilename = QNetworkDiskCachePrivate::uniqueFileName(metaData.url());
673 if (!device->fileName().endsWith(expectedFilename))
674 return false;
675
676 return metaData.isValid() && !metaData.headers().isEmpty();
677}
678
679QT_END_NAMESPACE
680
681#include "moc_qnetworkdiskcache.cpp"
void prepareLayout()
Create subdirectories and other housekeeping on the filesystem.
void storeItem(QCacheItem *item)
#define CACHE_VERSION
#define DATA_DIR
#define CACHE_POSTFIX
@ CurrentCacheVersion
#define MAX_COMPRESSION_SIZE