1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "trashimpl.h"
9#include "discspaceutil.h"
10#include "kiotrashdebug.h"
11#include "trashsizecache.h"
12
13#include "../utils_p.h"
14#include <kdirnotify.h>
15#include <kfileitem.h>
16#include <kio/chmodjob.h>
17#include <kio/copyjob.h>
18#include <kio/deletejob.h>
19#include <kmountpoint.h>
20
21#include <KConfigGroup>
22#include <KFileUtils>
23#include <KJobUiDelegate>
24#include <KLocalizedString>
25#include <KSharedConfig>
26#include <solid/block.h>
27#include <solid/device.h>
28#include <solid/networkshare.h>
29#include <solid/storageaccess.h>
30
31#include <QCoreApplication>
32#include <QDebug>
33#include <QDir>
34#include <QEventLoop>
35#include <QFile>
36#include <QLockFile>
37#include <QStandardPaths>
38#include <QUrl>
39
40#include <cerrno>
41#include <dirent.h>
42#include <fcntl.h>
43#include <stdlib.h>
44#include <sys/param.h>
45#include <sys/stat.h>
46#include <sys/types.h>
47#include <unistd.h>
48
49TrashImpl::TrashImpl()
50 : QObject()
51 , m_lastErrorCode(0)
52 , m_initStatus(InitToBeDone)
53 , m_homeDevice(0)
54 , m_trashDirectoriesScanned(false)
55 ,
56 // not using kio_trashrc since KIO uses that one already for kio_trash
57 // so better have a separate one, for faster parsing by e.g. kmimetype.cpp
58 m_config(QStringLiteral("trashrc"), KConfig::SimpleConfig)
59{
60 QT_STATBUF buff;
61 if (QT_LSTAT(file: QFile::encodeName(fileName: QDir::homePath()).constData(), buf: &buff) == 0) {
62 m_homeDevice = buff.st_dev;
63 } else {
64 qCWarning(KIO_TRASH) << "Should never happen: couldn't stat $HOME" << strerror(errno);
65 }
66}
67
68/*!
69 * Test if a directory exists, create otherwise
70 * \a _name full path of the directory
71 * Returns errorcode, or 0 if the dir was created or existed already
72 * Warning, don't use return value like a bool
73 */
74int TrashImpl::testDir(const QString &_name) const
75{
76 DIR *dp = ::opendir(name: QFile::encodeName(fileName: _name).constData());
77 if (!dp) {
78 QString name = Utils::trailingSlashRemoved(s: _name);
79
80 bool ok = QDir().mkdir(dirName: name);
81 if (!ok && QFile::exists(fileName: name)) {
82 QString new_name = name;
83 name.append(QStringLiteral(".orig"));
84 if (QFile::rename(oldName: name, newName: new_name)) {
85 ok = QDir().mkdir(dirName: name);
86 } else { // foo.orig existed already. How likely is that?
87 ok = false;
88 }
89 if (!ok) {
90 return KIO::ERR_DIR_ALREADY_EXIST;
91 }
92 }
93 if (!ok) {
94 // KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) );
95 qCWarning(KIO_TRASH) << "could not create" << name;
96 return KIO::ERR_CANNOT_MKDIR;
97 } else {
98 // qCDebug(KIO_TRASH) << name << "created.";
99 }
100 } else { // exists already
101 closedir(dirp: dp);
102 }
103 return 0; // success
104}
105
106void TrashImpl::deleteEmptyTrashInfrastructure()
107{
108#ifdef Q_OS_OSX
109 // For each known trash directory...
110 if (!m_trashDirectoriesScanned) {
111 scanTrashDirectories();
112 }
113
114 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
115 const QString trashPath = it.value();
116 QString infoPath = trashPath + QLatin1String("/info");
117
118 // qCDebug(KIO_TRASH) << "empty Trash" << trashPath << "; removing infrastructure";
119 synchronousDel(infoPath, false, true);
120 synchronousDel(trashPath + QLatin1String("/files"), false, true);
121 if (trashPath.endsWith(QLatin1String("/KDE.trash"))) {
122 synchronousDel(trashPath, false, true);
123 }
124 }
125#endif
126}
127
128bool TrashImpl::createTrashInfrastructure(int trashId, const QString &path)
129{
130 const QString trashDir = path.isEmpty() ? trashDirectoryPath(trashId) : path;
131 if (const int err = testDir(name: trashDir)) {
132 error(e: err, s: trashDir);
133 return false;
134 }
135
136 const QString infoDir = trashDir + QLatin1String("/info");
137 if (const int err = testDir(name: infoDir)) {
138 error(e: err, s: infoDir);
139 return false;
140 }
141
142 const QString filesDir = trashDir + QLatin1String("/files");
143 if (const int err = testDir(name: filesDir)) {
144 error(e: err, s: filesDir);
145 return false;
146 }
147
148 return true;
149}
150
151bool TrashImpl::init()
152{
153 if (m_initStatus == InitOK) {
154 return true;
155 }
156 if (m_initStatus == InitError) {
157 return false;
158 }
159
160 // Check the trash directory and its info and files subdirs
161 // see also kdesktop/init.cc for first time initialization
162 m_initStatus = InitError;
163#ifndef Q_OS_OSX
164 // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default.
165 const QString xdgDataDir = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/');
166 if (!QDir().mkpath(dirPath: xdgDataDir)) {
167 qCWarning(KIO_TRASH) << "failed to create" << xdgDataDir;
168 return false;
169 }
170
171 const QString trashDir = xdgDataDir + QLatin1String("Trash");
172 if (!createTrashInfrastructure(trashId: 0, path: trashDir)) {
173 return false;
174 }
175#else
176 // we DO NOT create ~/.Trash on OS X, that's the operating system's privilege
177 QString trashDir = QDir::homePath() + QLatin1String("/.Trash");
178 if (!QFileInfo(trashDir).isDir()) {
179 error(KIO::ERR_DOES_NOT_EXIST, trashDir);
180 return false;
181 }
182 trashDir += QLatin1String("/KDE.trash");
183 // we don't have to call createTrashInfrastructure() here because it'll be called when needed.
184#endif
185 m_trashDirectories.insert(key: 0, value: trashDir);
186 m_initStatus = InitOK;
187 // qCDebug(KIO_TRASH) << "initialization OK, home trash dir:" << trashDir;
188 return true;
189}
190
191void TrashImpl::migrateOldTrash()
192{
193 qCDebug(KIO_TRASH);
194
195 KConfigGroup g(KSharedConfig::openConfig(), QStringLiteral("Paths"));
196 const QString oldTrashDir = g.readPathEntry(key: "Trash", aDefault: QString());
197
198 if (oldTrashDir.isEmpty()) {
199 return;
200 }
201
202 const QStringList entries = listDir(physicalPath: oldTrashDir);
203 bool allOK = true;
204 for (QString srcPath : entries) {
205 if (srcPath == QLatin1Char('.') || srcPath == QLatin1String("..") || srcPath == QLatin1String(".directory")) {
206 continue;
207 }
208 srcPath.prepend(s: oldTrashDir); // make absolute
209 int trashId;
210 QString fileId;
211 if (!createInfo(origPath: srcPath, trashId, fileId)) {
212 qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
213 allOK = false;
214 } else {
215 bool ok = moveToTrash(origPath: srcPath, trashId, fileId);
216 if (!ok) {
217 (void)deleteInfo(trashId, fileId);
218 qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
219 allOK = false;
220 } else {
221 qCDebug(KIO_TRASH) << "Trash migration: moved" << srcPath;
222 }
223 }
224 }
225 if (allOK) {
226 // We need to remove the old one, otherwise the desktop will have two trashcans...
227 qCDebug(KIO_TRASH) << "Trash migration: all OK, removing old trash directory";
228 synchronousDel(path: oldTrashDir, setLastErrorCode: false, isDir: true);
229 }
230}
231
232bool TrashImpl::createInfo(const QString &origPath, int &trashId, QString &fileId)
233{
234 // off_t should be 64bit on Unix systems to have large file support
235 // FIXME: on windows this gets disabled until trash gets integrated
236 // BUG: 165449
237#ifndef Q_OS_WIN
238 Q_STATIC_ASSERT(sizeof(off_t) >= 8);
239#endif
240
241 // qCDebug(KIO_TRASH) << origPath;
242 // Check source
243 QT_STATBUF buff_src;
244 if (QT_LSTAT(file: QFile::encodeName(fileName: origPath).constData(), buf: &buff_src) == -1) {
245 if (errno == EACCES) {
246 error(e: KIO::ERR_ACCESS_DENIED, s: origPath);
247 } else {
248 error(e: KIO::ERR_DOES_NOT_EXIST, s: origPath);
249 }
250 return false;
251 }
252
253 // Choose destination trash
254 trashId = findTrashDirectory(origPath);
255 if (trashId < 0) {
256 qCWarning(KIO_TRASH) << "OUCH - internal error, TrashImpl::findTrashDirectory returned" << trashId;
257 return false; // ### error() needed?
258 }
259 // qCDebug(KIO_TRASH) << "trashing to" << trashId;
260
261 // Grab original filename
262 auto url = QUrl::fromLocalFile(localfile: origPath);
263 url = url.adjusted(options: QUrl::StripTrailingSlash);
264 const QString origFileName = url.fileName();
265
266 // Make destination file in info/
267#ifdef Q_OS_OSX
268 createTrashInfrastructure(trashId);
269#endif
270 url.setPath(path: infoPath(trashId, fileId: origFileName)); // we first try with origFileName
271 QUrl baseDirectory = QUrl::fromLocalFile(localfile: url.path());
272 // Here we need to use O_EXCL to avoid race conditions with other kioworker processes
273 int fd = 0;
274 QString fileName;
275 do {
276 // qCDebug(KIO_TRASH) << "trying to create" << url.path();
277 fd = ::open(file: QFile::encodeName(fileName: url.path()).constData(), O_WRONLY | O_CREAT | O_EXCL, 0600);
278 if (fd < 0) {
279 if (errno == EEXIST) {
280 fileName = url.fileName();
281 url = url.adjusted(options: QUrl::RemoveFilename);
282 url.setPath(path: url.path() + KFileUtils::suggestName(baseURL: baseDirectory, oldName: fileName));
283 // and try again on the next iteration
284 } else {
285 error(e: KIO::ERR_CANNOT_WRITE, s: url.path());
286 return false;
287 }
288 }
289 } while (fd < 0);
290 const QString infoPath = url.path();
291 fileId = url.fileName();
292 Q_ASSERT(fileId.endsWith(QLatin1String(".trashinfo")));
293 fileId.chop(n: 10); // remove .trashinfo from fileId
294
295 FILE *file = ::fdopen(fd: fd, modes: "w");
296 if (!file) { // can't see how this would happen
297 error(e: KIO::ERR_CANNOT_WRITE, s: infoPath);
298 return false;
299 }
300
301 // Contents of the info file. We could use KSimpleConfig, but that would
302 // mean closing and reopening fd, i.e. opening a race condition...
303 QByteArray info = "[Trash Info]\n";
304 info += "Path=";
305 // Escape filenames according to the way they are encoded on the filesystem
306 // All this to basically get back to the raw 8-bit representation of the filename...
307 if (trashId == 0) { // home trash: absolute path
308 info += QUrl::toPercentEncoding(origPath, exclude: "/");
309 } else {
310 info += QUrl::toPercentEncoding(makeRelativePath(topdir: topDirectoryPath(trashId), path: origPath), exclude: "/");
311 }
312 info += '\n';
313 info += "DeletionDate=" + QDateTime::currentDateTime().toString(format: Qt::ISODate).toLatin1() + '\n';
314 size_t sz = info.size();
315
316 size_t written = ::fwrite(ptr: info.data(), size: 1, n: sz, s: file);
317 if (written != sz) {
318 ::fclose(stream: file);
319 QFile::remove(fileName: infoPath);
320 error(e: KIO::ERR_DISK_FULL, s: infoPath);
321 return false;
322 }
323
324 ::fclose(stream: file);
325
326 // qCDebug(KIO_TRASH) << "info file created in trashId=" << trashId << ":" << fileId;
327 return true;
328}
329
330QString TrashImpl::makeRelativePath(const QString &topdir, const QString &path)
331{
332 QString realPath = QFileInfo(path).canonicalFilePath();
333 if (realPath.isEmpty()) { // shouldn't happen
334 realPath = path;
335 }
336 // topdir ends with '/'
337#ifndef Q_OS_WIN
338 if (realPath.startsWith(s: topdir)) {
339#else
340 if (realPath.startsWith(topdir, Qt::CaseInsensitive)) {
341#endif
342 const QString rel = realPath.mid(position: topdir.length());
343 Q_ASSERT(rel[0] != QLatin1Char('/'));
344 return rel;
345 } else { // shouldn't happen...
346 qCWarning(KIO_TRASH) << "Couldn't make relative path for" << realPath << "(" << path << "), with topdir=" << topdir;
347 return realPath;
348 }
349}
350
351void TrashImpl::enterLoop()
352{
353 QEventLoop eventLoop;
354 connect(sender: this, signal: &TrashImpl::leaveModality, context: &eventLoop, slot: &QEventLoop::quit);
355 eventLoop.exec(flags: QEventLoop::ExcludeUserInputEvents);
356}
357
358QString TrashImpl::infoPath(int trashId, const QString &fileId) const
359{
360 const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo");
361 return trashPath;
362}
363
364QString TrashImpl::filesPath(int trashId, const QString &fileId) const
365{
366 const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/") + fileId;
367 return trashPath;
368}
369
370bool TrashImpl::deleteInfo(int trashId, const QString &fileId)
371{
372#ifdef Q_OS_OSX
373 createTrashInfrastructure(trashId);
374#endif
375
376 if (QFile::remove(fileName: infoPath(trashId, fileId))) {
377 fileRemoved();
378 return true;
379 }
380
381 return false;
382}
383
384bool TrashImpl::moveToTrash(const QString &origPath, int trashId, const QString &fileId)
385{
386 // qCDebug(KIO_TRASH) << "Trashing" << origPath << trashId << fileId;
387 if (!adaptTrashSize(origPath, trashId)) {
388 return false;
389 }
390
391#ifdef Q_OS_OSX
392 createTrashInfrastructure(trashId);
393#endif
394 const QString dest = filesPath(trashId, fileId);
395 if (!move(src: origPath, dest)) {
396 // Maybe the move failed due to no permissions to delete source.
397 // In that case, delete dest to keep things consistent, since KIO doesn't do it.
398 if (QFileInfo(dest).isFile()) {
399 QFile::remove(fileName: dest);
400 } else {
401 synchronousDel(path: dest, setLastErrorCode: false, isDir: true);
402 }
403 return false;
404 }
405
406 if (QFileInfo(dest).isDir()) {
407 TrashSizeCache trashSize(trashDirectoryPath(trashId));
408 const qint64 pathSize = DiscSpaceUtil::sizeOfPath(path: dest);
409 trashSize.add(directoryName: fileId, directorySize: pathSize);
410 }
411
412 fileAdded();
413 return true;
414}
415
416bool TrashImpl::moveFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
417{
418 QString src = filesPath(trashId, fileId);
419 if (!relativePath.isEmpty()) {
420 src += QLatin1Char('/') + relativePath;
421 }
422 if (!move(src, dest)) {
423 return false;
424 }
425
426 TrashSizeCache trashSize(trashDirectoryPath(trashId));
427 trashSize.remove(directoryName: fileId);
428
429 return true;
430}
431
432bool TrashImpl::move(const QString &src, const QString &dest)
433{
434 if (directRename(src, dest)) {
435 // This notification is done by KIO::moveAs when using the code below
436 // But if we do a direct rename we need to do the notification ourselves
437#ifdef WITH_QTDBUS
438 org::kde::KDirNotify::emitFilesAdded(directory: QUrl::fromLocalFile(localfile: dest));
439#endif
440 return true;
441 }
442 if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) {
443 return false;
444 }
445
446 const auto urlSrc = QUrl::fromLocalFile(localfile: src);
447 const auto urlDest = QUrl::fromLocalFile(localfile: dest);
448
449 // qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest;
450 KIO::CopyJob *job = KIO::moveAs(src: urlSrc, dest: urlDest, flags: KIO::HideProgressInfo);
451 job->setUiDelegate(nullptr);
452 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
453 enterLoop();
454
455 return m_lastErrorCode == 0;
456}
457
458void TrashImpl::jobFinished(KJob *job)
459{
460 // qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText();
461 error(e: job->error(), s: job->errorText());
462
463 Q_EMIT leaveModality();
464}
465
466bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId)
467{
468 // qCDebug(KIO_TRASH);
469 if (!adaptTrashSize(origPath, trashId)) {
470 return false;
471 }
472
473#ifdef Q_OS_OSX
474 createTrashInfrastructure(trashId);
475#endif
476 const QString dest = filesPath(trashId, fileId);
477 if (!copy(src: origPath, dest)) {
478 return false;
479 }
480
481 if (QFileInfo(dest).isDir()) {
482 TrashSizeCache trashSize(trashDirectoryPath(trashId));
483 const qint64 pathSize = DiscSpaceUtil::sizeOfPath(path: dest);
484 trashSize.add(directoryName: fileId, directorySize: pathSize);
485 }
486
487 fileAdded();
488 return true;
489}
490
491bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
492{
493 const QString src = physicalPath(trashId, fileId, relativePath);
494 return copy(src, dest);
495}
496
497bool TrashImpl::copy(const QString &src, const QString &dest)
498{
499 // kio_file's copy() method is quite complex (in order to be fast), let's just call it...
500 m_lastErrorCode = 0;
501 const auto urlSrc = QUrl::fromLocalFile(localfile: src);
502 const auto urlDest = QUrl::fromLocalFile(localfile: dest);
503 // qCDebug(KIO_TRASH) << "copying" << src << "to" << dest;
504 KIO::CopyJob *job = KIO::copyAs(src: urlSrc, dest: urlDest, flags: KIO::HideProgressInfo);
505 job->setUiDelegate(nullptr);
506 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
507 enterLoop();
508
509 return m_lastErrorCode == 0;
510}
511
512bool TrashImpl::directRename(const QString &src, const QString &dest)
513{
514 // qCDebug(KIO_TRASH) << src << "->" << dest;
515 // Do not use QFile::rename here, we need to be able to move broken symlinks too
516 // (and we need to make sure errno is set)
517 if (::rename(old: QFile::encodeName(fileName: src).constData(), new: QFile::encodeName(fileName: dest).constData()) != 0) {
518 if (errno == EXDEV) {
519 error(e: KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
520 } else {
521 if ((errno == EACCES) || (errno == EPERM)) {
522 error(e: KIO::ERR_ACCESS_DENIED, s: dest);
523 } else if (errno == EROFS) { // The file is on a read-only filesystem
524 error(e: KIO::ERR_CANNOT_DELETE, s: src);
525 } else if (errno == ENOENT) {
526 const QString marker(QStringLiteral("Trash/files/"));
527 const int idx = src.lastIndexOf(s: marker) + marker.size();
528 const QString displayName = QLatin1String("trash:/") + src.mid(position: idx);
529 error(e: KIO::ERR_DOES_NOT_EXIST, s: displayName);
530 } else {
531 error(e: KIO::ERR_CANNOT_RENAME, s: src);
532 }
533 }
534 return false;
535 }
536 return true;
537}
538
539bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId)
540{
541 m_lastErrorCode = 0;
542
543 const QString oldInfo = infoPath(trashId, fileId: oldFileId);
544 const QString oldFile = filesPath(trashId, fileId: oldFileId);
545 const QString newInfo = infoPath(trashId, fileId: newFileId);
546 const QString newFile = filesPath(trashId, fileId: newFileId);
547
548 if (directRename(src: oldInfo, dest: newInfo)) {
549 if (directRename(src: oldFile, dest: newFile)) {
550 // success
551
552 if (QFileInfo(newFile).isDir()) {
553 TrashSizeCache trashSize(trashDirectoryPath(trashId));
554 trashSize.rename(oldDirectoryName: oldFileId, newDirectoryName: newFileId);
555 }
556 return true;
557 } else {
558 // rollback
559 directRename(src: newInfo, dest: oldInfo);
560 }
561 }
562 return false;
563}
564
565bool TrashImpl::del(int trashId, const QString &fileId)
566{
567#ifdef Q_OS_OSX
568 createTrashInfrastructure(trashId);
569#endif
570
571 const QString info = infoPath(trashId, fileId);
572 const QString file = filesPath(trashId, fileId);
573
574 QT_STATBUF buff;
575 if (QT_LSTAT(file: QFile::encodeName(fileName: info).constData(), buf: &buff) == -1) {
576 if (errno == EACCES) {
577 error(e: KIO::ERR_ACCESS_DENIED, s: file);
578 } else {
579 error(e: KIO::ERR_DOES_NOT_EXIST, s: file);
580 }
581 return false;
582 }
583
584 const bool isDir = QFileInfo(file).isDir();
585 if (!synchronousDel(path: file, setLastErrorCode: true, isDir)) {
586 return false;
587 }
588
589 if (isDir) {
590 TrashSizeCache trashSize(trashDirectoryPath(trashId));
591 trashSize.remove(directoryName: fileId);
592 }
593
594 QFile::remove(fileName: info);
595 fileRemoved();
596 return true;
597}
598
599bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir)
600{
601 const int oldErrorCode = m_lastErrorCode;
602 const QString oldErrorMsg = m_lastErrorMessage;
603 const auto url = QUrl::fromLocalFile(localfile: path);
604 // First ensure that all dirs have u+w permissions,
605 // otherwise we won't be able to delete files in them (#130780).
606 if (isDir) {
607 // qCDebug(KIO_TRASH) << "chmod'ing" << url;
608 KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown);
609 KFileItemList fileItemList;
610 fileItemList.append(t: fileItem);
611 KIO::ChmodJob *chmodJob = KIO::chmod(lstItems: fileItemList, permissions: 0200, mask: 0200, newOwner: QString(), newGroup: QString(), recursive: true /*recursive*/, flags: KIO::HideProgressInfo);
612 connect(sender: chmodJob, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
613 enterLoop();
614 }
615
616 KIO::DeleteJob *job = KIO::del(src: url, flags: KIO::HideProgressInfo);
617 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
618 enterLoop();
619 bool ok = m_lastErrorCode == 0;
620 if (!setLastErrorCode) {
621 m_lastErrorCode = oldErrorCode;
622 m_lastErrorMessage = oldErrorMsg;
623 }
624 return ok;
625}
626
627bool TrashImpl::emptyTrash()
628{
629 // qCDebug(KIO_TRASH);
630 // The naive implementation "delete info and files in every trash directory"
631 // breaks when deleted directories contain files owned by other users.
632 // We need to ensure that the .trashinfo file is only removed when the
633 // corresponding files could indeed be removed (#116371)
634
635 // On the other hand, we certainly want to remove any file that has no associated
636 // .trashinfo file for some reason (#167051)
637
638 QSet<QString> unremovableFiles;
639
640 int myErrorCode = 0;
641 QString myErrorMsg;
642 const TrashedFileInfoList fileInfoList = list();
643 for (const auto &info : fileInfoList) {
644 const QString filesPath = info.physicalPath;
645 if (synchronousDel(path: filesPath, setLastErrorCode: true, isDir: true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) {
646 QFile::remove(fileName: infoPath(trashId: info.trashId, fileId: info.fileId));
647 } else {
648 // error code is set by synchronousDel, let's remember it
649 // (so that successfully removing another file doesn't erase the error)
650 myErrorCode = m_lastErrorCode;
651 myErrorMsg = m_lastErrorMessage;
652 // and remember not to remove this file
653 unremovableFiles.insert(value: filesPath);
654 qCDebug(KIO_TRASH) << "Unremovable:" << filesPath;
655 }
656
657 TrashSizeCache trashSize(trashDirectoryPath(trashId: info.trashId));
658 trashSize.clear();
659 }
660
661 // Now do the orphaned-files cleanup
662 for (auto trit = m_trashDirectories.cbegin(); trit != m_trashDirectories.cend(); ++trit) {
663 // const int trashId = trit.key();
664 const QString filesDir = trit.value() + QLatin1String("/files");
665 const QStringList list = listDir(physicalPath: filesDir);
666 for (const QString &fileName : list) {
667 if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
668 continue;
669 }
670 const QString filePath = filesDir + QLatin1Char('/') + fileName;
671 if (!unremovableFiles.contains(value: filePath)) {
672 qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath;
673 QFile::remove(fileName: filePath);
674 }
675 }
676 }
677
678 m_lastErrorCode = myErrorCode;
679 m_lastErrorMessage = myErrorMsg;
680
681 fileRemoved();
682
683 return m_lastErrorCode == 0;
684}
685
686TrashImpl::TrashedFileInfoList TrashImpl::list()
687{
688 // Here we scan for trash directories unconditionally. This allows
689 // noticing plugged-in [e.g. removable] devices, or new mounts etc.
690 scanTrashDirectories();
691
692 TrashedFileInfoList lst;
693 // For each known trash directory...
694 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
695 const int trashId = it.key();
696 QString infoPath = it.value();
697 infoPath += QLatin1String("/info");
698 // Code taken from kio_file
699 const QStringList entryNames = listDir(physicalPath: infoPath);
700 // char path_buffer[PATH_MAX];
701 // getcwd(path_buffer, PATH_MAX - 1);
702 // if ( chdir( infoPathEnc ) )
703 // continue;
704
705 const QLatin1String tail(".trashinfo");
706 const int tailLength = tail.size();
707 for (const QString &fileName : entryNames) {
708 if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
709 continue;
710 }
711 if (!fileName.endsWith(s: tail)) {
712 qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName;
713 continue;
714 }
715
716 TrashedFileInfo info;
717 if (infoForFile(trashId, fileId: fileName.chopped(n: tailLength), info)) {
718 lst << info;
719 }
720 }
721 }
722 return lst;
723}
724
725// Returns the entries in a given directory - including "." and ".."
726QStringList TrashImpl::listDir(const QString &physicalPath)
727{
728 return QDir(physicalPath).entryList(filters: QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System);
729}
730
731bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info)
732{
733 // qCDebug(KIO_TRASH) << trashId << fileId;
734 info.trashId = trashId; // easy :)
735 info.fileId = fileId; // equally easy
736 info.physicalPath = filesPath(trashId, fileId);
737 return readInfoFile(infoPath: infoPath(trashId, fileId), info, trashId);
738}
739
740bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info)
741{
742 const int trashId = findTrashDirectory(origPath: path);
743 if (trashId < 0) {
744 qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId;
745 return false;
746 }
747
748 const KConfig config(QStringLiteral("ktrashrc"));
749
750 const QString trashPath = trashDirectoryPath(trashId);
751 const auto group = config.group(group: trashPath);
752
753 const bool useSizeLimit = group.readEntry(key: "UseSizeLimit", defaultValue: true);
754 const double percent = group.readEntry(key: "Percent", defaultValue: 10.0);
755
756 DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
757 qint64 total = util.size();
758 if (useSizeLimit) {
759 total *= percent / 100.0;
760 }
761
762 TrashSizeCache trashSize(trashPath);
763 const qint64 used = trashSize.calculateSize();
764
765 info.totalSize = total;
766 info.availableSize = total - used;
767
768 return true;
769}
770
771bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId)
772{
773 KConfig cfg(infoPath, KConfig::SimpleConfig);
774 if (!cfg.hasGroup(QStringLiteral("Trash Info"))) {
775 error(e: KIO::ERR_CANNOT_OPEN_FOR_READING, s: infoPath);
776 return false;
777 }
778 const KConfigGroup group = cfg.group(QStringLiteral("Trash Info"));
779 info.origPath = QUrl::fromPercentEncoding(group.readEntry(key: "Path").toLatin1());
780 if (info.origPath.isEmpty()) {
781 return false; // path is mandatory...
782 }
783 if (trashId == 0) {
784 Q_ASSERT(info.origPath[0] == QLatin1Char('/'));
785 } else {
786 if (!info.origPath.startsWith(c: QLatin1Char('/'))) {
787 const QString topdir = topDirectoryPath(trashId); // includes trailing slash
788 info.origPath.prepend(s: topdir);
789 }
790 }
791 const QString line = group.readEntry(key: "DeletionDate");
792 if (!line.isEmpty()) {
793 info.deletionDate = QDateTime::fromString(string: line, format: Qt::ISODate);
794 }
795 return true;
796}
797
798QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath)
799{
800 QString filePath = filesPath(trashId, fileId);
801 if (!relativePath.isEmpty()) {
802 filePath += QLatin1Char('/') + relativePath;
803 }
804 return filePath;
805}
806
807void TrashImpl::error(int e, const QString &s)
808{
809 if (e) {
810 qCDebug(KIO_TRASH) << e << s;
811 }
812 m_lastErrorCode = e;
813 m_lastErrorMessage = s;
814}
815
816bool TrashImpl::isEmpty() const
817{
818 // For each known trash directory...
819 if (!m_trashDirectoriesScanned) {
820 scanTrashDirectories();
821 }
822
823 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
824 const QString infoPath = it.value() + QLatin1String("/info");
825
826 DIR *dp = ::opendir(name: QFile::encodeName(fileName: infoPath).constData());
827 if (dp) {
828 struct dirent *ep;
829 ep = readdir(dirp: dp);
830 ep = readdir(dirp: dp); // ignore '.' and '..' dirent
831 ep = readdir(dirp: dp); // look for third file
832 closedir(dirp: dp);
833 if (ep != nullptr) {
834 // qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty";
835 return false; // not empty
836 }
837 }
838 }
839 return true;
840}
841
842void TrashImpl::fileAdded()
843{
844 m_config.reparseConfiguration();
845 KConfigGroup group = m_config.group(QStringLiteral("Status"));
846 if (group.readEntry(key: "Empty", defaultValue: true) == true) {
847 group.writeEntry(key: "Empty", value: false);
848 m_config.sync();
849 }
850 // The apps showing the trash (e.g. kdesktop) will be notified
851 // of this change when KDirNotify::FilesAdded("trash:/") is emitted,
852 // which will be done by the job soon after this.
853}
854
855void TrashImpl::fileRemoved()
856{
857 if (isEmpty()) {
858 deleteEmptyTrashInfrastructure();
859
860 // the file may have have changed by another trash:/ worker instance
861 m_config.reparseConfiguration();
862 KConfigGroup group = m_config.group(QStringLiteral("Status"));
863 group.writeEntry(key: "Empty", value: true);
864 m_config.sync();
865#ifdef WITH_QTDBUS
866 org::kde::KDirNotify::emitFilesChanged(fileList: {QUrl::fromEncoded(input: "trash:/")});
867#endif
868 }
869 // The apps showing the trash (e.g. kdesktop) will be notified
870 // of this change when KDirNotify::FilesRemoved(...) is emitted,
871 // which will be done by the job soon after this.
872}
873
874#ifdef Q_OS_OSX
875#include <CoreFoundation/CoreFoundation.h>
876#include <DiskArbitration/DiskArbitration.h>
877#include <sys/mount.h>
878
879int TrashImpl::idForMountPoint(const QString &mountPoint) const
880{
881 DADiskRef disk;
882 CFDictionaryRef descDict;
883 DASessionRef session = DASessionCreate(NULL);
884 int devId = -1;
885 if (session) {
886 QByteArray mp = QFile::encodeName(mountPoint);
887 struct statfs statFS;
888 statfs(mp.constData(), &statFS);
889 disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname);
890 if (disk) {
891 descDict = DADiskCopyDescription(disk);
892 if (descDict) {
893 CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey);
894 CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey);
895 int major, minor;
896 if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) {
897 qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor;
898 devId = 1000 * major + minor;
899 }
900 CFRelease(cfMajor);
901 CFRelease(cfMinor);
902 } else {
903 qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk;
904 }
905 CFRelease(disk);
906 } else {
907 qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp;
908 }
909 CFRelease(session);
910 } else {
911 qCWarning(KIO_TRASH) << "couldn't create DASession";
912 }
913 return devId;
914}
915
916#else
917
918int TrashImpl::idForDevice(const Solid::Device &device) const
919{
920 const Solid::Block *block = device.as<Solid::Block>();
921 if (block) {
922 // qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor();
923 return block->deviceMajor() * 1000 + block->deviceMinor();
924 } else {
925 const Solid::NetworkShare *netshare = device.as<Solid::NetworkShare>();
926
927 if (netshare) {
928 QString url = netshare->url().url();
929
930 QLockFile configLock(QStandardPaths::writableLocation(type: QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock"));
931
932 if (!configLock.lock()) {
933 return -1;
934 }
935
936 m_config.reparseConfiguration();
937 KConfigGroup group = m_config.group(QStringLiteral("NetworkShares"));
938 int id = group.readEntry(key: url, aDefault: -1);
939
940 if (id == -1) {
941 id = group.readEntry(key: "NextID", defaultValue: 0);
942 // qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id;
943
944 group.writeEntry(key: url, value: id);
945 group.writeEntry(key: "NextID", value: id + 1);
946 group.sync();
947 }
948
949 return 6000000 + id;
950 }
951
952 // Not a block device nor a network share
953 return -1;
954 }
955}
956
957void TrashImpl::refreshDevices() const
958{
959 // this is needed because Solid's fstab backend uses QSocketNotifier
960 // to get notifications about changes to mtab
961 // otherwise we risk getting old device list
962 qApp->processEvents(flags: QEventLoop::ExcludeUserInputEvents);
963}
964#endif
965
966void TrashImpl::insertTrashDir(int id, const QString &trashDir, const QString &topdir) const
967{
968 m_trashDirectories.insert(key: id, value: trashDir);
969 qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id;
970 m_topDirectories.insert(key: id, value: Utils::slashAppended(s: topdir));
971}
972
973int TrashImpl::findTrashDirectory(const QString &origPath)
974{
975 // qCDebug(KIO_TRASH) << origPath;
976 // Check if it's on the same device as $HOME
977 QT_STATBUF buff;
978 if (QT_LSTAT(file: QFile::encodeName(fileName: origPath).constData(), buf: &buff) == 0 && buff.st_dev == m_homeDevice) {
979 return 0;
980 }
981
982 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: origPath);
983 if (!mp) {
984 // qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath;
985 return 0;
986 }
987
988 QString mountPoint = mp->mountPoint();
989 const QString trashDir = trashForMountPoint(topdir: mountPoint, createIfNeeded: true);
990 // qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir;
991
992#ifndef Q_OS_OSX
993 if (trashDir.isEmpty()) {
994 return 0; // no trash available on partition
995 }
996#endif
997
998 int id = idForTrashDirectory(trashDir);
999 if (id > -1) {
1000 qCDebug(KIO_TRASH) << "Found Trash dir" << trashDir << "with id" << id;
1001 return id;
1002 }
1003
1004#ifdef Q_OS_OSX
1005 id = idForMountPoint(mountPoint);
1006#else
1007 refreshDevices();
1008 const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '%1']").arg(args&: mountPoint);
1009 const QList<Solid::Device> lst = Solid::Device::listFromQuery(predicate: query);
1010 qCDebug(KIO_TRASH) << "Queried Solid with" << query << "got" << lst.count() << "devices";
1011 if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance.
1012 return 0;
1013 }
1014
1015 // Pretend we got exactly one...
1016 const Solid::Device device = lst.at(i: 0);
1017 id = idForDevice(device);
1018#endif
1019 if (id == -1) {
1020 return 0;
1021 }
1022
1023 // New trash dir found, register it
1024 insertTrashDir(id, trashDir, topdir: mountPoint);
1025 return id;
1026}
1027
1028KIO::UDSEntry TrashImpl::trashUDSEntry(KIO::StatDetails details)
1029{
1030 KIO::UDSEntry entry;
1031 if (details & KIO::StatRecursiveSize) {
1032 KIO::filesize_t size = 0;
1033 long latestModifiedDate = 0;
1034
1035 for (const QString &trashPath : std::as_const(t&: m_trashDirectories)) {
1036 TrashSizeCache trashSize(trashPath);
1037 TrashSizeCache::SizeAndModTime res = trashSize.calculateSizeAndLatestModDate();
1038 size += res.size;
1039
1040 // Find latest modification date
1041 if (res.mtime > latestModifiedDate) {
1042 latestModifiedDate = res.mtime;
1043 }
1044 }
1045
1046 entry.reserve(size: 3);
1047 entry.fastInsert(field: KIO::UDSEntry::UDS_RECURSIVE_SIZE, l: static_cast<long long>(size));
1048
1049 entry.fastInsert(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, l: latestModifiedDate / 1000);
1050 // access date is unreliable for the trash folder, use the modified date instead
1051 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS_TIME, l: latestModifiedDate / 1000);
1052 }
1053 return entry;
1054}
1055
1056void TrashImpl::scanTrashDirectories() const
1057{
1058#ifndef Q_OS_OSX
1059 refreshDevices();
1060#endif
1061
1062 const QList<Solid::Device> lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true"));
1063 for (const Solid::Device &device : lst) {
1064 QString topdir = device.as<Solid::StorageAccess>()->filePath();
1065 QString trashDir = trashForMountPoint(topdir, createIfNeeded: false);
1066 if (!trashDir.isEmpty()) {
1067 // OK, trashDir is a valid trash directory. Ensure it's registered.
1068 int trashId = idForTrashDirectory(trashDir);
1069 if (trashId == -1) {
1070 // new trash dir found, register it
1071#ifdef Q_OS_OSX
1072 trashId = idForMountPoint(topdir);
1073#else
1074 trashId = idForDevice(device);
1075#endif
1076 if (trashId == -1) {
1077 continue;
1078 }
1079
1080 insertTrashDir(id: trashId, trashDir, topdir);
1081 }
1082 }
1083 }
1084 m_trashDirectoriesScanned = true;
1085}
1086
1087TrashImpl::TrashDirMap TrashImpl::trashDirectories() const
1088{
1089 if (!m_trashDirectoriesScanned) {
1090 scanTrashDirectories();
1091 }
1092 return m_trashDirectories;
1093}
1094
1095TrashImpl::TrashDirMap TrashImpl::topDirectories() const
1096{
1097 if (!m_trashDirectoriesScanned) {
1098 scanTrashDirectories();
1099 }
1100 return m_topDirectories;
1101}
1102
1103QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const
1104{
1105 // (1) Administrator-created $topdir/.Trash directory
1106
1107#ifndef Q_OS_OSX
1108 const QString rootTrashDir = topdir + QLatin1String("/.Trash");
1109#else
1110 const QString rootTrashDir = topdir + QLatin1String("/.Trashes");
1111#endif
1112 const QByteArray rootTrashDir_c = QFile::encodeName(fileName: rootTrashDir);
1113 // Can't use QFileInfo here since we need to test for the sticky bit
1114 uid_t uid = getuid();
1115 QT_STATBUF buff;
1116 const unsigned int requiredBits = S_ISVTX; // Sticky bit required
1117 if (QT_LSTAT(file: rootTrashDir_c.constData(), buf: &buff) == 0) {
1118 if ((S_ISDIR(buff.st_mode)) // must be a dir
1119 && (!S_ISLNK(buff.st_mode)) // not a symlink
1120 && ((buff.st_mode & requiredBits) == requiredBits) //
1121 && (::access(name: rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable
1122 ) {
1123 if (buff.st_dev == m_homeDevice) // bind mount, maybe
1124 return QString();
1125#ifndef Q_OS_OSX
1126 const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1127#else
1128 QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1129#endif
1130 const QByteArray trashDir_c = QFile::encodeName(fileName: trashDir);
1131 if (QT_LSTAT(file: trashDir_c.constData(), buf: &buff) == 0) {
1132 if ((buff.st_uid == uid) // must be owned by user
1133 && (S_ISDIR(buff.st_mode)) // must be a dir
1134 && (!S_ISLNK(buff.st_mode)) // not a symlink
1135 && (buff.st_mode & 0777) == 0700) { // rwx for user
1136#ifdef Q_OS_OSX
1137 trashDir += QStringLiteral("/KDE.trash");
1138#endif
1139 return trashDir;
1140 }
1141 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1142 } else if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1143 return trashDir;
1144 }
1145 } else {
1146 qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it";
1147 }
1148 }
1149
1150#ifndef Q_OS_OSX
1151 // (2) $topdir/.Trash-$uid
1152 const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid);
1153 const QByteArray trashDir_c = QFile::encodeName(fileName: trashDir);
1154 if (QT_LSTAT(file: trashDir_c.constData(), buf: &buff) == 0) {
1155 if ((buff.st_uid == uid) // must be owned by user
1156 && S_ISDIR(buff.st_mode) // must be a dir
1157 && !S_ISLNK(buff.st_mode) // not a symlink
1158 && ((buff.st_mode & 0700) == 0700)) { // and we need write access to it
1159
1160 if (buff.st_dev == m_homeDevice) // bind mount, maybe
1161 return QString();
1162 if (checkTrashSubdirs(trashDir_c)) {
1163 return trashDir;
1164 }
1165 }
1166 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1167 // Exists, but not usable
1168 return QString();
1169 }
1170 if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1171 return trashDir;
1172 }
1173#endif
1174 return QString();
1175}
1176
1177int TrashImpl::idForTrashDirectory(const QString &trashDir) const
1178{
1179 // If this is too slow we can always use a reverse map...
1180 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
1181 if (it.value() == trashDir) {
1182 return it.key();
1183 }
1184 }
1185 return -1;
1186}
1187
1188bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const
1189{
1190 if (mkdir(path: trashDir_c.constData(), mode: 0700) != 0) {
1191 return false;
1192 }
1193 return checkTrashSubdirs(trashDir_c);
1194}
1195
1196bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const
1197{
1198 const QString trashDir = QFile::decodeName(localFileName: trashDir_c);
1199 const QString info = trashDir + QLatin1String("/info");
1200 const QString files = trashDir + QLatin1String("/files");
1201 return testDir(name: info) == 0 && testDir(name: files) == 0;
1202}
1203
1204QString TrashImpl::trashDirectoryPath(int trashId) const
1205{
1206 // Never scanned for trash dirs? (This can happen after killing kio_trash
1207 // and reusing a directory listing from the earlier instance.)
1208 if (!m_trashDirectoriesScanned) {
1209 scanTrashDirectories();
1210 }
1211 Q_ASSERT(m_trashDirectories.contains(trashId));
1212 return m_trashDirectories[trashId];
1213}
1214
1215QString TrashImpl::topDirectoryPath(int trashId) const
1216{
1217 if (!m_trashDirectoriesScanned) {
1218 scanTrashDirectories();
1219 }
1220 assert(trashId != 0);
1221 Q_ASSERT(m_topDirectories.contains(trashId));
1222 return m_topDirectories[trashId];
1223}
1224
1225// Helper method. Creates a URL with the format trash:/trashid-fileid or
1226// trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory.
1227QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath)
1228{
1229 QUrl url;
1230 url.setScheme(QStringLiteral("trash"));
1231 QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId;
1232 if (!relativePath.isEmpty()) {
1233 path += QLatin1Char('/') + relativePath;
1234 }
1235 url.setPath(path);
1236 return url;
1237}
1238
1239// Helper method. Parses a trash URL with the URL scheme defined in makeURL.
1240// The trash:/ URL itself isn't parsed here, must be caught by the caller before hand.
1241bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath)
1242{
1243 if (url.scheme() != QLatin1String("trash")) {
1244 return false;
1245 }
1246 const QString path = url.path();
1247 if (path.isEmpty()) {
1248 return false;
1249 }
1250 int start = 0;
1251 if (path[0] == QLatin1Char('/')) { // always true I hope
1252 start = 1;
1253 }
1254 int slashPos = path.indexOf(ch: QLatin1Char('-'), from: 0); // don't match leading slash
1255 if (slashPos <= 0) {
1256 return false;
1257 }
1258 bool ok = false;
1259
1260 trashId = QStringView(path).mid(pos: start, n: slashPos - start).toInt(ok: &ok);
1261
1262 Q_ASSERT_X(ok, Q_FUNC_INFO, qUtf8Printable(url.toString()));
1263 if (!ok) {
1264 return false;
1265 }
1266 start = slashPos + 1;
1267 slashPos = path.indexOf(ch: QLatin1Char('/'), from: start);
1268 if (slashPos <= 0) {
1269 fileId = path.mid(position: start);
1270 relativePath.clear();
1271 return true;
1272 }
1273 fileId = path.mid(position: start, n: slashPos - start);
1274 relativePath = path.mid(position: slashPos + 1);
1275 return true;
1276}
1277
1278bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId)
1279{
1280 KConfig config(QStringLiteral("ktrashrc"));
1281
1282 const QString trashPath = trashDirectoryPath(trashId);
1283 KConfigGroup group = config.group(group: trashPath);
1284
1285 const bool useTimeLimit = group.readEntry(key: "UseTimeLimit", defaultValue: false);
1286 const bool useSizeLimit = group.readEntry(key: "UseSizeLimit", defaultValue: true);
1287 const double percent = group.readEntry(key: "Percent", defaultValue: 10.0);
1288 const int actionType = group.readEntry(key: "LimitReachedAction", defaultValue: 0);
1289
1290 if (useTimeLimit) { // delete all files in trash older than X days
1291 const int maxDays = group.readEntry(key: "Days", defaultValue: 7);
1292 const QDateTime currentDate = QDateTime::currentDateTime();
1293
1294 const TrashedFileInfoList trashedFiles = list();
1295 for (const auto &info : trashedFiles) {
1296 if (info.trashId != trashId) {
1297 continue;
1298 }
1299
1300 if (info.deletionDate.daysTo(currentDate) > maxDays) {
1301 del(trashId: info.trashId, fileId: info.fileId);
1302 }
1303 }
1304 }
1305
1306 if (!useSizeLimit) { // check if size limit exceeded
1307 return true;
1308 }
1309
1310 // calculate size of the files to be put into the trash
1311 const qint64 additionalSize = DiscSpaceUtil::sizeOfPath(path: origPath);
1312
1313#ifdef Q_OS_OSX
1314 createTrashInfrastructure(trashId);
1315#endif
1316 DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
1317 auto cache = TrashSizeCache(trashPath);
1318 auto trashSize = cache.calculateSize();
1319
1320 if (util.usage(size: trashSize + additionalSize) < percent) {
1321 return true;
1322 }
1323
1324 // before we start to remove any files from the trash,
1325 // check whether the new file will fit into the trash
1326 // at all...
1327 const qint64 partitionSize = util.size();
1328
1329 if ((util.usage(size: partitionSize + additionalSize)) >= percent) {
1330 m_lastErrorCode = KIO::ERR_TRASH_FILE_TOO_LARGE;
1331 m_lastErrorMessage = KIO::buildErrorString(errorCode: m_lastErrorCode, errorText: {});
1332 return false;
1333 }
1334
1335 if (actionType == 0) { // warn the user only
1336 m_lastErrorCode = KIO::ERR_WORKER_DEFINED;
1337 m_lastErrorMessage = i18n("The trash is full. Empty it or remove items manually.");
1338 return false;
1339 }
1340
1341 // Start removing some other files from the trash
1342
1343 QDir::SortFlags sortFlags;
1344 if (actionType == 1) {
1345 sortFlags = QDir::Time | QDir::Reversed; // Delete oldest files first
1346 } else if (actionType == 2) {
1347 sortFlags = QDir::Size; // Delete biggest files first
1348 } else {
1349 qWarning() << "Called with actionType" << actionType << ", which theoretically should never happen!";
1350 return false; // Bail out
1351 }
1352
1353 const auto dirCache = cache.readDirCache();
1354 constexpr QDir::Filters dirFilters = QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot;
1355 const QFileInfoList infoList = QDir(trashPath + QLatin1String("/files")).entryInfoList(filters: dirFilters, sort: sortFlags);
1356 for (const auto &info : infoList) {
1357 auto fileSizeFreed = info.size();
1358 if (info.isDir()) {
1359 fileSizeFreed = dirCache.constFind(key: info.path().toUtf8())->size;
1360 }
1361
1362 del(trashId, fileId: info.fileName()); // delete trashed file
1363 trashSize -= fileSizeFreed;
1364
1365 if (util.usage(size: trashSize + additionalSize) < percent) { // check whether we have enough space now
1366 return true;
1367 }
1368 }
1369
1370 return true;
1371}
1372
1373#include "moc_trashimpl.cpp"
1374

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