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
63QT_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 */
104QNetworkDiskCache::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 */
112QNetworkDiskCache::~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*/
121QString 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*/
137void 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*/
158qint64 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*/
176QIODevice *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 headers = metaData.rawHeaders();
191 for (const auto &header : 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*/
228void 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*/
250void 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
265void 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*/
307bool 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 */
332bool 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*/
354QNetworkCacheMetaData 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 */
370QNetworkCacheMetaData 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*/
390QIODevice *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*/
428void 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 */
463qint64 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 */
476void 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 */
503qint64 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*/
572void 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 */
587QString 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
605QString 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 */
614QString 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 */
626bool QCacheItem::canCompress() const
627{
628 bool sizeOk = false;
629 bool typeOk = false;
630 const auto headers = metaData.rawHeaders();
631 for (const auto &header : 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
655enum
656{
657 CacheMagic = 0xe8,
658 CurrentCacheVersion = CACHE_VERSION
659};
660
661void QCacheItem::writeHeader(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
673void 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 */
684bool 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
726QT_END_NAMESPACE
727

source code of qtbase/src/network/access/qnetworkdiskcache.cpp