1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2009 Tobias Koenig <tokoe@kde.org>
4     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "trashsizecache.h"
10 
11 #include "discspaceutil.h"
12 #include "kiotrashdebug.h"
13 
14 #include <QDateTime>
15 #include <QDir>
16 #include <QDirIterator>
17 #include <QFile>
18 #include <QSaveFile>
19 #include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF
20 
TrashSizeCache(const QString & path)21 TrashSizeCache::TrashSizeCache(const QString &path)
22     : mTrashSizeCachePath(path + QLatin1String("/directorysizes"))
23     , mTrashPath(path)
24 {
25     // qCDebug(KIO_TRASH) << "CACHE:" << mTrashSizeCachePath;
26 }
27 
28 // Only the last part of the line: space, directory name, '\n'
spaceAndDirectoryAndNewline(const QString & directoryName)29 static QByteArray spaceAndDirectoryAndNewline(const QString &directoryName)
30 {
31     const QByteArray encodedDir = QFile::encodeName(directoryName).toPercentEncoding();
32     return ' ' + encodedDir + '\n';
33 }
34 
add(const QString & directoryName,qint64 directorySize)35 void TrashSizeCache::add(const QString &directoryName, qint64 directorySize)
36 {
37     // qCDebug(KIO_TRASH) << directoryName << directorySize;
38     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName);
39     QFile file(mTrashSizeCachePath);
40     QSaveFile out(mTrashSizeCachePath);
41     if (out.open(QIODevice::WriteOnly)) {
42         if (file.open(QIODevice::ReadOnly)) {
43             while (!file.atEnd()) {
44                 const QByteArray line = file.readLine();
45                 if (line.endsWith(spaceAndDirAndNewline)) {
46                     // Already there!
47                     out.cancelWriting();
48                     // qCDebug(KIO_TRASH) << "already there!";
49                     return;
50                 }
51                 out.write(line);
52             }
53         }
54 
55         QDateTime mtime = getTrashFileInfo(directoryName).lastModified();
56         QByteArray newLine = QByteArray::number(directorySize) + ' ' + QByteArray::number(mtime.toMSecsSinceEpoch()) + spaceAndDirAndNewline;
57         out.write(newLine);
58         out.commit();
59     }
60     // qCDebug(KIO_TRASH) << mTrashSizeCachePath << "exists:" << QFile::exists(mTrashSizeCachePath);
61 }
62 
remove(const QString & directoryName)63 void TrashSizeCache::remove(const QString &directoryName)
64 {
65     // qCDebug(KIO_TRASH) << directoryName;
66     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName);
67     QFile file(mTrashSizeCachePath);
68     QSaveFile out(mTrashSizeCachePath);
69     if (file.open(QIODevice::ReadOnly) && out.open(QIODevice::WriteOnly)) {
70         while (!file.atEnd()) {
71             const QByteArray line = file.readLine();
72             if (line.endsWith(spaceAndDirAndNewline)) {
73                 // Found it -> skip it
74                 continue;
75             }
76             out.write(line);
77         }
78     }
79     out.commit();
80 }
81 
rename(const QString & oldDirectoryName,const QString & newDirectoryName)82 void TrashSizeCache::rename(const QString &oldDirectoryName, const QString &newDirectoryName)
83 {
84     const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(oldDirectoryName);
85     QFile file(mTrashSizeCachePath);
86     QSaveFile out(mTrashSizeCachePath);
87     if (file.open(QIODevice::ReadOnly) && out.open(QIODevice::WriteOnly)) {
88         while (!file.atEnd()) {
89             QByteArray line = file.readLine();
90             if (line.endsWith(spaceAndDirAndNewline)) {
91                 // Found it -> rename it, keeping the size
92                 line = line.left(line.length() - spaceAndDirAndNewline.length()) + spaceAndDirectoryAndNewline(newDirectoryName);
93             }
94             out.write(line);
95         }
96     }
97     out.commit();
98 }
99 
clear()100 void TrashSizeCache::clear()
101 {
102     QFile::remove(mTrashSizeCachePath);
103 }
104 
calculateSize()105 qint64 TrashSizeCache::calculateSize()
106 {
107     return this->calculateSizeAndLatestModDate().size;
108 }
109 
getTrashFileInfo(const QString & fileName)110 QFileInfo TrashSizeCache::getTrashFileInfo(const QString &fileName)
111 {
112     const QString fileInfoPath = mTrashPath + QLatin1String("/info/") + fileName + QLatin1String(".trashinfo");
113     return QFileInfo(fileInfoPath);
114 }
115 
calculateSizeAndLatestModDate()116 TrashSizeCache::SizeAndModTime TrashSizeCache::calculateSizeAndLatestModDate()
117 {
118     // First read the directorysizes cache into memory
119     QFile file(mTrashSizeCachePath);
120     typedef QHash<QByteArray, SizeAndModTime> DirCacheHash;
121     DirCacheHash dirCache;
122     if (file.open(QIODevice::ReadOnly)) {
123         while (!file.atEnd()) {
124             const QByteArray line = file.readLine();
125             const int firstSpace = line.indexOf(' ');
126             const int secondSpace = line.indexOf(' ', firstSpace + 1);
127             SizeAndModTime data;
128             data.size = line.left(firstSpace).toLongLong();
129             // "012 4567 name" -> firstSpace=3, secondSpace=8, we want mid(4,4)
130             data.mtime = line.mid(firstSpace + 1, secondSpace - firstSpace - 1).toLongLong();
131             dirCache.insert(line.mid(secondSpace + 1), data);
132         }
133     }
134     // Iterate over the actual trashed files.
135     // Orphan items (no .fileinfo) still take space.
136     QDirIterator it(mTrashPath + QLatin1String("/files/"), QDirIterator::NoIteratorFlags);
137 
138     qint64 sum = 0;
139     qint64 max_mtime = 0;
140     const auto checkMaxTime = [&max_mtime](const qint64 lastModTime) {
141         if (lastModTime > max_mtime) {
142             max_mtime = lastModTime;
143         }
144     };
145     const auto checkLastModTime = [this, checkMaxTime](const QString &fileName) {
146         const auto trashFileInfo = getTrashFileInfo(fileName);
147         if (!trashFileInfo.exists()) {
148             return;
149         }
150         checkMaxTime(trashFileInfo.lastModified().toMSecsSinceEpoch());
151     };
152     while (it.hasNext()) {
153         const QFileInfo fileInfo = it.next();
154         const QString fileName = fileInfo.fileName();
155         if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
156             continue;
157         }
158         if (fileInfo.isSymLink()) {
159             // QFileInfo::size does not return the actual size of a symlink. #253776
160             QT_STATBUF buff;
161             if (QT_LSTAT(QFile::encodeName(fileInfo.absoluteFilePath()).constData(), &buff) == 0) {
162                 sum += static_cast<unsigned long long>(buff.st_size);
163                 checkLastModTime(fileName);
164             }
165         } else if (fileInfo.isFile()) {
166             sum += static_cast<unsigned long long>(fileInfo.size());
167             checkLastModTime(fileName);
168         } else {
169             // directories
170             bool usableCache = false;
171             auto it = dirCache.constFind(QFile::encodeName(fileName));
172             if (it != dirCache.constEnd()) {
173                 const SizeAndModTime &data = *it;
174                 const auto trashFileInfo = getTrashFileInfo(fileName);
175                 if (trashFileInfo.exists() && trashFileInfo.lastModified().toMSecsSinceEpoch() == data.mtime) {
176                     sum += data.size;
177                     usableCache = true;
178                     checkMaxTime(data.mtime);
179                 }
180             }
181             if (!usableCache) {
182                 // directories with no cache data (or outdated)
183                 const qint64 size = DiscSpaceUtil::sizeOfPath(fileInfo.absoluteFilePath());
184                 sum += size;
185                 // NOTE: this does not take into account the directory content modification date
186                 checkMaxTime(QFileInfo(fileInfo.absolutePath()).lastModified().toMSecsSinceEpoch());
187                 add(fileName, size);
188             }
189         }
190     }
191     return {sum, max_mtime};
192 }
193