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