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

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