File indexing completed on 2024-10-13 09:34:44
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> 0004 SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> 0006 SPDX-FileCopyrightText: 2021 Ahmad Samir <a.samirh78@gmail.com> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "copyjob.h" 0012 #include "../utils_p.h" 0013 #include "deletejob.h" 0014 #include "filecopyjob.h" 0015 #include "global.h" 0016 #include "job.h" // buildErrorString 0017 #include "kcoredirlister.h" 0018 #include "kfileitem.h" 0019 #include "kiocoredebug.h" 0020 #include "kioglobal_p.h" 0021 #include "listjob.h" 0022 #include "mkdirjob.h" 0023 #include "statjob.h" 0024 #include <cerrno> 0025 0026 #include <KConfigGroup> 0027 #include <KDesktopFile> 0028 #include <KLocalizedString> 0029 0030 #include "kprotocolmanager.h" 0031 #include "slave.h" 0032 #include <KDirWatch> 0033 0034 #include "askuseractioninterface.h" 0035 #include <jobuidelegateextension.h> 0036 #include <kio/jobuidelegatefactory.h> 0037 0038 #include <kdirnotify.h> 0039 0040 #ifdef Q_OS_UNIX 0041 #include <utime.h> 0042 #endif 0043 0044 #include <QFile> 0045 #include <QFileInfo> 0046 #include <QPointer> 0047 #include <QTemporaryFile> 0048 #include <QTimer> 0049 0050 #include <sys/stat.h> // mode_t 0051 0052 #include "job_p.h" 0053 #include <KFileSystemType> 0054 #include <KFileUtils> 0055 #include <KIO/FileSystemFreeSpaceJob> 0056 0057 #include <list> 0058 #include <set> 0059 0060 #include <QLoggingCategory> 0061 Q_DECLARE_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG) 0062 Q_LOGGING_CATEGORY(KIO_COPYJOB_DEBUG, "kf.kio.core.copyjob", QtWarningMsg) 0063 0064 using namespace KIO; 0065 0066 // this will update the report dialog with 5 Hz, I think this is fast enough, aleXXX 0067 static constexpr int s_reportTimeout = 200; 0068 0069 #if !defined(NAME_MAX) 0070 #if defined(_MAX_FNAME) 0071 static constexpr int NAME_MAX = _MAX_FNAME; // For Windows 0072 #else 0073 static constexpr NAME_MAX = 0; 0074 #endif 0075 #endif 0076 0077 enum DestinationState { 0078 DEST_NOT_STATED, 0079 DEST_IS_DIR, 0080 DEST_IS_FILE, 0081 DEST_DOESNT_EXIST, 0082 }; 0083 0084 /** 0085 * States: 0086 * STATE_INITIAL the constructor was called 0087 * STATE_STATING for the dest 0088 * statCurrentSrc then does, for each src url: 0089 * STATE_RENAMING if direct rename looks possible 0090 * (on already exists, and user chooses rename, TODO: go to STATE_RENAMING again) 0091 * STATE_STATING 0092 * and then, if dir -> STATE_LISTING (filling 'd->dirs' and 'd->files') 0093 * STATE_CREATING_DIRS (createNextDir, iterating over 'd->dirs') 0094 * if conflict: STATE_CONFLICT_CREATING_DIRS 0095 * STATE_COPYING_FILES (copyNextFile, iterating over 'd->files') 0096 * if conflict: STATE_CONFLICT_COPYING_FILES 0097 * STATE_DELETING_DIRS (deleteNextDir) (if moving) 0098 * STATE_SETTING_DIR_ATTRIBUTES (setNextDirAttribute, iterating over d->m_directoriesCopied) 0099 * done. 0100 */ 0101 enum CopyJobState { 0102 STATE_INITIAL, 0103 STATE_STATING, 0104 STATE_RENAMING, 0105 STATE_LISTING, 0106 STATE_CREATING_DIRS, 0107 STATE_CONFLICT_CREATING_DIRS, 0108 STATE_COPYING_FILES, 0109 STATE_CONFLICT_COPYING_FILES, 0110 STATE_DELETING_DIRS, 0111 STATE_SETTING_DIR_ATTRIBUTES, 0112 }; 0113 0114 static QUrl addPathToUrl(const QUrl &url, const QString &relPath) 0115 { 0116 QUrl u(url); 0117 u.setPath(Utils::concatPaths(url.path(), relPath)); 0118 return u; 0119 } 0120 0121 static bool compareUrls(const QUrl &srcUrl, const QUrl &destUrl) 0122 { 0123 /* clang-format off */ 0124 return srcUrl.scheme() == destUrl.scheme() 0125 && srcUrl.host() == destUrl.host() 0126 && srcUrl.port() == destUrl.port() 0127 && srcUrl.userName() == destUrl.userName() 0128 && srcUrl.password() == destUrl.password(); 0129 /* clang-format on */ 0130 } 0131 0132 // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions 0133 static const char s_msdosInvalidChars[] = R"(<>:"/\|?*)"; 0134 0135 static bool hasInvalidChars(const QString &dest) 0136 { 0137 return std::any_of(std::begin(s_msdosInvalidChars), std::end(s_msdosInvalidChars), [=](const char c) { 0138 return dest.contains(QLatin1Char(c)); 0139 }); 0140 } 0141 0142 static void cleanMsdosDestName(QString &name) 0143 { 0144 for (const char c : s_msdosInvalidChars) { 0145 name.replace(QLatin1Char(c), QLatin1String("_")); 0146 } 0147 } 0148 0149 static bool isFatFs(KFileSystemType::Type fsType) 0150 { 0151 return fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat; 0152 } 0153 0154 static bool isFatOrNtfs(KFileSystemType::Type fsType) 0155 { 0156 return fsType == KFileSystemType::Ntfs || isFatFs(fsType); 0157 } 0158 0159 static QString symlinkSupportMsg(const QString &path, const QString &fsName) 0160 { 0161 const QString msg = i18nc( 0162 "The first arg is the path to the symlink that couldn't be created, the second" 0163 "arg is the filesystem type (e.g. vfat, exfat)", 0164 "Could not create symlink \"%1\".\n" 0165 "The destination filesystem (%2) doesn't support symlinks.", 0166 path, 0167 fsName); 0168 return msg; 0169 } 0170 0171 static QString invalidCharsSupportMsg(const QString &path, const QString &fsName, bool isDir = false) 0172 { 0173 QString msg; 0174 if (isDir) { 0175 msg = i18n( 0176 "Could not create \"%1\".\n" 0177 "The destination filesystem (%2) disallows the following characters in folder names: %3\n" 0178 "Selecting Replace will replace any invalid characters (in the destination folder name) with an underscore \"_\".", 0179 path, 0180 fsName, 0181 QLatin1String(s_msdosInvalidChars)); 0182 } else { 0183 msg = i18n( 0184 "Could not create \"%1\".\n" 0185 "The destination filesystem (%2) disallows the following characters in file names: %3\n" 0186 "Selecting Replace will replace any invalid characters (in the destination file name) with an underscore \"_\".", 0187 path, 0188 fsName, 0189 QLatin1String(s_msdosInvalidChars)); 0190 } 0191 0192 return msg; 0193 } 0194 0195 /** @internal */ 0196 class KIO::CopyJobPrivate : public KIO::JobPrivate 0197 { 0198 public: 0199 CopyJobPrivate(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod) 0200 : m_globalDest(dest) 0201 , m_globalDestinationState(DEST_NOT_STATED) 0202 , m_defaultPermissions(false) 0203 , m_bURLDirty(false) 0204 , m_mode(mode) 0205 , m_asMethod(asMethod) 0206 , destinationState(DEST_NOT_STATED) 0207 , state(STATE_INITIAL) 0208 , m_freeSpace(-1) 0209 , m_totalSize(0) 0210 , m_processedSize(0) 0211 , m_fileProcessedSize(0) 0212 , m_filesHandledByDirectRename(0) 0213 , m_processedFiles(0) 0214 , m_processedDirs(0) 0215 , m_srcList(src) 0216 , m_currentStatSrc(m_srcList.constBegin()) 0217 , m_bCurrentOperationIsLink(false) 0218 , m_bSingleFileCopy(false) 0219 , m_bOnlyRenames(mode == CopyJob::Move) 0220 , m_dest(dest) 0221 , m_bAutoRenameFiles(false) 0222 , m_bAutoRenameDirs(false) 0223 , m_bAutoSkipFiles(false) 0224 , m_bAutoSkipDirs(false) 0225 , m_bOverwriteAllFiles(false) 0226 , m_bOverwriteAllDirs(false) 0227 , m_bOverwriteWhenOlder(false) 0228 , m_conflictError(0) 0229 , m_reportTimer(nullptr) 0230 { 0231 } 0232 0233 // This is the dest URL that was initially given to CopyJob 0234 // It is copied into m_dest, which can be changed for a given src URL 0235 // (when using the RENAME dialog in slotResult), 0236 // and which will be reset for the next src URL. 0237 QUrl m_globalDest; 0238 // The state info about that global dest 0239 DestinationState m_globalDestinationState; 0240 // See setDefaultPermissions 0241 bool m_defaultPermissions; 0242 // Whether URLs changed (and need to be emitted by the next slotReport call) 0243 bool m_bURLDirty; 0244 // Used after copying all the files into the dirs, to set mtime (TODO: and permissions?) 0245 // after the copy is done 0246 std::list<CopyInfo> m_directoriesCopied; 0247 std::list<CopyInfo>::const_iterator m_directoriesCopiedIterator; 0248 0249 CopyJob::CopyMode m_mode; 0250 bool m_asMethod; // See copyAs() method 0251 DestinationState destinationState; 0252 CopyJobState state; 0253 0254 KIO::filesize_t m_freeSpace; 0255 0256 KIO::filesize_t m_totalSize; 0257 KIO::filesize_t m_processedSize; 0258 KIO::filesize_t m_fileProcessedSize; 0259 int m_filesHandledByDirectRename; 0260 int m_processedFiles; 0261 int m_processedDirs; 0262 QList<CopyInfo> files; 0263 QList<CopyInfo> dirs; 0264 // List of dirs that will be copied then deleted when CopyMode is Move 0265 QList<QUrl> dirsToRemove; 0266 QList<QUrl> m_srcList; 0267 QList<QUrl> m_successSrcList; // Entries in m_srcList that have successfully been moved 0268 QList<QUrl>::const_iterator m_currentStatSrc; 0269 bool m_bCurrentSrcIsDir; 0270 bool m_bCurrentOperationIsLink; 0271 bool m_bSingleFileCopy; 0272 bool m_bOnlyRenames; 0273 QUrl m_dest; 0274 QUrl m_currentDest; // set during listing, used by slotEntries 0275 // 0276 QStringList m_skipList; 0277 QSet<QString> m_overwriteList; 0278 bool m_bAutoRenameFiles; 0279 bool m_bAutoRenameDirs; 0280 bool m_bAutoSkipFiles; 0281 bool m_bAutoSkipDirs; 0282 bool m_bOverwriteAllFiles; 0283 bool m_bOverwriteAllDirs; 0284 bool m_bOverwriteWhenOlder; 0285 0286 bool m_autoSkipDirsWithInvalidChars = false; 0287 bool m_autoSkipFilesWithInvalidChars = false; 0288 bool m_autoReplaceInvalidChars = false; 0289 0290 bool m_autoSkipFatSymlinks = false; 0291 0292 enum SkipType { 0293 // No skip dialog is involved 0294 NoSkipType = 0, 0295 // SkipDialog is asking about invalid chars in destination file/dir names 0296 SkipInvalidChars, 0297 // SkipDialog is asking about how to handle symlinks why copying to a 0298 // filesystem that doesn't support symlinks 0299 SkipFatSymlinks, 0300 }; 0301 0302 int m_conflictError; 0303 0304 QTimer *m_reportTimer; 0305 0306 // The current src url being stat'ed or copied 0307 // During the stat phase, this is initially equal to *m_currentStatSrc but it can be resolved to a local file equivalent (#188903). 0308 QUrl m_currentSrcURL; 0309 QUrl m_currentDestURL; 0310 0311 std::set<QString> m_parentDirs; 0312 bool m_ignoreSourcePermissions = false; 0313 0314 void statCurrentSrc(); 0315 void statNextSrc(); 0316 0317 // Those aren't slots but submethods for slotResult. 0318 void slotResultStating(KJob *job); 0319 void startListing(const QUrl &src); 0320 0321 void slotResultCreatingDirs(KJob *job); 0322 void slotResultConflictCreatingDirs(KJob *job); 0323 void createNextDir(); 0324 void processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result); 0325 0326 void slotResultCopyingFiles(KJob *job); 0327 void slotResultErrorCopyingFiles(KJob *job); 0328 void processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, RenameDialog_Result result, const QUrl &newUrl, const QDateTime &destmtime); 0329 0330 // KIO::Job* linkNextFile( const QUrl& uSource, const QUrl& uDest, bool overwrite ); 0331 KIO::Job *linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags); 0332 // MsDos filesystems don't allow certain characters in filenames, and VFAT and ExFAT 0333 // don't support symlinks, this method detects those conditions and tries to handle it 0334 bool handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType); 0335 void copyNextFile(); 0336 void processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType); 0337 0338 void slotResultDeletingDirs(KJob *job); 0339 void deleteNextDir(); 0340 void sourceStated(const UDSEntry &entry, const QUrl &sourceUrl); 0341 // Removes a dir from the "dirsToRemove" list 0342 void skip(const QUrl &sourceURL, bool isDir); 0343 0344 void slotResultRenaming(KJob *job); 0345 void directRenamingFailed(const QUrl &dest); 0346 void processDirectRenamingConflictResult(RenameDialog_Result result, 0347 bool srcIsDir, 0348 bool destIsDir, 0349 const QDateTime &mtimeSrc, 0350 const QDateTime &mtimeDest, 0351 const QUrl &dest, 0352 const QUrl &newUrl); 0353 0354 void slotResultSettingDirAttributes(KJob *job); 0355 void setNextDirAttribute(); 0356 0357 void startRenameJob(const QUrl &workerUrl); 0358 bool shouldOverwriteDir(const QString &path) const; 0359 bool shouldOverwriteFile(const QString &path) const; 0360 bool shouldSkip(const QString &path) const; 0361 void skipSrc(bool isDir); 0362 void renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl); 0363 QUrl finalDestUrl(const QUrl &src, const QUrl &dest) const; 0364 0365 void slotStart(); 0366 void slotEntries(KIO::Job *, const KIO::UDSEntryList &list); 0367 void slotSubError(KIO::ListJob *job, KIO::ListJob *subJob); 0368 void addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest); 0369 /** 0370 * Forward signal from subjob 0371 */ 0372 void slotProcessedSize(KJob *, qulonglong data_size); 0373 /** 0374 * Forward signal from subjob 0375 * @param size the total size 0376 */ 0377 void slotTotalSize(KJob *, qulonglong size); 0378 0379 void slotReport(); 0380 0381 Q_DECLARE_PUBLIC(CopyJob) 0382 0383 static inline CopyJob *newJob(const QList<QUrl> &src, const QUrl &dest, CopyJob::CopyMode mode, bool asMethod, JobFlags flags) 0384 { 0385 CopyJob *job = new CopyJob(*new CopyJobPrivate(src, dest, mode, asMethod)); 0386 job->setUiDelegate(KIO::createDefaultJobUiDelegate()); 0387 if (!(flags & HideProgressInfo)) { 0388 KIO::getJobTracker()->registerJob(job); 0389 } 0390 if (flags & KIO::Overwrite) { 0391 job->d_func()->m_bOverwriteAllDirs = true; 0392 job->d_func()->m_bOverwriteAllFiles = true; 0393 } 0394 if (!(flags & KIO::NoPrivilegeExecution)) { 0395 job->d_func()->m_privilegeExecutionEnabled = true; 0396 FileOperationType copyType; 0397 switch (mode) { 0398 case CopyJob::Copy: 0399 copyType = Copy; 0400 break; 0401 case CopyJob::Move: 0402 copyType = Move; 0403 break; 0404 case CopyJob::Link: 0405 copyType = Symlink; 0406 break; 0407 default: 0408 Q_UNREACHABLE(); 0409 } 0410 job->d_func()->m_operationType = copyType; 0411 } 0412 return job; 0413 } 0414 }; 0415 0416 CopyJob::CopyJob(CopyJobPrivate &dd) 0417 : Job(dd) 0418 { 0419 Q_D(CopyJob); 0420 setProperty("destUrl", d_func()->m_dest.toString()); 0421 QTimer::singleShot(0, this, [d]() { 0422 d->slotStart(); 0423 }); 0424 qRegisterMetaType<KIO::UDSEntry>(); 0425 } 0426 0427 CopyJob::~CopyJob() 0428 { 0429 } 0430 0431 QList<QUrl> CopyJob::srcUrls() const 0432 { 0433 return d_func()->m_srcList; 0434 } 0435 0436 QUrl CopyJob::destUrl() const 0437 { 0438 return d_func()->m_dest; 0439 } 0440 0441 void CopyJobPrivate::slotStart() 0442 { 0443 Q_Q(CopyJob); 0444 if (q->isSuspended()) { 0445 return; 0446 } 0447 0448 if (m_mode == CopyJob::CopyMode::Move) { 0449 for (const QUrl &url : std::as_const(m_srcList)) { 0450 if (m_dest.scheme() == url.scheme() && m_dest.host() == url.host()) { 0451 const QString srcPath = Utils::slashAppended(url.path()); 0452 if (m_dest.path().startsWith(srcPath)) { 0453 q->setError(KIO::ERR_CANNOT_MOVE_INTO_ITSELF); 0454 q->emitResult(); 0455 return; 0456 } 0457 } 0458 } 0459 } 0460 0461 if (m_mode == CopyJob::CopyMode::Link && m_globalDest.isLocalFile()) { 0462 const QString destPath = m_globalDest.toLocalFile(); 0463 const auto destFs = KFileSystemType::fileSystemType(destPath); 0464 if (isFatFs(destFs)) { 0465 q->setError(ERR_SYMLINKS_NOT_SUPPORTED); 0466 const QString errText = destPath + QLatin1String(" [") + KFileSystemType::fileSystemName(destFs) + QLatin1Char(']'); 0467 q->setErrorText(errText); 0468 q->emitResult(); 0469 return; 0470 } 0471 } 0472 0473 /** 0474 We call the functions directly instead of using signals. 0475 Calling a function via a signal takes approx. 65 times the time 0476 compared to calling it directly (at least on my machine). aleXXX 0477 */ 0478 m_reportTimer = new QTimer(q); 0479 0480 q->connect(m_reportTimer, &QTimer::timeout, q, [this]() { 0481 slotReport(); 0482 }); 0483 m_reportTimer->start(s_reportTimeout); 0484 0485 // Stat the dest 0486 state = STATE_STATING; 0487 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0488 // We need isDir() and UDS_LOCAL_PATH (for workers who set it). Let's assume the latter is part of StatBasic too. 0489 KIO::Job *job = KIO::statDetails(dest, StatJob::DestinationSide, KIO::StatBasic | KIO::StatResolveSymlink, KIO::HideProgressInfo); 0490 qCDebug(KIO_COPYJOB_DEBUG) << "CopyJob: stating the dest" << dest; 0491 q->addSubjob(job); 0492 } 0493 0494 // For unit test purposes 0495 KIOCORE_EXPORT bool kio_resolve_local_urls = true; 0496 0497 void CopyJobPrivate::slotResultStating(KJob *job) 0498 { 0499 Q_Q(CopyJob); 0500 qCDebug(KIO_COPYJOB_DEBUG); 0501 // Was there an error while stating the src ? 0502 if (job->error() && destinationState != DEST_NOT_STATED) { 0503 const QUrl srcurl = static_cast<SimpleJob *>(job)->url(); 0504 if (!srcurl.isLocalFile()) { 0505 // Probably : src doesn't exist. Well, over some protocols (e.g. FTP) 0506 // this info isn't really reliable (thanks to MS FTP servers). 0507 // We'll assume a file, and try to download anyway. 0508 qCDebug(KIO_COPYJOB_DEBUG) << "Error while stating source. Activating hack"; 0509 q->removeSubjob(job); 0510 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 0511 struct CopyInfo info; 0512 info.permissions = (mode_t)-1; 0513 info.size = KIO::invalidFilesize; 0514 info.uSource = srcurl; 0515 info.uDest = m_dest; 0516 // Append filename or dirname to destination URL, if allowed 0517 if (destinationState == DEST_IS_DIR && !m_asMethod) { 0518 const QString fileName = srcurl.scheme() == QLatin1String("data") ? QStringLiteral("data") : srcurl.fileName(); // #379093 0519 info.uDest = addPathToUrl(info.uDest, fileName); 0520 } 0521 0522 files.append(info); 0523 statNextSrc(); 0524 return; 0525 } 0526 // Local file. If stat fails, the file definitely doesn't exist. 0527 // yes, q->Job::, because we don't want to call our override 0528 q->Job::slotResult(job); // will set the error and emit result(this) 0529 return; 0530 } 0531 0532 // Keep copy of the stat result 0533 auto statJob = static_cast<StatJob *>(job); 0534 const UDSEntry entry = statJob->statResult(); 0535 0536 if (destinationState == DEST_NOT_STATED) { 0537 const bool isGlobalDest = m_dest == m_globalDest; 0538 0539 // we were stating the dest 0540 if (job->error()) { 0541 destinationState = DEST_DOESNT_EXIST; 0542 qCDebug(KIO_COPYJOB_DEBUG) << "dest does not exist"; 0543 } else { 0544 const bool isDir = entry.isDir(); 0545 0546 // Check for writability, before spending time stat'ing everything (#141564). 0547 // This assumes all KIO workers set permissions correctly... 0548 const int permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); 0549 const bool isWritable = (permissions != -1) && (permissions & S_IWUSR); 0550 if (!m_privilegeExecutionEnabled && !isWritable) { 0551 const QUrl dest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0552 q->setError(ERR_WRITE_ACCESS_DENIED); 0553 q->setErrorText(dest.toDisplayString(QUrl::PreferLocalFile)); 0554 q->emitResult(); 0555 return; 0556 } 0557 0558 // Treat symlinks to dirs as dirs here, so no test on isLink 0559 destinationState = isDir ? DEST_IS_DIR : DEST_IS_FILE; 0560 qCDebug(KIO_COPYJOB_DEBUG) << "dest is dir:" << isDir; 0561 0562 if (isGlobalDest) { 0563 m_globalDestinationState = destinationState; 0564 } 0565 0566 const QString sLocalPath = entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); 0567 if (!sLocalPath.isEmpty() && kio_resolve_local_urls && statJob->url().scheme() != QStringLiteral("trash")) { 0568 const QString fileName = m_dest.fileName(); 0569 m_dest = QUrl::fromLocalFile(sLocalPath); 0570 if (m_asMethod) { 0571 m_dest = addPathToUrl(m_dest, fileName); 0572 } 0573 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to the local path:" << sLocalPath; 0574 if (isGlobalDest) { 0575 m_globalDest = m_dest; 0576 } 0577 } 0578 } 0579 0580 q->removeSubjob(job); 0581 Q_ASSERT(!q->hasSubjobs()); 0582 0583 // In copy-as mode, we want to check the directory to which we're 0584 // copying. The target file or directory does not exist yet, which 0585 // might confuse FileSystemFreeSpaceJob. 0586 const QUrl existingDest = m_asMethod ? m_dest.adjusted(QUrl::RemoveFilename) : m_dest; 0587 KIO::FileSystemFreeSpaceJob *spaceJob = KIO::fileSystemFreeSpace(existingDest); 0588 q->connect(spaceJob, 0589 &KIO::FileSystemFreeSpaceJob::result, 0590 q, 0591 [this, existingDest](KIO::Job *spaceJob, KIO::filesize_t /*size*/, KIO::filesize_t available) { 0592 if (!spaceJob->error()) { 0593 m_freeSpace = available; 0594 } else { 0595 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't determine free space information for" << existingDest; 0596 } 0597 // After knowing what the dest is, we can start stat'ing the first src. 0598 statCurrentSrc(); 0599 }); 0600 return; 0601 } else { 0602 sourceStated(entry, static_cast<SimpleJob *>(job)->url()); 0603 q->removeSubjob(job); 0604 } 0605 } 0606 0607 void CopyJobPrivate::sourceStated(const UDSEntry &entry, const QUrl &sourceUrl) 0608 { 0609 const QString sLocalPath = sourceUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString(); 0610 const bool isDir = entry.isDir(); 0611 0612 // We were stating the current source URL 0613 // Is it a file or a dir ? 0614 0615 // There 6 cases, and all end up calling addCopyInfoFromUDSEntry first : 0616 // 1 - src is a dir, destination is a directory, 0617 // slotEntries will append the source-dir-name to the destination 0618 // 2 - src is a dir, destination is a file -- will offer to overwrite, later on. 0619 // 3 - src is a dir, destination doesn't exist, then it's the destination dirname, 0620 // so slotEntries will use it as destination. 0621 0622 // 4 - src is a file, destination is a directory, 0623 // slotEntries will append the filename to the destination. 0624 // 5 - src is a file, destination is a file, m_dest is the exact destination name 0625 // 6 - src is a file, destination doesn't exist, m_dest is the exact destination name 0626 0627 QUrl srcurl; 0628 if (!sLocalPath.isEmpty() && destinationState != DEST_DOESNT_EXIST) { 0629 qCDebug(KIO_COPYJOB_DEBUG) << "Using sLocalPath. destinationState=" << destinationState; 0630 // Prefer the local path -- but only if we were able to stat() the dest. 0631 // Otherwise, renaming a desktop:/ url would copy from src=file to dest=desktop (#218719) 0632 srcurl = QUrl::fromLocalFile(sLocalPath); 0633 } else { 0634 srcurl = sourceUrl; 0635 } 0636 addCopyInfoFromUDSEntry(entry, srcurl, false, m_dest); 0637 0638 m_currentDest = m_dest; 0639 m_bCurrentSrcIsDir = false; 0640 0641 if (isDir // 0642 && !entry.isLink() // treat symlinks as files (no recursion) 0643 && m_mode != CopyJob::Link) { // No recursion in Link mode either. 0644 qCDebug(KIO_COPYJOB_DEBUG) << "Source is a directory"; 0645 0646 if (srcurl.isLocalFile()) { 0647 const QString parentDir = srcurl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); 0648 m_parentDirs.insert(parentDir); 0649 } 0650 0651 m_bCurrentSrcIsDir = true; // used by slotEntries 0652 if (destinationState == DEST_IS_DIR) { // (case 1) 0653 if (!m_asMethod) { 0654 // Use <desturl>/<directory_copied> as destination, from now on 0655 QString directory = srcurl.fileName(); 0656 const QString sName = entry.stringValue(KIO::UDSEntry::UDS_NAME); 0657 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(srcurl); 0658 if (fnu == KProtocolInfo::Name) { 0659 if (!sName.isEmpty()) { 0660 directory = sName; 0661 } 0662 } else if (fnu == KProtocolInfo::DisplayName) { 0663 const QString dispName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); 0664 if (!dispName.isEmpty()) { 0665 directory = dispName; 0666 } else if (!sName.isEmpty()) { 0667 directory = sName; 0668 } 0669 } 0670 m_currentDest = addPathToUrl(m_currentDest, directory); 0671 } 0672 } else { // (case 3) 0673 // otherwise dest is new name for toplevel dir 0674 // so the destination exists, in fact, from now on. 0675 // (This even works with other src urls in the list, since the 0676 // dir has effectively been created) 0677 destinationState = DEST_IS_DIR; 0678 if (m_dest == m_globalDest) { 0679 m_globalDestinationState = destinationState; 0680 } 0681 } 0682 0683 startListing(srcurl); 0684 } else { 0685 qCDebug(KIO_COPYJOB_DEBUG) << "Source is a file (or a symlink), or we are linking -> no recursive listing"; 0686 0687 if (srcurl.isLocalFile()) { 0688 const QString parentDir = srcurl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); 0689 m_parentDirs.insert(parentDir); 0690 } 0691 0692 statNextSrc(); 0693 } 0694 } 0695 0696 bool CopyJob::doSuspend() 0697 { 0698 Q_D(CopyJob); 0699 d->slotReport(); 0700 return Job::doSuspend(); 0701 } 0702 0703 bool CopyJob::doResume() 0704 { 0705 Q_D(CopyJob); 0706 switch (d->state) { 0707 case STATE_INITIAL: 0708 QTimer::singleShot(0, this, [d]() { 0709 d->slotStart(); 0710 }); 0711 break; 0712 default: 0713 // not implemented 0714 break; 0715 } 0716 return Job::doResume(); 0717 } 0718 0719 void CopyJobPrivate::slotReport() 0720 { 0721 Q_Q(CopyJob); 0722 if (q->isSuspended()) { 0723 return; 0724 } 0725 0726 // If showProgressInfo was set, progressId() is > 0. 0727 switch (state) { 0728 case STATE_RENAMING: 0729 if (m_bURLDirty) { 0730 m_bURLDirty = false; 0731 Q_ASSERT(m_mode == CopyJob::Move); 0732 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0733 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL); 0734 } 0735 // "N" files renamed shouldn't include skipped files 0736 q->setProcessedAmount(KJob::Files, m_processedFiles); 0737 // % value should include skipped files 0738 q->emitPercent(m_filesHandledByDirectRename, q->totalAmount(KJob::Files)); 0739 break; 0740 0741 case STATE_COPYING_FILES: 0742 q->setProcessedAmount(KJob::Files, m_processedFiles); 0743 q->setProcessedAmount(KJob::Bytes, m_processedSize + m_fileProcessedSize); 0744 if (m_bURLDirty) { 0745 // Only emit urls when they changed. This saves time, and fixes #66281 0746 m_bURLDirty = false; 0747 if (m_mode == CopyJob::Move) { 0748 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0749 Q_EMIT q->moving(q, m_currentSrcURL, m_currentDestURL); 0750 } else if (m_mode == CopyJob::Link) { 0751 emitCopying(q, m_currentSrcURL, m_currentDestURL); // we don't have a delegate->linking 0752 Q_EMIT q->linking(q, m_currentSrcURL.path(), m_currentDestURL); 0753 } else { 0754 emitCopying(q, m_currentSrcURL, m_currentDestURL); 0755 Q_EMIT q->copying(q, m_currentSrcURL, m_currentDestURL); 0756 } 0757 } 0758 break; 0759 0760 case STATE_CREATING_DIRS: 0761 q->setProcessedAmount(KJob::Directories, m_processedDirs); 0762 if (m_bURLDirty) { 0763 m_bURLDirty = false; 0764 Q_EMIT q->creatingDir(q, m_currentDestURL); 0765 emitCreatingDir(q, m_currentDestURL); 0766 } 0767 break; 0768 0769 case STATE_STATING: 0770 case STATE_LISTING: 0771 if (m_bURLDirty) { 0772 m_bURLDirty = false; 0773 if (m_mode == CopyJob::Move) { 0774 emitMoving(q, m_currentSrcURL, m_currentDestURL); 0775 } else { 0776 emitCopying(q, m_currentSrcURL, m_currentDestURL); 0777 } 0778 } 0779 q->setProgressUnit(KJob::Bytes); 0780 q->setTotalAmount(KJob::Bytes, m_totalSize); 0781 q->setTotalAmount(KJob::Files, files.count() + m_filesHandledByDirectRename); 0782 q->setTotalAmount(KJob::Directories, dirs.count()); 0783 break; 0784 0785 default: 0786 break; 0787 } 0788 } 0789 0790 void CopyJobPrivate::slotEntries(KIO::Job *job, const UDSEntryList &list) 0791 { 0792 // Q_Q(CopyJob); 0793 UDSEntryList::ConstIterator it = list.constBegin(); 0794 UDSEntryList::ConstIterator end = list.constEnd(); 0795 for (; it != end; ++it) { 0796 const UDSEntry &entry = *it; 0797 addCopyInfoFromUDSEntry(entry, static_cast<SimpleJob *>(job)->url(), m_bCurrentSrcIsDir, m_currentDest); 0798 } 0799 } 0800 0801 void CopyJobPrivate::slotSubError(ListJob *job, ListJob *subJob) 0802 { 0803 const QUrl &url = subJob->url(); 0804 qCWarning(KIO_CORE) << url << subJob->errorString(); 0805 0806 Q_Q(CopyJob); 0807 0808 Q_EMIT q->warning(job, subJob->errorString(), QString()); 0809 skip(url, true); 0810 } 0811 0812 void CopyJobPrivate::addCopyInfoFromUDSEntry(const UDSEntry &entry, const QUrl &srcUrl, bool srcIsDir, const QUrl ¤tDest) 0813 { 0814 struct CopyInfo info; 0815 info.permissions = entry.numberValue(KIO::UDSEntry::UDS_ACCESS, -1); 0816 const auto timeVal = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 0817 if (timeVal != -1) { 0818 info.mtime = QDateTime::fromSecsSinceEpoch(timeVal, Qt::UTC); 0819 } 0820 info.ctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); 0821 info.size = static_cast<KIO::filesize_t>(entry.numberValue(KIO::UDSEntry::UDS_SIZE, -1)); 0822 const bool isDir = entry.isDir(); 0823 0824 if (!isDir && info.size != KIO::invalidFilesize) { 0825 m_totalSize += info.size; 0826 } 0827 0828 // recursive listing, displayName can be a/b/c/d 0829 const QString fileName = entry.stringValue(KIO::UDSEntry::UDS_NAME); 0830 const QString urlStr = entry.stringValue(KIO::UDSEntry::UDS_URL); 0831 QUrl url; 0832 if (!urlStr.isEmpty()) { 0833 url = QUrl(urlStr); 0834 } 0835 QString localPath = srcUrl.scheme() != QStringLiteral("trash") ? entry.stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : QString(); 0836 info.linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 0837 0838 if (fileName != QLatin1String("..") && fileName != QLatin1String(".")) { 0839 const bool hasCustomURL = !url.isEmpty() || !localPath.isEmpty(); 0840 if (!hasCustomURL) { 0841 // Make URL from displayName 0842 url = srcUrl; 0843 if (srcIsDir) { // Only if src is a directory. Otherwise uSource is fine as is 0844 qCDebug(KIO_COPYJOB_DEBUG) << "adding path" << fileName; 0845 url = addPathToUrl(url, fileName); 0846 } 0847 } 0848 qCDebug(KIO_COPYJOB_DEBUG) << "fileName=" << fileName << "url=" << url; 0849 if (!localPath.isEmpty() && kio_resolve_local_urls && destinationState != DEST_DOESNT_EXIST) { 0850 url = QUrl::fromLocalFile(localPath); 0851 } 0852 0853 info.uSource = url; 0854 info.uDest = currentDest; 0855 qCDebug(KIO_COPYJOB_DEBUG) << "uSource=" << info.uSource << "uDest(1)=" << info.uDest; 0856 // Append filename or dirname to destination URL, if allowed 0857 if (destinationState == DEST_IS_DIR && 0858 // "copy/move as <foo>" means 'foo' is the dest for the base srcurl 0859 // (passed here during stating) but not its children (during listing) 0860 (!(m_asMethod && state == STATE_STATING))) { 0861 QString destFileName; 0862 KProtocolInfo::FileNameUsedForCopying fnu = KProtocolManager::fileNameUsedForCopying(url); 0863 if (hasCustomURL && fnu == KProtocolInfo::FromUrl) { 0864 // destFileName = url.fileName(); // Doesn't work for recursive listing 0865 // Count the number of prefixes used by the recursive listjob 0866 int numberOfSlashes = fileName.count(QLatin1Char('/')); // don't make this a find()! 0867 QString path = url.path(); 0868 int pos = 0; 0869 for (int n = 0; n < numberOfSlashes + 1; ++n) { 0870 pos = path.lastIndexOf(QLatin1Char('/'), pos - 1); 0871 if (pos == -1) { // error 0872 qCWarning(KIO_CORE) << "KIO worker bug: not enough slashes in UDS_URL" << path << "- looking for" << numberOfSlashes << "slashes"; 0873 break; 0874 } 0875 } 0876 if (pos >= 0) { 0877 destFileName = path.mid(pos + 1); 0878 } 0879 0880 } else if (fnu == KProtocolInfo::Name) { // destination filename taken from UDS_NAME 0881 destFileName = fileName; 0882 } else { // from display name (with fallback to name) 0883 const QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); 0884 destFileName = displayName.isEmpty() ? fileName : displayName; 0885 } 0886 0887 // Here we _really_ have to add some filename to the dest. 0888 // Otherwise, we end up with e.g. dest=..../Desktop/ itself. 0889 // (This can happen when dropping a link to a webpage with no path) 0890 if (destFileName.isEmpty()) { 0891 destFileName = KIO::encodeFileName(info.uSource.toDisplayString()); 0892 } 0893 0894 qCDebug(KIO_COPYJOB_DEBUG) << " adding destFileName=" << destFileName; 0895 info.uDest = addPathToUrl(info.uDest, destFileName); 0896 } 0897 qCDebug(KIO_COPYJOB_DEBUG) << " uDest(2)=" << info.uDest; 0898 qCDebug(KIO_COPYJOB_DEBUG) << " " << info.uSource << "->" << info.uDest; 0899 if (info.linkDest.isEmpty() && isDir && m_mode != CopyJob::Link) { // Dir 0900 dirs.append(info); // Directories 0901 if (m_mode == CopyJob::Move) { 0902 dirsToRemove.append(info.uSource); 0903 } 0904 } else { 0905 files.append(info); // Files and any symlinks 0906 } 0907 } 0908 } 0909 0910 // Adjust for kio_trash choosing its own dest url... 0911 QUrl CopyJobPrivate::finalDestUrl(const QUrl &src, const QUrl &dest) const 0912 { 0913 Q_Q(const CopyJob); 0914 if (dest.scheme() == QLatin1String("trash")) { 0915 const QMap<QString, QString> &metaData = q->metaData(); 0916 QMap<QString, QString>::ConstIterator it = metaData.find(QLatin1String("trashURL-") + src.path()); 0917 if (it != metaData.constEnd()) { 0918 qCDebug(KIO_COPYJOB_DEBUG) << "finalDestUrl=" << it.value(); 0919 return QUrl(it.value()); 0920 } 0921 } 0922 return dest; 0923 } 0924 0925 void CopyJobPrivate::skipSrc(bool isDir) 0926 { 0927 m_dest = m_globalDest; 0928 destinationState = m_globalDestinationState; 0929 skip(*m_currentStatSrc, isDir); 0930 ++m_currentStatSrc; 0931 statCurrentSrc(); 0932 } 0933 0934 void CopyJobPrivate::statNextSrc() 0935 { 0936 /* Revert to the global destination, the one that applies to all source urls. 0937 * Imagine you copy the items a b and c into /d, but /d/b exists so the user uses "Rename" to put it in /foo/b instead. 0938 * d->m_dest is /foo/b for b, but we have to revert to /d for item c and following. 0939 */ 0940 m_dest = m_globalDest; 0941 qCDebug(KIO_COPYJOB_DEBUG) << "Setting m_dest to" << m_dest; 0942 destinationState = m_globalDestinationState; 0943 ++m_currentStatSrc; 0944 statCurrentSrc(); 0945 } 0946 0947 void CopyJobPrivate::statCurrentSrc() 0948 { 0949 Q_Q(CopyJob); 0950 if (m_currentStatSrc != m_srcList.constEnd()) { 0951 m_currentSrcURL = (*m_currentStatSrc); 0952 m_bURLDirty = true; 0953 m_ignoreSourcePermissions = !KProtocolManager::supportsListing(m_currentSrcURL) || m_currentSrcURL.scheme() == QLatin1String("trash"); 0954 0955 if (m_mode == CopyJob::Link) { 0956 // Skip the "stating the source" stage, we don't need it for linking 0957 m_currentDest = m_dest; 0958 struct CopyInfo info; 0959 info.permissions = -1; 0960 info.size = KIO::invalidFilesize; 0961 info.uSource = m_currentSrcURL; 0962 info.uDest = m_currentDest; 0963 // Append filename or dirname to destination URL, if allowed 0964 if (destinationState == DEST_IS_DIR && !m_asMethod) { 0965 if (compareUrls(m_currentSrcURL, info.uDest)) { 0966 // This is the case of creating a real symlink 0967 info.uDest = addPathToUrl(info.uDest, m_currentSrcURL.fileName()); 0968 } else { 0969 // Different protocols, we'll create a .desktop file 0970 // We have to change the extension anyway, so while we're at it, 0971 // name the file like the URL 0972 QByteArray encodedFilename = QFile::encodeName(m_currentSrcURL.toDisplayString()); 0973 const int truncatePos = NAME_MAX - (info.uDest.toDisplayString().length() + 8); // length(.desktop) = 8 0974 if (truncatePos > 0) { 0975 encodedFilename.truncate(truncatePos); 0976 } 0977 const QString decodedFilename = QFile::decodeName(encodedFilename); 0978 info.uDest = addPathToUrl(info.uDest, KIO::encodeFileName(decodedFilename) + QLatin1String(".desktop")); 0979 } 0980 } 0981 files.append(info); // Files and any symlinks 0982 statNextSrc(); // we could use a loop instead of a recursive call :) 0983 return; 0984 } 0985 0986 // Let's see if we can skip stat'ing, for the case where a directory view has the info already 0987 KIO::UDSEntry entry; 0988 const KFileItem cachedItem = KCoreDirLister::cachedItemForUrl(m_currentSrcURL); 0989 if (!cachedItem.isNull()) { 0990 entry = cachedItem.entry(); 0991 if (destinationState != DEST_DOESNT_EXIST 0992 && m_currentSrcURL.scheme() != QStringLiteral("trash")) { // only resolve src if we could resolve dest (#218719) 0993 0994 m_currentSrcURL = cachedItem.mostLocalUrl(); // #183585 0995 } 0996 } 0997 0998 // Don't go renaming right away if we need a stat() to find out the destination filename 0999 const bool needStat = 1000 KProtocolManager::fileNameUsedForCopying(m_currentSrcURL) == KProtocolInfo::FromUrl || destinationState != DEST_IS_DIR || m_asMethod; 1001 if (m_mode == CopyJob::Move && needStat) { 1002 // If moving, before going for the full stat+[list+]copy+del thing, try to rename 1003 // The logic is pretty similar to FileCopyJobPrivate::slotStart() 1004 if (compareUrls(m_currentSrcURL, m_dest)) { 1005 startRenameJob(m_currentSrcURL); 1006 return; 1007 } else if (m_currentSrcURL.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) { 1008 startRenameJob(m_dest); 1009 return; 1010 } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_currentSrcURL)) { 1011 startRenameJob(m_currentSrcURL); 1012 return; 1013 } 1014 } 1015 1016 // if the source file system doesn't support deleting, we do not even stat 1017 if (m_mode == CopyJob::Move && !KProtocolManager::supportsDeleting(m_currentSrcURL)) { 1018 QPointer<CopyJob> that = q; 1019 Q_EMIT q->warning(q, buildErrorString(ERR_CANNOT_DELETE, m_currentSrcURL.toDisplayString())); 1020 if (that) { 1021 statNextSrc(); // we could use a loop instead of a recursive call :) 1022 } 1023 return; 1024 } 1025 1026 m_bOnlyRenames = false; 1027 1028 // Testing for entry.count()>0 here is not good enough; KFileItem inserts 1029 // entries for UDS_USER and UDS_GROUP even on initially empty UDSEntries (#192185) 1030 if (entry.contains(KIO::UDSEntry::UDS_NAME)) { 1031 qCDebug(KIO_COPYJOB_DEBUG) << "fast path! found info about" << m_currentSrcURL << "in KCoreDirLister"; 1032 // sourceStated(entry, m_currentSrcURL); // don't recurse, see #319747, use queued invokeMethod instead 1033 auto srcStatedFunc = [this, entry]() { 1034 sourceStated(entry, m_currentSrcURL); 1035 }; 1036 QMetaObject::invokeMethod(q, srcStatedFunc, Qt::QueuedConnection); 1037 return; 1038 } 1039 1040 // Stat the next src url 1041 Job *job = KIO::statDetails(m_currentSrcURL, StatJob::SourceSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 1042 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; 1043 state = STATE_STATING; 1044 q->addSubjob(job); 1045 m_currentDestURL = m_dest; 1046 m_bURLDirty = true; 1047 } else { 1048 // Finished the stat'ing phase 1049 // First make sure that the totals were correctly emitted 1050 m_bURLDirty = true; 1051 slotReport(); 1052 1053 qCDebug(KIO_COPYJOB_DEBUG) << "Stating finished. To copy:" << m_totalSize << ", available:" << m_freeSpace; 1054 1055 if (m_totalSize > m_freeSpace && m_freeSpace != static_cast<KIO::filesize_t>(-1)) { 1056 q->setError(ERR_DISK_FULL); 1057 q->setErrorText(m_currentSrcURL.toDisplayString()); 1058 q->emitResult(); 1059 return; 1060 } 1061 1062 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2) 1063 if (!dirs.isEmpty()) { 1064 Q_EMIT q->aboutToCreate(q, dirs); 1065 } 1066 if (!files.isEmpty()) { 1067 Q_EMIT q->aboutToCreate(q, files); 1068 } 1069 #endif 1070 // Check if we are copying a single file 1071 m_bSingleFileCopy = (files.count() == 1 && dirs.isEmpty()); 1072 // Then start copying things 1073 state = STATE_CREATING_DIRS; 1074 createNextDir(); 1075 } 1076 } 1077 1078 void CopyJobPrivate::startRenameJob(const QUrl &workerUrl) 1079 { 1080 Q_Q(CopyJob); 1081 1082 // Silence KDirWatch notifications, otherwise performance is horrible 1083 if (m_currentSrcURL.isLocalFile()) { 1084 const QString parentDir = m_currentSrcURL.adjusted(QUrl::RemoveFilename).path(); 1085 const auto [it, isInserted] = m_parentDirs.insert(parentDir); 1086 if (isInserted) { 1087 KDirWatch::self()->stopDirScan(parentDir); 1088 } 1089 } 1090 1091 QUrl dest = m_dest; 1092 // Append filename or dirname to destination URL, if allowed 1093 if (destinationState == DEST_IS_DIR && !m_asMethod) { 1094 dest = addPathToUrl(dest, m_currentSrcURL.fileName()); 1095 } 1096 m_currentDestURL = dest; 1097 qCDebug(KIO_COPYJOB_DEBUG) << m_currentSrcURL << "->" << dest << "trying direct rename first"; 1098 if (state != STATE_RENAMING) { 1099 q->setTotalAmount(KJob::Files, m_srcList.count()); 1100 } 1101 state = STATE_RENAMING; 1102 1103 struct CopyInfo info; 1104 info.permissions = -1; 1105 info.size = KIO::invalidFilesize; 1106 info.uSource = m_currentSrcURL; 1107 info.uDest = dest; 1108 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2) 1109 QList<CopyInfo> files; 1110 files.append(info); 1111 Q_EMIT q->aboutToCreate(q, files); 1112 #endif 1113 1114 KIO_ARGS << m_currentSrcURL << dest << (qint8) false /*no overwrite*/; 1115 SimpleJob *newJob = SimpleJobPrivate::newJobNoUi(workerUrl, CMD_RENAME, packedArgs); 1116 newJob->setParentJob(q); 1117 q->addSubjob(newJob); 1118 if (m_currentSrcURL.adjusted(QUrl::RemoveFilename) != dest.adjusted(QUrl::RemoveFilename)) { // For the user, moving isn't renaming. Only renaming is. 1119 m_bOnlyRenames = false; 1120 } 1121 } 1122 1123 void CopyJobPrivate::startListing(const QUrl &src) 1124 { 1125 Q_Q(CopyJob); 1126 state = STATE_LISTING; 1127 m_bURLDirty = true; 1128 ListJob *newjob = listRecursive(src, KIO::HideProgressInfo); 1129 newjob->setUnrestricted(true); 1130 q->connect(newjob, &ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) { 1131 slotEntries(job, list); 1132 }); 1133 q->connect(newjob, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *subJob) { 1134 slotSubError(job, subJob); 1135 }); 1136 q->addSubjob(newjob); 1137 } 1138 1139 void CopyJobPrivate::skip(const QUrl &sourceUrl, bool isDir) 1140 { 1141 QUrl dir(sourceUrl); 1142 if (!isDir) { 1143 // Skipping a file: make sure not to delete the parent dir (#208418) 1144 dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1145 } 1146 while (dirsToRemove.removeAll(dir) > 0) { 1147 // Do not rely on rmdir() on the parent directories aborting. 1148 // Exclude the parent dirs explicitly. 1149 dir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1150 } 1151 } 1152 1153 bool CopyJobPrivate::shouldOverwriteDir(const QString &path) const 1154 { 1155 if (m_bOverwriteAllDirs) { 1156 return true; 1157 } 1158 return m_overwriteList.contains(path); 1159 } 1160 1161 bool CopyJobPrivate::shouldOverwriteFile(const QString &path) const 1162 { 1163 if (m_bOverwriteAllFiles) { 1164 return true; 1165 } 1166 return m_overwriteList.contains(path); 1167 } 1168 1169 bool CopyJobPrivate::shouldSkip(const QString &path) const 1170 { 1171 for (const QString &skipPath : std::as_const(m_skipList)) { 1172 if (path.startsWith(skipPath)) { 1173 return true; 1174 } 1175 } 1176 return false; 1177 } 1178 1179 void CopyJobPrivate::renameDirectory(const QList<CopyInfo>::iterator &it, const QUrl &newUrl) 1180 { 1181 Q_Q(CopyJob); 1182 Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. KPropertiesDialog 1183 1184 const QString oldPath = Utils::slashAppended((*it).uDest.path()); 1185 1186 // Change the current one and strip the trailing '/' 1187 (*it).uDest = newUrl.adjusted(QUrl::StripTrailingSlash); 1188 1189 const QString newPath = Utils::slashAppended(newUrl.path()); // With trailing slash 1190 1191 QList<CopyInfo>::Iterator renamedirit = it; 1192 ++renamedirit; 1193 // Change the name of subdirectories inside the directory 1194 for (; renamedirit != dirs.end(); ++renamedirit) { 1195 QString path = (*renamedirit).uDest.path(); 1196 if (path.startsWith(oldPath)) { 1197 QString n = path; 1198 n.replace(0, oldPath.length(), newPath); 1199 /*qDebug() << "dirs list:" << (*renamedirit).uSource.path() 1200 << "was going to be" << path 1201 << ", changed into" << n;*/ 1202 (*renamedirit).uDest.setPath(n, QUrl::DecodedMode); 1203 } 1204 } 1205 // Change filenames inside the directory 1206 QList<CopyInfo>::Iterator renamefileit = files.begin(); 1207 for (; renamefileit != files.end(); ++renamefileit) { 1208 QString path = (*renamefileit).uDest.path(QUrl::FullyDecoded); 1209 if (path.startsWith(oldPath)) { 1210 QString n = path; 1211 n.replace(0, oldPath.length(), newPath); 1212 /*qDebug() << "files list:" << (*renamefileit).uSource.path() 1213 << "was going to be" << path 1214 << ", changed into" << n;*/ 1215 (*renamefileit).uDest.setPath(n, QUrl::DecodedMode); 1216 } 1217 } 1218 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2) 1219 if (!dirs.isEmpty()) { 1220 Q_EMIT q->aboutToCreate(q, dirs); 1221 } 1222 if (!files.isEmpty()) { 1223 Q_EMIT q->aboutToCreate(q, files); 1224 } 1225 #endif 1226 } 1227 1228 void CopyJobPrivate::slotResultCreatingDirs(KJob *job) 1229 { 1230 Q_Q(CopyJob); 1231 // The dir we are trying to create: 1232 QList<CopyInfo>::Iterator it = dirs.begin(); 1233 // Was there an error creating a dir ? 1234 if (job->error()) { 1235 m_conflictError = job->error(); 1236 if (m_conflictError == ERR_DIR_ALREADY_EXIST // 1237 || m_conflictError == ERR_FILE_ALREADY_EXIST) { // can't happen? 1238 QUrl oldURL = ((SimpleJob *)job)->url(); 1239 // Should we skip automatically ? 1240 if (m_bAutoSkipDirs) { 1241 // We don't want to copy files in this directory, so we put it on the skip list 1242 const QString path = Utils::slashAppended(oldURL.path()); 1243 m_skipList.append(path); 1244 skip(oldURL, true); 1245 dirs.erase(it); // Move on to next dir 1246 } else { 1247 // Did the user choose to overwrite already? 1248 const QString destDir = (*it).uDest.path(); 1249 if (shouldOverwriteDir(destDir)) { // overwrite => just skip 1250 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1251 dirs.erase(it); // Move on to next dir 1252 ++m_processedDirs; 1253 } else { 1254 if (m_bAutoRenameDirs) { 1255 const QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1256 const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); 1257 QUrl newUrl(destDirectory); 1258 newUrl.setPath(Utils::concatPaths(newUrl.path(), newName)); 1259 renameDirectory(it, newUrl); 1260 } else { 1261 if (!KIO::delegateExtension<AskUserActionInterface *>(q)) { 1262 q->Job::slotResult(job); // will set the error and emit result(this) 1263 return; 1264 } 1265 1266 Q_ASSERT(((SimpleJob *)job)->url() == (*it).uDest); 1267 q->removeSubjob(job); 1268 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1269 1270 // We need to stat the existing dir, to get its last-modification time 1271 QUrl existingDest((*it).uDest); 1272 SimpleJob *newJob = KIO::statDetails(existingDest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 1273 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingDest; 1274 state = STATE_CONFLICT_CREATING_DIRS; 1275 q->addSubjob(newJob); 1276 return; // Don't move to next dir yet ! 1277 } 1278 } 1279 } 1280 } else { 1281 // Severe error, abort 1282 q->Job::slotResult(job); // will set the error and emit result(this) 1283 return; 1284 } 1285 } else { // no error : remove from list, to move on to next dir 1286 // this is required for the undo feature 1287 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false); 1288 m_directoriesCopied.push_back(*it); 1289 dirs.erase(it); 1290 ++m_processedDirs; 1291 } 1292 1293 q->removeSubjob(job); 1294 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1295 createNextDir(); 1296 } 1297 1298 void CopyJobPrivate::slotResultConflictCreatingDirs(KJob *job) 1299 { 1300 Q_Q(CopyJob); 1301 // We come here after a conflict has been detected and we've stated the existing dir 1302 1303 // The dir we were trying to create: 1304 QList<CopyInfo>::Iterator it = dirs.begin(); 1305 1306 const UDSEntry entry = ((KIO::StatJob *)job)->statResult(); 1307 1308 QDateTime destmtime; 1309 QDateTime destctime; 1310 const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); 1311 const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 1312 1313 q->removeSubjob(job); 1314 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1315 1316 // Always multi and skip (since there are files after that) 1317 RenameDialog_Options options(RenameDialog_MultipleItems | RenameDialog_Skip | RenameDialog_DestIsDirectory); 1318 // Overwrite only if the existing thing is a dir (no chance with a file) 1319 if (m_conflictError == ERR_DIR_ALREADY_EXIST) { 1320 // We are in slotResultConflictCreatingDirs(), so the source is a dir 1321 options |= RenameDialog_SourceIsDirectory; 1322 1323 if ((*it).uSource == (*it).uDest 1324 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { 1325 options |= RenameDialog_OverwriteItself; 1326 } else { 1327 options |= RenameDialog_Overwrite; 1328 destmtime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1), Qt::UTC); 1329 destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); 1330 } 1331 } 1332 1333 if (m_reportTimer) { 1334 m_reportTimer->stop(); 1335 } 1336 1337 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 1338 1339 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 1340 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 1341 Q_ASSERT(parentJob == q); 1342 // Only receive askUserRenameResult once per rename dialog 1343 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 1344 1345 if (m_reportTimer) { 1346 m_reportTimer->start(s_reportTimeout); 1347 } 1348 1349 const QString existingDest = (*it).uDest.path(); 1350 1351 switch (result) { 1352 case Result_Cancel: 1353 q->setError(ERR_USER_CANCELED); 1354 q->emitResult(); 1355 return; 1356 case Result_AutoRename: 1357 m_bAutoRenameDirs = true; 1358 // fall through 1359 Q_FALLTHROUGH(); 1360 case Result_Rename: 1361 renameDirectory(it, newUrl); 1362 break; 1363 case Result_AutoSkip: 1364 m_bAutoSkipDirs = true; 1365 // fall through 1366 Q_FALLTHROUGH(); 1367 case Result_Skip: 1368 m_skipList.append(Utils::slashAppended(existingDest)); 1369 skip((*it).uSource, true); 1370 // Move on to next dir 1371 dirs.erase(it); 1372 ++m_processedDirs; 1373 break; 1374 case Result_Overwrite: 1375 m_overwriteList.insert(existingDest); 1376 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1377 // Move on to next dir 1378 dirs.erase(it); 1379 ++m_processedDirs; 1380 break; 1381 case Result_OverwriteAll: 1382 m_bOverwriteAllDirs = true; 1383 Q_EMIT q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true /* directory */, false /* renamed */); 1384 // Move on to next dir 1385 dirs.erase(it); 1386 ++m_processedDirs; 1387 break; 1388 default: 1389 Q_ASSERT(0); 1390 } 1391 state = STATE_CREATING_DIRS; 1392 createNextDir(); 1393 }); 1394 1395 /* clang-format off */ 1396 askUserActionInterface->askUserRename(q, i18n("Folder Already Exists"), 1397 (*it).uSource, (*it).uDest, 1398 options, 1399 (*it).size, destsize, 1400 (*it).ctime, destctime, 1401 (*it).mtime, destmtime); 1402 /* clang-format on */ 1403 } 1404 1405 void CopyJobPrivate::createNextDir() 1406 { 1407 Q_Q(CopyJob); 1408 1409 // Take first dir to create out of list 1410 QList<CopyInfo>::Iterator it = dirs.begin(); 1411 // Is this URL on the skip list or the overwrite list ? 1412 while (it != dirs.end()) { 1413 const QString dir = it->uDest.path(); 1414 if (shouldSkip(dir)) { 1415 it = dirs.erase(it); 1416 } else { 1417 break; 1418 } 1419 } 1420 1421 if (it != dirs.end()) { // any dir to create, finally ? 1422 if (it->uDest.isLocalFile()) { 1423 // uDest doesn't exist yet, check the filesystem of the parent dir 1424 const auto destFileSystem = KFileSystemType::fileSystemType(it->uDest.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile()); 1425 if (isFatOrNtfs(destFileSystem)) { 1426 const QString dirName = it->uDest.adjusted(QUrl::StripTrailingSlash).fileName(); 1427 if (hasInvalidChars(dirName)) { 1428 // We already asked the user? 1429 if (m_autoReplaceInvalidChars) { 1430 processCreateNextDir(it, KIO::Result_ReplaceInvalidChars); 1431 return; 1432 } else if (m_autoSkipDirsWithInvalidChars) { 1433 processCreateNextDir(it, KIO::Result_Skip); 1434 return; 1435 } 1436 1437 const QString msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), 1438 KFileSystemType::fileSystemName(destFileSystem), 1439 true /* isDir */); 1440 1441 if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) { 1442 SkipDialog_Options options = KIO::SkipDialog_Replace_Invalid_Chars; 1443 if (dirs.size() > 1) { 1444 options |= SkipDialog_MultipleItems; 1445 } 1446 1447 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1448 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1449 Q_ASSERT(parentJob == q); 1450 1451 // Only receive askUserSkipResult once per skip dialog 1452 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1453 1454 processCreateNextDir(it, result); 1455 }); 1456 1457 askUserActionInterface->askUserSkip(q, options, msg); 1458 1459 return; 1460 } else { // No Job Ui delegate 1461 qCWarning(KIO_COPYJOB_DEBUG) << msg; 1462 q->emitResult(); 1463 return; 1464 } 1465 } 1466 } 1467 } 1468 1469 processCreateNextDir(it, -1); 1470 } else { // we have finished creating dirs 1471 q->setProcessedAmount(KJob::Directories, m_processedDirs); // make sure final number appears 1472 1473 if (m_mode == CopyJob::Move) { 1474 // Now we know which dirs hold the files we're going to delete. 1475 // To speed things up and prevent double-notification, we disable KDirWatch 1476 // on those dirs temporarily (using KDirWatch::self, that's the instance 1477 // used by e.g. kdirlister). 1478 for (const auto &dir : m_parentDirs) { 1479 KDirWatch::self()->stopDirScan(dir); 1480 } 1481 } 1482 1483 state = STATE_COPYING_FILES; 1484 ++m_processedFiles; // Ralf wants it to start at 1, not 0 1485 copyNextFile(); 1486 } 1487 } 1488 1489 void CopyJobPrivate::processCreateNextDir(const QList<CopyInfo>::Iterator &it, int result) 1490 { 1491 Q_Q(CopyJob); 1492 1493 switch (result) { 1494 case Result_Cancel: 1495 q->setError(ERR_USER_CANCELED); 1496 q->emitResult(); 1497 return; 1498 case KIO::Result_ReplaceAllInvalidChars: 1499 m_autoReplaceInvalidChars = true; 1500 Q_FALLTHROUGH(); 1501 case KIO::Result_ReplaceInvalidChars: { 1502 it->uDest = it->uDest.adjusted(QUrl::StripTrailingSlash); 1503 QString dirName = it->uDest.fileName(); 1504 const int len = dirName.size(); 1505 cleanMsdosDestName(dirName); 1506 QString path = it->uDest.path(); 1507 path.replace(path.size() - len, len, dirName); 1508 it->uDest.setPath(path); 1509 break; 1510 } 1511 case KIO::Result_AutoSkip: 1512 m_autoSkipDirsWithInvalidChars = true; 1513 Q_FALLTHROUGH(); 1514 case KIO::Result_Skip: 1515 m_skipList.append(Utils::slashAppended(it->uDest.path())); 1516 skip(it->uSource, true); 1517 dirs.erase(it); // Move on to next dir 1518 ++m_processedDirs; 1519 createNextDir(); 1520 return; 1521 default: 1522 break; 1523 } 1524 1525 // Create the directory - with default permissions so that we can put files into it 1526 // TODO : change permissions once all is finished; but for stuff coming from CDROM it sucks... 1527 KIO::SimpleJob *newjob = KIO::mkdir(it->uDest, -1); 1528 newjob->setParentJob(q); 1529 if (shouldOverwriteFile(it->uDest.path())) { // if we are overwriting an existing file or symlink 1530 newjob->addMetaData(QStringLiteral("overwrite"), QStringLiteral("true")); 1531 } 1532 1533 m_currentDestURL = it->uDest; 1534 m_bURLDirty = true; 1535 1536 q->addSubjob(newjob); 1537 } 1538 1539 void CopyJobPrivate::slotResultCopyingFiles(KJob *job) 1540 { 1541 Q_Q(CopyJob); 1542 // The file we were trying to copy: 1543 QList<CopyInfo>::Iterator it = files.begin(); 1544 if (job->error()) { 1545 // Should we skip automatically ? 1546 if (m_bAutoSkipFiles) { 1547 skip((*it).uSource, false); 1548 m_fileProcessedSize = (*it).size; 1549 files.erase(it); // Move on to next file 1550 } else { 1551 m_conflictError = job->error(); // save for later 1552 // Existing dest ? 1553 if (m_conflictError == ERR_FILE_ALREADY_EXIST // 1554 || m_conflictError == ERR_DIR_ALREADY_EXIST // 1555 || m_conflictError == ERR_IDENTICAL_FILES) { 1556 if (m_bAutoRenameFiles) { 1557 QUrl destDirectory = (*it).uDest.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1558 const QString newName = KFileUtils::suggestName(destDirectory, (*it).uDest.fileName()); 1559 QUrl newDest(destDirectory); 1560 newDest.setPath(Utils::concatPaths(newDest.path(), newName)); 1561 Q_EMIT q->renamed(q, (*it).uDest, newDest); // for e.g. kpropsdlg 1562 (*it).uDest = newDest; 1563 1564 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2) 1565 QList<CopyInfo> files; 1566 files.append(*it); 1567 Q_EMIT q->aboutToCreate(q, files); 1568 #endif 1569 } else { 1570 if (!KIO::delegateExtension<AskUserActionInterface *>(q)) { 1571 q->Job::slotResult(job); // will set the error and emit result(this) 1572 return; 1573 } 1574 1575 q->removeSubjob(job); 1576 Q_ASSERT(!q->hasSubjobs()); 1577 // We need to stat the existing file, to get its last-modification time 1578 QUrl existingFile((*it).uDest); 1579 SimpleJob *newJob = 1580 KIO::statDetails(existingFile, StatJob::DestinationSide, KIO::StatDetail::StatBasic | KIO::StatDetail::StatTime, KIO::HideProgressInfo); 1581 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat for resolving conflict on" << existingFile; 1582 state = STATE_CONFLICT_COPYING_FILES; 1583 q->addSubjob(newJob); 1584 return; // Don't move to next file yet ! 1585 } 1586 } else { 1587 if (m_bCurrentOperationIsLink && qobject_cast<KIO::DeleteJob *>(job)) { 1588 // Very special case, see a few lines below 1589 // We are deleting the source of a symlink we successfully moved... ignore error 1590 m_fileProcessedSize = (*it).size; 1591 ++m_processedFiles; 1592 files.erase(it); 1593 } else { 1594 if (!KIO::delegateExtension<AskUserActionInterface *>(q)) { 1595 q->Job::slotResult(job); // will set the error and emit result(this) 1596 return; 1597 } 1598 1599 // Go directly to the conflict resolution, there is nothing to stat 1600 slotResultErrorCopyingFiles(job); 1601 return; 1602 } 1603 } 1604 } 1605 } else { // no error 1606 // Special case for moving links. That operation needs two jobs, unlike others. 1607 if (m_bCurrentOperationIsLink // 1608 && m_mode == CopyJob::Move // 1609 && !qobject_cast<KIO::DeleteJob *>(job) // Deleting source not already done 1610 ) { 1611 q->removeSubjob(job); 1612 Q_ASSERT(!q->hasSubjobs()); 1613 // The only problem with this trick is that the error handling for this del operation 1614 // is not going to be right... see 'Very special case' above. 1615 KIO::Job *newjob = KIO::del((*it).uSource, HideProgressInfo); 1616 newjob->setParentJob(q); 1617 q->addSubjob(newjob); 1618 return; // Don't move to next file yet ! 1619 } 1620 1621 const QUrl finalUrl = finalDestUrl((*it).uSource, (*it).uDest); 1622 1623 if (m_bCurrentOperationIsLink) { 1624 QString target = (m_mode == CopyJob::Link ? (*it).uSource.path() : (*it).linkDest); 1625 // required for the undo feature 1626 Q_EMIT q->copyingLinkDone(q, (*it).uSource, target, finalUrl); 1627 } else { 1628 // required for the undo feature 1629 Q_EMIT q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false); 1630 if (m_mode == CopyJob::Move) { 1631 #ifndef KIO_ANDROID_STUB 1632 org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl); 1633 #endif 1634 } 1635 m_successSrcList.append((*it).uSource); 1636 if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) { 1637 m_freeSpace -= (*it).size; 1638 } 1639 } 1640 // remove from list, to move on to next file 1641 files.erase(it); 1642 ++m_processedFiles; 1643 } 1644 1645 // clear processed size for last file and add it to overall processed size 1646 m_processedSize += m_fileProcessedSize; 1647 m_fileProcessedSize = 0; 1648 1649 qCDebug(KIO_COPYJOB_DEBUG) << files.count() << "files remaining"; 1650 1651 // Merge metadata from subjob 1652 KIO::Job *kiojob = qobject_cast<KIO::Job *>(job); 1653 Q_ASSERT(kiojob); 1654 m_incomingMetaData += kiojob->metaData(); 1655 q->removeSubjob(job); 1656 Q_ASSERT(!q->hasSubjobs()); // We should have only one job at a time ... 1657 copyNextFile(); 1658 } 1659 1660 void CopyJobPrivate::slotResultErrorCopyingFiles(KJob *job) 1661 { 1662 Q_Q(CopyJob); 1663 // We come here after a conflict has been detected and we've stated the existing file 1664 // The file we were trying to create: 1665 QList<CopyInfo>::Iterator it = files.begin(); 1666 1667 RenameDialog_Result res = Result_Cancel; 1668 1669 if (m_reportTimer) { 1670 m_reportTimer->stop(); 1671 } 1672 1673 q->removeSubjob(job); 1674 Q_ASSERT(!q->hasSubjobs()); 1675 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 1676 1677 if (m_conflictError == ERR_FILE_ALREADY_EXIST // 1678 || m_conflictError == ERR_DIR_ALREADY_EXIST // 1679 || m_conflictError == ERR_IDENTICAL_FILES) { 1680 // Its modification time: 1681 const UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult(); 1682 1683 QDateTime destmtime; 1684 QDateTime destctime; 1685 const KIO::filesize_t destsize = entry.numberValue(KIO::UDSEntry::UDS_SIZE); 1686 const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); 1687 1688 // Offer overwrite only if the existing thing is a file 1689 // If src==dest, use "overwrite-itself" 1690 RenameDialog_Options options; 1691 bool isDir = true; 1692 1693 if (m_conflictError == ERR_DIR_ALREADY_EXIST) { 1694 options = RenameDialog_DestIsDirectory; 1695 } else { 1696 if ((*it).uSource == (*it).uDest 1697 || ((*it).uSource.scheme() == (*it).uDest.scheme() && (*it).uSource.adjusted(QUrl::StripTrailingSlash).path() == linkDest)) { 1698 options = RenameDialog_OverwriteItself; 1699 } else { 1700 const qint64 destMTimeStamp = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 1701 if (m_bOverwriteWhenOlder && (*it).mtime.isValid() && destMTimeStamp != -1) { 1702 if ((*it).mtime.currentSecsSinceEpoch() > destMTimeStamp) { 1703 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest; 1704 res = Result_Overwrite; 1705 } else { 1706 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest; 1707 res = Result_Skip; 1708 } 1709 } else { 1710 // These timestamps are used only when RenameDialog_Overwrite is set. 1711 destmtime = QDateTime::fromSecsSinceEpoch(destMTimeStamp, Qt::UTC); 1712 destctime = QDateTime::fromSecsSinceEpoch(entry.numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1), Qt::UTC); 1713 1714 options = RenameDialog_Overwrite; 1715 } 1716 } 1717 isDir = false; 1718 } 1719 1720 // if no preset value was set 1721 if (res == Result_Cancel) { 1722 if (!m_bSingleFileCopy) { 1723 options = RenameDialog_Options(options | RenameDialog_MultipleItems | RenameDialog_Skip); 1724 } 1725 1726 const QString title = !isDir ? i18n("File Already Exists") : i18n("Already Exists as Folder"); 1727 1728 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 1729 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 1730 Q_ASSERT(parentJob == q); 1731 // Only receive askUserRenameResult once per rename dialog 1732 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 1733 processFileRenameDialogResult(it, result, newUrl, destmtime); 1734 }); 1735 1736 /* clang-format off */ 1737 askUserActionInterface->askUserRename(q, title, 1738 (*it).uSource, (*it).uDest, 1739 options, 1740 (*it).size, destsize, 1741 (*it).ctime, destctime, 1742 (*it).mtime, destmtime); /* clang-format on */ 1743 return; 1744 } 1745 } else { 1746 if (job->error() == ERR_USER_CANCELED) { 1747 res = Result_Cancel; 1748 } else if (!askUserActionInterface) { 1749 q->Job::slotResult(job); // will set the error and emit result(this) 1750 return; 1751 } else { 1752 SkipDialog_Options options; 1753 if (files.count() > 1) { 1754 options |= SkipDialog_MultipleItems; 1755 } 1756 1757 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1758 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1759 Q_ASSERT(parentJob == q); 1760 // Only receive askUserSkipResult once per skip dialog 1761 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1762 processFileRenameDialogResult(it, result, QUrl() /* no new url in skip */, QDateTime{}); 1763 }); 1764 1765 askUserActionInterface->askUserSkip(q, options, job->errorString()); 1766 return; 1767 } 1768 } 1769 1770 processFileRenameDialogResult(it, res, QUrl{}, QDateTime{}); 1771 } 1772 1773 void CopyJobPrivate::processFileRenameDialogResult(const QList<CopyInfo>::Iterator &it, 1774 RenameDialog_Result result, 1775 const QUrl &newUrl, 1776 const QDateTime &destmtime) 1777 { 1778 Q_Q(CopyJob); 1779 1780 if (m_reportTimer) { 1781 m_reportTimer->start(s_reportTimeout); 1782 } 1783 1784 if (result == Result_OverwriteWhenOlder) { 1785 m_bOverwriteWhenOlder = true; 1786 if ((*it).mtime > destmtime) { 1787 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << (*it).uDest; 1788 result = Result_Overwrite; 1789 } else { 1790 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << (*it).uDest; 1791 result = Result_Skip; 1792 } 1793 } 1794 1795 switch (result) { 1796 case Result_Cancel: 1797 q->setError(ERR_USER_CANCELED); 1798 q->emitResult(); 1799 return; 1800 case Result_AutoRename: 1801 m_bAutoRenameFiles = true; 1802 // fall through 1803 Q_FALLTHROUGH(); 1804 case Result_Rename: { 1805 Q_EMIT q->renamed(q, (*it).uDest, newUrl); // for e.g. kpropsdlg 1806 (*it).uDest = newUrl; 1807 m_bURLDirty = true; 1808 1809 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 2) 1810 QList<CopyInfo> files; 1811 files.append(*it); 1812 Q_EMIT q->aboutToCreate(q, files); 1813 #endif 1814 break; 1815 } 1816 case Result_AutoSkip: 1817 m_bAutoSkipFiles = true; 1818 // fall through 1819 Q_FALLTHROUGH(); 1820 case Result_Skip: 1821 // Move on to next file 1822 skip((*it).uSource, false); 1823 m_processedSize += (*it).size; 1824 files.erase(it); 1825 break; 1826 case Result_OverwriteAll: 1827 m_bOverwriteAllFiles = true; 1828 break; 1829 case Result_Overwrite: 1830 // Add to overwrite list, so that copyNextFile knows to overwrite 1831 m_overwriteList.insert((*it).uDest.path()); 1832 break; 1833 case Result_Retry: 1834 // Do nothing, copy file again 1835 break; 1836 default: 1837 Q_ASSERT(0); 1838 } 1839 state = STATE_COPYING_FILES; 1840 copyNextFile(); 1841 } 1842 1843 KIO::Job *CopyJobPrivate::linkNextFile(const QUrl &uSource, const QUrl &uDest, JobFlags flags) 1844 { 1845 qCDebug(KIO_COPYJOB_DEBUG) << "Linking"; 1846 if (compareUrls(uSource, uDest)) { 1847 // This is the case of creating a real symlink 1848 KIO::SimpleJob *newJob = KIO::symlink(uSource.path(), uDest, flags | HideProgressInfo /*no GUI*/); 1849 newJob->setParentJob(q_func()); 1850 qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << uSource.path() << "link=" << uDest; 1851 // emit linking( this, uSource.path(), uDest ); 1852 m_bCurrentOperationIsLink = true; 1853 m_currentSrcURL = uSource; 1854 m_currentDestURL = uDest; 1855 m_bURLDirty = true; 1856 // Observer::self()->slotCopying( this, uSource, uDest ); // should be slotLinking perhaps 1857 return newJob; 1858 } else { 1859 Q_Q(CopyJob); 1860 qCDebug(KIO_COPYJOB_DEBUG) << "Linking URL=" << uSource << "link=" << uDest; 1861 if (uDest.isLocalFile()) { 1862 // if the source is a devices url, handle it a littlebit special 1863 1864 QString path = uDest.toLocalFile(); 1865 qCDebug(KIO_COPYJOB_DEBUG) << "path=" << path; 1866 QFile f(path); 1867 if (f.open(QIODevice::ReadWrite)) { 1868 f.close(); 1869 KDesktopFile desktopFile(path); 1870 KConfigGroup config = desktopFile.desktopGroup(); 1871 QUrl url = uSource; 1872 url.setPassword(QString()); 1873 config.writePathEntry("URL", url.toString()); 1874 config.writeEntry("Name", url.toString()); 1875 config.writeEntry("Type", QStringLiteral("Link")); 1876 QString protocol = uSource.scheme(); 1877 if (protocol == QLatin1String("ftp")) { 1878 config.writeEntry("Icon", QStringLiteral("folder-remote")); 1879 } else if (protocol == QLatin1String("http") || protocol == QLatin1String("https")) { 1880 config.writeEntry("Icon", QStringLiteral("text-html")); 1881 } else if (protocol == QLatin1String("info")) { 1882 config.writeEntry("Icon", QStringLiteral("text-x-texinfo")); 1883 } else if (protocol == QLatin1String("mailto")) { // sven: 1884 config.writeEntry("Icon", QStringLiteral("internet-mail")); // added mailto: support 1885 } else if (protocol == QLatin1String("trash") && url.path().length() <= 1) { // trash:/ link 1886 config.writeEntry("Name", i18n("Trash")); 1887 config.writeEntry("Icon", QStringLiteral("user-trash-full")); 1888 config.writeEntry("EmptyIcon", QStringLiteral("user-trash")); 1889 } else { 1890 config.writeEntry("Icon", QStringLiteral("unknown")); 1891 } 1892 config.sync(); 1893 files.erase(files.begin()); // done with this one, move on 1894 ++m_processedFiles; 1895 copyNextFile(); 1896 return nullptr; 1897 } else { 1898 qCDebug(KIO_COPYJOB_DEBUG) << "ERR_CANNOT_OPEN_FOR_WRITING"; 1899 q->setError(ERR_CANNOT_OPEN_FOR_WRITING); 1900 q->setErrorText(uDest.toLocalFile()); 1901 q->emitResult(); 1902 return nullptr; 1903 } 1904 } else { 1905 // Todo: not show "link" on remote dirs if the src urls are not from the same protocol+host+... 1906 q->setError(ERR_CANNOT_SYMLINK); 1907 q->setErrorText(uDest.toDisplayString()); 1908 q->emitResult(); 1909 return nullptr; 1910 } 1911 } 1912 } 1913 1914 bool CopyJobPrivate::handleMsdosFsQuirks(QList<CopyInfo>::Iterator it, KFileSystemType::Type fsType) 1915 { 1916 Q_Q(CopyJob); 1917 1918 QString msg; 1919 SkipDialog_Options options; 1920 SkipType skipType = NoSkipType; 1921 1922 if (isFatFs(fsType) && !it->linkDest.isEmpty()) { // Copying a symlink 1923 skipType = SkipFatSymlinks; 1924 if (m_autoSkipFatSymlinks) { // Have we already asked the user? 1925 processCopyNextFile(it, KIO::Result_Skip, skipType); 1926 return true; 1927 } 1928 options = KIO::SkipDialog_Hide_Retry; 1929 msg = symlinkSupportMsg(it->uDest.toLocalFile(), KFileSystemType::fileSystemName(fsType)); 1930 } else if (hasInvalidChars(it->uDest.fileName())) { 1931 skipType = SkipInvalidChars; 1932 if (m_autoReplaceInvalidChars) { // Have we already asked the user? 1933 processCopyNextFile(it, KIO::Result_ReplaceInvalidChars, skipType); 1934 return true; 1935 } else if (m_autoSkipFilesWithInvalidChars) { // Have we already asked the user? 1936 processCopyNextFile(it, KIO::Result_Skip, skipType); 1937 return true; 1938 } 1939 1940 options = KIO::SkipDialog_Replace_Invalid_Chars; 1941 msg = invalidCharsSupportMsg(it->uDest.toDisplayString(QUrl::PreferLocalFile), KFileSystemType::fileSystemName(fsType)); 1942 } 1943 1944 if (!msg.isEmpty()) { 1945 if (auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q)) { 1946 if (files.size() > 1) { 1947 options |= SkipDialog_MultipleItems; 1948 } 1949 1950 auto skipSignal = &KIO::AskUserActionInterface::askUserSkipResult; 1951 QObject::connect(askUserActionInterface, skipSignal, q, [=](SkipDialog_Result result, KJob *parentJob) { 1952 Q_ASSERT(parentJob == q); 1953 // Only receive askUserSkipResult once per skip dialog 1954 QObject::disconnect(askUserActionInterface, skipSignal, q, nullptr); 1955 1956 processCopyNextFile(it, result, skipType); 1957 }); 1958 1959 askUserActionInterface->askUserSkip(q, options, msg); 1960 1961 return true; 1962 } else { // No Job Ui delegate 1963 qCWarning(KIO_COPYJOB_DEBUG) << msg; 1964 q->emitResult(); 1965 return true; 1966 } 1967 } 1968 1969 return false; // Not handled, move on 1970 } 1971 1972 void CopyJobPrivate::copyNextFile() 1973 { 1974 Q_Q(CopyJob); 1975 bool bCopyFile = false; 1976 qCDebug(KIO_COPYJOB_DEBUG); 1977 1978 bool isDestLocal = m_globalDest.isLocalFile(); 1979 1980 // Take the first file in the list 1981 QList<CopyInfo>::Iterator it = files.begin(); 1982 // Is this URL on the skip list ? 1983 while (it != files.end() && !bCopyFile) { 1984 const QString destFile = (*it).uDest.path(); 1985 bCopyFile = !shouldSkip(destFile); 1986 if (!bCopyFile) { 1987 it = files.erase(it); 1988 } 1989 1990 if (it != files.end() && isDestLocal && (*it).size > 0xFFFFFFFF) { // 4GB-1 1991 const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile()); 1992 if (destFileSystem == KFileSystemType::Fat) { 1993 q->setError(ERR_FILE_TOO_LARGE_FOR_FAT32); 1994 q->setErrorText((*it).uDest.toDisplayString()); 1995 q->emitResult(); 1996 return; 1997 } 1998 } 1999 } 2000 2001 if (bCopyFile) { // any file to create, finally ? 2002 if (isDestLocal) { 2003 const auto destFileSystem = KFileSystemType::fileSystemType(m_globalDest.toLocalFile()); 2004 if (isFatOrNtfs(destFileSystem)) { 2005 if (handleMsdosFsQuirks(it, destFileSystem)) { 2006 return; 2007 } 2008 } 2009 } 2010 2011 processCopyNextFile(it, -1, NoSkipType); 2012 } else { 2013 // We're done 2014 qCDebug(KIO_COPYJOB_DEBUG) << "copyNextFile finished"; 2015 --m_processedFiles; // undo the "start at 1" hack 2016 slotReport(); // display final numbers, important if progress dialog stays up 2017 2018 deleteNextDir(); 2019 } 2020 } 2021 2022 void CopyJobPrivate::processCopyNextFile(const QList<CopyInfo>::Iterator &it, int result, SkipType skipType) 2023 { 2024 Q_Q(CopyJob); 2025 2026 switch (result) { 2027 case Result_Cancel: 2028 q->setError(ERR_USER_CANCELED); 2029 q->emitResult(); 2030 return; 2031 case KIO::Result_ReplaceAllInvalidChars: 2032 m_autoReplaceInvalidChars = true; 2033 Q_FALLTHROUGH(); 2034 case KIO::Result_ReplaceInvalidChars: { 2035 QString fileName = it->uDest.fileName(); 2036 const int len = fileName.size(); 2037 cleanMsdosDestName(fileName); 2038 QString path = it->uDest.path(); 2039 path.replace(path.size() - len, len, fileName); 2040 it->uDest.setPath(path); 2041 break; 2042 } 2043 case KIO::Result_AutoSkip: 2044 if (skipType == SkipInvalidChars) { 2045 m_autoSkipFilesWithInvalidChars = true; 2046 } else if (skipType == SkipFatSymlinks) { 2047 m_autoSkipFatSymlinks = true; 2048 } 2049 Q_FALLTHROUGH(); 2050 case KIO::Result_Skip: 2051 // Move on the next file 2052 files.erase(it); 2053 copyNextFile(); 2054 return; 2055 default: 2056 break; 2057 } 2058 2059 qCDebug(KIO_COPYJOB_DEBUG) << "preparing to copy" << (*it).uSource << (*it).size << m_freeSpace; 2060 if (m_freeSpace != KIO::invalidFilesize && (*it).size != KIO::invalidFilesize) { 2061 if (m_freeSpace < (*it).size) { 2062 q->setError(ERR_DISK_FULL); 2063 q->emitResult(); 2064 return; 2065 } 2066 } 2067 2068 const QUrl &uSource = (*it).uSource; 2069 const QUrl &uDest = (*it).uDest; 2070 // Do we set overwrite ? 2071 bool bOverwrite; 2072 const QString destFile = uDest.path(); 2073 qCDebug(KIO_COPYJOB_DEBUG) << "copying" << destFile; 2074 if (uDest == uSource) { 2075 bOverwrite = false; 2076 } else { 2077 bOverwrite = shouldOverwriteFile(destFile); 2078 } 2079 2080 // If source isn't local and target is local, we ignore the original permissions 2081 // Otherwise, files downloaded from HTTP end up with -r--r--r-- 2082 int permissions = (*it).permissions; 2083 if (m_defaultPermissions || (m_ignoreSourcePermissions && uDest.isLocalFile())) { 2084 permissions = -1; 2085 } 2086 const JobFlags flags = bOverwrite ? Overwrite : DefaultFlags; 2087 2088 m_bCurrentOperationIsLink = false; 2089 KIO::Job *newjob = nullptr; 2090 if (m_mode == CopyJob::Link) { 2091 // User requested that a symlink be made 2092 newjob = linkNextFile(uSource, uDest, flags); 2093 if (!newjob) { 2094 return; 2095 } 2096 } else if (!(*it).linkDest.isEmpty() && compareUrls(uSource, uDest)) 2097 // Copying a symlink - only on the same protocol/host/etc. (#5601, downloading an FTP file through its link), 2098 { 2099 KIO::SimpleJob *newJob = KIO::symlink((*it).linkDest, uDest, flags | HideProgressInfo /*no GUI*/); 2100 newJob->setParentJob(q); 2101 newjob = newJob; 2102 qCDebug(KIO_COPYJOB_DEBUG) << "Linking target=" << (*it).linkDest << "link=" << uDest; 2103 m_currentSrcURL = QUrl::fromUserInput((*it).linkDest); 2104 m_currentDestURL = uDest; 2105 m_bURLDirty = true; 2106 // emit linking( this, (*it).linkDest, uDest ); 2107 // Observer::self()->slotCopying( this, m_currentSrcURL, uDest ); // should be slotLinking perhaps 2108 m_bCurrentOperationIsLink = true; 2109 // NOTE: if we are moving stuff, the deletion of the source will be done in slotResultCopyingFiles 2110 } else if (m_mode == CopyJob::Move) { // Moving a file 2111 KIO::FileCopyJob *moveJob = KIO::file_move(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/); 2112 moveJob->setParentJob(q); 2113 moveJob->setSourceSize((*it).size); 2114 moveJob->setModificationTime((*it).mtime); // #55804 2115 newjob = moveJob; 2116 qCDebug(KIO_COPYJOB_DEBUG) << "Moving" << uSource << "to" << uDest; 2117 // emit moving( this, uSource, uDest ); 2118 m_currentSrcURL = uSource; 2119 m_currentDestURL = uDest; 2120 m_bURLDirty = true; 2121 // Observer::self()->slotMoving( this, uSource, uDest ); 2122 } else { // Copying a file 2123 KIO::FileCopyJob *copyJob = KIO::file_copy(uSource, uDest, permissions, flags | HideProgressInfo /*no GUI*/); 2124 copyJob->setParentJob(q); // in case of rename dialog 2125 copyJob->setSourceSize((*it).size); 2126 copyJob->setModificationTime((*it).mtime); 2127 newjob = copyJob; 2128 qCDebug(KIO_COPYJOB_DEBUG) << "Copying" << uSource << "to" << uDest; 2129 m_currentSrcURL = uSource; 2130 m_currentDestURL = uDest; 2131 m_bURLDirty = true; 2132 } 2133 q->addSubjob(newjob); 2134 q->connect(newjob, &Job::processedSize, q, [this](KJob *job, qulonglong processedSize) { 2135 slotProcessedSize(job, processedSize); 2136 }); 2137 q->connect(newjob, &Job::totalSize, q, [this](KJob *job, qulonglong totalSize) { 2138 slotTotalSize(job, totalSize); 2139 }); 2140 } 2141 2142 void CopyJobPrivate::deleteNextDir() 2143 { 2144 Q_Q(CopyJob); 2145 if (m_mode == CopyJob::Move && !dirsToRemove.isEmpty()) { // some dirs to delete ? 2146 state = STATE_DELETING_DIRS; 2147 m_bURLDirty = true; 2148 // Take first dir to delete out of list - last ones first ! 2149 QList<QUrl>::Iterator it = --dirsToRemove.end(); 2150 SimpleJob *job = KIO::rmdir(*it); 2151 job->setParentJob(q); 2152 dirsToRemove.erase(it); 2153 q->addSubjob(job); 2154 } else { 2155 // This step is done, move on 2156 state = STATE_SETTING_DIR_ATTRIBUTES; 2157 m_directoriesCopiedIterator = m_directoriesCopied.cbegin(); 2158 setNextDirAttribute(); 2159 } 2160 } 2161 2162 void CopyJobPrivate::setNextDirAttribute() 2163 { 2164 Q_Q(CopyJob); 2165 while (m_directoriesCopiedIterator != m_directoriesCopied.cend() && !(*m_directoriesCopiedIterator).mtime.isValid()) { 2166 ++m_directoriesCopiedIterator; 2167 } 2168 if (m_directoriesCopiedIterator != m_directoriesCopied.cend()) { 2169 const QUrl url = (*m_directoriesCopiedIterator).uDest; 2170 const QDateTime dt = (*m_directoriesCopiedIterator).mtime; 2171 ++m_directoriesCopiedIterator; 2172 2173 KIO::SimpleJob *job = KIO::setModificationTime(url, dt); 2174 job->setParentJob(q); 2175 q->addSubjob(job); 2176 } else { 2177 if (m_reportTimer) { 2178 m_reportTimer->stop(); 2179 } 2180 2181 q->emitResult(); 2182 } 2183 } 2184 2185 void CopyJob::emitResult() 2186 { 2187 Q_D(CopyJob); 2188 // Before we go, tell the world about the changes that were made. 2189 // Even if some error made us abort midway, we might still have done 2190 // part of the job so we better update the views! (#118583) 2191 if (!d->m_bOnlyRenames) { 2192 // If only renaming happened, KDirNotify::FileRenamed was emitted by the rename jobs 2193 QUrl url(d->m_globalDest); 2194 if (d->m_globalDestinationState != DEST_IS_DIR || d->m_asMethod) { 2195 url = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 2196 } 2197 qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesAdded" << url; 2198 #ifndef KIO_ANDROID_STUB 2199 org::kde::KDirNotify::emitFilesAdded(url); 2200 #endif 2201 2202 if (d->m_mode == CopyJob::Move && !d->m_successSrcList.isEmpty()) { 2203 qCDebug(KIO_COPYJOB_DEBUG) << "KDirNotify'ing FilesRemoved" << d->m_successSrcList; 2204 #ifndef KIO_ANDROID_STUB 2205 org::kde::KDirNotify::emitFilesRemoved(d->m_successSrcList); 2206 #endif 2207 } 2208 } 2209 2210 // Re-enable watching on the dirs that held the deleted/moved files 2211 if (d->m_mode == CopyJob::Move) { 2212 for (const auto &dir : d->m_parentDirs) { 2213 KDirWatch::self()->restartDirScan(dir); 2214 } 2215 } 2216 Job::emitResult(); 2217 } 2218 2219 void CopyJobPrivate::slotProcessedSize(KJob *, qulonglong data_size) 2220 { 2221 Q_Q(CopyJob); 2222 qCDebug(KIO_COPYJOB_DEBUG) << data_size; 2223 m_fileProcessedSize = data_size; 2224 2225 if (m_processedSize + m_fileProcessedSize > m_totalSize) { 2226 // Example: download any attachment from bugs.kde.org 2227 m_totalSize = m_processedSize + m_fileProcessedSize; 2228 qCDebug(KIO_COPYJOB_DEBUG) << "Adjusting m_totalSize to" << m_totalSize; 2229 q->setTotalAmount(KJob::Bytes, m_totalSize); // safety 2230 } 2231 qCDebug(KIO_COPYJOB_DEBUG) << "emit processedSize" << (unsigned long)(m_processedSize + m_fileProcessedSize); 2232 } 2233 2234 void CopyJobPrivate::slotTotalSize(KJob *, qulonglong size) 2235 { 2236 Q_Q(CopyJob); 2237 qCDebug(KIO_COPYJOB_DEBUG) << size; 2238 // Special case for copying a single file 2239 // This is because some protocols don't implement stat properly 2240 // (e.g. HTTP), and don't give us a size in some cases (redirection) 2241 // so we'd rather rely on the size given for the transfer 2242 if (m_bSingleFileCopy && size != m_totalSize) { 2243 qCDebug(KIO_COPYJOB_DEBUG) << "slotTotalSize: updating totalsize to" << size; 2244 m_totalSize = size; 2245 q->setTotalAmount(KJob::Bytes, size); 2246 } 2247 } 2248 2249 void CopyJobPrivate::slotResultDeletingDirs(KJob *job) 2250 { 2251 Q_Q(CopyJob); 2252 if (job->error()) { 2253 // Couldn't remove directory. Well, perhaps it's not empty 2254 // because the user pressed Skip for a given file in it. 2255 // Let's not display "Could not remove dir ..." for each of those dir ! 2256 } else { 2257 m_successSrcList.append(static_cast<KIO::SimpleJob *>(job)->url()); 2258 } 2259 q->removeSubjob(job); 2260 Q_ASSERT(!q->hasSubjobs()); 2261 deleteNextDir(); 2262 } 2263 2264 void CopyJobPrivate::slotResultSettingDirAttributes(KJob *job) 2265 { 2266 Q_Q(CopyJob); 2267 if (job->error()) { 2268 // Couldn't set directory attributes. Ignore the error, it can happen 2269 // with inferior file systems like VFAT. 2270 // Let's not display warnings for each dir like "cp -a" does. 2271 } 2272 q->removeSubjob(job); 2273 Q_ASSERT(!q->hasSubjobs()); 2274 setNextDirAttribute(); 2275 } 2276 2277 void CopyJobPrivate::directRenamingFailed(const QUrl &dest) 2278 { 2279 Q_Q(CopyJob); 2280 2281 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", reverting to normal way, starting with stat"; 2282 qCDebug(KIO_COPYJOB_DEBUG) << "KIO::stat on" << m_currentSrcURL; 2283 2284 KIO::Job *job = KIO::statDetails(m_currentSrcURL, StatJob::SourceSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 2285 state = STATE_STATING; 2286 q->addSubjob(job); 2287 m_bOnlyRenames = false; 2288 } 2289 2290 // We were trying to do a direct renaming, before even stat'ing 2291 void CopyJobPrivate::slotResultRenaming(KJob *job) 2292 { 2293 Q_Q(CopyJob); 2294 int err = job->error(); 2295 const QString errText = job->errorText(); 2296 // Merge metadata from subjob 2297 KIO::Job *kiojob = qobject_cast<KIO::Job *>(job); 2298 Q_ASSERT(kiojob); 2299 m_incomingMetaData += kiojob->metaData(); 2300 q->removeSubjob(job); 2301 Q_ASSERT(!q->hasSubjobs()); 2302 // Determine dest again 2303 QUrl dest = m_dest; 2304 if (destinationState == DEST_IS_DIR && !m_asMethod) { 2305 dest = addPathToUrl(dest, m_currentSrcURL.fileName()); 2306 } 2307 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(q); 2308 2309 if (err) { 2310 // This code is similar to CopyJobPrivate::slotResultErrorCopyingFiles 2311 // but here it's about the base src url being moved/renamed 2312 // (m_currentSrcURL) and its dest (m_dest), not about a single file. 2313 // It also means we already stated the dest, here. 2314 // On the other hand we haven't stated the src yet (we skipped doing it 2315 // to save time, since it's not necessary to rename directly!)... 2316 2317 // Existing dest? 2318 if (err == ERR_DIR_ALREADY_EXIST || err == ERR_FILE_ALREADY_EXIST || err == ERR_IDENTICAL_FILES) { 2319 // Should we skip automatically ? 2320 bool isDir = (err == ERR_DIR_ALREADY_EXIST); // ## technically, isDir means "source is dir", not "dest is dir" ####### 2321 if ((isDir && m_bAutoSkipDirs) || (!isDir && m_bAutoSkipFiles)) { 2322 // Move on to next source url 2323 ++m_filesHandledByDirectRename; 2324 skipSrc(isDir); 2325 return; 2326 } else if ((isDir && m_bOverwriteAllDirs) || (!isDir && m_bOverwriteAllFiles)) { 2327 ; // nothing to do, stat+copy+del will overwrite 2328 } else if ((isDir && m_bAutoRenameDirs) || (!isDir && m_bAutoRenameFiles)) { 2329 QUrl destDirectory = m_currentDestURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // m_currendDestURL includes filename 2330 const QString newName = KFileUtils::suggestName(destDirectory, m_currentDestURL.fileName()); 2331 2332 m_dest = destDirectory; 2333 m_dest.setPath(Utils::concatPaths(m_dest.path(), newName)); 2334 Q_EMIT q->renamed(q, dest, m_dest); 2335 KIO::Job *job = KIO::statDetails(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 2336 state = STATE_STATING; 2337 destinationState = DEST_NOT_STATED; 2338 q->addSubjob(job); 2339 return; 2340 } else if (askUserActionInterface) { 2341 // we lack mtime info for both the src (not stated) 2342 // and the dest (stated but this info wasn't stored) 2343 // Let's do it for local files, at least 2344 KIO::filesize_t sizeSrc = KIO::invalidFilesize; 2345 KIO::filesize_t sizeDest = KIO::invalidFilesize; 2346 QDateTime ctimeSrc; 2347 QDateTime ctimeDest; 2348 QDateTime mtimeSrc; 2349 QDateTime mtimeDest; 2350 2351 bool destIsDir = err == ERR_DIR_ALREADY_EXIST; 2352 2353 // ## TODO we need to stat the source using KIO::stat 2354 // so that this code is properly network-transparent. 2355 2356 if (m_currentSrcURL.isLocalFile()) { 2357 QFileInfo info(m_currentSrcURL.toLocalFile()); 2358 if (info.exists()) { 2359 sizeSrc = info.size(); 2360 ctimeSrc = info.birthTime(); 2361 mtimeSrc = info.lastModified(); 2362 isDir = info.isDir(); 2363 } 2364 } 2365 if (dest.isLocalFile()) { 2366 QFileInfo destInfo(dest.toLocalFile()); 2367 if (destInfo.exists()) { 2368 sizeDest = destInfo.size(); 2369 ctimeDest = destInfo.birthTime(); 2370 mtimeDest = destInfo.lastModified(); 2371 destIsDir = destInfo.isDir(); 2372 } 2373 } 2374 2375 // If src==dest, use "overwrite-itself" 2376 RenameDialog_Options options = (m_currentSrcURL == dest) ? RenameDialog_OverwriteItself : RenameDialog_Overwrite; 2377 if (!isDir && destIsDir) { 2378 // We can't overwrite a dir with a file. 2379 options = RenameDialog_Options(); 2380 } 2381 2382 if (m_srcList.count() > 1) { 2383 options |= RenameDialog_Options(RenameDialog_MultipleItems | RenameDialog_Skip); 2384 } 2385 2386 if (destIsDir) { 2387 options |= RenameDialog_DestIsDirectory; 2388 } 2389 2390 if (m_reportTimer) { 2391 m_reportTimer->stop(); 2392 } 2393 2394 RenameDialog_Result r; 2395 if (m_bOverwriteWhenOlder && mtimeSrc.isValid() && mtimeDest.isValid()) { 2396 if (mtimeSrc > mtimeDest) { 2397 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest; 2398 r = Result_Overwrite; 2399 } else { 2400 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest; 2401 r = Result_Skip; 2402 } 2403 2404 processDirectRenamingConflictResult(r, isDir, destIsDir, mtimeSrc, mtimeDest, dest, QUrl{}); 2405 return; 2406 } else { 2407 auto renameSignal = &KIO::AskUserActionInterface::askUserRenameResult; 2408 QObject::connect(askUserActionInterface, renameSignal, q, [=](RenameDialog_Result result, const QUrl &newUrl, KJob *parentJob) { 2409 Q_ASSERT(parentJob == q); 2410 // Only receive askUserRenameResult once per rename dialog 2411 QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr); 2412 2413 processDirectRenamingConflictResult(result, isDir, destIsDir, mtimeSrc, mtimeDest, dest, newUrl); 2414 }); 2415 2416 const QString title = err != ERR_DIR_ALREADY_EXIST ? i18n("File Already Exists") : i18n("Already Exists as Folder"); 2417 2418 /* clang-format off */ 2419 askUserActionInterface->askUserRename(q, title, 2420 m_currentSrcURL, dest, 2421 options, 2422 sizeSrc, sizeDest, 2423 ctimeSrc, ctimeDest, 2424 mtimeSrc, mtimeDest); 2425 /* clang-format on */ 2426 2427 return; 2428 } 2429 } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { 2430 // Dest already exists, and job is not interactive -> abort with error 2431 q->setError(err); 2432 q->setErrorText(errText); 2433 q->emitResult(); 2434 return; 2435 } 2436 } else if (err != KIO::ERR_UNSUPPORTED_ACTION) { 2437 qCDebug(KIO_COPYJOB_DEBUG) << "Couldn't rename" << m_currentSrcURL << "to" << dest << ", aborting"; 2438 q->setError(err); 2439 q->setErrorText(errText); 2440 q->emitResult(); 2441 return; 2442 } 2443 2444 directRenamingFailed(dest); 2445 return; 2446 } 2447 2448 // No error 2449 qCDebug(KIO_COPYJOB_DEBUG) << "Renaming succeeded, move on"; 2450 ++m_processedFiles; 2451 ++m_filesHandledByDirectRename; 2452 // Emit copyingDone for FileUndoManager to remember what we did. 2453 // Use resolved URL m_currentSrcURL since that's what we just used for renaming. See bug 391606 and kio_desktop's testTrashAndUndo(). 2454 const bool srcIsDir = false; // # TODO: we just don't know, since we never stat'ed it 2455 Q_EMIT q->copyingDone(q, m_currentSrcURL, finalDestUrl(m_currentSrcURL, dest), QDateTime() /*mtime unknown, and not needed*/, srcIsDir, true); 2456 m_successSrcList.append(*m_currentStatSrc); 2457 statNextSrc(); 2458 } 2459 2460 void CopyJobPrivate::processDirectRenamingConflictResult(RenameDialog_Result result, 2461 bool srcIsDir, 2462 bool destIsDir, 2463 const QDateTime &mtimeSrc, 2464 const QDateTime &mtimeDest, 2465 const QUrl &dest, 2466 const QUrl &newUrl) 2467 { 2468 Q_Q(CopyJob); 2469 2470 if (m_reportTimer) { 2471 m_reportTimer->start(s_reportTimeout); 2472 } 2473 2474 if (result == Result_OverwriteWhenOlder) { 2475 m_bOverwriteWhenOlder = true; 2476 if (mtimeSrc > mtimeDest) { 2477 qCDebug(KIO_COPYJOB_DEBUG) << "dest is older, overwriting" << dest; 2478 result = Result_Overwrite; 2479 } else { 2480 qCDebug(KIO_COPYJOB_DEBUG) << "dest is newer, skipping" << dest; 2481 result = Result_Skip; 2482 } 2483 } 2484 2485 switch (result) { 2486 case Result_Cancel: { 2487 q->setError(ERR_USER_CANCELED); 2488 q->emitResult(); 2489 return; 2490 } 2491 case Result_AutoRename: 2492 if (srcIsDir) { 2493 m_bAutoRenameDirs = true; 2494 } else { 2495 m_bAutoRenameFiles = true; 2496 } 2497 // fall through 2498 Q_FALLTHROUGH(); 2499 case Result_Rename: { 2500 // Set m_dest to the chosen destination 2501 // This is only for this src url; the next one will revert to m_globalDest 2502 m_dest = newUrl; 2503 Q_EMIT q->renamed(q, dest, m_dest); // For e.g. KPropertiesDialog 2504 KIO::Job *job = KIO::statDetails(m_dest, StatJob::DestinationSide, KIO::StatDefaultDetails, KIO::HideProgressInfo); 2505 state = STATE_STATING; 2506 destinationState = DEST_NOT_STATED; 2507 q->addSubjob(job); 2508 return; 2509 } 2510 case Result_AutoSkip: 2511 if (srcIsDir) { 2512 m_bAutoSkipDirs = true; 2513 } else { 2514 m_bAutoSkipFiles = true; 2515 } 2516 // fall through 2517 Q_FALLTHROUGH(); 2518 case Result_Skip: 2519 // Move on to next url 2520 ++m_filesHandledByDirectRename; 2521 skipSrc(srcIsDir); 2522 return; 2523 case Result_OverwriteAll: 2524 if (destIsDir) { 2525 m_bOverwriteAllDirs = true; 2526 } else { 2527 m_bOverwriteAllFiles = true; 2528 } 2529 break; 2530 case Result_Overwrite: 2531 // Add to overwrite list 2532 // Note that we add dest, not m_dest. 2533 // This ensures that when moving several urls into a dir (m_dest), 2534 // we only overwrite for the current one, not for all. 2535 // When renaming a single file (m_asMethod), it makes no difference. 2536 qCDebug(KIO_COPYJOB_DEBUG) << "adding to overwrite list: " << dest.path(); 2537 m_overwriteList.insert(dest.path()); 2538 break; 2539 default: 2540 // Q_ASSERT( 0 ); 2541 break; 2542 } 2543 2544 directRenamingFailed(dest); 2545 } 2546 2547 void CopyJob::slotResult(KJob *job) 2548 { 2549 Q_D(CopyJob); 2550 qCDebug(KIO_COPYJOB_DEBUG) << "d->state=" << (int)d->state; 2551 // In each case, what we have to do is : 2552 // 1 - check for errors and treat them 2553 // 2 - removeSubjob(job); 2554 // 3 - decide what to do next 2555 2556 switch (d->state) { 2557 case STATE_STATING: // We were trying to stat a src url or the dest 2558 d->slotResultStating(job); 2559 break; 2560 case STATE_RENAMING: { // We were trying to do a direct renaming, before even stat'ing 2561 d->slotResultRenaming(job); 2562 break; 2563 } 2564 case STATE_LISTING: // recursive listing finished 2565 qCDebug(KIO_COPYJOB_DEBUG) << "totalSize:" << (unsigned int)d->m_totalSize << "files:" << d->files.count() << "d->dirs:" << d->dirs.count(); 2566 // Was there an error ? 2567 if (job->error()) { 2568 Job::slotResult(job); // will set the error and emit result(this) 2569 return; 2570 } 2571 2572 removeSubjob(job); 2573 Q_ASSERT(!hasSubjobs()); 2574 2575 d->statNextSrc(); 2576 break; 2577 case STATE_CREATING_DIRS: 2578 d->slotResultCreatingDirs(job); 2579 break; 2580 case STATE_CONFLICT_CREATING_DIRS: 2581 d->slotResultConflictCreatingDirs(job); 2582 break; 2583 case STATE_COPYING_FILES: 2584 d->slotResultCopyingFiles(job); 2585 break; 2586 case STATE_CONFLICT_COPYING_FILES: 2587 d->slotResultErrorCopyingFiles(job); 2588 break; 2589 case STATE_DELETING_DIRS: 2590 d->slotResultDeletingDirs(job); 2591 break; 2592 case STATE_SETTING_DIR_ATTRIBUTES: 2593 d->slotResultSettingDirAttributes(job); 2594 break; 2595 default: 2596 Q_ASSERT(0); 2597 } 2598 } 2599 2600 void KIO::CopyJob::setDefaultPermissions(bool b) 2601 { 2602 d_func()->m_defaultPermissions = b; 2603 } 2604 2605 KIO::CopyJob::CopyMode KIO::CopyJob::operationMode() const 2606 { 2607 return d_func()->m_mode; 2608 } 2609 2610 void KIO::CopyJob::setAutoSkip(bool autoSkip) 2611 { 2612 d_func()->m_bAutoSkipFiles = autoSkip; 2613 d_func()->m_bAutoSkipDirs = autoSkip; 2614 } 2615 2616 void KIO::CopyJob::setAutoRename(bool autoRename) 2617 { 2618 d_func()->m_bAutoRenameFiles = autoRename; 2619 d_func()->m_bAutoRenameDirs = autoRename; 2620 } 2621 2622 void KIO::CopyJob::setWriteIntoExistingDirectories(bool overwriteAll) // #65926 2623 { 2624 d_func()->m_bOverwriteAllDirs = overwriteAll; 2625 } 2626 2627 CopyJob *KIO::copy(const QUrl &src, const QUrl &dest, JobFlags flags) 2628 { 2629 qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; 2630 QList<QUrl> srcList; 2631 srcList.append(src); 2632 return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, false, flags); 2633 } 2634 2635 CopyJob *KIO::copyAs(const QUrl &src, const QUrl &dest, JobFlags flags) 2636 { 2637 qCDebug(KIO_COPYJOB_DEBUG) << "src=" << src << "dest=" << dest; 2638 QList<QUrl> srcList; 2639 srcList.append(src); 2640 return CopyJobPrivate::newJob(srcList, dest, CopyJob::Copy, true, flags); 2641 } 2642 2643 CopyJob *KIO::copy(const QList<QUrl> &src, const QUrl &dest, JobFlags flags) 2644 { 2645 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2646 return CopyJobPrivate::newJob(src, dest, CopyJob::Copy, false, flags); 2647 } 2648 2649 CopyJob *KIO::move(const QUrl &src, const QUrl &dest, JobFlags flags) 2650 { 2651 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2652 QList<QUrl> srcList; 2653 srcList.append(src); 2654 CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, false, flags); 2655 if (job->uiDelegateExtension()) { 2656 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2657 } 2658 return job; 2659 } 2660 2661 CopyJob *KIO::moveAs(const QUrl &src, const QUrl &dest, JobFlags flags) 2662 { 2663 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2664 QList<QUrl> srcList; 2665 srcList.append(src); 2666 CopyJob *job = CopyJobPrivate::newJob(srcList, dest, CopyJob::Move, true, flags); 2667 if (job->uiDelegateExtension()) { 2668 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2669 } 2670 return job; 2671 } 2672 2673 CopyJob *KIO::move(const QList<QUrl> &src, const QUrl &dest, JobFlags flags) 2674 { 2675 qCDebug(KIO_COPYJOB_DEBUG) << src << dest; 2676 CopyJob *job = CopyJobPrivate::newJob(src, dest, CopyJob::Move, false, flags); 2677 if (job->uiDelegateExtension()) { 2678 job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); 2679 } 2680 return job; 2681 } 2682 2683 CopyJob *KIO::link(const QUrl &src, const QUrl &destDir, JobFlags flags) 2684 { 2685 QList<QUrl> srcList; 2686 srcList.append(src); 2687 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); 2688 } 2689 2690 CopyJob *KIO::link(const QList<QUrl> &srcList, const QUrl &destDir, JobFlags flags) 2691 { 2692 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, false, flags); 2693 } 2694 2695 CopyJob *KIO::linkAs(const QUrl &src, const QUrl &destDir, JobFlags flags) 2696 { 2697 QList<QUrl> srcList; 2698 srcList.append(src); 2699 return CopyJobPrivate::newJob(srcList, destDir, CopyJob::Link, true, flags); 2700 } 2701 2702 CopyJob *KIO::trash(const QUrl &src, JobFlags flags) 2703 { 2704 QList<QUrl> srcList; 2705 srcList.append(src); 2706 return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); 2707 } 2708 2709 CopyJob *KIO::trash(const QList<QUrl> &srcList, JobFlags flags) 2710 { 2711 return CopyJobPrivate::newJob(srcList, QUrl(QStringLiteral("trash:/")), CopyJob::Move, false, flags); 2712 } 2713 2714 #include "moc_copyjob.cpp"