1 /*****************************************************************************
2  * Copyright (C) 2000 Rafi Yanai <krusader@users.sourceforge.net>            *
3  * Copyright (C) 2004-2019 Krusader Krew [https://krusader.org]              *
4  *                                                                           *
5  * This file is part of Krusader [https://krusader.org].                     *
6  *                                                                           *
7  * Krusader is free software: you can redistribute it and/or modify          *
8  * it under the terms of the GNU General Public License as published by      *
9  * the Free Software Foundation, either version 2 of the License, or         *
10  * (at your option) any later version.                                       *
11  *                                                                           *
12  * Krusader is distributed in the hope that it will be useful,               *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
15  * GNU General Public License for more details.                              *
16  *                                                                           *
17  * You should have received a copy of the GNU General Public License         *
18  * along with Krusader.  If not, see [http://www.gnu.org/licenses/].         *
19  *****************************************************************************/
20 
21 #include "defaultfilesystem.h"
22 
23 // QtCore
24 #include <QDebug>
25 #include <QDir>
26 #include <QEventLoop>
27 
28 #include <KConfigCore/KSharedConfig>
29 #include <KCoreAddons/KUrlMimeData>
30 #include <KI18n/KLocalizedString>
31 #include <KIO/DropJob>
32 #include <KIO/MkpathJob>
33 #include <KIO/FileUndoManager>
34 #include <KIO/JobUiDelegate>
35 #include <KIO/ListJob>
36 #include <KIOCore/KDiskFreeSpaceInfo>
37 #include <KIOCore/KFileItem>
38 #include <KIOCore/KMountPoint>
39 #include <KIOCore/KProtocolManager>
40 #include <kio_version.h>
41 
42 #include "fileitem.h"
43 #include "../defaults.h"
44 #include "../krglobal.h"
45 #include "../krservices.h"
46 #include "../JobMan/krjob.h"
47 
DefaultFileSystem()48 DefaultFileSystem::DefaultFileSystem(): FileSystem(), _watcher()
49 {
50     _type = FS_DEFAULT;
51 }
52 
copyFiles(const QList<QUrl> & urls,const QUrl & destination,KIO::CopyJob::CopyMode mode,bool showProgressInfo,JobMan::StartMode startMode)53 void DefaultFileSystem::copyFiles(const QList<QUrl> &urls, const QUrl &destination,
54                             KIO::CopyJob::CopyMode mode, bool showProgressInfo,
55                             JobMan::StartMode startMode)
56 {
57     // resolve relative path before resolving symlinks
58     const QUrl dest = resolveRelativePath(destination);
59 
60     KIO::JobFlags flags = showProgressInfo ? KIO::DefaultFlags : KIO::HideProgressInfo;
61 
62     KrJob *krJob = KrJob::createCopyJob(mode, urls, dest, flags);
63     // destination can be a full path with filename when copying/moving a single file
64     const QUrl destDir = dest.adjusted(QUrl::RemoveFilename);
65     connect(krJob, &KrJob::started, this, [=](KIO::Job *job) { connectJobToDestination(job, destDir); });
66     if (mode == KIO::CopyJob::Move) { // notify source about removed files
67         connect(krJob, &KrJob::started, this, [=](KIO::Job *job) { connectJobToSources(job, urls); });
68     }
69 
70     krJobMan->manageJob(krJob, startMode);
71 }
72 
dropFiles(const QUrl & destination,QDropEvent * event)73 void DefaultFileSystem::dropFiles(const QUrl &destination, QDropEvent *event)
74 {
75     qDebug() << "destination=" << destination;
76 
77     // resolve relative path before resolving symlinks
78     const QUrl dest = resolveRelativePath(destination);
79 
80     KIO::DropJob *job = KIO::drop(event, dest);
81 #if KIO_VERSION >= QT_VERSION_CHECK(5, 30, 0)
82     // NOTE: a DropJob "starts" with showing a menu. If the operation is choosen (copy/move/link)
83     // the actual CopyJob starts automatically - we cannot manage the start of the CopyJob (see
84     // documentation for KrJob)
85     connect(job, &KIO::DropJob::copyJobStarted, this, [=](KIO::CopyJob *kJob) {
86         connectJobToDestination(job, dest); // now we have to refresh the destination
87 
88         KrJob *krJob = KrJob::createDropJob(job, kJob);
89         krJobMan->manageStartedJob(krJob, kJob);
90         if (kJob->operationMode() == KIO::CopyJob::Move) { // notify source about removed files
91             connectJobToSources(kJob, kJob->srcUrls());
92         }
93     });
94 #else
95     // NOTE: DropJob does not provide information about the actual user choice
96     // (move/copy/link/abort). We have to assume the worst (move)
97     connectJobToDestination(job, dest);
98     connectJobToSources(job, KUrlMimeData::urlsFromMimeData(event->mimeData()));
99 #endif
100 }
101 
addFiles(const QList<QUrl> & fileUrls,KIO::CopyJob::CopyMode mode,const QString & dir)102 void DefaultFileSystem::addFiles(const QList<QUrl> &fileUrls, KIO::CopyJob::CopyMode mode,
103                                  const QString &dir)
104 {
105     QUrl destination(_currentDirectory);
106     if (!dir.isEmpty()) {
107         destination.setPath(QDir::cleanPath(destination.path() + '/' + dir));
108         const QString scheme = destination.scheme();
109         if (scheme == "tar" || scheme == "zip" || scheme == "krarc") {
110             if (QDir(destination.path()).exists())
111                 // if we get out from the archive change the protocol
112                 destination.setScheme("file");
113         }
114     }
115 
116     destination = ensureTrailingSlash(destination); // destination is always a directory
117     copyFiles(fileUrls, destination, mode);
118 }
119 
mkDir(const QString & name)120 void DefaultFileSystem::mkDir(const QString &name)
121 {
122     KJob *job;
123     if (name.contains('/')) {
124         job = KIO::mkpath(getUrl(name));
125     } else {
126         job = KIO::mkdir(getUrl(name));
127     }
128     connectJobToDestination(job, currentDirectory());
129 }
130 
rename(const QString & oldName,const QString & newName)131 void DefaultFileSystem::rename(const QString &oldName, const QString &newName)
132 {
133     const QUrl oldUrl = getUrl(oldName);
134     const QUrl newUrl = getUrl(newName);
135     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
136     connectJobToDestination(job, currentDirectory());
137 
138     KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job);
139 }
140 
getUrl(const QString & name) const141 QUrl DefaultFileSystem::getUrl(const QString& name) const
142 {
143     // NOTE: on non-local fs file URL does not have to be path + name!
144     FileItem *fileItem = getFileItem(name);
145     if (fileItem)
146         return fileItem->getUrl();
147 
148     QUrl absoluteUrl(_currentDirectory);
149     if (name.startsWith('/')) {
150         absoluteUrl.setPath(name);
151     } else {
152         absoluteUrl.setPath(absoluteUrl.path() + '/' + name);
153     }
154     return absoluteUrl;
155 }
156 
updateFilesystemInfo()157 void DefaultFileSystem::updateFilesystemInfo()
158 {
159     if (!KConfigGroup(krConfig, "Look&Feel").readEntry("ShowSpaceInformation", true)) {
160         _mountPoint = "";
161         emit fileSystemInfoChanged(i18n("Space information disabled"), "", 0, 0);
162         return;
163     }
164 
165     // TODO get space info for trash:/ with KIO spaceInfo job
166     if (!_currentDirectory.isLocalFile()) {
167         _mountPoint = "";
168         emit fileSystemInfoChanged(i18n("No space information on non-local filesystems"), "", 0, 0);
169         return;
170     }
171 
172     const QString path = _currentDirectory.path();
173     const KDiskFreeSpaceInfo info = KDiskFreeSpaceInfo::freeSpaceInfo(path);
174     if (!info.isValid()) {
175         _mountPoint = "";
176         emit fileSystemInfoChanged(i18n("Space information unavailable"), "", 0, 0);
177         return;
178     }
179     _mountPoint = info.mountPoint();
180 
181     const KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(path);
182     const QString fsType = mountPoint ? mountPoint->mountType() : "";
183 
184     emit fileSystemInfoChanged("", fsType, info.size(), info.available());
185 }
186 
187 // ==== protected ====
188 
refreshInternal(const QUrl & directory,bool onlyScan)189 bool DefaultFileSystem::refreshInternal(const QUrl &directory, bool onlyScan)
190 {
191     qDebug() << "refresh internal to URL=" << directory.toDisplayString();
192     if (!KProtocolManager::supportsListing(directory)) {
193         emit error(i18n("Protocol not supported by Krusader:\n%1", directory.url()));
194         return false;
195     }
196 
197     delete _watcher; // stop watching the old dir
198 
199     if (directory.isLocalFile()) {
200         qDebug() << "start local refresh to URL=" << directory.toDisplayString();
201         // we could read local directories with KIO but using Qt is a lot faster!
202         return refreshLocal(directory, onlyScan);
203     }
204 
205     _currentDirectory = cleanUrl(directory);
206 
207     // start the listing job
208     KIO::ListJob *job = KIO::listDir(_currentDirectory, KIO::HideProgressInfo, showHiddenFiles());
209     connect(job, &KIO::ListJob::entries, this, &DefaultFileSystem::slotAddFiles);
210     connect(job, &KIO::ListJob::redirection, this, &DefaultFileSystem::slotRedirection);
211     connect(job, &KIO::ListJob::permanentRedirection, this, &DefaultFileSystem::slotRedirection);
212     connect(job, &KIO::Job::result, this, &DefaultFileSystem::slotListResult);
213 
214     // ensure connection credentials are asked only once
215     if(!parentWindow.isNull()) {
216         KIO::JobUiDelegate *ui = static_cast<KIO::JobUiDelegate*>(job->uiDelegate());
217         ui->setWindow(parentWindow);
218     }
219 
220     emit refreshJobStarted(job);
221 
222     _listError = false;
223     // ugly: we have to wait here until the list job is finished
224     QEventLoop eventLoop;
225     connect(job, &KJob::finished, &eventLoop, &QEventLoop::quit);
226     eventLoop.exec(); // blocking until quit()
227 
228     return !_listError;
229 }
230 
231 // ==== protected slots ====
232 
slotListResult(KJob * job)233 void DefaultFileSystem::slotListResult(KJob *job)
234 {
235     qDebug() << "got list result";
236     if (job && job->error()) {
237         // we failed to refresh
238         _listError = true;
239         qDebug() << "error=" << job->errorString() << "; text=" << job->errorText();
240         emit error(job->errorString()); // display error message (in panel)
241     }
242 }
243 
slotAddFiles(KIO::Job *,const KIO::UDSEntryList & entries)244 void DefaultFileSystem::slotAddFiles(KIO::Job *, const KIO::UDSEntryList& entries)
245 {
246     for (const KIO::UDSEntry entry : entries) {
247         FileItem *fileItem = FileSystem::createFileItemFromKIO(entry, _currentDirectory);
248         if (fileItem) {
249             addFileItem(fileItem);
250         }
251     }
252 }
253 
slotRedirection(KIO::Job * job,const QUrl & url)254 void DefaultFileSystem::slotRedirection(KIO::Job *job, const QUrl &url)
255 {
256    qDebug() << "redirection to URL=" << url.toDisplayString();
257 
258    // some protocols (zip, tar) send redirect to local URL without scheme
259    const QUrl newUrl = preferLocalUrl(url);
260 
261    if (newUrl.scheme() != _currentDirectory.scheme()) {
262        // abort and start over again,
263        // some protocols (iso, zip, tar) do this on transition to local fs
264        job->kill();
265        _isRefreshing = false;
266        refresh(newUrl);
267        return;
268    }
269 
270     _currentDirectory = cleanUrl(newUrl);
271 }
272 
slotWatcherCreated(const QString & path)273 void DefaultFileSystem::slotWatcherCreated(const QString& path)
274 {
275     qDebug() << "path created (doing nothing): " << path;
276 }
277 
slotWatcherDirty(const QString & path)278 void DefaultFileSystem::slotWatcherDirty(const QString& path)
279 {
280     qDebug() << "path dirty: " << path;
281     if (path == realPath()) {
282         // this happens
283         //   1. if a directory was created/deleted/renamed inside this directory.
284         //   2. during and after a file operation (create/delete/rename/touch) inside this directory
285         // KDirWatcher doesn't reveal the name of changed directories and we have to refresh.
286         // (QFileSystemWatcher in Qt5.7 can't help here either)
287         refresh();
288         return;
289     }
290 
291     const QString name = QUrl::fromLocalFile(path).fileName();
292 
293     FileItem *fileItem = getFileItem(name);
294     if (!fileItem) {
295         qWarning() << "file not found (unexpected), path=" << path;
296         // this happens at least for cifs mounted filesystems: when a new file is created, a dirty
297         // signal with its file path but no other signals are sent (buggy behaviour of KDirWatch)
298         refresh();
299         return;
300     }
301 
302     // we have an updated file..
303     FileItem *newFileItem = createLocalFileItem(name);
304     addFileItem(newFileItem);
305     emit updatedFileItem(newFileItem);
306 
307     delete fileItem;
308 }
309 
slotWatcherDeleted(const QString & path)310 void DefaultFileSystem::slotWatcherDeleted(const QString& path)
311 {
312     qDebug() << "path deleted: " << path;
313     if (path != _currentDirectory.toLocalFile()) {
314         // ignore deletion of files here, a 'dirty' signal will be send anyway
315         return;
316     }
317 
318     // the current directory was deleted. Try a refresh, which will fail. An error message will
319     // be emitted and the empty (non-existing) directory remains.
320     refresh();
321 }
322 
refreshLocal(const QUrl & directory,bool onlyScan)323 bool DefaultFileSystem::refreshLocal(const QUrl &directory, bool onlyScan) {
324     const QString path = KrServices::urlToLocalPath(directory);
325 
326 #ifdef Q_WS_WIN
327     if (!path.contains("/")) { // change C: to C:/
328         path = path + QString("/");
329     }
330 #endif
331 
332     // check if the new directory exists
333     if (!QDir(path).exists()) {
334         emit error(i18n("The folder %1 does not exist.", path));
335         return false;
336     }
337 
338     // mount if needed
339     emit aboutToOpenDir(path);
340 
341     // set the current directory...
342     _currentDirectory = directory;
343     _currentDirectory.setPath(QDir::cleanPath(_currentDirectory.path()));
344 
345     // Note: we are using low-level Qt functions here.
346     // It's around twice as fast as using the QDir class.
347 
348     QT_DIR* dir = QT_OPENDIR(path.toLocal8Bit());
349     if (!dir) {
350         emit error(i18n("Cannot open the folder %1.", path));
351         return false;
352     }
353 
354     // change directory to the new directory
355     const QString savedDir = QDir::currentPath();
356     if (!QDir::setCurrent(path)) {
357         emit error(i18nc("%1=folder path", "Access to %1 denied", path));
358         QT_CLOSEDIR(dir);
359         return false;
360     }
361 
362     QT_DIRENT* dirEnt;
363     QString name;
364     const bool showHidden = showHiddenFiles();
365     while ((dirEnt = QT_READDIR(dir)) != NULL) {
366         name = QString::fromLocal8Bit(dirEnt->d_name);
367 
368         // show hidden files?
369         if (!showHidden && name.left(1) == ".") continue;
370         // we don't need the "." and ".." entries
371         if (name == "." || name == "..") continue;
372 
373         FileItem* temp = createLocalFileItem(name);
374         addFileItem(temp);
375     }
376     // clean up
377     QT_CLOSEDIR(dir);
378     QDir::setCurrent(savedDir);
379 
380     if (!onlyScan) {
381         // start watching the new dir for file changes
382         _watcher = new KDirWatch(this);
383         // if the current dir is a link path the watcher needs to watch the real path - and signal
384         // parameters will be the real path
385         _watcher->addDir(realPath(), KDirWatch::WatchFiles);
386         connect(_watcher.data(), &KDirWatch::dirty, this, &DefaultFileSystem::slotWatcherDirty);
387         // NOTE: not connecting 'created' signal. A 'dirty' is send after that anyway
388         //connect(_watcher, SIGNAL(created(QString)), this, SLOT(slotWatcherCreated(QString)));
389         connect(_watcher.data(), &KDirWatch::deleted, this, &DefaultFileSystem::slotWatcherDeleted);
390         _watcher->startScan(false);
391     }
392 
393     return true;
394 }
395 
createLocalFileItem(const QString & name)396 FileItem *DefaultFileSystem::createLocalFileItem(const QString &name)
397 {
398     return FileSystem::createLocalFileItem(name, _currentDirectory.path());
399 }
400 
realPath()401 QString DefaultFileSystem::DefaultFileSystem::realPath()
402 {
403     // NOTE: current dir must exist
404     return QDir(_currentDirectory.toLocalFile()).canonicalPath();
405 }
406 
resolveRelativePath(const QUrl & url)407 QUrl DefaultFileSystem::resolveRelativePath(const QUrl &url)
408 {
409     // if e.g. "/tmp/bin" is a link to "/bin",
410     // resolve "/tmp/bin/.." to "/tmp" and not "/"
411     return url.adjusted(QUrl::NormalizePathSegments);
412 }
413