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
21TrashSizeCache::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'
29static QByteArray spaceAndDirectoryAndNewline(const QString &directoryName)
30{
31 const QByteArray encodedDir = QFile::encodeName(fileName: directoryName).toPercentEncoding();
32 return ' ' + encodedDir + '\n';
33}
34
35void 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(flags: QIODevice::WriteOnly)) {
42 if (file.open(flags: QIODevice::ReadOnly)) {
43 while (!file.atEnd()) {
44 const QByteArray line = file.readLine();
45 if (line.endsWith(bv: spaceAndDirAndNewline)) {
46 // Already there!
47 out.cancelWriting();
48 // qCDebug(KIO_TRASH) << "already there!";
49 return;
50 }
51 out.write(data: line);
52 }
53 }
54
55 const auto trashInfo = getTrashFileInfo(fileName: directoryName);
56 if (trashInfo) {
57 const qint64 mtime = trashInfo->lastModified().toMSecsSinceEpoch();
58 QByteArray newLine = QByteArray::number(directorySize) + ' ' + QByteArray::number(mtime) + spaceAndDirAndNewline;
59 out.write(data: newLine);
60 out.commit();
61 }
62 }
63 // qCDebug(KIO_TRASH) << mTrashSizeCachePath << "exists:" << QFile::exists(mTrashSizeCachePath);
64}
65
66void TrashSizeCache::remove(const QString &directoryName)
67{
68 // qCDebug(KIO_TRASH) << directoryName;
69 const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName);
70 QFile file(mTrashSizeCachePath);
71 QSaveFile out(mTrashSizeCachePath);
72 if (file.open(flags: QIODevice::ReadOnly) && out.open(flags: QIODevice::WriteOnly)) {
73 while (!file.atEnd()) {
74 const QByteArray line = file.readLine();
75 if (line.endsWith(bv: spaceAndDirAndNewline)) {
76 // Found it -> skip it
77 continue;
78 }
79 out.write(data: line);
80 }
81 }
82 out.commit();
83}
84
85void TrashSizeCache::rename(const QString &oldDirectoryName, const QString &newDirectoryName)
86{
87 const QByteArray spaceAndDirAndNewline = spaceAndDirectoryAndNewline(directoryName: oldDirectoryName);
88 QFile file(mTrashSizeCachePath);
89 QSaveFile out(mTrashSizeCachePath);
90 if (file.open(flags: QIODevice::ReadOnly) && out.open(flags: QIODevice::WriteOnly)) {
91 while (!file.atEnd()) {
92 QByteArray line = file.readLine();
93 if (line.endsWith(bv: spaceAndDirAndNewline)) {
94 // Found it -> rename it, keeping the size
95 line = line.left(n: line.length() - spaceAndDirAndNewline.length()) + spaceAndDirectoryAndNewline(directoryName: newDirectoryName);
96 }
97 out.write(data: line);
98 }
99 }
100 out.commit();
101}
102
103void TrashSizeCache::clear()
104{
105 QFile::remove(fileName: mTrashSizeCachePath);
106}
107
108std::optional<QFileInfo> TrashSizeCache::getTrashFileInfo(const QString &fileName)
109{
110 const QString fileInfoPath = mTrashPath + QLatin1String("/info/") + fileName + QLatin1String(".trashinfo");
111 auto info = QFileInfo(fileInfoPath);
112 if (info.exists()) {
113 return {info};
114 } else {
115 return {};
116 }
117}
118
119QHash<QByteArray, TrashSizeCache::SizeAndModTime> TrashSizeCache::readDirCache()
120{
121 // First read the directorysizes cache into memory
122 QFile file(mTrashSizeCachePath);
123 QHash<QByteArray, SizeAndModTime> dirCache;
124 if (file.open(flags: QIODevice::ReadOnly)) {
125 while (!file.atEnd()) {
126 const QByteArray line = file.readLine();
127 const int firstSpace = line.indexOf(ch: ' ');
128 const int secondSpace = line.indexOf(ch: ' ', from: firstSpace + 1);
129 SizeAndModTime data;
130 data.size = line.left(n: firstSpace).toLongLong();
131 // "012 4567 name\n" -> firstSpace=3, secondSpace=8, we want mid(4,4)
132 data.mtime = line.mid(index: firstSpace + 1, len: secondSpace - firstSpace - 1).toLongLong();
133 const auto name = line.mid(index: secondSpace + 1, len: line.length() - secondSpace - 2);
134 dirCache.insert(key: name, value: data);
135 }
136 }
137 return dirCache;
138}
139
140qint64 TrashSizeCache::calculateSize()
141{
142 return scanFilesInTrash(checkDateTime: ScanFilesInTrashOption::DontCheckModificationTime).size;
143}
144
145TrashSizeCache::SizeAndModTime TrashSizeCache::calculateSizeAndLatestModDate()
146{
147 return scanFilesInTrash(checkDateTime: ScanFilesInTrashOption::CheckModificationTime);
148}
149
150TrashSizeCache::SizeAndModTime TrashSizeCache::scanFilesInTrash(ScanFilesInTrashOption checkDateTime)
151{
152 const QHash<QByteArray, SizeAndModTime> dirCache = readDirCache();
153
154 // Iterate over the actual trashed files.
155 // Orphan items (no .fileinfo) still take space.
156 QDirIterator it(mTrashPath + QLatin1String("/files/"), QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
157 qint64 sum = 0;
158 qint64 max_mtime = 0;
159 const auto checkMaxTime = [&max_mtime](const qint64 lastModTime) {
160 if (lastModTime > max_mtime) {
161 max_mtime = lastModTime;
162 }
163 };
164 const auto checkLastModTime = [this, checkMaxTime](const QString &fileName) {
165 const auto trashFileInfo = getTrashFileInfo(fileName);
166 if (!trashFileInfo) {
167 return;
168 }
169 checkMaxTime(trashFileInfo->lastModified().toMSecsSinceEpoch());
170 };
171 while (it.hasNext()) {
172 it.next();
173 const QString fileName = it.fileName();
174 const QFileInfo fileInfo = it.fileInfo();
175 if (fileInfo.isSymLink()) {
176 // QFileInfo::size does not return the actual size of a symlink. #253776
177 QT_STATBUF buff;
178 if (QT_LSTAT(file: QFile::encodeName(fileName: fileInfo.absoluteFilePath()).constData(), buf: &buff) == 0) {
179 sum += static_cast<unsigned long long>(buff.st_size);
180 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
181 checkLastModTime(fileName);
182 }
183 }
184 } else if (fileInfo.isFile()) {
185 sum += static_cast<unsigned long long>(fileInfo.size());
186 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
187 checkLastModTime(fileName);
188 }
189 } else {
190 // directories
191 bool usableCache = false;
192 auto dirIt = dirCache.constFind(key: QFile::encodeName(fileName));
193 if (dirIt != dirCache.constEnd()) {
194 const SizeAndModTime &data = *dirIt;
195 const auto trashFileInfo = getTrashFileInfo(fileName);
196 if (trashFileInfo && trashFileInfo->lastModified().toMSecsSinceEpoch() == data.mtime) {
197 sum += data.size;
198 usableCache = true;
199 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
200 checkMaxTime(data.mtime);
201 }
202 }
203 }
204 if (!usableCache) {
205 // directories with no cache data (or outdated)
206 const qint64 size = DiscSpaceUtil::sizeOfPath(path: fileInfo.absoluteFilePath());
207 sum += size;
208 if (checkDateTime == ScanFilesInTrashOption::CheckModificationTime) {
209 // NOTE: this does not take into account the directory content modification date
210 checkMaxTime(QFileInfo(fileInfo.absolutePath()).lastModified().toMSecsSinceEpoch());
211 }
212 add(directoryName: fileName, directorySize: size);
213 }
214 }
215 }
216 return {.size: sum, .mtime: max_mtime};
217}
218

source code of kio/src/kioworkers/trash/trashsizecache.cpp