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