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