source: trunk/src/network/access/qnetworkdiskcache.cpp@ 846

Last change on this file since 846 was 846, checked in by Dmitry A. Kuminov, 14 years ago

trunk: Merged in qt 4.7.2 sources from branches/vendor/nokia/qt.

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