KIOSlave
http_cache_cleaner.cpp
Go to the documentation of this file.
00001 /* 00002 This file is part of KDE 00003 00004 Copyright (C) 1999-2000 Waldo Bastian (bastian@kde.org) 00005 Copyright (C) 2009 Andreas Hartmetz (ahartmetz@gmail.com) 00006 00007 Permission is hereby granted, free of charge, to any person obtaining a copy 00008 of this software and associated documentation files (the "Software"), to deal 00009 in the Software without restriction, including without limitation the rights 00010 to use, copy, modify, merge, publish, distribute, and/or sell 00011 copies of the Software, and to permit persons to whom the Software is 00012 furnished to do so, subject to the following conditions: 00013 00014 The above copyright notice and this permission notice shall be included in 00015 all copies or substantial portions of the Software. 00016 00017 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 00018 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 00019 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 00020 AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 00021 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 00022 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 00023 */ 00024 //---------------------------------------------------------------------------- 00025 // 00026 // KDE HTTP Cache cleanup tool 00027 00028 #include <cstring> 00029 #include <time.h> 00030 #include <stdlib.h> 00031 #include <zlib.h> 00032 00033 #include <QtCore/QDir> 00034 #include <QtCore/QString> 00035 #include <QtCore/QTime> 00036 #include <QtDBus/QtDBus> 00037 #include <QtNetwork/QLocalServer> 00038 #include <QtNetwork/QLocalSocket> 00039 00040 #include <kcmdlineargs.h> 00041 #include <kcomponentdata.h> 00042 #include <kdatetime.h> 00043 #include <kdebug.h> 00044 #include <kglobal.h> 00045 #include <klocale.h> 00046 #include <kprotocolmanager.h> 00047 #include <kstandarddirs.h> 00048 00049 #include <unistd.h> 00050 00051 time_t g_currentDate; 00052 int g_maxCacheAge; 00053 qint64 g_maxCacheSize; 00054 00055 static const char appFullName[] = "org.kde.kio_http_cache_cleaner"; 00056 static const char appName[] = "kio_http_cache_cleaner"; 00057 00058 // !START OF SYNC! 00059 // Keep the following in sync with the cache code in http.cpp 00060 00061 static const int s_hashedUrlBits = 160; // this number should always be divisible by eight 00062 static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; 00063 static const int s_hashedUrlBytes = s_hashedUrlBits / 8; 00064 00065 static const char version[] = "A\n"; 00066 00067 // never instantiated, on-disk / wire format only 00068 struct SerializedCacheFileInfo { 00069 // from http.cpp 00070 quint8 version[2]; 00071 quint8 compression; // for now fixed to 0 00072 quint8 reserved; // for now; also alignment 00073 static const int useCountOffset = 4; 00074 qint32 useCount; 00075 qint64 servedDate; 00076 qint64 lastModifiedDate; 00077 qint64 expireDate; 00078 qint32 bytesCached; 00079 static const int size = 36; 00080 00081 QString url; 00082 QString etag; 00083 QString mimeType; 00084 QStringList responseHeaders; // including status response like "HTTP 200 OK" 00085 }; 00086 00087 static QString dateString(qint64 date) 00088 { 00089 KDateTime dt; 00090 dt.setTime_t(date); 00091 return dt.toString(KDateTime::ISODate); 00092 } 00093 00094 struct MiniCacheFileInfo { 00095 // data from cache entry file, or from scoreboard file 00096 qint32 useCount; 00097 // from filesystem 00098 qint64 lastUsedDate; 00099 qint32 sizeOnDisk; 00100 // we want to delete the least "useful" files and we'll have to sort a list for that... 00101 bool operator<(const MiniCacheFileInfo &other) const; 00102 void debugPrint() const 00103 { 00104 kDebug(7113) << "useCount:" << useCount 00105 << "\nlastUsedDate:" << lastUsedDate 00106 << "\nsizeOnDisk:" << sizeOnDisk << '\n'; 00107 } 00108 }; 00109 00110 struct CacheFileInfo : MiniCacheFileInfo { 00111 quint8 version[2]; 00112 quint8 compression; // for now fixed to 0 00113 quint8 reserved; // for now; also alignment 00114 00115 00116 qint64 servedDate; 00117 qint64 lastModifiedDate; 00118 qint64 expireDate; 00119 qint32 bytesCached; 00120 00121 QString baseName; 00122 QString url; 00123 QString etag; 00124 QString mimeType; 00125 QStringList responseHeaders; // including status response like "HTTP 200 OK" 00126 00127 void prettyPrint() const 00128 { 00129 QTextStream out(stdout, QIODevice::WriteOnly); 00130 out << "File " << baseName << " version " << version[0] << version[1]; 00131 out << "\n cached bytes " << bytesCached << " useCount " << useCount; 00132 out << "\n servedDate " << dateString(servedDate); 00133 out << "\n lastModifiedDate " << dateString(lastModifiedDate); 00134 out << "\n expireDate " << dateString(expireDate); 00135 out << "\n entity tag " << etag; 00136 out << "\n encoded URL " << url; 00137 out << "\n mimetype " << mimeType; 00138 out << "\nResponse headers follow...\n"; 00139 Q_FOREACH (const QString &h, responseHeaders) { 00140 out << h << '\n'; 00141 } 00142 } 00143 }; 00144 00145 00146 bool MiniCacheFileInfo::operator<(const MiniCacheFileInfo &other) const 00147 { 00148 const int thisUseful = useCount / qMax(g_currentDate - lastUsedDate, qint64(1)); 00149 const int otherUseful = other.useCount / qMax(g_currentDate - other.lastUsedDate, qint64(1)); 00150 return thisUseful < otherUseful; 00151 } 00152 00153 bool CacheFileInfoPtrLessThan(const CacheFileInfo *cf1, const CacheFileInfo *cf2) 00154 { 00155 return *cf1 < *cf2; 00156 } 00157 00158 enum OperationMode { 00159 CleanCache = 0, 00160 DeleteCache, 00161 FileInfo 00162 }; 00163 00164 static bool timeSizeFits(qint64 intTime) 00165 { 00166 time_t tTime = static_cast<time_t>(intTime); 00167 qint64 check = static_cast<qint64>(tTime); 00168 return check == intTime; 00169 } 00170 00171 static bool readBinaryHeader(const QByteArray &d, CacheFileInfo *fi) 00172 { 00173 if (d.size() < SerializedCacheFileInfo::size) { 00174 kDebug(7113) << "readBinaryHeader(): file too small?"; 00175 return false; 00176 } 00177 QDataStream stream(d); 00178 stream.setVersion(QDataStream::Qt_4_5); 00179 00180 stream >> fi->version[0]; 00181 stream >> fi->version[1]; 00182 if (fi->version[0] != version[0] || fi->version[1] != version[1]) { 00183 kDebug(7113) << "readBinaryHeader(): wrong magic bytes"; 00184 return false; 00185 } 00186 stream >> fi->compression; 00187 stream >> fi->reserved; 00188 00189 stream >> fi->useCount; 00190 00191 stream >> fi->servedDate; 00192 stream >> fi->lastModifiedDate; 00193 stream >> fi->expireDate; 00194 bool timeSizeOk = timeSizeFits(fi->servedDate) && timeSizeFits(fi->lastModifiedDate) && 00195 timeSizeFits(fi->expireDate); 00196 00197 stream >> fi->bytesCached; 00198 return timeSizeOk; 00199 } 00200 00201 static QString filenameFromUrl(const QByteArray &url) 00202 { 00203 QCryptographicHash hash(QCryptographicHash::Sha1); 00204 hash.addData(url); 00205 return QString::fromLatin1(hash.result().toHex()); 00206 } 00207 00208 static QString filePath(const QString &baseName) 00209 { 00210 QString cacheDirName = KGlobal::dirs()->saveLocation("cache", "http"); 00211 if (!cacheDirName.endsWith('/')) { 00212 cacheDirName.append('/'); 00213 } 00214 return cacheDirName + baseName; 00215 } 00216 00217 static bool readLineChecked(QIODevice *dev, QByteArray *line) 00218 { 00219 *line = dev->readLine(8192); 00220 // if nothing read or the line didn't fit into 8192 bytes(!) 00221 if (line->isEmpty() || !line->endsWith('\n')) { 00222 return false; 00223 } 00224 // we don't actually want the newline! 00225 line->chop(1); 00226 return true; 00227 } 00228 00229 static bool readTextHeader(QFile *file, CacheFileInfo *fi, OperationMode mode) 00230 { 00231 bool ok = true; 00232 QByteArray readBuf; 00233 00234 ok = ok && readLineChecked(file, &readBuf); 00235 fi->url = QString::fromLatin1(readBuf); 00236 if (filenameFromUrl(readBuf) != QFileInfo(*file).baseName()) { 00237 kDebug(7103) << "You have witnessed a very improbable hash collision!"; 00238 return false; 00239 } 00240 00241 // only read the necessary info for cache cleaning. Saves time and (more importantly) memory. 00242 if (mode != FileInfo) { 00243 return true; 00244 } 00245 00246 ok = ok && readLineChecked(file, &readBuf); 00247 fi->etag = QString::fromLatin1(readBuf); 00248 00249 ok = ok && readLineChecked(file, &readBuf); 00250 fi->mimeType = QString::fromLatin1(readBuf); 00251 00252 // read as long as no error and no empty line found 00253 while (true) { 00254 ok = ok && readLineChecked(file, &readBuf); 00255 if (ok && !readBuf.isEmpty()) { 00256 fi->responseHeaders.append(QString::fromLatin1(readBuf)); 00257 } else { 00258 break; 00259 } 00260 } 00261 return ok; // it may still be false ;) 00262 } 00263 00264 // TODO common include file with http.cpp? 00265 enum CacheCleanerCommand { 00266 InvalidCommand = 0, 00267 CreateFileNotificationCommand, 00268 UpdateFileCommand 00269 }; 00270 00271 static bool readCacheFile(const QString &baseName, CacheFileInfo *fi, OperationMode mode) 00272 { 00273 QFile file(filePath(baseName)); 00274 file.open(QIODevice::ReadOnly); 00275 if (file.openMode() != QIODevice::ReadOnly) { 00276 return false; 00277 } 00278 fi->baseName = baseName; 00279 00280 QByteArray header = file.read(SerializedCacheFileInfo::size); 00281 // do *not* modify/delete the file if we're in file info mode. 00282 if (!(readBinaryHeader(header, fi) && readTextHeader(&file, fi, mode)) && mode != FileInfo) { 00283 kDebug(7113) << "read(Text|Binary)Header() returned false, deleting file" << baseName; 00284 file.remove(); 00285 return false; 00286 } 00287 // get meta-information from the filesystem 00288 QFileInfo fileInfo(file); 00289 fi->lastUsedDate = fileInfo.lastModified().toTime_t(); 00290 fi->sizeOnDisk = fileInfo.size(); 00291 return true; 00292 } 00293 00294 class Scoreboard; 00295 00296 class CacheIndex 00297 { 00298 public: 00299 explicit CacheIndex(const QString &baseName) 00300 { 00301 QByteArray ba = baseName.toLatin1(); 00302 const int sz = ba.size(); 00303 const char *input = ba.constData(); 00304 Q_ASSERT(sz == s_hashedUrlNibbles); 00305 00306 int translated = 0; 00307 for (int i = 0; i < sz; i++) { 00308 int c = input[i]; 00309 00310 if (c >= '0' && c <= '9') { 00311 translated |= c - '0'; 00312 } else if (c >= 'a' && c <= 'f') { 00313 translated |= c - 'a' + 10; 00314 } else { 00315 Q_ASSERT(false); 00316 } 00317 00318 if (i & 1) { 00319 // odd index 00320 m_index[i >> 1] = translated; 00321 translated = 0; 00322 } else { 00323 translated = translated << 4; 00324 } 00325 } 00326 00327 computeHash(); 00328 } 00329 00330 bool operator==(const CacheIndex &other) const 00331 { 00332 const bool isEqual = memcmp(m_index, other.m_index, s_hashedUrlBytes) == 0; 00333 if (isEqual) { 00334 Q_ASSERT(m_hash == other.m_hash); 00335 } 00336 return isEqual; 00337 } 00338 00339 private: 00340 explicit CacheIndex(const QByteArray &index) 00341 { 00342 Q_ASSERT(index.length() >= s_hashedUrlBytes); 00343 memcpy(m_index, index.constData(), s_hashedUrlBytes); 00344 computeHash(); 00345 } 00346 00347 void computeHash() 00348 { 00349 uint hash = 0; 00350 const int ints = s_hashedUrlBytes / sizeof(uint); 00351 for (int i = 0; i < ints; i++) { 00352 hash ^= reinterpret_cast<uint *>(&m_index[0])[i]; 00353 } 00354 if (const int bytesLeft = s_hashedUrlBytes % sizeof(uint)) { 00355 // dead code until a new url hash algorithm or architecture with sizeof(uint) != 4 appears. 00356 // we have the luxury of ignoring endianness because the hash is never written to disk. 00357 // just merge the bits into the the hash in some way. 00358 const int offset = ints * sizeof(uint); 00359 for (int i = 0; i < bytesLeft; i++) { 00360 hash ^= static_cast<uint>(m_index[offset + i]) << (i * 8); 00361 } 00362 } 00363 m_hash = hash; 00364 } 00365 00366 friend uint qHash(const CacheIndex &); 00367 friend class Scoreboard; 00368 00369 quint8 m_index[s_hashedUrlBytes]; // packed binary version of the hexadecimal name 00370 uint m_hash; 00371 }; 00372 00373 uint qHash(const CacheIndex &ci) 00374 { 00375 return ci.m_hash; 00376 } 00377 00378 00379 static CacheCleanerCommand readCommand(const QByteArray &cmd, CacheFileInfo *fi) 00380 { 00381 readBinaryHeader(cmd, fi); 00382 QDataStream stream(cmd); 00383 stream.skipRawData(SerializedCacheFileInfo::size); 00384 00385 quint32 ret; 00386 stream >> ret; 00387 00388 QByteArray baseName; 00389 baseName.resize(s_hashedUrlNibbles); 00390 stream.readRawData(baseName.data(), s_hashedUrlNibbles); 00391 Q_ASSERT(stream.atEnd()); 00392 fi->baseName = QString::fromLatin1(baseName); 00393 00394 Q_ASSERT(ret == CreateFileNotificationCommand || ret == UpdateFileCommand); 00395 return static_cast<CacheCleanerCommand>(ret); 00396 } 00397 00398 00399 // never istantiated, on-disk format only 00400 struct ScoreboardEntry { 00401 // from scoreboard file 00402 quint8 index[s_hashedUrlBytes]; 00403 static const int indexSize = s_hashedUrlBytes; 00404 qint32 useCount; 00405 // from scoreboard file, but compared with filesystem to see if scoreboard has current data 00406 qint64 lastUsedDate; 00407 qint32 sizeOnDisk; 00408 static const int size = 36; 00409 // we want to delete the least "useful" files and we'll have to sort a list for that... 00410 bool operator<(const MiniCacheFileInfo &other) const; 00411 }; 00412 00413 00414 class Scoreboard 00415 { 00416 public: 00417 Scoreboard() 00418 { 00419 // read in the scoreboard... 00420 QFile sboard(filePath(QLatin1String("scoreboard"))); 00421 sboard.open(QIODevice::ReadOnly); 00422 while (true) { 00423 QByteArray baIndex = sboard.read(ScoreboardEntry::indexSize); 00424 QByteArray baRest = sboard.read(ScoreboardEntry::size - ScoreboardEntry::indexSize); 00425 if (baIndex.size() + baRest.size() != ScoreboardEntry::size) { 00426 break; 00427 } 00428 00429 const QString entryBasename = QString::fromLatin1(baIndex.toHex()); 00430 MiniCacheFileInfo mcfi; 00431 if (readAndValidateMcfi(baRest, entryBasename, &mcfi)) { 00432 m_scoreboard.insert(CacheIndex(baIndex), mcfi); 00433 } 00434 } 00435 } 00436 00437 void writeOut() 00438 { 00439 // write out the scoreboard 00440 QFile sboard(filePath(QLatin1String("scoreboard"))); 00441 sboard.open(QIODevice::WriteOnly | QIODevice::Truncate); 00442 QDataStream stream(&sboard); 00443 00444 QHash<CacheIndex, MiniCacheFileInfo>::ConstIterator it = m_scoreboard.constBegin(); 00445 for (; it != m_scoreboard.constEnd(); ++it) { 00446 const char *indexData = reinterpret_cast<const char *>(it.key().m_index); 00447 stream.writeRawData(indexData, s_hashedUrlBytes); 00448 00449 stream << it.value().useCount; 00450 stream << it.value().lastUsedDate; 00451 stream << it.value().sizeOnDisk; 00452 } 00453 } 00454 00455 bool fillInfo(const QString &baseName, MiniCacheFileInfo *mcfi) 00456 { 00457 QHash<CacheIndex, MiniCacheFileInfo>::ConstIterator it = 00458 m_scoreboard.constFind(CacheIndex(baseName)); 00459 if (it == m_scoreboard.constEnd()) { 00460 return false; 00461 } 00462 *mcfi = it.value(); 00463 return true; 00464 } 00465 00466 int runCommand(const QByteArray &cmd) 00467 { 00468 // execute the command; return number of bytes if a new file was created, zero otherwise. 00469 Q_ASSERT(cmd.size() == 80); 00470 CacheFileInfo fi; 00471 const CacheCleanerCommand ccc = readCommand(cmd, &fi); 00472 QString fileName = filePath(fi.baseName); 00473 00474 switch (ccc) { 00475 case CreateFileNotificationCommand: 00476 kDebug(7113) << "CreateNotificationCommand for" << fi.baseName; 00477 if (!readBinaryHeader(cmd, &fi)) { 00478 return 0; 00479 } 00480 break; 00481 00482 case UpdateFileCommand: { 00483 kDebug(7113) << "UpdateFileCommand for" << fi.baseName; 00484 QFile file(fileName); 00485 file.open(QIODevice::ReadWrite); 00486 00487 CacheFileInfo fiFromDisk; 00488 QByteArray header = file.read(SerializedCacheFileInfo::size); 00489 if (!readBinaryHeader(header, &fiFromDisk) || fiFromDisk.bytesCached != fi.bytesCached) { 00490 return 0; 00491 } 00492 00493 // adjust the use count, to make sure that we actually count up. (slaves read the file 00494 // asynchronously...) 00495 const quint32 newUseCount = fiFromDisk.useCount + 1; 00496 QByteArray newHeader = cmd.mid(0, SerializedCacheFileInfo::size); 00497 { 00498 QDataStream stream(&newHeader, QIODevice::WriteOnly); 00499 stream.skipRawData(SerializedCacheFileInfo::useCountOffset); 00500 stream << newUseCount; 00501 } 00502 00503 file.seek(0); 00504 file.write(newHeader); 00505 file.close(); 00506 00507 if (!readBinaryHeader(newHeader, &fi)) { 00508 return 0; 00509 } 00510 break; 00511 } 00512 00513 default: 00514 kDebug(7113) << "received invalid command"; 00515 return 0; 00516 } 00517 00518 QFileInfo fileInfo(fileName); 00519 fi.lastUsedDate = fileInfo.lastModified().toTime_t(); 00520 fi.sizeOnDisk = fileInfo.size(); 00521 fi.debugPrint(); 00522 // a CacheFileInfo is-a MiniCacheFileInfo which enables the following assignment... 00523 add(fi); 00524 // finally, return cache dir growth (only relevant if a file was actually created!) 00525 return ccc == CreateFileNotificationCommand ? fi.sizeOnDisk : 0; 00526 } 00527 00528 void add(const CacheFileInfo &fi) 00529 { 00530 m_scoreboard[CacheIndex(fi.baseName)] = fi; 00531 } 00532 00533 void remove(const QString &basename) 00534 { 00535 m_scoreboard.remove(CacheIndex(basename)); 00536 } 00537 00538 // keep memory usage reasonably low - otherwise entries of nonexistent files don't hurt. 00539 void maybeRemoveStaleEntries(const QList<CacheFileInfo *> &fiList) 00540 { 00541 // don't bother when there are a few bogus entries 00542 if (m_scoreboard.count() < fiList.count() + 100) { 00543 return; 00544 } 00545 kDebug(7113) << "we have too many fake/stale entries, cleaning up..."; 00546 QSet<CacheIndex> realFiles; 00547 Q_FOREACH (CacheFileInfo *fi, fiList) { 00548 realFiles.insert(CacheIndex(fi->baseName)); 00549 } 00550 QHash<CacheIndex, MiniCacheFileInfo>::Iterator it = m_scoreboard.begin(); 00551 while (it != m_scoreboard.end()) { 00552 if (realFiles.contains(it.key())) { 00553 ++it; 00554 } else { 00555 it = m_scoreboard.erase(it); 00556 } 00557 } 00558 } 00559 00560 private: 00561 bool readAndValidateMcfi(const QByteArray &rawData, const QString &basename, MiniCacheFileInfo *mcfi) 00562 { 00563 QDataStream stream(rawData); 00564 stream >> mcfi->useCount; 00565 // check those against filesystem 00566 stream >> mcfi->lastUsedDate; 00567 stream >> mcfi->sizeOnDisk; 00568 00569 QFileInfo fileInfo(filePath(basename)); 00570 if (!fileInfo.exists()) { 00571 return false; 00572 } 00573 bool ok = true; 00574 ok = ok && fileInfo.lastModified().toTime_t() == mcfi->lastUsedDate; 00575 ok = ok && fileInfo.size() == mcfi->sizeOnDisk; 00576 if (!ok) { 00577 // size or last-modified date not consistent with entry file; reload useCount 00578 // note that avoiding to open the file is the whole purpose of the scoreboard - we only 00579 // open the file if we really have to. 00580 QFile entryFile(fileInfo.absoluteFilePath()); 00581 entryFile.open(QIODevice::ReadOnly); 00582 if (entryFile.size() < SerializedCacheFileInfo::size) { 00583 return false; 00584 } 00585 QDataStream stream(&entryFile); 00586 stream.skipRawData(SerializedCacheFileInfo::useCountOffset); 00587 00588 stream >> mcfi->useCount; 00589 mcfi->lastUsedDate = fileInfo.lastModified().toTime_t(); 00590 mcfi->sizeOnDisk = fileInfo.size(); 00591 ok = true; 00592 } 00593 return ok; 00594 } 00595 00596 QHash<CacheIndex, MiniCacheFileInfo> m_scoreboard; 00597 }; 00598 00599 00600 // Keep the above in sync with the cache code in http.cpp 00601 // !END OF SYNC! 00602 00603 // remove files and directories used by earlier versions of the HTTP cache. 00604 static void removeOldFiles() 00605 { 00606 const char *oldDirs = "0abcdefghijklmnopqrstuvwxyz"; 00607 const int n = strlen(oldDirs); 00608 QDir cacheRootDir(filePath(QString())); 00609 for (int i = 0; i < n; i++) { 00610 QString dirName = QString::fromLatin1(&oldDirs[i], 1); 00611 // delete files in directory... 00612 Q_FOREACH (const QString &baseName, QDir(filePath(dirName)).entryList()) { 00613 QFile::remove(filePath(dirName + '/' + baseName)); 00614 } 00615 // delete the (now hopefully empty!) directory itself 00616 cacheRootDir.rmdir(dirName); 00617 } 00618 QFile::remove(filePath(QLatin1String("cleaned"))); 00619 } 00620 00621 class CacheCleaner 00622 { 00623 public: 00624 CacheCleaner(const QDir &cacheDir) 00625 : m_totalSizeOnDisk(0) 00626 { 00627 kDebug(7113); 00628 m_fileNameList = cacheDir.entryList(); 00629 } 00630 00631 // Delete some of the files that need to be deleted. Return true when done, false otherwise. 00632 // This makes interleaved cleaning / serving ioslaves possible. 00633 bool processSlice(Scoreboard *scoreboard = 0) 00634 { 00635 QTime t; 00636 t.start(); 00637 // phase one: gather information about cache files 00638 if (!m_fileNameList.isEmpty()) { 00639 while (t.elapsed() < 100 && !m_fileNameList.isEmpty()) { 00640 QString baseName = m_fileNameList.takeFirst(); 00641 // check if the filename is of the $s_hashedUrlNibbles letters, 0...f type 00642 if (baseName.length() < s_hashedUrlNibbles) { 00643 continue; 00644 } 00645 bool nameOk = true; 00646 for (int i = 0; i < s_hashedUrlNibbles && nameOk; i++) { 00647 QChar c = baseName[i]; 00648 nameOk = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); 00649 } 00650 if (!nameOk) { 00651 continue; 00652 } 00653 if (baseName.length() > s_hashedUrlNibbles) { 00654 if (g_currentDate - QFileInfo(filePath(baseName)).lastModified().toTime_t() > 15*60) { 00655 // it looks like a temporary file that hasn't been touched in > 15 minutes... 00656 QFile::remove(filePath(baseName)); 00657 } 00658 // the temporary file might still be written to, leave it alone 00659 continue; 00660 } 00661 00662 CacheFileInfo *fi = new CacheFileInfo(); 00663 fi->baseName = baseName; 00664 00665 bool gotInfo = false; 00666 if (scoreboard) { 00667 gotInfo = scoreboard->fillInfo(baseName, fi); 00668 } 00669 if (!gotInfo) { 00670 gotInfo = readCacheFile(baseName, fi, CleanCache); 00671 if (gotInfo && scoreboard) { 00672 scoreboard->add(*fi); 00673 } 00674 } 00675 if (gotInfo) { 00676 m_fiList.append(fi); 00677 m_totalSizeOnDisk += fi->sizeOnDisk; 00678 } else { 00679 delete fi; 00680 } 00681 } 00682 kDebug(7113) << "total size of cache files is" << m_totalSizeOnDisk; 00683 00684 if (m_fileNameList.isEmpty()) { 00685 // final step of phase one 00686 qSort(m_fiList.begin(), m_fiList.end(), CacheFileInfoPtrLessThan); 00687 } 00688 return false; 00689 } 00690 00691 // phase two: delete files until cache is under maximum allowed size 00692 00693 // TODO: delete files larger than allowed for a single file 00694 while (t.elapsed() < 100) { 00695 if (m_totalSizeOnDisk <= g_maxCacheSize || m_fiList.isEmpty()) { 00696 kDebug(7113) << "total size of cache files after cleaning is" << m_totalSizeOnDisk; 00697 if (scoreboard) { 00698 scoreboard->maybeRemoveStaleEntries(m_fiList); 00699 scoreboard->writeOut(); 00700 } 00701 qDeleteAll(m_fiList); 00702 m_fiList.clear(); 00703 return true; 00704 } 00705 CacheFileInfo *fi = m_fiList.takeFirst(); 00706 QString filename = filePath(fi->baseName); 00707 if (QFile::remove(filename)) { 00708 m_totalSizeOnDisk -= fi->sizeOnDisk; 00709 if (scoreboard) { 00710 scoreboard->remove(fi->baseName); 00711 } 00712 } 00713 delete fi; 00714 } 00715 return false; 00716 } 00717 00718 private: 00719 QStringList m_fileNameList; 00720 QList<CacheFileInfo *> m_fiList; 00721 qint64 m_totalSizeOnDisk; 00722 }; 00723 00724 00725 extern "C" KDE_EXPORT int kdemain(int argc, char **argv) 00726 { 00727 KCmdLineArgs::init(argc, argv, appName, "kdelibs4", 00728 ki18n("KDE HTTP cache maintenance tool"), version, 00729 ki18n("KDE HTTP cache maintenance tool"), KCmdLineArgs::CmdLineArgNone); 00730 00731 KCmdLineOptions options; 00732 options.add("clear-all", ki18n("Empty the cache")); 00733 options.add("file-info <filename>", ki18n("Display information about cache file")); 00734 00735 KCmdLineArgs::addCmdLineOptions( options ); 00736 KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); 00737 KComponentData componentData(appName); 00738 00739 // we need a QCoreApplication so QCoreApplication::processEvents() works as intended 00740 QCoreApplication app(argc, argv); 00741 00742 OperationMode mode = CleanCache; 00743 if (args->isSet("clear-all")) { 00744 mode = DeleteCache; 00745 } else if (args->isSet("file-info")) { 00746 mode = FileInfo; 00747 } 00748 00749 // file info mode: no scanning of directories, just output info and exit. 00750 if (mode == FileInfo) { 00751 CacheFileInfo fi; 00752 if (!readCacheFile(args->getOption("file-info"), &fi, mode)) { 00753 return 1; 00754 } 00755 fi.prettyPrint(); 00756 return 0; 00757 } 00758 00759 // make sure we're the only running instance of the cleaner service 00760 if (mode == CleanCache) { 00761 if (!QDBusConnection::sessionBus().isConnected()) { 00762 QDBusError error(QDBusConnection::sessionBus().lastError()); 00763 fprintf(stderr, "%s: Could not connect to D-Bus! (%s: %s)\n", appName, 00764 qPrintable(error.name()), qPrintable(error.message())); 00765 return 1; 00766 } 00767 00768 if (!QDBusConnection::sessionBus().registerService(appFullName)) { 00769 fprintf(stderr, "%s: Already running!\n", appName); 00770 return 0; 00771 } 00772 } 00773 00774 00775 g_currentDate = time(0); 00776 g_maxCacheAge = KProtocolManager::maxCacheAge(); 00777 g_maxCacheSize = mode == DeleteCache ? -1 : KProtocolManager::maxCacheSize() * 1024; 00778 00779 QString cacheDirName = KGlobal::dirs()->saveLocation("cache", "http"); 00780 QDir cacheDir(cacheDirName); 00781 if (!cacheDir.exists()) { 00782 fprintf(stderr, "%s: '%s' does not exist.\n", appName, qPrintable(cacheDirName)); 00783 return 0; 00784 } 00785 00786 removeOldFiles(); 00787 00788 if (mode == DeleteCache) { 00789 QTime t; 00790 t.start(); 00791 cacheDir.refresh(); 00792 //qDebug() << "time to refresh the cacheDir QDir:" << t.elapsed(); 00793 CacheCleaner cleaner(cacheDir); 00794 while (!cleaner.processSlice()) { } 00795 return 0; 00796 } 00797 00798 QLocalServer lServer; 00799 QString socketFileName = KStandardDirs::locateLocal("socket", "kio_http_cache_cleaner"); 00800 // we need to create the file by opening the socket, otherwise it won't work 00801 QFile::remove(socketFileName); 00802 lServer.listen(socketFileName); 00803 QList<QLocalSocket *> sockets; 00804 int newBytesCounter = INT_MAX; // force cleaner run on startup 00805 00806 Scoreboard scoreboard; 00807 CacheCleaner *cleaner = 0; 00808 while (true) { 00809 g_currentDate = time(0); 00810 if (cleaner) { 00811 QCoreApplication::processEvents(QEventLoop::AllEvents, 100); 00812 } else { 00813 // We will not immediately know when a socket was disconnected. Causes: 00814 // - WaitForMoreEvents does not make processEvents() return when a socket disconnects 00815 // - WaitForMoreEvents *and* a timeout is not possible. 00816 QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); 00817 } 00818 if (!lServer.isListening()) { 00819 return 1; 00820 } 00821 lServer.waitForNewConnection(1); 00822 00823 while (QLocalSocket *sock = lServer.nextPendingConnection()) { 00824 sock->waitForConnected(); 00825 sockets.append(sock); 00826 } 00827 00828 for (int i = 0; i < sockets.size(); i++) { 00829 QLocalSocket *sock = sockets[i]; 00830 if (sock->state() != QLocalSocket::ConnectedState) { 00831 if (sock->state() != QLocalSocket::UnconnectedState) { 00832 sock->waitForDisconnected(); 00833 } 00834 delete sock; 00835 sockets.removeAll(sock); 00836 i--; 00837 continue; 00838 } 00839 sock->waitForReadyRead(0); 00840 while (true) { 00841 QByteArray recv = sock->read(80); 00842 if (recv.isEmpty()) { 00843 break; 00844 } 00845 Q_ASSERT(recv.size() == 80); 00846 newBytesCounter += scoreboard.runCommand(recv); 00847 } 00848 } 00849 00850 // interleave cleaning with serving ioslaves to reduce "garbage collection pauses" 00851 if (cleaner) { 00852 if (cleaner->processSlice(&scoreboard)) { 00853 // that was the last slice, done 00854 delete cleaner; 00855 cleaner = 0; 00856 } 00857 } else if (newBytesCounter > g_maxCacheSize / 8) { 00858 cacheDir.refresh(); 00859 cleaner = new CacheCleaner(cacheDir); 00860 newBytesCounter = 0; 00861 } 00862 } 00863 return 0; 00864 }
KDE 4.6 API Reference