File indexing completed on 2024-05-05 17:56:54

0001 /*
0002     SPDX-FileCopyrightText: 2000 Rafi Yanai <krusader@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2004-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "defaultfilesystem.h"
0009 
0010 // QtCore
0011 #include <QDebug>
0012 #include <QDir>
0013 #include <QEventLoop>
0014 
0015 #include <KConfigCore/KSharedConfig>
0016 #include <KCoreAddons/KUrlMimeData>
0017 #include <KI18n/KLocalizedString>
0018 #include <KIO/DropJob>
0019 #include <KIO/FileUndoManager>
0020 #include <KIO/JobUiDelegate>
0021 #include <KIO/ListJob>
0022 #include <KIO/MkpathJob>
0023 #include <KIOCore/KDiskFreeSpaceInfo>
0024 #include <KIOCore/KFileItem>
0025 #include <KIOCore/KMountPoint>
0026 #include <KIOCore/KProtocolManager>
0027 #include <kio_version.h>
0028 
0029 #include "../JobMan/krjob.h"
0030 #include "../defaults.h"
0031 #include "../krglobal.h"
0032 #include "../krservices.h"
0033 #include "fileitem.h"
0034 
0035 DefaultFileSystem::DefaultFileSystem()
0036 {
0037     _type = FS_DEFAULT;
0038 }
0039 
0040 void DefaultFileSystem::copyFiles(const QList<QUrl> &urls,
0041                                   const QUrl &destination,
0042                                   KIO::CopyJob::CopyMode mode,
0043                                   bool showProgressInfo,
0044                                   JobMan::StartMode startMode)
0045 {
0046     // resolve relative path before resolving symlinks
0047     const QUrl dest = resolveRelativePath(destination);
0048 
0049     KIO::JobFlags flags = showProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo;
0050 
0051     KrJob *krJob = KrJob::createCopyJob(mode, urls, dest, flags);
0052     // destination can be a full path with filename when copying/moving a single file
0053     const QUrl destDir = dest.adjusted(QUrl::RemoveFilename);
0054     connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
0055         connectJobToDestination(job, destDir);
0056     });
0057     if (mode == KIO::CopyJob::Move) { // notify source about removed files
0058         connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
0059             connectJobToSources(job, urls);
0060         });
0061     }
0062 
0063     krJobMan->manageJob(krJob, startMode);
0064 }
0065 
0066 void DefaultFileSystem::dropFiles(const QUrl &destination, QDropEvent *event)
0067 {
0068     qDebug() << "destination=" << destination;
0069 
0070     // resolve relative path before resolving symlinks
0071     const QUrl dest = resolveRelativePath(destination);
0072 
0073     KIO::DropJob *job = KIO::drop(event, dest);
0074 
0075     // NOTE: a DropJob "starts" with showing a menu. If the operation is chosen (copy/move/link)
0076     // the actual CopyJob starts automatically - we cannot manage the start of the CopyJob (see
0077     // documentation for KrJob)
0078     connect(job, &KIO::DropJob::copyJobStarted, this, [=](KIO::CopyJob *kJob) {
0079         connectJobToDestination(job, dest); // now we have to refresh the destination
0080 
0081         KrJob *krJob = KrJob::createDropJob(job, kJob);
0082         krJobMan->manageStartedJob(krJob, kJob);
0083         if (kJob->operationMode() == KIO::CopyJob::Move) { // notify source about removed files
0084             connectJobToSources(kJob, kJob->srcUrls());
0085         }
0086     });
0087 }
0088 
0089 void DefaultFileSystem::addFiles(const QList<QUrl> &fileUrls, KIO::CopyJob::CopyMode mode, const QString &dir)
0090 {
0091     QUrl destination(_currentDirectory);
0092     if (!dir.isEmpty()) {
0093         destination.setPath(QDir::cleanPath(destination.path() + '/' + dir));
0094         const QString scheme = destination.scheme();
0095         if (scheme == "tar" || scheme == "zip" || scheme == "krarc") {
0096             if (QDir(destination.path()).exists())
0097                 // if we get out from the archive change the protocol
0098                 destination.setScheme("file");
0099         }
0100     }
0101 
0102     destination = ensureTrailingSlash(destination); // destination is always a directory
0103     copyFiles(fileUrls, destination, mode);
0104 }
0105 
0106 void DefaultFileSystem::mkDir(const QString &name)
0107 {
0108     KJob *job;
0109     if (name.contains('/')) {
0110         job = KIO::mkpath(getUrl(name));
0111     } else {
0112         job = KIO::mkdir(getUrl(name));
0113     }
0114     connectJobToDestination(job, currentDirectory());
0115 }
0116 
0117 void DefaultFileSystem::rename(const QString &oldName, const QString &newName)
0118 {
0119     const QUrl oldUrl = getUrl(oldName);
0120     const QUrl newUrl = getUrl(newName);
0121     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
0122     connectJobToDestination(job, currentDirectory());
0123 
0124     KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job);
0125 }
0126 
0127 QUrl DefaultFileSystem::getUrl(const QString &name) const
0128 {
0129     // NOTE: on non-local fs file URL does not have to be path + name!
0130     FileItem *fileItem = getFileItem(name);
0131     if (fileItem)
0132         return fileItem->getUrl();
0133 
0134     QUrl absoluteUrl(_currentDirectory);
0135     if (name.startsWith('/')) {
0136         absoluteUrl.setPath(name);
0137     } else {
0138         absoluteUrl.setPath(absoluteUrl.path() + '/' + name);
0139     }
0140     return absoluteUrl;
0141 }
0142 
0143 void DefaultFileSystem::updateFilesystemInfo()
0144 {
0145     if (!KConfigGroup(krConfig, "Look&Feel").readEntry("ShowSpaceInformation", true)) {
0146         _mountPoint = "";
0147         emit fileSystemInfoChanged(i18n("Space information disabled"), "", 0, 0);
0148         return;
0149     }
0150 
0151     // TODO get space info for trash:/ with KIO spaceInfo job
0152     if (!_currentDirectory.isLocalFile()) {
0153         _mountPoint = "";
0154         emit fileSystemInfoChanged(i18n("No space information on non-local filesystems"), "", 0, 0);
0155         return;
0156     }
0157 
0158     const QString path = _currentDirectory.path();
0159     const KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(path);
0160     if (!info.isValid()) {
0161         _mountPoint = "";
0162         emit fileSystemInfoChanged(i18n("Space information unavailable"), "", 0, 0);
0163         return;
0164     }
0165     _mountPoint = info.mountPoint();
0166 
0167     const KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(path);
0168     const QString fsType = mountPoint ? mountPoint->mountType() : "";
0169 
0170     emit fileSystemInfoChanged("", fsType, info.size(), info.available());
0171 }
0172 
0173 // ==== protected ====
0174 
0175 bool DefaultFileSystem::refreshInternal(const QUrl &directory, bool onlyScan)
0176 {
0177     qDebug() << "refresh internal to URL=" << directory.toDisplayString();
0178     if (!KProtocolManager::supportsListing(directory)) {
0179         emit error(i18n("Protocol not supported by Krusader:\n%1", directory.url()));
0180         return false;
0181     }
0182 
0183     delete _watcher; // stop watching the old dir
0184 
0185     if (directory.isLocalFile()) {
0186         qDebug() << "start local refresh to URL=" << directory.toDisplayString();
0187         // we could read local directories with KIO but using Qt is a lot faster!
0188         return refreshLocal(directory, onlyScan);
0189     }
0190 
0191     _currentDirectory = cleanUrl(directory);
0192 
0193     // start the listing job
0194     KIO::ListJob *job = KIO::listDir(_currentDirectory, KIO::HideProgressInfo, showHiddenFiles());
0195     connect(job, &KIO::ListJob::entries, this, &DefaultFileSystem::slotAddFiles);
0196     connect(job, &KIO::ListJob::redirection, this, &DefaultFileSystem::slotRedirection);
0197     connect(job, &KIO::ListJob::permanentRedirection, this, &DefaultFileSystem::slotRedirection);
0198     connect(job, &KIO::Job::result, this, &DefaultFileSystem::slotListResult);
0199 
0200     // ensure connection credentials are asked only once
0201     if (!parentWindow.isNull()) {
0202         auto *ui = dynamic_cast<KIO::JobUiDelegate *>(job->uiDelegate());
0203         ui->setWindow(parentWindow);
0204     }
0205 
0206     emit refreshJobStarted(job);
0207 
0208     _listError = false;
0209     // ugly: we have to wait here until the list job is finished
0210     QEventLoop eventLoop;
0211     connect(job, &KJob::finished, &eventLoop, &QEventLoop::quit);
0212     eventLoop.exec(); // blocking until quit()
0213 
0214     return !_listError;
0215 }
0216 
0217 // ==== protected slots ====
0218 
0219 void DefaultFileSystem::slotListResult(KJob *job)
0220 {
0221     qDebug() << "got list result";
0222     if (job && job->error()) {
0223         // we failed to refresh
0224         _listError = true;
0225         qDebug() << "error=" << job->errorString() << "; text=" << job->errorText();
0226         emit error(job->errorString()); // display error message (in panel)
0227     }
0228 }
0229 
0230 void DefaultFileSystem::slotAddFiles(KIO::Job *, const KIO::UDSEntryList &entries)
0231 {
0232     for (const KIO::UDSEntry &entry : entries) {
0233         FileItem *fileItem = FileSystem::createFileItemFromKIO(entry, _currentDirectory);
0234         if (fileItem) {
0235             addFileItem(fileItem);
0236         }
0237     }
0238 }
0239 
0240 void DefaultFileSystem::slotRedirection(KIO::Job *job, const QUrl &url)
0241 {
0242     qDebug() << "redirection to URL=" << url.toDisplayString();
0243 
0244     // some protocols (zip, tar) send redirect to local URL without scheme
0245     const QUrl newUrl = preferLocalUrl(url);
0246 
0247     if (newUrl.scheme() != _currentDirectory.scheme()) {
0248         // abort and start over again,
0249         // some protocols (iso, zip, tar) do this on transition to local fs
0250         job->kill();
0251         _isRefreshing = false;
0252         refresh(newUrl);
0253         return;
0254     }
0255 
0256     _currentDirectory = cleanUrl(newUrl);
0257 }
0258 
0259 void DefaultFileSystem::slotWatcherCreated(const QString &path)
0260 {
0261     qDebug() << "path created (doing nothing): " << path;
0262 }
0263 
0264 void DefaultFileSystem::slotWatcherDirty(const QString &path)
0265 {
0266     qDebug() << "path dirty: " << path;
0267     if (path == realPath()) {
0268         // this happens
0269         //   1. if a directory was created/deleted/renamed inside this directory.
0270         //   2. during and after a file operation (create/delete/rename/touch) inside this directory
0271         // KDirWatcher doesn't reveal the name of changed directories and we have to refresh.
0272         // (QFileSystemWatcher in Qt5.7 can't help here either)
0273         refresh();
0274         return;
0275     }
0276 
0277     const QString name = QUrl::fromLocalFile(path).fileName();
0278 
0279     FileItem *fileItem = getFileItem(name);
0280     if (!fileItem) {
0281         qWarning() << "file not found (unexpected), path=" << path;
0282         // this happens at least for cifs mounted filesystems: when a new file is created, a dirty
0283         // signal with its file path but no other signals are sent (buggy behaviour of KDirWatch)
0284         refresh();
0285         return;
0286     }
0287 
0288     // we have an updated file..
0289     FileItem *newFileItem = createLocalFileItem(name);
0290     addFileItem(newFileItem);
0291     emit updatedFileItem(newFileItem);
0292 
0293     delete fileItem;
0294 }
0295 
0296 void DefaultFileSystem::slotWatcherDeleted(const QString &path)
0297 {
0298     qDebug() << "path deleted: " << path;
0299     if (path != _currentDirectory.toLocalFile()) {
0300         // ignore deletion of files here, a 'dirty' signal will be send anyway
0301         return;
0302     }
0303 
0304     // the current directory was deleted. Try a refresh, which will fail. An error message will
0305     // be emitted and the empty (non-existing) directory remains.
0306     refresh();
0307 }
0308 
0309 bool DefaultFileSystem::refreshLocal(const QUrl &directory, bool onlyScan)
0310 {
0311     const QString path = KrServices::urlToLocalPath(directory);
0312 
0313 #ifdef Q_OS_WIN
0314     if (!path.contains("/")) { // change C: to C:/
0315         path = path + QString("/");
0316     }
0317 #endif
0318 
0319     // check if the new directory exists
0320     if (!QDir(path).exists()) {
0321         emit error(i18n("The folder %1 does not exist.", path));
0322         return false;
0323     }
0324 
0325     // mount if needed
0326     emit aboutToOpenDir(path);
0327 
0328     // set the current directory...
0329     _currentDirectory = directory;
0330     _currentDirectory.setPath(QDir::cleanPath(_currentDirectory.path()));
0331 
0332     // Note: we are using low-level Qt functions here.
0333     // It's around twice as fast as using the QDir class.
0334 
0335     QT_DIR *dir = QT_OPENDIR(path.toLocal8Bit());
0336     if (!dir) {
0337         emit error(i18n("Cannot open the folder %1.", path));
0338         return false;
0339     }
0340 
0341     // change directory to the new directory
0342     const QString savedDir = QDir::currentPath();
0343     if (!QDir::setCurrent(path)) {
0344         emit error(i18nc("%1=folder path", "Access to %1 denied", path));
0345         QT_CLOSEDIR(dir);
0346         return false;
0347     }
0348 
0349     QT_DIRENT *dirEnt;
0350     QString name;
0351     const bool showHidden = showHiddenFiles();
0352     QSet<QString> hiddenFiles = filesInDotHidden(path);
0353     while ((dirEnt = QT_READDIR(dir)) != nullptr) {
0354         name = QString::fromLocal8Bit(dirEnt->d_name);
0355 
0356         // show hidden files?
0357         if (!showHidden && name.left(1) == ".")
0358             continue;
0359         // show file in .hidden file?
0360         if (!showHidden && hiddenFiles.contains(name))
0361             continue;
0362         // we don't need the "." and ".." entries
0363         if (name == "." || name == "..")
0364             continue;
0365 
0366         FileItem *temp = createLocalFileItem(name);
0367         addFileItem(temp);
0368     }
0369     // clean up
0370     QT_CLOSEDIR(dir);
0371     QDir::setCurrent(savedDir);
0372 
0373     if (!onlyScan) {
0374         // start watching the new dir for file changes
0375         _watcher = new KDirWatch(this);
0376         // if the current dir is a link path the watcher needs to watch the real path - and signal
0377         // parameters will be the real path
0378         _watcher->addDir(realPath(), KDirWatch::WatchFiles);
0379         connect(_watcher.data(), &KDirWatch::dirty, this, &DefaultFileSystem::slotWatcherDirty);
0380         // NOTE: not connecting 'created' signal. A 'dirty' is send after that anyway
0381         // connect(_watcher.data(), &KDirWatch::created, this, &DefaultFileSystem::slotWatcherCreated);
0382         connect(_watcher.data(), &KDirWatch::deleted, this, &DefaultFileSystem::slotWatcherDeleted);
0383         _watcher->startScan(false);
0384     }
0385 
0386     return true;
0387 }
0388 
0389 QSet<QString> DefaultFileSystem::filesInDotHidden(const QString &dir)
0390 {
0391     // code "borrowed" from KIO, Copyright (C) by Bruno Nova <brunomb.nova@gmail.com>
0392     const QString path = dir + QLatin1String("/.hidden");
0393     QFile dotHiddenFile(path);
0394 
0395     if (dotHiddenFile.exists()) {
0396         if (dotHiddenFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
0397             QSet<QString> filesToHide;
0398             QTextStream stream(&dotHiddenFile);
0399             while (!stream.atEnd()) {
0400                 QString name = stream.readLine();
0401                 if (!name.isEmpty()) {
0402                     filesToHide.insert(name);
0403                 }
0404             }
0405             return filesToHide;
0406         }
0407     }
0408 
0409     return QSet<QString>();
0410 }
0411 
0412 FileItem *DefaultFileSystem::createLocalFileItem(const QString &name)
0413 {
0414     return FileSystem::createLocalFileItem(name, _currentDirectory.path());
0415 }
0416 
0417 QString DefaultFileSystem::realPath()
0418 {
0419     // NOTE: current dir must exist
0420     return QDir(_currentDirectory.toLocalFile()).canonicalPath();
0421 }
0422 
0423 QUrl DefaultFileSystem::resolveRelativePath(const QUrl &url)
0424 {
0425     // if e.g. "/tmp/bin" is a link to "/bin",
0426     // resolve "/tmp/bin/.." to "/tmp" and not "/"
0427     return url.adjusted(QUrl::NormalizePathSegments);
0428 }