File indexing completed on 2024-04-21 05:42:28

0001 // clang-format off
0002 /**
0003  * KDiff3 - Text Diff And Merge Tool
0004  *
0005  * SPDX-FileCopyrightText: 2021 Michael Reeves <reeves.87@gmail.com>
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  *
0008  */
0009 // clang-format on
0010 
0011 #include "DefaultFileAccessJobHandler.h"
0012 
0013 #include "compat.h"
0014 #include "defmac.h"
0015 #include "fileaccess.h"
0016 #include "IgnoreList.h"
0017 #include "Logging.h"
0018 #include "progress.h"
0019 #include "ProgressProxyExtender.h"
0020 #include "TypeUtils.h"
0021 
0022 #include <algorithm>
0023 #include <list>
0024 #include <utility>
0025 
0026 #include <QFileInfoList>
0027 #include <QDir>
0028 #include <QUrl>
0029 
0030 #include <KIO/CopyJob>
0031 #include <KIO/FileCopyJob>
0032 #include <KIO/ListJob>
0033 #include <KIO/MkdirJob>
0034 #include <KIO/SimpleJob>
0035 #include <KIO/StatJob>
0036 #include <KIO/TransferJob>
0037 #include <KJob>
0038 #include <KLocalizedString>
0039 
0040 #include <KIO/JobUiDelegate>
0041 #include <KMessageBox>
0042 
0043 bool DefaultFileAccessJobHandler::stat(bool bWantToWrite)
0044 {
0045     m_bSuccess = false;
0046 #if HAS_KFKIO
0047     mFileAccess->setStatusText(QString());
0048 
0049     //For testing kf6 build.
0050     #if KF_VERSION < KF_VERSION_CHECK(5,240,0)
0051     KIO::StatJob* pStatJob = KIO::statDetails(mFileAccess->url(),
0052                                        bWantToWrite ? KIO::StatJob::DestinationSide : KIO::StatJob::SourceSide,
0053                                        KIO::StatDefaultDetails, KIO::HideProgressInfo);
0054     #else
0055     KIO::StatJob* pStatJob = KIO::stat(mFileAccess->url(),
0056                                        bWantToWrite ? KIO::StatJob::DestinationSide : KIO::StatJob::SourceSide,
0057                                        KIO::StatDefaultDetails, KIO::HideProgressInfo);
0058 
0059     #endif
0060     chk_connect(pStatJob, &KIO::StatJob::result, this, &DefaultFileAccessJobHandler::slotStatResult);
0061     chk_connect(pStatJob, &KIO::StatJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0062 
0063     ProgressProxy::enterEventLoop(pStatJob, i18n("Getting file status: %1", mFileAccess->prettyAbsPath()));
0064 #endif
0065     return m_bSuccess;
0066 }
0067 
0068 #if HAS_KFKIO
0069 void DefaultFileAccessJobHandler::slotStatResult(KJob* pJob)
0070 {
0071     qint32 err = pJob->error();
0072     if(err != KJob::NoError)
0073     {
0074         qCDebug(kdiffFileAccess) << "slotStatResult: pJob->error() = " << pJob->error();
0075         if(err != KIO::ERR_DOES_NOT_EXIST)
0076         {
0077             pJob->uiDelegate()->showErrorMessage();
0078             m_bSuccess = false;
0079             mFileAccess->reset();
0080         }
0081         else
0082         {
0083             mFileAccess->doError();
0084             m_bSuccess = true;
0085         }
0086     }
0087     else
0088     {
0089         m_bSuccess = true;
0090 
0091         const KIO::UDSEntry e = static_cast<KIO::StatJob*>(pJob)->statResult();
0092 
0093         mFileAccess->setFromUdsEntry(e, mFileAccess->parent());
0094         m_bSuccess = mFileAccess->isValid();
0095     }
0096 }
0097 #endif
0098 
0099 bool DefaultFileAccessJobHandler::get(void* pDestBuffer, long maxLength)
0100 {
0101     ProgressProxyExtender pp; // Implicitly used in slotPercent()
0102 
0103     if(maxLength > 0 && !ProgressProxy::wasCancelled())
0104     {
0105         m_bSuccess = false;
0106 #if HAS_KFKIO
0107         KIO::TransferJob* pJob = KIO::get(mFileAccess->url(), KIO::NoReload);
0108         m_transferredBytes = 0;
0109         m_pTransferBuffer = (char*)pDestBuffer;
0110         m_maxLength = maxLength;
0111         mFileAccess->setStatusText(QString());
0112 
0113         chk_connect(pJob, &KIO::TransferJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0114         chk_connect(pJob, &KIO::TransferJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0115         chk_connect(pJob, &KIO::TransferJob::data, this, &DefaultFileAccessJobHandler::slotGetData);
0116         chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong)));
0117 
0118         ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Reading file: %1", mFileAccess->prettyAbsPath()));
0119 #endif
0120         return m_bSuccess;
0121     }
0122     else
0123         return true;
0124 }
0125 
0126 #if HAS_KFKIO
0127 void DefaultFileAccessJobHandler::slotGetData(KJob* pJob, const QByteArray& newData)
0128 {
0129     if(pJob->error() != KJob::NoError)
0130     {
0131         qCDebug(kdiffFileAccess) << "slotGetData: pJob->error() = " << pJob->error();
0132         pJob->uiDelegate()->showErrorMessage();
0133     }
0134     else
0135     {
0136         qint64 length = std::min(qint64(newData.size()), m_maxLength - m_transferredBytes);
0137         ::memcpy(m_pTransferBuffer + m_transferredBytes, newData.data(), newData.size());
0138         m_transferredBytes += length;
0139     }
0140 }
0141 #endif
0142 
0143 bool DefaultFileAccessJobHandler::put(const void* pSrcBuffer, long maxLength, bool bOverwrite, bool bResume, qint32 permissions)
0144 {
0145     ProgressProxyExtender pp; // Implicitly used in slotPercent()
0146     if(maxLength > 0)
0147     {
0148         m_bSuccess = false;
0149 #if HAS_KFKIO
0150         KIO::TransferJob* pJob = KIO::put(mFileAccess->url(), permissions,
0151                                           KIO::HideProgressInfo | (bOverwrite ? KIO::Overwrite : KIO::DefaultFlags) | (bResume ? KIO::Resume : KIO::DefaultFlags));
0152         m_transferredBytes = 0;
0153         m_pTransferBuffer = (char*)pSrcBuffer;
0154         m_maxLength = maxLength;
0155         mFileAccess->setStatusText(QString());
0156 
0157         chk_connect(pJob, &KIO::TransferJob::result, this, &DefaultFileAccessJobHandler::slotPutJobResult);
0158         chk_connect(pJob, &KIO::TransferJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0159         chk_connect(pJob, &KIO::TransferJob::dataReq, this, &DefaultFileAccessJobHandler::slotPutData);
0160         chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong)));
0161 
0162         ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Writing file: %1", mFileAccess->prettyAbsPath()));
0163 #endif
0164         return m_bSuccess;
0165     }
0166     else
0167         return true;
0168 }
0169 
0170 #if HAS_KFKIO
0171 void DefaultFileAccessJobHandler::slotPutData(KIO::Job* pJob, QByteArray& data)
0172 {
0173     if(pJob->error() != KJob::NoError)
0174     {
0175         qCDebug(kdiffFileAccess) << "slotPutData: pJob->error() = " << pJob->error();
0176         pJob->uiDelegate()->showErrorMessage();
0177     }
0178     else
0179     {
0180         /*
0181             Think twice before doing this in new code.
0182             The maxChunkSize must be able to fit a 32-bit int. Given that the fallowing is safe for qt5.
0183             Qt6 resolves this issue as it uses 64 bit sizes.
0184         */
0185         qint64 maxChunkSize = 100000;
0186         qint64 length = std::min(maxChunkSize, m_maxLength - m_transferredBytes);
0187         if(length > 0)
0188         {
0189             data.resize((QtSizeType)length);
0190             if(data.size() == (QtSizeType)length)
0191             {
0192                 ::memcpy(data.data(), m_pTransferBuffer + m_transferredBytes, data.size());
0193                 m_transferredBytes += length;
0194             }
0195         }
0196         else
0197         {
0198             KMessageBox::error(g_pProgressDialog, i18n("Out of memory"));
0199             data.resize(0);
0200             m_bSuccess = false;
0201         }
0202     }
0203 }
0204 
0205 void DefaultFileAccessJobHandler::slotPutJobResult(KJob* pJob)
0206 {
0207     if(pJob->error() != KJob::NoError)
0208     {
0209         qCDebug(kdiffFileAccess) << "slotPutJobResult: pJob->error() = " << pJob->error();
0210         pJob->uiDelegate()->showErrorMessage();
0211     }
0212     else
0213     {
0214         m_bSuccess = (m_transferredBytes == m_maxLength); // Special success condition
0215     }
0216 }
0217 #endif
0218 
0219 bool DefaultFileAccessJobHandler::mkDirImp(const QString& dirName)
0220 {
0221     if(dirName.isEmpty())
0222         return false;
0223 
0224     FileAccess dir(dirName);
0225     if(dir.isLocal())
0226     {
0227         return QDir().mkdir(dir.absoluteFilePath());
0228     }
0229     else
0230     {
0231         m_bSuccess = false;
0232 #if HAS_KFKIO
0233         KIO::SimpleJob* pJob = KIO::mkdir(dir.url());
0234         chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0235         chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0236 
0237         ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Making folder: %1", dirName));
0238 #endif
0239         return m_bSuccess;
0240     }
0241 }
0242 
0243 bool DefaultFileAccessJobHandler::rmDirImp(const QString& dirName)
0244 {
0245     if(dirName.isEmpty())
0246         return false;
0247 
0248     FileAccess fa(dirName);
0249     if(fa.isLocal())
0250     {
0251         return QDir().rmdir(fa.absoluteFilePath());
0252     }
0253     else
0254     {
0255         m_bSuccess = false;
0256 #if HAS_KFKIO
0257         KIO::SimpleJob* pJob = KIO::rmdir(fa.url());
0258         chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0259         chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0260 
0261         ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Removing folder: %1", dirName));
0262 #endif
0263         return m_bSuccess;
0264     }
0265 }
0266 
0267 bool DefaultFileAccessJobHandler::removeFile(const QUrl& fileName)
0268 {
0269     if(fileName.isEmpty())
0270         return false;
0271     else
0272     {
0273         m_bSuccess = false;
0274 #if HAS_KFKIO
0275         KIO::SimpleJob* pJob = KIO::file_delete(fileName, KIO::HideProgressInfo);
0276         chk_connect(pJob, &KIO::SimpleJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0277         chk_connect(pJob, &KIO::SimpleJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0278 
0279         ProgressProxy::enterEventLoop(pJob, i18nc("Message for progress dialog %1 = path to file", "Removing file: %1", FileAccess::prettyAbsPath(fileName)));
0280 #else
0281         if(FileAccess::isLocal(fileName))
0282             m_bSuccess = QFile::remove(fileName)
0283 #endif
0284         return m_bSuccess;
0285     }
0286 }
0287 
0288 bool DefaultFileAccessJobHandler::symLink(const QUrl& linkTarget, const QUrl& linkLocation)
0289 {
0290     if(linkTarget.isEmpty() || linkLocation.isEmpty())
0291         return false;
0292     else
0293     {
0294         m_bSuccess = false;
0295 #if HAS_KFKIO
0296         KIO::CopyJob* pJob = KIO::link(linkTarget, linkLocation, KIO::HideProgressInfo);
0297         chk_connect(pJob, &KIO::CopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0298         chk_connect(pJob, &KIO::CopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0299 
0300         ProgressProxy::enterEventLoop(pJob,
0301                                       i18n("Creating symbolic link: %1 -> %2", FileAccess::prettyAbsPath(linkLocation), FileAccess::prettyAbsPath(linkTarget)));
0302 #else
0303         //TODO: Not implemented.
0304         //if(FileAccess::isLocal(fileName))
0305         //    m_bSuccess = QFile::link(linkTarget, linkLocation)
0306 #endif
0307         return m_bSuccess;
0308     }
0309 }
0310 
0311 bool DefaultFileAccessJobHandler::rename(const FileAccess& destFile)
0312 {
0313     if(destFile.fileName().isEmpty())
0314         return false;
0315 
0316     if(mFileAccess->isLocal() && destFile.isLocal())
0317     {
0318         return QDir().rename(mFileAccess->absoluteFilePath(), destFile.absoluteFilePath());
0319     }
0320     else
0321     {
0322         ProgressProxyExtender pp;
0323         qint32 permissions = -1;
0324         m_bSuccess = false;
0325         KIO::FileCopyJob* pJob = KIO::file_move(mFileAccess->url(), destFile.url(), permissions, KIO::HideProgressInfo);
0326         chk_connect(pJob, &KIO::FileCopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0327         chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong)));
0328         chk_connect(pJob, &KIO::FileCopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0329 
0330         ProgressProxy::enterEventLoop(pJob,
0331                                       i18n("Renaming file: %1 -> %2", mFileAccess->prettyAbsPath(), destFile.prettyAbsPath()));
0332         return m_bSuccess;
0333     }
0334 }
0335 
0336 void DefaultFileAccessJobHandler::slotJobEnded([[maybe_unused]] KJob* pJob)
0337 {
0338     ProgressProxy::exitEventLoop(); // Close the dialog, return from exec()
0339 }
0340 
0341 void DefaultFileAccessJobHandler::slotSimpleJobResult(KJob* pJob)
0342 {
0343     if(pJob->error() != KJob::NoError)
0344     {
0345         qCDebug(kdiffFileAccess) << "slotSimpleJobResult: pJob->error() = " << pJob->error();
0346         pJob->uiDelegate()->showErrorMessage();
0347     }
0348     else
0349     {
0350         m_bSuccess = true;
0351     }
0352 }
0353 
0354 // Copy local or remote files.
0355 bool DefaultFileAccessJobHandler::copyFile(const QString& inDest)
0356 {
0357     ProgressProxyExtender pp;
0358     FileAccess dest;
0359     dest.setFile(inDest);
0360 
0361     mFileAccess->setStatusText(QString());
0362     if(!mFileAccess->isNormal() || !dest.isNormal()) return false;
0363 
0364     qint32 permissions = (mFileAccess->isExecutable() ? 0111 : 0) + (mFileAccess->isWritable() ? 0222 : 0) + (mFileAccess->isReadable() ? 0444 : 0);
0365     m_bSuccess = false;
0366     KIO::FileCopyJob* pJob = KIO::file_copy(mFileAccess->url(), dest.url(), permissions, KIO::HideProgressInfo|KIO::Overwrite);
0367     chk_connect(pJob, &KIO::FileCopyJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0368     chk_connect(pJob, SIGNAL(percent(KJob*,ulong)), &pp, SLOT(slotPercent(KJob*,ulong)));
0369     chk_connect(pJob, &KIO::FileCopyJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0370 
0371     ProgressProxy::enterEventLoop(pJob,
0372                                   i18n("Copying file: %1 -> %2", mFileAccess->prettyAbsPath(), dest.prettyAbsPath()));
0373 
0374     return m_bSuccess;
0375     // Note that the KIO-slave preserves the original date, if this is supported.
0376 }
0377 
0378 bool DefaultFileAccessJobHandler::listDir(DirectoryList* pDirList, bool bRecursive, bool bFindHidden, const QString& filePattern,
0379                                    const QString& fileAntiPattern, const QString& dirAntiPattern, bool bFollowDirLinks, IgnoreList& ignoreList)
0380 {
0381     ProgressProxyExtender pp;
0382     m_pDirList = pDirList;
0383     m_pDirList->clear();
0384     m_bFindHidden = bFindHidden;
0385     m_bRecursive = bRecursive;
0386     m_bFollowDirLinks = bFollowDirLinks; // Only relevant if bRecursive==true.
0387     m_fileAntiPattern = fileAntiPattern;
0388     m_filePattern = filePattern;
0389     m_dirAntiPattern = dirAntiPattern;
0390 
0391     if(ProgressProxy::wasCancelled())
0392         return true; // Cancelled is not an error.
0393 
0394     ProgressProxy::setInformation(i18nc("Status message", "Reading folder: %1", mFileAccess->absoluteFilePath()), 0, false);
0395     qCInfo(kdiffFileAccess) << "Reading folder: " << mFileAccess->absoluteFilePath();
0396 
0397     if(mFileAccess->isLocal())
0398     {
0399         m_bSuccess = true;
0400         QDir dir(mFileAccess->absoluteFilePath());
0401 
0402         dir.setSorting(QDir::Name | QDir::DirsFirst);
0403         if(bFindHidden)
0404             dir.setFilter(QDir::Files | QDir::Dirs | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
0405         else
0406             dir.setFilter(QDir::Files | QDir::Dirs | QDir::System | QDir::NoDotAndDotDot);
0407 
0408         const QFileInfoList fiList = dir.entryInfoList();
0409         if(fiList.isEmpty())
0410         {
0411             /*
0412                 Sadly Qt provides no error information making this case ambiguous.
0413                 A readability check is the best we can do.
0414             */
0415             m_bSuccess = dir.isReadable();
0416         }
0417         else
0418         {
0419             for(const QFileInfo& fi: fiList) // for each file...
0420             {
0421                 if(ProgressProxy::wasCancelled())
0422                     break;
0423 
0424                 assert(fi.fileName() != "." && fi.fileName() != "..");
0425 
0426                 FileAccess fa;
0427 
0428                 fa.setFile(mFileAccess, fi);
0429                 pDirList->push_back(fa);
0430             }
0431         }
0432     }
0433     else
0434     {
0435         KIO::ListJob* pListJob = nullptr;
0436 #if KF_VERSION < KF_VERSION_CHECK(5, 240, 0)
0437         pListJob = KIO::listDir(mFileAccess->url(), KIO::HideProgressInfo, true /*bFindHidden*/);
0438 #else
0439         pListJob = KIO::listDir(mFileAccess->url(), KIO::HideProgressInfo, KIO::ListJob::ListFlag::IncludeHidden /*bFindHidden*/);
0440 #endif
0441         m_bSuccess = false;
0442         if(pListJob != nullptr)
0443         {
0444             chk_connect(pListJob, &KIO::ListJob::entries, this, &DefaultFileAccessJobHandler::slotListDirProcessNewEntries);
0445             chk_connect(pListJob, &KIO::ListJob::result, this, &DefaultFileAccessJobHandler::slotSimpleJobResult);
0446             chk_connect(pListJob, &KIO::ListJob::finished, this, &DefaultFileAccessJobHandler::slotJobEnded);
0447             chk_connect(pListJob, &KIO::ListJob::infoMessage, &pp, &ProgressProxyExtender::slotListDirInfoMessage);
0448 
0449             // This line makes the transfer via fish unreliable.:-(
0450             /*if(mFileAccess->url().scheme() != u8"fish"){
0451                 chk_connect( pListJob, static_cast<void (KIO::ListJob::*)(KJob*,qint64)>(&KIO::ListJob::percent), &pp, &ProgressProxyExtender::slotPercent);
0452             }*/
0453 
0454             ProgressProxy::enterEventLoop(pListJob,
0455                                           i18n("Listing directory: %1", mFileAccess->prettyAbsPath()));
0456         }
0457     }
0458 
0459     ignoreList.enterDir(mFileAccess->absoluteFilePath(), *pDirList);
0460     mFileAccess->filterList(mFileAccess->absoluteFilePath(), pDirList, filePattern, fileAntiPattern, dirAntiPattern, ignoreList);
0461 
0462     if(bRecursive)
0463     {
0464         DirectoryList::iterator i;
0465         DirectoryList subDirsList;
0466 
0467         for(i = m_pDirList->begin(); i != m_pDirList->end(); ++i)
0468         {
0469             assert(i->isValid());
0470             if(i->isDir() && (!i->isSymLink() || m_bFollowDirLinks))
0471             {
0472                 DirectoryList dirList;
0473                 i->listDir(&dirList, bRecursive, bFindHidden,
0474                            filePattern, fileAntiPattern, dirAntiPattern, bFollowDirLinks, ignoreList);
0475 
0476                 // append data onto the main list
0477                 subDirsList.splice(subDirsList.end(), dirList);
0478             }
0479         }
0480 
0481         m_pDirList->splice(m_pDirList->end(), subDirsList);
0482     }
0483 
0484     return m_bSuccess;
0485 }
0486 
0487 void DefaultFileAccessJobHandler::slotListDirProcessNewEntries(KIO::Job*, const KIO::UDSEntryList& l)
0488 {
0489     //This function is called for non-local urls. Don't use QUrl::fromLocalFile here as it does not handle these.
0490     for(const KIO::UDSEntry& e: l)
0491     {
0492         FileAccess fa;
0493 
0494         fa.setFromUdsEntry(e, mFileAccess);
0495 
0496         //must be manually filtered KDE does not supply API for ignoring these.
0497         if(fa.fileName() != "." && fa.fileName() != ".." && fa.isValid())
0498         {
0499             m_pDirList->push_back(std::move(fa));
0500         }
0501     }
0502 }