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