File indexing completed on 2024-05-05 03:56:09

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2009 Tobias Koenig <tokoe@kde.org>
0004     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "trashsizecache.h"
0010 
0011 #include "discspaceutil.h"
0012 #include "kiotrashdebug.h"
0013 
0014 #include <QDateTime>
0015 #include <QDir>
0016 #include <QDirIterator>
0017 #include <QFile>
0018 #include <QSaveFile>
0019 #include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF
0020 
0021 TrashSizeCache::TrashSizeCache(const QString &path)
0022     : mTrashSizeCachePath(path + QLatin1String("/directorysizes"))
0023     , mTrashPath(path)
0024 {
0025     // qCDebug(KIO_TRASH) << "CACHE:" << mTrashSizeCachePath;
0026 }
0027 
0028 // Only the last part of the line: space, directory name, '\n'
0029 static QByteArray spaceAndDirectoryAndNewline(const QString &directoryName)
0030 {
0031     const QByteArray encodedDir = QFile::encodeName(directoryName).toPercentEncoding();
0032     return ' ' + encodedDir + '\n';
0033 }
0034 
0035 void TrashSizeCache::add(const QString &directoryName, qint64 directorySize)
0036 {
0037     // qCDebug(KIO_TRASH) << directoryName << directorySize;
0038     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName);
0039     QFile file(mTrashSizeCachePath);
0040     QSaveFile out(mTrashSizeCachePath);
0041     if (out.open(QIODevice::WriteOnly)) {
0042         if (file.open(QIODevice::ReadOnly)) {
0043             while (!file.atEnd()) {
0044                 const QByteArray line = file.readLine();
0045                 if (line.endsWith(spaceAndDirAndNewline)) {
0046                     // Already there!
0047                     out.cancelWriting();
0048                     // qCDebug(KIO_TRASH) << "already there!";
0049                     return;
0050                 }
0051                 out.write(line);
0052             }
0053         }
0054 
0055         const qint64 mtime = getTrashFileInfo(directoryName).lastModified().toMSecsSinceEpoch();
0056         QByteArray newLine = QByteArray::number(directorySize) + ' ' + QByteArray::number(mtime) + spaceAndDirAndNewline;
0057         out.write(newLine);
0058         out.commit();
0059     }
0060     // qCDebug(KIO_TRASH) << mTrashSizeCachePath << "exists:" << QFile::exists(mTrashSizeCachePath);
0061 }
0062 
0063 void TrashSizeCache::remove(const QString &directoryName)
0064 {
0065     // qCDebug(KIO_TRASH) << directoryName;
0066     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName);
0067     QFile file(mTrashSizeCachePath);
0068     QSaveFile out(mTrashSizeCachePath);
0069     if (file.open(QIODevice::ReadOnly) && out.open(QIODevice::WriteOnly)) {
0070         while (!file.atEnd()) {
0071             const QByteArray line = file.readLine();
0072             if (line.endsWith(spaceAndDirAndNewline)) {
0073                 // Found it -> skip it
0074                 continue;
0075             }
0076             out.write(line);
0077         }
0078     }
0079     out.commit();
0080 }
0081 
0082 void TrashSizeCache::rename(const QString &oldDirectoryName, const QString &newDirectoryName)
0083 {
0084     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(oldDirectoryName);
0085     QFile file(mTrashSizeCachePath);
0086     QSaveFile out(mTrashSizeCachePath);
0087     if (file.open(QIODevice::ReadOnly) && out.open(QIODevice::WriteOnly)) {
0088         while (!file.atEnd()) {
0089             QByteArray line = file.readLine();
0090             if (line.endsWith(spaceAndDirAndNewline)) {
0091                 // Found it -> rename it, keeping the size
0092                 line = line.left(line.length() - spaceAndDirAndNewline.length()) + spaceAndDirectoryAndNewline(newDirectoryName);
0093             }
0094             out.write(line);
0095         }
0096     }
0097     out.commit();
0098 }
0099 
0100 void TrashSizeCache::clear()
0101 {
0102     QFile::remove(mTrashSizeCachePath);
0103 }
0104 
0105 QFileInfo TrashSizeCache::getTrashFileInfo(const QString &fileName)
0106 {
0107     const QString fileInfoPath = mTrashPath + QLatin1String("/info/") + fileName + QLatin1String(".trashinfo");
0108     Q_ASSERT(QFile::exists(fileInfoPath));
0109     return QFileInfo(fileInfoPath);
0110 }
0111 
0112 QHash<QByteArray, TrashSizeCache::SizeAndModTime> TrashSizeCache::readDirCache()
0113 {
0114     // First read the directorysizes cache into memory
0115     QFile file(mTrashSizeCachePath);
0116     QHash<QByteArray, SizeAndModTime> dirCache;
0117     if (file.open(QIODevice::ReadOnly)) {
0118         while (!file.atEnd()) {
0119             const QByteArray line = file.readLine();
0120             const int firstSpace = line.indexOf(' ');
0121             const int secondSpace = line.indexOf(' ', firstSpace + 1);
0122             SizeAndModTime data;
0123             data.size = line.left(firstSpace).toLongLong();
0124             // "012 4567 name\n" -> firstSpace=3, secondSpace=8, we want mid(4,4)
0125             data.mtime = line.mid(firstSpace + 1, secondSpace - firstSpace - 1).toLongLong();
0126             const auto name = line.mid(secondSpace + 1, line.length() - secondSpace - 2);
0127             dirCache.insert(name, data);
0128         }
0129     }
0130     return dirCache;
0131 }
0132 
0133 qint64 TrashSizeCache::calculateSize()
0134 {
0135     return scanFilesInTrash(ScanFilesInTrashOption::DonTcheckModificationTime).size;
0136 }
0137 
0138 TrashSizeCache::SizeAndModTime TrashSizeCache::calculateSizeAndLatestModDate()
0139 {
0140     return scanFilesInTrash(ScanFilesInTrashOption::CheckModificationTime);
0141 }
0142 
0143 TrashSizeCache::SizeAndModTime TrashSizeCache::scanFilesInTrash(ScanFilesInTrashOption checkDateTime)
0144 {
0145     const QHash<QByteArray, SizeAndModTime> dirCache = readDirCache();
0146 
0147     // Iterate over the actual trashed files.
0148     // Orphan items (no .fileinfo) still take space.
0149     QDirIterator it(mTrashPath + QLatin1String("/files/"), QDir::NoDotAndDotDot);
0150     qint64 sum = 0;
0151     qint64 max_mtime = 0;
0152     const auto checkMaxTime = [&max_mtime](const qint64 lastModTime) {
0153         if (lastModTime > max_mtime) {
0154             max_mtime = lastModTime;
0155         }
0156     };
0157     const auto checkLastModTime = [this, checkMaxTime](const QString &fileName) {
0158         const auto trashFileInfo = getTrashFileInfo(fileName);
0159         if (!trashFileInfo.exists()) {
0160             return;
0161         }
0162         checkMaxTime(trashFileInfo.lastModified().toMSecsSinceEpoch());
0163     };
0164     while (it.hasNext()) {
0165         it.next();
0166         const QString fileName = it.fileName();
0167         const QFileInfo fileInfo = it.fileInfo();
0168         if (fileInfo.isSymLink()) {
0169             // QFileInfo::size does not return the actual size of a symlink. #253776
0170             QT_STATBUF buff;
0171             if (QT_LSTAT(QFile::encodeName(fileInfo.absoluteFilePath()).constData(), &buff) == 0) {
0172                 sum += static_cast<unsigned long long>(buff.st_size);
0173                 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
0174                     checkLastModTime(fileName);
0175                 }
0176             }
0177         } else if (fileInfo.isFile()) {
0178             sum += static_cast<unsigned long long>(fileInfo.size());
0179             if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
0180                 checkLastModTime(fileName);
0181             }
0182         } else {
0183             // directories
0184             bool usableCache = false;
0185             auto dirIt = dirCache.constFind(QFile::encodeName(fileName));
0186             if (dirIt != dirCache.constEnd()) {
0187                 const SizeAndModTime &data = *dirIt;
0188                 const auto trashFileInfo = getTrashFileInfo(fileName);
0189                 if (trashFileInfo.exists() && trashFileInfo.lastModified().toMSecsSinceEpoch() == data.mtime) {
0190                     sum += data.size;
0191                     usableCache = true;
0192                     if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
0193                         checkMaxTime(data.mtime);
0194                     }
0195                 }
0196             }
0197             if (!usableCache) {
0198                 // directories with no cache data (or outdated)
0199                 const qint64 size = DiscSpaceUtil::sizeOfPath(fileInfo.absoluteFilePath());
0200                 sum += size;
0201                 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
0202                     // NOTE: this does not take into account the directory content modification date
0203                     checkMaxTime(QFileInfo(fileInfo.absolutePath()).lastModified().toMSecsSinceEpoch());
0204                 }
0205                 add(fileName, size);
0206             }
0207         }
0208     }
0209     return {sum, max_mtime};
0210 }