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 9
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 static constexpr auto dirPermissions =
218 QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner;
219
220 //Create directory and subdirectories 0-F
221 //Ensure the cache directory and any parents exist.
222 helper.mkpath(cacheDirectory);
223 // Create the data directory with owner-only permissions.
224 // If it already exists, tighten its permissions
225 if (!helper.mkdir(dataDirectory, dirPermissions)
226 && !QFile::setPermissions(dataDirectory, dirPermissions)) {
227 qWarning("QNetworkDiskCache::prepareLayout: could not create or set permissions on %ls",
228 qUtf16Printable(dataDirectory));
229 return;
230 }
231 for (uint i = 0; i < 16 ; i++) {
232 QString str = QString::number(i, 16);
233 QString subdir = dataDirectory + str;
234 helper.mkdir(subdir, dirPermissions);
235 }
236}
237
238
239void QNetworkDiskCachePrivate::storeItem(QCacheItem *cacheItem)
240{
241 Q_Q(QNetworkDiskCache);
242 Q_ASSERT(cacheItem->metaData.saveToDisk());
243
244 QString fileName = cacheFileName(cacheItem->metaData.url());
245 Q_ASSERT(!fileName.isEmpty());
246
247 if (QFile::exists(fileName)) {
248 if (!removeFile(fileName)) {
249 qWarning() << "QNetworkDiskCache: couldn't remove the cache file " << fileName;
250 return;
251 }
252 }
253
254 currentCacheSize = q->expire();
255 if (!cacheItem->file) {
256 cacheItem->file = new QSaveFile(fileName, &cacheItem->data);
257 if (cacheItem->file->open(QFileDevice::WriteOnly)) {
258 cacheItem->writeHeader(cacheItem->file);
259 cacheItem->writeCompressedData(cacheItem->file);
260 }
261 }
262
263 if (cacheItem->file
264 && cacheItem->file->isOpen()
265 && cacheItem->file->error() == QFileDevice::NoError) {
266 // We have to call size() here instead of inside the if-body because
267 // commit() invalidates the file-engine, and size() will create a new
268 // one, pointing at an empty filename.
269 qint64 size = cacheItem->file->size();
270 if (cacheItem->file->commit())
271 currentCacheSize += size;
272 // Delete and unset the QSaveFile, it's invalid now.
273 delete std::exchange(cacheItem->file, nullptr);
274 }
275 if (cacheItem->metaData.url() == lastItem.metaData.url())
276 lastItem.reset();
277}
278
279/*!
280 \reimp
281*/
282bool QNetworkDiskCache::remove(const QUrl &url)
283{
284#if defined(QNETWORKDISKCACHE_DEBUG)
285 qDebug() << "QNetworkDiskCache::remove()" << url;
286#endif
287 Q_D(QNetworkDiskCache);
288
289 // remove is also used to cancel insertions, not a common operation
290 for (auto it = d->inserting.cbegin(), end = d->inserting.cend(); it != end; ++it) {
291 QCacheItem *item = it.value();
292 if (item && item->metaData.url() == url) {
293 delete item;
294 d->inserting.erase(it);
295 return true;
296 }
297 }
298
299 if (d->lastItem.metaData.url() == url)
300 d->lastItem.reset();
301 return d->removeFile(d->cacheFileName(url));
302}
303
304/*!
305 Put all of the misc file removing into one function to be extra safe
306 */
307bool QNetworkDiskCachePrivate::removeFile(const QString &file)
308{
309#if defined(QNETWORKDISKCACHE_DEBUG)
310 qDebug() << "QNetworkDiskCache::removFile()" << file;
311#endif
312 if (file.isEmpty())
313 return false;
314 QFileInfo info(file);
315 QString fileName = info.fileName();
316 if (!fileName.endsWith(CACHE_POSTFIX))
317 return false;
318 qint64 size = info.size();
319 if (QFile::remove(file)) {
320 currentCacheSize -= size;
321 return true;
322 }
323 return false;
324}
325
326/*!
327 \reimp
328*/
329QNetworkCacheMetaData QNetworkDiskCache::metaData(const QUrl &url)
330{
331#if defined(QNETWORKDISKCACHE_DEBUG)
332 qDebug() << "QNetworkDiskCache::metaData()" << url;
333#endif
334 Q_D(QNetworkDiskCache);
335 if (d->lastItem.metaData.url() == url)
336 return d->lastItem.metaData;
337 return fileMetaData(d->cacheFileName(url));
338}
339
340/*!
341 Returns the QNetworkCacheMetaData for the cache file \a fileName.
342
343 If \a fileName is not a cache file QNetworkCacheMetaData will be invalid.
344 */
345QNetworkCacheMetaData QNetworkDiskCache::fileMetaData(const QString &fileName) const
346{
347#if defined(QNETWORKDISKCACHE_DEBUG)
348 qDebug() << "QNetworkDiskCache::fileMetaData()" << fileName;
349#endif
350 Q_D(const QNetworkDiskCache);
351 QFile file(fileName);
352 if (!file.open(QFile::ReadOnly))
353 return QNetworkCacheMetaData();
354 if (!d->lastItem.read(&file, false)) {
355 file.close();
356 QNetworkDiskCachePrivate *that = const_cast<QNetworkDiskCachePrivate*>(d);
357 that->removeFile(fileName);
358 }
359 return d->lastItem.metaData;
360}
361
362/*!
363 \reimp
364*/
365QIODevice *QNetworkDiskCache::data(const QUrl &url)
366{
367#if defined(QNETWORKDISKCACHE_DEBUG)
368 qDebug() << "QNetworkDiskCache::data()" << url;
369#endif
370 Q_D(QNetworkDiskCache);
371 std::unique_ptr<QBuffer> buffer;
372 if (!url.isValid())
373 return nullptr;
374 if (d->lastItem.metaData.url() == url && d->lastItem.data.isOpen()) {
375 buffer.reset(new QBuffer);
376 buffer->setData(d->lastItem.data.data());
377 } else {
378 QFile file(d->cacheFileName(url));
379 if (!file.open(QFile::ReadOnly | QIODevice::Unbuffered))
380 return nullptr;
381
382 if (!d->lastItem.read(&file, true)) {
383 file.close(); // On Windows the file can't be removed if it's open
384 remove(url);
385 return nullptr;
386 }
387 if (d->lastItem.data.isOpen()) {
388 // compressed
389 buffer.reset(new QBuffer);
390 buffer->setData(d->lastItem.data.data());
391 } else {
392 buffer.reset(new QBuffer);
393 buffer->setData(file.readAll());
394 }
395 }
396 buffer->open(QBuffer::ReadOnly);
397 return buffer.release();
398}
399
400/*!
401 \reimp
402*/
403void QNetworkDiskCache::updateMetaData(const QNetworkCacheMetaData &metaData)
404{
405#if defined(QNETWORKDISKCACHE_DEBUG)
406 qDebug() << "QNetworkDiskCache::updateMetaData()" << metaData.url();
407#endif
408 QUrl url = metaData.url();
409 QIODevice *oldDevice = data(url);
410 if (!oldDevice) {
411#if defined(QNETWORKDISKCACHE_DEBUG)
412 qDebug("QNetworkDiskCache::updateMetaData(), no device!");
413#endif
414 return;
415 }
416
417 QIODevice *newDevice = prepare(metaData);
418 if (!newDevice) {
419#if defined(QNETWORKDISKCACHE_DEBUG)
420 qDebug() << "QNetworkDiskCache::updateMetaData(), no new device!" << url;
421#endif
422 return;
423 }
424 char data[1024];
425 while (!oldDevice->atEnd()) {
426 qint64 s = oldDevice->read(data, 1024);
427 newDevice->write(data, s);
428 }
429 delete oldDevice;
430 insert(newDevice);
431}
432
433/*!
434 Returns the current maximum size for the disk cache.
435
436 \sa setMaximumCacheSize()
437 */
438qint64 QNetworkDiskCache::maximumCacheSize() const
439{
440 Q_D(const QNetworkDiskCache);
441 return d->maximumCacheSize;
442}
443
444/*!
445 Sets the maximum size of the disk cache to be \a size.
446
447 If the new size is smaller then the current cache size then the cache will call expire().
448
449 \sa maximumCacheSize()
450 */
451void QNetworkDiskCache::setMaximumCacheSize(qint64 size)
452{
453 Q_D(QNetworkDiskCache);
454 bool expireCache = (size < d->maximumCacheSize);
455 d->maximumCacheSize = size;
456 if (expireCache)
457 d->currentCacheSize = expire();
458}
459
460/*!
461 Cleans the cache so that its size is under the maximum cache size.
462 Returns the current size of the cache.
463
464 When the current size of the cache is greater than the maximumCacheSize()
465 older cache files are removed until the total size is less then 90% of
466 maximumCacheSize() starting with the oldest ones first using the file
467 creation date to determine how old a cache file is.
468
469 Subclasses can reimplement this function to change the order that cache
470 files are removed taking into account information in the application
471 knows about that QNetworkDiskCache does not, for example the number of times
472 a cache is accessed.
473
474 \note cacheSize() calls expire if the current cache size is unknown.
475
476 \sa maximumCacheSize(), fileMetaData()
477 */
478qint64 QNetworkDiskCache::expire()
479{
480 Q_D(QNetworkDiskCache);
481 if (d->currentCacheSize >= 0 && d->currentCacheSize < maximumCacheSize())
482 return d->currentCacheSize;
483
484 if (cacheDirectory().isEmpty()) {
485 qWarning("QNetworkDiskCache::expire() The cache directory is not set");
486 return 0;
487 }
488
489 // close file handle to prevent "in use" error when QFile::remove() is called
490 d->lastItem.reset();
491
492 struct CacheItem
493 {
494 std::chrono::milliseconds msecs;
495 QString path;
496 qint64 size = 0;
497 };
498 std::vector<CacheItem> cacheItems;
499 qint64 totalSize = 0;
500 using F = QDirListing::IteratorFlag;
501 for (const auto &dirEntry : QDirListing(cacheDirectory(), F::FilesOnly | F::Recursive)) {
502 if (!dirEntry.fileName().endsWith(CACHE_POSTFIX))
503 continue;
504
505 const QFileInfo &info = dirEntry.fileInfo();
506 QDateTime fileTime = info.birthTime(QTimeZone::UTC);
507 if (!fileTime.isValid())
508 fileTime = info.metadataChangeTime(QTimeZone::UTC);
509 const std::chrono::milliseconds msecs{fileTime.toMSecsSinceEpoch()};
510 const qint64 size = info.size();
511 cacheItems.push_back(CacheItem{msecs, info.filePath(), size});
512 totalSize += size;
513 }
514
515 const qint64 goal = (maximumCacheSize() * 9) / 10;
516 if (totalSize < goal)
517 return totalSize; // Nothing to do
518
519 auto byFileTime = [&](const auto &a, const auto &b) { return a.msecs < b.msecs; };
520 std::sort(cacheItems.begin(), cacheItems.end(), byFileTime);
521
522 [[maybe_unused]] int removedFiles = 0; // used under QNETWORKDISKCACHE_DEBUG
523 for (const CacheItem &cached : cacheItems) {
524 QFile::remove(cached.path);
525 ++removedFiles;
526 totalSize -= cached.size;
527 if (totalSize < goal)
528 break;
529 }
530#if defined(QNETWORKDISKCACHE_DEBUG)
531 if (removedFiles > 0) {
532 qDebug() << "QNetworkDiskCache::expire()"
533 << "Removed:" << removedFiles
534 << "Kept:" << cacheItems.count() - removedFiles;
535 }
536#endif
537 return totalSize;
538}
539
540/*!
541 \reimp
542*/
543void QNetworkDiskCache::clear()
544{
545#if defined(QNETWORKDISKCACHE_DEBUG)
546 qDebug("QNetworkDiskCache::clear()");
547#endif
548 Q_D(QNetworkDiskCache);
549 qint64 size = d->maximumCacheSize;
550 d->maximumCacheSize = 0;
551 d->currentCacheSize = expire();
552 d->maximumCacheSize = size;
553}
554
555/*!
556 Given a URL, generates a unique enough filename (and subdirectory)
557 */
558QString QNetworkDiskCachePrivate::uniqueFileName(const QUrl &url)
559{
560 QUrl cleanUrl = url;
561 cleanUrl.setPassword(QString());
562 cleanUrl.setFragment(QString());
563
564 const QByteArray hash = QCryptographicHash::hash(cleanUrl.toEncoded(), QCryptographicHash::Sha1);
565 // convert sha1 to base36 form and return first 8 bytes for use as string
566 const QByteArray id = QByteArray::number(*(qlonglong*)hash.data(), 36).left(8);
567 // generates <one-char subdir>/<8-char filename.d>
568 uint code = (uint)id.at(id.size()-1) % 16;
569 QString pathFragment = QString::number(code, 16) + u'/' + QLatin1StringView(id) + CACHE_POSTFIX;
570
571 return pathFragment;
572}
573
574/*!
575 Generates fully qualified path of cached resource from a URL.
576 */
577QString QNetworkDiskCachePrivate::cacheFileName(const QUrl &url) const
578{
579 if (!url.isValid())
580 return QString();
581
582 QString fullpath = dataDirectory + uniqueFileName(url);
583 return fullpath;
584}
585
586/*!
587 \class QCacheItem
588 \internal
589 */
590
591/*!
592 We compress small text and JavaScript files.
593 */
594bool QCacheItem::canCompress() const
595{
596 const auto h = metaData.headers();
597
598 const auto sizeValue = h.value(QHttpHeaders::WellKnownHeader::ContentLength);
599 if (sizeValue.empty())
600 return false;
601
602 qint64 size = sizeValue.toLongLong();
603 if (size > MAX_COMPRESSION_SIZE)
604 return false;
605
606 const auto type = h.value(QHttpHeaders::WellKnownHeader::ContentType);
607 if (type.empty())
608 return false;
609
610 if (!type.startsWith("text/")
611 && !(type.startsWith("application/")
612 && (type.endsWith("javascript") || type.endsWith("ecmascript")))) {
613 return false;
614 }
615
616 return true;
617}
618
619enum
620{
623};
624
625void QCacheItem::writeHeader(QFileDevice *device) const
626{
627 QDataStream out(device);
628
629 out << qint32(CacheMagic);
630 out << qint32(CurrentCacheVersion);
631 out << static_cast<qint32>(out.version());
632 out << metaData;
633 bool compressed = canCompress();
634 out << compressed;
635}
636
637void QCacheItem::writeCompressedData(QFileDevice *device) const
638{
639 QDataStream out(device);
640
641 out << qCompress(data.data());
642}
643
644/*!
645 Returns \c false if the file is a cache file,
646 but is an older version and should be removed otherwise true.
647 */
648bool QCacheItem::read(QFileDevice *device, bool readData)
649{
650 reset();
651
652 QDataStream in(device);
653
654 qint32 marker;
655 qint32 v;
656 in >> marker;
657 in >> v;
658 if (marker != CacheMagic)
659 return true;
660
661 // If the cache magic is correct, but the version is not we should remove it
662 if (v != CurrentCacheVersion)
663 return false;
664
665 qint32 streamVersion;
666 in >> streamVersion;
667 // Default stream version is also the highest we can handle
668 if (streamVersion > in.version())
669 return false;
670 in.setVersion(streamVersion);
671
672 bool compressed;
673 QByteArray dataBA;
674 in >> metaData;
675 in >> compressed;
676 if (readData && compressed) {
677 in >> dataBA;
678 data.setData(qUncompress(dataBA));
679 data.open(QBuffer::ReadOnly);
680 }
681
682 // quick and dirty check if metadata's URL field and the file's name are in synch
683 QString expectedFilename = QNetworkDiskCachePrivate::uniqueFileName(metaData.url());
684 if (!device->fileName().endsWith(expectedFilename))
685 return false;
686
687 return metaData.isValid() && !metaData.headers().isEmpty();
688}
689
690QT_END_NAMESPACE
691
692#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