33 #include <QtCore/QDir>
34 #include <QtCore/QString>
35 #include <QtCore/QTime>
36 #include <QtDBus/QtDBus>
37 #include <QtNetwork/QLocalServer>
38 #include <QtNetwork/QLocalSocket>
55 static const char appFullName[] =
"org.kde.kio_http_cache_cleaner";
56 static const char appName[] =
"kio_http_cache_cleaner";
68 struct SerializedCacheFileInfo {
73 static const int useCountOffset = 4;
79 static const int size = 36;
94 struct MiniCacheFileInfo {
101 bool operator<(
const MiniCacheFileInfo &other)
const;
102 void debugPrint()
const
104 kDebug(7113) <<
"useCount:" << useCount
105 <<
"\nlastUsedDate:" << lastUsedDate
106 <<
"\nsizeOnDisk:" << sizeOnDisk <<
'\n';
110 struct CacheFileInfo : MiniCacheFileInfo {
127 void prettyPrint()
const
129 QTextStream out(stdout, QIODevice::WriteOnly);
130 out <<
"File " << baseName <<
" version " <<
version[0] <<
version[1];
131 out <<
"\n cached bytes " << bytesCached <<
" useCount " << useCount;
132 out <<
"\n servedDate " <<
dateString(servedDate);
133 out <<
"\n lastModifiedDate " <<
dateString(lastModifiedDate);
134 out <<
"\n expireDate " <<
dateString(expireDate);
135 out <<
"\n entity tag " << etag;
136 out <<
"\n encoded URL " << url;
137 out <<
"\n mimetype " << mimeType;
138 out <<
"\nResponse headers follow...\n";
139 Q_FOREACH (
const QString &h, responseHeaders) {
146 bool MiniCacheFileInfo::operator<(
const MiniCacheFileInfo &other)
const
149 const int otherUseful = other.useCount / qMax(
g_currentDate - other.lastUsedDate,
qint64(1));
150 return thisUseful < otherUseful;
166 time_t tTime =
static_cast<time_t
>(intTime);
168 return check == intTime;
173 if (d.size() < SerializedCacheFileInfo::size) {
174 kDebug(7113) <<
"readBinaryHeader(): file too small?";
177 QDataStream stream(d);
178 stream.setVersion(QDataStream::Qt_4_5);
180 stream >> fi->version[0];
181 stream >> fi->version[1];
182 if (fi->version[0] !=
version[0] || fi->version[1] !=
version[1]) {
183 kDebug(7113) <<
"readBinaryHeader(): wrong magic bytes";
186 stream >> fi->compression;
187 stream >> fi->reserved;
189 stream >> fi->useCount;
191 stream >> fi->servedDate;
192 stream >> fi->lastModifiedDate;
193 stream >> fi->expireDate;
197 stream >> fi->bytesCached;
203 QCryptographicHash hash(QCryptographicHash::Sha1);
205 return QString::fromLatin1(hash.result().toHex());
211 if (!cacheDirName.endsWith(
'/')) {
212 cacheDirName.append(
'/');
214 return cacheDirName + baseName;
219 *line = dev->readLine(8192);
221 if (line->isEmpty() || !line->endsWith(
'\n')) {
235 fi->url = QString::fromLatin1(readBuf);
237 kDebug(7103) <<
"You have witnessed a very improbable hash collision!";
247 fi->etag = QString::fromLatin1(readBuf);
250 fi->mimeType = QString::fromLatin1(readBuf);
255 if (ok && !readBuf.isEmpty()) {
256 fi->responseHeaders.append(QString::fromLatin1(readBuf));
274 file.open(QIODevice::ReadOnly);
275 if (file.openMode() != QIODevice::ReadOnly) {
278 fi->baseName = baseName;
280 QByteArray
header = file.read(SerializedCacheFileInfo::size);
283 kDebug(7113) <<
"read(Text|Binary)Header() returned false, deleting file" << baseName;
288 QFileInfo fileInfo(file);
289 fi->lastUsedDate = fileInfo.lastModified().toTime_t();
290 fi->sizeOnDisk = fileInfo.size();
299 explicit CacheIndex(
const QString &baseName)
301 QByteArray ba = baseName.toLatin1();
302 const int sz = ba.size();
303 const char *input = ba.constData();
307 for (
int i = 0; i < sz; i++) {
310 if (c >=
'0' && c <=
'9') {
311 translated |= c -
'0';
312 }
else if (c >=
'a' && c <=
'f') {
313 translated |= c -
'a' + 10;
320 m_index[i >> 1] = translated;
323 translated = translated << 4;
330 bool operator==(
const CacheIndex &other)
const
334 Q_ASSERT(m_hash == other.m_hash);
340 explicit CacheIndex(
const QByteArray &index)
351 for (
int i = 0; i < ints; i++) {
352 hash ^=
reinterpret_cast<uint *
>(&m_index[0])[i];
358 const int offset = ints *
sizeof(uint);
359 for (
int i = 0; i < bytesLeft; i++) {
360 hash ^=
static_cast<uint
>(m_index[offset + i]) << (i * 8);
366 friend uint
qHash(
const CacheIndex &);
367 friend class Scoreboard;
382 QDataStream stream(cmd);
383 stream.skipRawData(SerializedCacheFileInfo::size);
391 Q_ASSERT(stream.atEnd());
392 fi->baseName = QString::fromLatin1(baseName);
400 struct ScoreboardEntry {
408 static const int size = 36;
410 bool operator<(
const MiniCacheFileInfo &other)
const;
420 QFile sboard(
filePath(QLatin1String(
"scoreboard")));
421 sboard.open(QIODevice::ReadOnly);
423 QByteArray baIndex = sboard.read(ScoreboardEntry::indexSize);
424 QByteArray baRest = sboard.read(ScoreboardEntry::size - ScoreboardEntry::indexSize);
425 if (baIndex.size() + baRest.size() != ScoreboardEntry::size) {
429 const QString entryBasename = QString::fromLatin1(baIndex.toHex());
430 MiniCacheFileInfo mcfi;
431 if (readAndValidateMcfi(baRest, entryBasename, &mcfi)) {
432 m_scoreboard.insert(CacheIndex(baIndex), mcfi);
440 QFile sboard(
filePath(QLatin1String(
"scoreboard")));
441 sboard.open(QIODevice::WriteOnly | QIODevice::Truncate);
442 QDataStream stream(&sboard);
445 for (; it != m_scoreboard.constEnd(); ++it) {
446 const char *indexData =
reinterpret_cast<const char *
>(it.key().m_index);
449 stream << it.value().useCount;
450 stream << it.value().lastUsedDate;
451 stream << it.value().sizeOnDisk;
455 bool fillInfo(
const QString &baseName, MiniCacheFileInfo *mcfi)
458 m_scoreboard.constFind(CacheIndex(baseName));
459 if (it == m_scoreboard.constEnd()) {
466 int runCommand(
const QByteArray &cmd)
469 Q_ASSERT(cmd.size() == 80);
476 kDebug(7113) <<
"CreateNotificationCommand for" << fi.baseName;
483 kDebug(7113) <<
"UpdateFileCommand for" << fi.baseName;
484 QFile file(fileName);
485 file.open(QIODevice::ReadWrite);
487 CacheFileInfo fiFromDisk;
488 QByteArray
header = file.read(SerializedCacheFileInfo::size);
489 if (!
readBinaryHeader(header, &fiFromDisk) || fiFromDisk.bytesCached != fi.bytesCached) {
495 const quint32 newUseCount = fiFromDisk.useCount + 1;
496 QByteArray newHeader = cmd.mid(0, SerializedCacheFileInfo::size);
498 QDataStream stream(&newHeader, QIODevice::WriteOnly);
499 stream.skipRawData(SerializedCacheFileInfo::useCountOffset);
500 stream << newUseCount;
504 file.write(newHeader);
514 kDebug(7113) <<
"received invalid command";
518 QFileInfo fileInfo(fileName);
519 fi.lastUsedDate = fileInfo.lastModified().toTime_t();
520 fi.sizeOnDisk = fileInfo.size();
528 void add(
const CacheFileInfo &fi)
530 m_scoreboard[CacheIndex(fi.baseName)] = fi;
533 void remove(
const QString &basename)
535 m_scoreboard.remove(CacheIndex(basename));
539 void maybeRemoveStaleEntries(
const QList<CacheFileInfo *> &fiList)
542 if (m_scoreboard.count() < fiList.count() + 100) {
545 kDebug(7113) <<
"we have too many fake/stale entries, cleaning up...";
546 QSet<CacheIndex> realFiles;
547 Q_FOREACH (CacheFileInfo *fi, fiList) {
548 realFiles.insert(CacheIndex(fi->baseName));
551 while (it != m_scoreboard.end()) {
552 if (realFiles.contains(it.key())) {
555 it = m_scoreboard.erase(it);
561 bool readAndValidateMcfi(
const QByteArray &rawData,
const QString &basename, MiniCacheFileInfo *mcfi)
563 QDataStream stream(rawData);
564 stream >> mcfi->useCount;
566 stream >> mcfi->lastUsedDate;
567 stream >> mcfi->sizeOnDisk;
569 QFileInfo fileInfo(
filePath(basename));
570 if (!fileInfo.exists()) {
574 ok = ok && fileInfo.lastModified().toTime_t() == mcfi->lastUsedDate;
575 ok = ok && fileInfo.size() == mcfi->sizeOnDisk;
580 QFile entryFile(fileInfo.absoluteFilePath());
581 entryFile.open(QIODevice::ReadOnly);
582 if (entryFile.size() < SerializedCacheFileInfo::size) {
585 QDataStream stream(&entryFile);
586 stream.skipRawData(SerializedCacheFileInfo::useCountOffset);
588 stream >> mcfi->useCount;
589 mcfi->lastUsedDate = fileInfo.lastModified().toTime_t();
590 mcfi->sizeOnDisk = fileInfo.size();
606 const char *oldDirs =
"0abcdefghijklmnopqrstuvwxyz";
607 const int n = strlen(oldDirs);
609 for (
int i = 0; i < n; i++) {
610 QString dirName = QString::fromLatin1(&oldDirs[i], 1);
612 Q_FOREACH (
const QString &baseName, QDir(
filePath(dirName)).entryList()) {
613 QFile::remove(
filePath(dirName +
'/' + baseName));
616 cacheRootDir.rmdir(dirName);
618 QFile::remove(
filePath(QLatin1String(
"cleaned")));
624 CacheCleaner(
const QDir &cacheDir)
625 : m_totalSizeOnDisk(0)
628 m_fileNameList = cacheDir.entryList();
633 bool processSlice(Scoreboard *scoreboard = 0)
638 if (!m_fileNameList.isEmpty()) {
639 while (t.elapsed() < 100 && !m_fileNameList.isEmpty()) {
640 QString baseName = m_fileNameList.takeFirst();
647 QChar c = baseName[i];
648 nameOk = (c >=
'0' && c <= '9') || (c >=
'a' && c <=
'f');
662 CacheFileInfo *fi =
new CacheFileInfo();
663 fi->baseName = baseName;
665 bool gotInfo =
false;
667 gotInfo = scoreboard->fillInfo(baseName, fi);
671 if (gotInfo && scoreboard) {
672 scoreboard->add(*fi);
677 m_totalSizeOnDisk += fi->sizeOnDisk;
682 kDebug(7113) <<
"total size of cache files is" << m_totalSizeOnDisk;
684 if (m_fileNameList.isEmpty()) {
694 while (t.elapsed() < 100) {
695 if (m_totalSizeOnDisk <= g_maxCacheSize || m_fiList.isEmpty()) {
696 kDebug(7113) <<
"total size of cache files after cleaning is" << m_totalSizeOnDisk;
698 scoreboard->maybeRemoveStaleEntries(m_fiList);
699 scoreboard->writeOut();
701 qDeleteAll(m_fiList);
705 CacheFileInfo *fi = m_fiList.takeFirst();
707 if (QFile::remove(filename)) {
708 m_totalSizeOnDisk -= fi->sizeOnDisk;
710 scoreboard->remove(fi->baseName);
720 QList<CacheFileInfo *> m_fiList;
732 options.
add(
"clear-all",
ki18n(
"Empty the cache"));
733 options.
add(
"file-info <filename>",
ki18n(
"Display information about cache file"));
740 QCoreApplication app(argc, argv);
743 if (args->
isSet(
"clear-all")) {
745 }
else if (args->
isSet(
"file-info")) {
761 if (!QDBusConnection::sessionBus().isConnected()) {
762 QDBusError error(QDBusConnection::sessionBus().lastError());
763 fprintf(stderr,
"%s: Could not connect to D-Bus! (%s: %s)\n",
appName,
764 qPrintable(error.name()), qPrintable(error.message()));
768 if (!QDBusConnection::sessionBus().registerService(
appFullName)) {
769 fprintf(stderr,
"%s: Already running!\n",
appName);
780 QDir cacheDir(cacheDirName);
781 if (!cacheDir.exists()) {
782 fprintf(stderr,
"%s: '%s' does not exist.\n",
appName, qPrintable(cacheDirName));
793 CacheCleaner cleaner(cacheDir);
794 while (!cleaner.processSlice()) { }
798 QLocalServer lServer;
801 QFile::remove(socketFileName);
802 lServer.listen(socketFileName);
803 QList<QLocalSocket *> sockets;
804 int newBytesCounter = INT_MAX;
806 Scoreboard scoreboard;
807 CacheCleaner *cleaner = 0;
811 QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
816 QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents);
818 if (!lServer.isListening()) {
821 lServer.waitForNewConnection(1);
823 while (QLocalSocket *sock = lServer.nextPendingConnection()) {
824 sock->waitForConnected();
825 sockets.append(sock);
828 for (
int i = 0; i < sockets.size(); i++) {
829 QLocalSocket *sock = sockets[i];
830 if (sock->state() != QLocalSocket::ConnectedState) {
831 if (sock->state() != QLocalSocket::UnconnectedState) {
832 sock->waitForDisconnected();
835 sockets.removeAll(sock);
839 sock->waitForReadyRead(0);
841 QByteArray recv = sock->read(80);
842 if (recv.isEmpty()) {
845 Q_ASSERT(recv.size() == 80);
846 newBytesCounter += scoreboard.runCommand(recv);
852 if (cleaner->processSlice(&scoreboard)) {
857 }
else if (newBytesCounter > g_maxCacheSize / 8) {
859 cleaner =
new CacheCleaner(cacheDir);