| 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 | |
| 49 | TrashImpl::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 | */ |
| 74 | int 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 | |
| 106 | void 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 | |
| 128 | bool 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 | |
| 151 | bool 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 | |
| 191 | void 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 | |
| 232 | bool 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 | |
| 330 | QString 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 | |
| 351 | void 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 | |
| 358 | QString 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 | |
| 364 | QString TrashImpl::filesPath(int trashId, const QString &fileId) const |
| 365 | { |
| 366 | const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/" ) + fileId; |
| 367 | return trashPath; |
| 368 | } |
| 369 | |
| 370 | bool 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 | |
| 384 | bool 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 | |
| 416 | bool 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 | |
| 432 | bool 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 | |
| 458 | void 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 | |
| 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(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 | |
| 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(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 | |
| 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(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 | |
| 539 | bool 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 | |
| 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(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 | |
| 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(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 | |
| 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(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 | |
| 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(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 ".." |
| 726 | QStringList TrashImpl::listDir(const QString &physicalPath) |
| 727 | { |
| 728 | return QDir(physicalPath).entryList(filters: 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: infoPath(trashId, fileId), info, trashId); |
| 738 | } |
| 739 | |
| 740 | bool 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 | |
| 771 | bool 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 | |
| 798 | QString 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 | |
| 807 | void 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 | |
| 816 | bool 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 | |
| 842 | void 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 | |
| 855 | void 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 | |
| 879 | int 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 | |
| 918 | int 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 | |
| 957 | void 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 | |
| 966 | void 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 | |
| 973 | int 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 | |
| 1028 | KIO::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 | |
| 1056 | void 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 | |
| 1087 | TrashImpl::TrashDirMap TrashImpl::trashDirectories() const |
| 1088 | { |
| 1089 | if (!m_trashDirectoriesScanned) { |
| 1090 | scanTrashDirectories(); |
| 1091 | } |
| 1092 | return m_trashDirectories; |
| 1093 | } |
| 1094 | |
| 1095 | TrashImpl::TrashDirMap TrashImpl::topDirectories() const |
| 1096 | { |
| 1097 | if (!m_trashDirectoriesScanned) { |
| 1098 | scanTrashDirectories(); |
| 1099 | } |
| 1100 | return m_topDirectories; |
| 1101 | } |
| 1102 | |
| 1103 | QString 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 | |
| 1177 | int 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 | |
| 1188 | bool 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 | |
| 1196 | bool 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 | |
| 1204 | QString 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 | |
| 1215 | QString 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. |
| 1227 | QUrl 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. |
| 1241 | bool 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 | |
| 1278 | bool 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 | |