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 * @param _name full path of the directory
71 * @return 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 org::kde::KDirNotify::emitFilesAdded(directory: QUrl::fromLocalFile(localfile: dest));
438 return true;
439 }
440 if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) {
441 return false;
442 }
443
444 const auto urlSrc = QUrl::fromLocalFile(localfile: src);
445 const auto urlDest = QUrl::fromLocalFile(localfile: dest);
446
447 // qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest;
448 KIO::CopyJob *job = KIO::moveAs(src: urlSrc, dest: urlDest, flags: KIO::HideProgressInfo);
449 job->setUiDelegate(nullptr);
450 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
451 enterLoop();
452
453 return m_lastErrorCode == 0;
454}
455
456void TrashImpl::jobFinished(KJob *job)
457{
458 // qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText();
459 error(e: job->error(), s: job->errorText());
460
461 Q_EMIT leaveModality();
462}
463
464bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId)
465{
466 // qCDebug(KIO_TRASH);
467 if (!adaptTrashSize(origPath, trashId)) {
468 return false;
469 }
470
471#ifdef Q_OS_OSX
472 createTrashInfrastructure(trashId);
473#endif
474 const QString dest = filesPath(trashId, fileId);
475 if (!copy(src: origPath, dest)) {
476 return false;
477 }
478
479 if (QFileInfo(dest).isDir()) {
480 TrashSizeCache trashSize(trashDirectoryPath(trashId));
481 const qint64 pathSize = DiscSpaceUtil::sizeOfPath(path: dest);
482 trashSize.add(directoryName: fileId, directorySize: pathSize);
483 }
484
485 fileAdded();
486 return true;
487}
488
489bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
490{
491 const QString src = physicalPath(trashId, fileId, relativePath);
492 return copy(src, dest);
493}
494
495bool TrashImpl::copy(const QString &src, const QString &dest)
496{
497 // kio_file's copy() method is quite complex (in order to be fast), let's just call it...
498 m_lastErrorCode = 0;
499 const auto urlSrc = QUrl::fromLocalFile(localfile: src);
500 const auto urlDest = QUrl::fromLocalFile(localfile: dest);
501 // qCDebug(KIO_TRASH) << "copying" << src << "to" << dest;
502 KIO::CopyJob *job = KIO::copyAs(src: urlSrc, dest: urlDest, flags: KIO::HideProgressInfo);
503 job->setUiDelegate(nullptr);
504 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
505 enterLoop();
506
507 return m_lastErrorCode == 0;
508}
509
510bool TrashImpl::directRename(const QString &src, const QString &dest)
511{
512 // qCDebug(KIO_TRASH) << src << "->" << dest;
513 // Do not use QFile::rename here, we need to be able to move broken symlinks too
514 // (and we need to make sure errno is set)
515 if (::rename(old: QFile::encodeName(fileName: src).constData(), new: QFile::encodeName(fileName: dest).constData()) != 0) {
516 if (errno == EXDEV) {
517 error(e: KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
518 } else {
519 if ((errno == EACCES) || (errno == EPERM)) {
520 error(e: KIO::ERR_ACCESS_DENIED, s: dest);
521 } else if (errno == EROFS) { // The file is on a read-only filesystem
522 error(e: KIO::ERR_CANNOT_DELETE, s: src);
523 } else if (errno == ENOENT) {
524 const QString marker(QStringLiteral("Trash/files/"));
525 const int idx = src.lastIndexOf(s: marker) + marker.size();
526 const QString displayName = QLatin1String("trash:/") + src.mid(position: idx);
527 error(e: KIO::ERR_DOES_NOT_EXIST, s: displayName);
528 } else {
529 error(e: KIO::ERR_CANNOT_RENAME, s: src);
530 }
531 }
532 return false;
533 }
534 return true;
535}
536
537bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId)
538{
539 m_lastErrorCode = 0;
540
541 const QString oldInfo = infoPath(trashId, fileId: oldFileId);
542 const QString oldFile = filesPath(trashId, fileId: oldFileId);
543 const QString newInfo = infoPath(trashId, fileId: newFileId);
544 const QString newFile = filesPath(trashId, fileId: newFileId);
545
546 if (directRename(src: oldInfo, dest: newInfo)) {
547 if (directRename(src: oldFile, dest: newFile)) {
548 // success
549
550 if (QFileInfo(newFile).isDir()) {
551 TrashSizeCache trashSize(trashDirectoryPath(trashId));
552 trashSize.rename(oldDirectoryName: oldFileId, newDirectoryName: newFileId);
553 }
554 return true;
555 } else {
556 // rollback
557 directRename(src: newInfo, dest: oldInfo);
558 }
559 }
560 return false;
561}
562
563bool TrashImpl::del(int trashId, const QString &fileId)
564{
565#ifdef Q_OS_OSX
566 createTrashInfrastructure(trashId);
567#endif
568
569 const QString info = infoPath(trashId, fileId);
570 const QString file = filesPath(trashId, fileId);
571
572 QT_STATBUF buff;
573 if (QT_LSTAT(file: QFile::encodeName(fileName: info).constData(), buf: &buff) == -1) {
574 if (errno == EACCES) {
575 error(e: KIO::ERR_ACCESS_DENIED, s: file);
576 } else {
577 error(e: KIO::ERR_DOES_NOT_EXIST, s: file);
578 }
579 return false;
580 }
581
582 const bool isDir = QFileInfo(file).isDir();
583 if (!synchronousDel(path: file, setLastErrorCode: true, isDir)) {
584 return false;
585 }
586
587 if (isDir) {
588 TrashSizeCache trashSize(trashDirectoryPath(trashId));
589 trashSize.remove(directoryName: fileId);
590 }
591
592 QFile::remove(fileName: info);
593 fileRemoved();
594 return true;
595}
596
597bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir)
598{
599 const int oldErrorCode = m_lastErrorCode;
600 const QString oldErrorMsg = m_lastErrorMessage;
601 const auto url = QUrl::fromLocalFile(localfile: path);
602 // First ensure that all dirs have u+w permissions,
603 // otherwise we won't be able to delete files in them (#130780).
604 if (isDir) {
605 // qCDebug(KIO_TRASH) << "chmod'ing" << url;
606 KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown);
607 KFileItemList fileItemList;
608 fileItemList.append(t: fileItem);
609 KIO::ChmodJob *chmodJob = KIO::chmod(lstItems: fileItemList, permissions: 0200, mask: 0200, newOwner: QString(), newGroup: QString(), recursive: true /*recursive*/, flags: KIO::HideProgressInfo);
610 connect(sender: chmodJob, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
611 enterLoop();
612 }
613
614 KIO::DeleteJob *job = KIO::del(src: url, flags: KIO::HideProgressInfo);
615 connect(sender: job, signal: &KJob::result, context: this, slot: &TrashImpl::jobFinished);
616 enterLoop();
617 bool ok = m_lastErrorCode == 0;
618 if (!setLastErrorCode) {
619 m_lastErrorCode = oldErrorCode;
620 m_lastErrorMessage = oldErrorMsg;
621 }
622 return ok;
623}
624
625bool TrashImpl::emptyTrash()
626{
627 // qCDebug(KIO_TRASH);
628 // The naive implementation "delete info and files in every trash directory"
629 // breaks when deleted directories contain files owned by other users.
630 // We need to ensure that the .trashinfo file is only removed when the
631 // corresponding files could indeed be removed (#116371)
632
633 // On the other hand, we certainly want to remove any file that has no associated
634 // .trashinfo file for some reason (#167051)
635
636 QSet<QString> unremovableFiles;
637
638 int myErrorCode = 0;
639 QString myErrorMsg;
640 const TrashedFileInfoList fileInfoList = list();
641 for (const auto &info : fileInfoList) {
642 const QString filesPath = info.physicalPath;
643 if (synchronousDel(path: filesPath, setLastErrorCode: true, isDir: true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) {
644 QFile::remove(fileName: infoPath(trashId: info.trashId, fileId: info.fileId));
645 } else {
646 // error code is set by synchronousDel, let's remember it
647 // (so that successfully removing another file doesn't erase the error)
648 myErrorCode = m_lastErrorCode;
649 myErrorMsg = m_lastErrorMessage;
650 // and remember not to remove this file
651 unremovableFiles.insert(value: filesPath);
652 qCDebug(KIO_TRASH) << "Unremovable:" << filesPath;
653 }
654
655 TrashSizeCache trashSize(trashDirectoryPath(trashId: info.trashId));
656 trashSize.clear();
657 }
658
659 // Now do the orphaned-files cleanup
660 for (auto trit = m_trashDirectories.cbegin(); trit != m_trashDirectories.cend(); ++trit) {
661 // const int trashId = trit.key();
662 const QString filesDir = trit.value() + QLatin1String("/files");
663 const QStringList list = listDir(physicalPath: filesDir);
664 for (const QString &fileName : list) {
665 if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
666 continue;
667 }
668 const QString filePath = filesDir + QLatin1Char('/') + fileName;
669 if (!unremovableFiles.contains(value: filePath)) {
670 qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath;
671 QFile::remove(fileName: filePath);
672 }
673 }
674 }
675
676 m_lastErrorCode = myErrorCode;
677 m_lastErrorMessage = myErrorMsg;
678
679 fileRemoved();
680
681 return m_lastErrorCode == 0;
682}
683
684TrashImpl::TrashedFileInfoList TrashImpl::list()
685{
686 // Here we scan for trash directories unconditionally. This allows
687 // noticing plugged-in [e.g. removable] devices, or new mounts etc.
688 scanTrashDirectories();
689
690 TrashedFileInfoList lst;
691 // For each known trash directory...
692 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
693 const int trashId = it.key();
694 QString infoPath = it.value();
695 infoPath += QLatin1String("/info");
696 // Code taken from kio_file
697 const QStringList entryNames = listDir(physicalPath: infoPath);
698 // char path_buffer[PATH_MAX];
699 // getcwd(path_buffer, PATH_MAX - 1);
700 // if ( chdir( infoPathEnc ) )
701 // continue;
702
703 const QLatin1String tail(".trashinfo");
704 const int tailLength = tail.size();
705 for (const QString &fileName : entryNames) {
706 if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
707 continue;
708 }
709 if (!fileName.endsWith(s: tail)) {
710 qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName;
711 continue;
712 }
713
714 TrashedFileInfo info;
715 if (infoForFile(trashId, fileId: fileName.chopped(n: tailLength), info)) {
716 lst << info;
717 }
718 }
719 }
720 return lst;
721}
722
723// Returns the entries in a given directory - including "." and ".."
724QStringList TrashImpl::listDir(const QString &physicalPath)
725{
726 return QDir(physicalPath).entryList(filters: QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System);
727}
728
729bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info)
730{
731 // qCDebug(KIO_TRASH) << trashId << fileId;
732 info.trashId = trashId; // easy :)
733 info.fileId = fileId; // equally easy
734 info.physicalPath = filesPath(trashId, fileId);
735 return readInfoFile(infoPath: infoPath(trashId, fileId), info, trashId);
736}
737
738bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info)
739{
740 const int trashId = findTrashDirectory(origPath: path);
741 if (trashId < 0) {
742 qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId;
743 return false;
744 }
745
746 const KConfig config(QStringLiteral("ktrashrc"));
747
748 const QString trashPath = trashDirectoryPath(trashId);
749 const auto group = config.group(group: trashPath);
750
751 const bool useSizeLimit = group.readEntry(key: "UseSizeLimit", defaultValue: true);
752 const double percent = group.readEntry(key: "Percent", defaultValue: 10.0);
753
754 DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
755 qint64 total = util.size();
756 if (useSizeLimit) {
757 total *= percent / 100.0;
758 }
759
760 TrashSizeCache trashSize(trashPath);
761 const qint64 used = trashSize.calculateSize();
762
763 info.totalSize = total;
764 info.availableSize = total - used;
765
766 return true;
767}
768
769bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId)
770{
771 KConfig cfg(infoPath, KConfig::SimpleConfig);
772 if (!cfg.hasGroup(QStringLiteral("Trash Info"))) {
773 error(e: KIO::ERR_CANNOT_OPEN_FOR_READING, s: infoPath);
774 return false;
775 }
776 const KConfigGroup group = cfg.group(QStringLiteral("Trash Info"));
777 info.origPath = QUrl::fromPercentEncoding(group.readEntry(key: "Path").toLatin1());
778 if (info.origPath.isEmpty()) {
779 return false; // path is mandatory...
780 }
781 if (trashId == 0) {
782 Q_ASSERT(info.origPath[0] == QLatin1Char('/'));
783 } else {
784 const QString topdir = topDirectoryPath(trashId); // includes trailing slash
785 info.origPath.prepend(s: topdir);
786 }
787 const QString line = group.readEntry(key: "DeletionDate");
788 if (!line.isEmpty()) {
789 info.deletionDate = QDateTime::fromString(string: line, format: Qt::ISODate);
790 }
791 return true;
792}
793
794QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath)
795{
796 QString filePath = filesPath(trashId, fileId);
797 if (!relativePath.isEmpty()) {
798 filePath += QLatin1Char('/') + relativePath;
799 }
800 return filePath;
801}
802
803void TrashImpl::error(int e, const QString &s)
804{
805 if (e) {
806 qCDebug(KIO_TRASH) << e << s;
807 }
808 m_lastErrorCode = e;
809 m_lastErrorMessage = s;
810}
811
812bool TrashImpl::isEmpty() const
813{
814 // For each known trash directory...
815 if (!m_trashDirectoriesScanned) {
816 scanTrashDirectories();
817 }
818
819 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
820 const QString infoPath = it.value() + QLatin1String("/info");
821
822 DIR *dp = ::opendir(name: QFile::encodeName(fileName: infoPath).constData());
823 if (dp) {
824 struct dirent *ep;
825 ep = readdir(dirp: dp);
826 ep = readdir(dirp: dp); // ignore '.' and '..' dirent
827 ep = readdir(dirp: dp); // look for third file
828 closedir(dirp: dp);
829 if (ep != nullptr) {
830 // qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty";
831 return false; // not empty
832 }
833 }
834 }
835 return true;
836}
837
838void TrashImpl::fileAdded()
839{
840 m_config.reparseConfiguration();
841 KConfigGroup group = m_config.group(QStringLiteral("Status"));
842 if (group.readEntry(key: "Empty", defaultValue: true) == true) {
843 group.writeEntry(key: "Empty", value: false);
844 m_config.sync();
845 }
846 // The apps showing the trash (e.g. kdesktop) will be notified
847 // of this change when KDirNotify::FilesAdded("trash:/") is emitted,
848 // which will be done by the job soon after this.
849}
850
851void TrashImpl::fileRemoved()
852{
853 if (isEmpty()) {
854 deleteEmptyTrashInfrastructure();
855 KConfigGroup group = m_config.group(QStringLiteral("Status"));
856 group.writeEntry(key: "Empty", value: true);
857 m_config.sync();
858 org::kde::KDirNotify::emitFilesChanged(fileList: {QUrl::fromEncoded(url: "trash:/")});
859 }
860 // The apps showing the trash (e.g. kdesktop) will be notified
861 // of this change when KDirNotify::FilesRemoved(...) is emitted,
862 // which will be done by the job soon after this.
863}
864
865#ifdef Q_OS_OSX
866#include <CoreFoundation/CoreFoundation.h>
867#include <DiskArbitration/DiskArbitration.h>
868#include <sys/mount.h>
869
870int TrashImpl::idForMountPoint(const QString &mountPoint) const
871{
872 DADiskRef disk;
873 CFDictionaryRef descDict;
874 DASessionRef session = DASessionCreate(NULL);
875 int devId = -1;
876 if (session) {
877 QByteArray mp = QFile::encodeName(mountPoint);
878 struct statfs statFS;
879 statfs(mp.constData(), &statFS);
880 disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname);
881 if (disk) {
882 descDict = DADiskCopyDescription(disk);
883 if (descDict) {
884 CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey);
885 CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey);
886 int major, minor;
887 if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) {
888 qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor;
889 devId = 1000 * major + minor;
890 }
891 CFRelease(cfMajor);
892 CFRelease(cfMinor);
893 } else {
894 qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk;
895 }
896 CFRelease(disk);
897 } else {
898 qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp;
899 }
900 CFRelease(session);
901 } else {
902 qCWarning(KIO_TRASH) << "couldn't create DASession";
903 }
904 return devId;
905}
906
907#else
908
909int TrashImpl::idForDevice(const Solid::Device &device) const
910{
911 const Solid::Block *block = device.as<Solid::Block>();
912 if (block) {
913 // qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor();
914 return block->deviceMajor() * 1000 + block->deviceMinor();
915 } else {
916 const Solid::NetworkShare *netshare = device.as<Solid::NetworkShare>();
917
918 if (netshare) {
919 QString url = netshare->url().url();
920
921 QLockFile configLock(QStandardPaths::writableLocation(type: QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock"));
922
923 if (!configLock.lock()) {
924 return -1;
925 }
926
927 m_config.reparseConfiguration();
928 KConfigGroup group = m_config.group(QStringLiteral("NetworkShares"));
929 int id = group.readEntry(key: url, aDefault: -1);
930
931 if (id == -1) {
932 id = group.readEntry(key: "NextID", defaultValue: 0);
933 // qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id;
934
935 group.writeEntry(key: url, value: id);
936 group.writeEntry(key: "NextID", value: id + 1);
937 group.sync();
938 }
939
940 return 6000000 + id;
941 }
942
943 // Not a block device nor a network share
944 return -1;
945 }
946}
947
948void TrashImpl::refreshDevices() const
949{
950 // this is needed because Solid's fstab backend uses QSocketNotifier
951 // to get notifications about changes to mtab
952 // otherwise we risk getting old device list
953 qApp->processEvents(flags: QEventLoop::ExcludeUserInputEvents);
954}
955#endif
956
957void TrashImpl::insertTrashDir(int id, const QString &trashDir, const QString &topdir) const
958{
959 m_trashDirectories.insert(key: id, value: trashDir);
960 qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id;
961 m_topDirectories.insert(key: id, value: Utils::slashAppended(s: topdir));
962}
963
964int TrashImpl::findTrashDirectory(const QString &origPath)
965{
966 // qCDebug(KIO_TRASH) << origPath;
967 // Check if it's on the same device as $HOME
968 QT_STATBUF buff;
969 if (QT_LSTAT(file: QFile::encodeName(fileName: origPath).constData(), buf: &buff) == 0 && buff.st_dev == m_homeDevice) {
970 return 0;
971 }
972
973 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: origPath);
974 if (!mp) {
975 // qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath;
976 return 0;
977 }
978
979 QString mountPoint = mp->mountPoint();
980 const QString trashDir = trashForMountPoint(topdir: mountPoint, createIfNeeded: true);
981 // qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir;
982
983#ifndef Q_OS_OSX
984 if (trashDir.isEmpty()) {
985 return 0; // no trash available on partition
986 }
987#endif
988
989 int id = idForTrashDirectory(trashDir);
990 if (id > -1) {
991 qCDebug(KIO_TRASH) << "Found Trash dir" << trashDir << "with id" << id;
992 return id;
993 }
994
995#ifdef Q_OS_OSX
996 id = idForMountPoint(mountPoint);
997#else
998 refreshDevices();
999 const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '%1']").arg(args&: mountPoint);
1000 const QList<Solid::Device> lst = Solid::Device::listFromQuery(predicate: query);
1001 qCDebug(KIO_TRASH) << "Queried Solid with" << query << "got" << lst.count() << "devices";
1002 if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance.
1003 return 0;
1004 }
1005
1006 // Pretend we got exactly one...
1007 const Solid::Device device = lst.at(i: 0);
1008 id = idForDevice(device);
1009#endif
1010 if (id == -1) {
1011 return 0;
1012 }
1013
1014 // New trash dir found, register it
1015 insertTrashDir(id, trashDir, topdir: mountPoint);
1016 return id;
1017}
1018
1019KIO::UDSEntry TrashImpl::trashUDSEntry(KIO::StatDetails details)
1020{
1021 KIO::UDSEntry entry;
1022 if (details & KIO::StatRecursiveSize) {
1023 KIO::filesize_t size = 0;
1024 long latestModifiedDate = 0;
1025
1026 for (const QString &trashPath : std::as_const(t&: m_trashDirectories)) {
1027 TrashSizeCache trashSize(trashPath);
1028 TrashSizeCache::SizeAndModTime res = trashSize.calculateSizeAndLatestModDate();
1029 size += res.size;
1030
1031 // Find latest modification date
1032 if (res.mtime > latestModifiedDate) {
1033 latestModifiedDate = res.mtime;
1034 }
1035 }
1036
1037 entry.reserve(size: 3);
1038 entry.fastInsert(field: KIO::UDSEntry::UDS_RECURSIVE_SIZE, l: static_cast<long long>(size));
1039
1040 entry.fastInsert(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, l: latestModifiedDate / 1000);
1041 // access date is unreliable for the trash folder, use the modified date instead
1042 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS_TIME, l: latestModifiedDate / 1000);
1043 }
1044 return entry;
1045}
1046
1047void TrashImpl::scanTrashDirectories() const
1048{
1049#ifndef Q_OS_OSX
1050 refreshDevices();
1051#endif
1052
1053 const QList<Solid::Device> lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true"));
1054 for (const Solid::Device &device : lst) {
1055 QString topdir = device.as<Solid::StorageAccess>()->filePath();
1056 QString trashDir = trashForMountPoint(topdir, createIfNeeded: false);
1057 if (!trashDir.isEmpty()) {
1058 // OK, trashDir is a valid trash directory. Ensure it's registered.
1059 int trashId = idForTrashDirectory(trashDir);
1060 if (trashId == -1) {
1061 // new trash dir found, register it
1062#ifdef Q_OS_OSX
1063 trashId = idForMountPoint(topdir);
1064#else
1065 trashId = idForDevice(device);
1066#endif
1067 if (trashId == -1) {
1068 continue;
1069 }
1070
1071 insertTrashDir(id: trashId, trashDir, topdir);
1072 }
1073 }
1074 }
1075 m_trashDirectoriesScanned = true;
1076}
1077
1078TrashImpl::TrashDirMap TrashImpl::trashDirectories() const
1079{
1080 if (!m_trashDirectoriesScanned) {
1081 scanTrashDirectories();
1082 }
1083 return m_trashDirectories;
1084}
1085
1086TrashImpl::TrashDirMap TrashImpl::topDirectories() const
1087{
1088 if (!m_trashDirectoriesScanned) {
1089 scanTrashDirectories();
1090 }
1091 return m_topDirectories;
1092}
1093
1094QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const
1095{
1096 // (1) Administrator-created $topdir/.Trash directory
1097
1098#ifndef Q_OS_OSX
1099 const QString rootTrashDir = topdir + QLatin1String("/.Trash");
1100#else
1101 const QString rootTrashDir = topdir + QLatin1String("/.Trashes");
1102#endif
1103 const QByteArray rootTrashDir_c = QFile::encodeName(fileName: rootTrashDir);
1104 // Can't use QFileInfo here since we need to test for the sticky bit
1105 uid_t uid = getuid();
1106 QT_STATBUF buff;
1107 const unsigned int requiredBits = S_ISVTX; // Sticky bit required
1108 if (QT_LSTAT(file: rootTrashDir_c.constData(), buf: &buff) == 0) {
1109 if ((S_ISDIR(buff.st_mode)) // must be a dir
1110 && (!S_ISLNK(buff.st_mode)) // not a symlink
1111 && ((buff.st_mode & requiredBits) == requiredBits) //
1112 && (::access(name: rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable
1113 ) {
1114 if (buff.st_dev == m_homeDevice) // bind mount, maybe
1115 return QString();
1116#ifndef Q_OS_OSX
1117 const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1118#else
1119 QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1120#endif
1121 const QByteArray trashDir_c = QFile::encodeName(fileName: trashDir);
1122 if (QT_LSTAT(file: trashDir_c.constData(), buf: &buff) == 0) {
1123 if ((buff.st_uid == uid) // must be owned by user
1124 && (S_ISDIR(buff.st_mode)) // must be a dir
1125 && (!S_ISLNK(buff.st_mode)) // not a symlink
1126 && (buff.st_mode & 0777) == 0700) { // rwx for user
1127#ifdef Q_OS_OSX
1128 trashDir += QStringLiteral("/KDE.trash");
1129#endif
1130 return trashDir;
1131 }
1132 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1133 } else if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1134 return trashDir;
1135 }
1136 } else {
1137 qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it";
1138 }
1139 }
1140
1141#ifndef Q_OS_OSX
1142 // (2) $topdir/.Trash-$uid
1143 const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid);
1144 const QByteArray trashDir_c = QFile::encodeName(fileName: trashDir);
1145 if (QT_LSTAT(file: trashDir_c.constData(), buf: &buff) == 0) {
1146 if ((buff.st_uid == uid) // must be owned by user
1147 && S_ISDIR(buff.st_mode) // must be a dir
1148 && !S_ISLNK(buff.st_mode) // not a symlink
1149 && ((buff.st_mode & 0700) == 0700)) { // and we need write access to it
1150
1151 if (buff.st_dev == m_homeDevice) // bind mount, maybe
1152 return QString();
1153 if (checkTrashSubdirs(trashDir_c)) {
1154 return trashDir;
1155 }
1156 }
1157 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1158 // Exists, but not usable
1159 return QString();
1160 }
1161 if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1162 return trashDir;
1163 }
1164#endif
1165 return QString();
1166}
1167
1168int TrashImpl::idForTrashDirectory(const QString &trashDir) const
1169{
1170 // If this is too slow we can always use a reverse map...
1171 for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
1172 if (it.value() == trashDir) {
1173 return it.key();
1174 }
1175 }
1176 return -1;
1177}
1178
1179bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const
1180{
1181 if (mkdir(path: trashDir_c.constData(), mode: 0700) != 0) {
1182 return false;
1183 }
1184 return checkTrashSubdirs(trashDir_c);
1185}
1186
1187bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const
1188{
1189 const QString trashDir = QFile::decodeName(localFileName: trashDir_c);
1190 const QString info = trashDir + QLatin1String("/info");
1191 const QString files = trashDir + QLatin1String("/files");
1192 return testDir(name: info) == 0 && testDir(name: files) == 0;
1193}
1194
1195QString TrashImpl::trashDirectoryPath(int trashId) const
1196{
1197 // Never scanned for trash dirs? (This can happen after killing kio_trash
1198 // and reusing a directory listing from the earlier instance.)
1199 if (!m_trashDirectoriesScanned) {
1200 scanTrashDirectories();
1201 }
1202 Q_ASSERT(m_trashDirectories.contains(trashId));
1203 return m_trashDirectories[trashId];
1204}
1205
1206QString TrashImpl::topDirectoryPath(int trashId) const
1207{
1208 if (!m_trashDirectoriesScanned) {
1209 scanTrashDirectories();
1210 }
1211 assert(trashId != 0);
1212 Q_ASSERT(m_topDirectories.contains(trashId));
1213 return m_topDirectories[trashId];
1214}
1215
1216// Helper method. Creates a URL with the format trash:/trashid-fileid or
1217// trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory.
1218QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath)
1219{
1220 QUrl url;
1221 url.setScheme(QStringLiteral("trash"));
1222 QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId;
1223 if (!relativePath.isEmpty()) {
1224 path += QLatin1Char('/') + relativePath;
1225 }
1226 url.setPath(path);
1227 return url;
1228}
1229
1230// Helper method. Parses a trash URL with the URL scheme defined in makeURL.
1231// The trash:/ URL itself isn't parsed here, must be caught by the caller before hand.
1232bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath)
1233{
1234 if (url.scheme() != QLatin1String("trash")) {
1235 return false;
1236 }
1237 const QString path = url.path();
1238 if (path.isEmpty()) {
1239 return false;
1240 }
1241 int start = 0;
1242 if (path[0] == QLatin1Char('/')) { // always true I hope
1243 start = 1;
1244 }
1245 int slashPos = path.indexOf(c: QLatin1Char('-'), from: 0); // don't match leading slash
1246 if (slashPos <= 0) {
1247 return false;
1248 }
1249 bool ok = false;
1250
1251 trashId = QStringView(path).mid(pos: start, n: slashPos - start).toInt(ok: &ok);
1252
1253 Q_ASSERT_X(ok, Q_FUNC_INFO, qUtf8Printable(url.toString()));
1254 if (!ok) {
1255 return false;
1256 }
1257 start = slashPos + 1;
1258 slashPos = path.indexOf(c: QLatin1Char('/'), from: start);
1259 if (slashPos <= 0) {
1260 fileId = path.mid(position: start);
1261 relativePath.clear();
1262 return true;
1263 }
1264 fileId = path.mid(position: start, n: slashPos - start);
1265 relativePath = path.mid(position: slashPos + 1);
1266 return true;
1267}
1268
1269bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId)
1270{
1271 KConfig config(QStringLiteral("ktrashrc"));
1272
1273 const QString trashPath = trashDirectoryPath(trashId);
1274 KConfigGroup group = config.group(group: trashPath);
1275
1276 const bool useTimeLimit = group.readEntry(key: "UseTimeLimit", defaultValue: false);
1277 const bool useSizeLimit = group.readEntry(key: "UseSizeLimit", defaultValue: true);
1278 const double percent = group.readEntry(key: "Percent", defaultValue: 10.0);
1279 const int actionType = group.readEntry(key: "LimitReachedAction", defaultValue: 0);
1280
1281 if (useTimeLimit) { // delete all files in trash older than X days
1282 const int maxDays = group.readEntry(key: "Days", defaultValue: 7);
1283 const QDateTime currentDate = QDateTime::currentDateTime();
1284
1285 const TrashedFileInfoList trashedFiles = list();
1286 for (const auto &info : trashedFiles) {
1287 if (info.trashId != trashId) {
1288 continue;
1289 }
1290
1291 if (info.deletionDate.daysTo(currentDate) > maxDays) {
1292 del(trashId: info.trashId, fileId: info.fileId);
1293 }
1294 }
1295 }
1296
1297 if (!useSizeLimit) { // check if size limit exceeded
1298 return true;
1299 }
1300
1301 // calculate size of the files to be put into the trash
1302 const qint64 additionalSize = DiscSpaceUtil::sizeOfPath(path: origPath);
1303
1304#ifdef Q_OS_OSX
1305 createTrashInfrastructure(trashId);
1306#endif
1307 DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
1308 auto cache = TrashSizeCache(trashPath);
1309 auto trashSize = cache.calculateSize();
1310
1311 if (util.usage(size: trashSize + additionalSize) < percent) {
1312 return true;
1313 }
1314
1315 // before we start to remove any files from the trash,
1316 // check whether the new file will fit into the trash
1317 // at all...
1318 const qint64 partitionSize = util.size();
1319
1320 if ((util.usage(size: partitionSize + additionalSize)) >= percent) {
1321 m_lastErrorCode = KIO::ERR_TRASH_FILE_TOO_LARGE;
1322 m_lastErrorMessage = KIO::buildErrorString(errorCode: m_lastErrorCode, errorText: {});
1323 return false;
1324 }
1325
1326 if (actionType == 0) { // warn the user only
1327 m_lastErrorCode = KIO::ERR_WORKER_DEFINED;
1328 m_lastErrorMessage = i18n("The trash is full. Empty it or remove items manually.");
1329 return false;
1330 }
1331
1332 // Start removing some other files from the trash
1333
1334 QDir::SortFlags sortFlags;
1335 if (actionType == 1) {
1336 sortFlags = QDir::Time | QDir::Reversed; // Delete oldest files first
1337 } else if (actionType == 2) {
1338 sortFlags = QDir::Size; // Delete biggest files first
1339 } else {
1340 qWarning() << "Called with actionType" << actionType << ", which theoretically should never happen!";
1341 return false; // Bail out
1342 }
1343
1344 const auto dirCache = cache.readDirCache();
1345 constexpr QDir::Filters dirFilters = QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot;
1346 const QFileInfoList infoList = QDir(trashPath + QLatin1String("/files")).entryInfoList(filters: dirFilters, sort: sortFlags);
1347 for (const auto &info : infoList) {
1348 auto fileSizeFreed = info.size();
1349 if (info.isDir()) {
1350 fileSizeFreed = dirCache.constFind(key: info.path().toUtf8())->size;
1351 }
1352
1353 del(trashId, fileId: info.fileName()); // delete trashed file
1354 trashSize -= fileSizeFreed;
1355
1356 if (util.usage(size: trashSize + additionalSize) < percent) { // check whether we have enough space now
1357 return true;
1358 }
1359 }
1360
1361 return true;
1362}
1363
1364#include "moc_trashimpl.cpp"
1365

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