1 /*****************************************************************************
2  * Copyright (C) 2003 Shie Erlich <erlich@users.sourceforge.net>             *
3  * Copyright (C) 2003 Rafi Yanai <yanai@users.sourceforge.net>               *
4  * Copyright (C) 2004-2019 Krusader Krew [https://krusader.org]              *
5  *                                                                           *
6  * This file is part of Krusader [https://krusader.org].                     *
7  *                                                                           *
8  * Krusader is free software: you can redistribute it and/or modify          *
9  * it under the terms of the GNU General Public License as published by      *
10  * the Free Software Foundation, either version 2 of the License, or         *
11  * (at your option) any later version.                                       *
12  *                                                                           *
13  * Krusader is distributed in the hope that it will be useful,               *
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
16  * GNU General Public License for more details.                              *
17  *                                                                           *
18  * You should have received a copy of the GNU General Public License         *
19  * along with Krusader.  If not, see [http://www.gnu.org/licenses/].         *
20  *****************************************************************************/
21 
22 #include "virtualfilesystem.h"
23 
24 // QtCore
25 #include <QDir>
26 #include <QEventLoop>
27 #include <QUrl>
28 // QtWidgets
29 #include <QApplication>
30 
31 #include <KCoreAddons/KUrlMimeData>
32 #include <KI18n/KLocalizedString>
33 #include <KIO/CopyJob>
34 #include <KIO/DeleteJob>
35 #include <KIO/DirectorySizeJob>
36 #include <KIO/StatJob>
37 #include <KIOCore/KFileItem>
38 #include <KWidgetsAddons/KMessageBox>
39 
40 #include "fileitem.h"
41 #include "../defaults.h"
42 #include "../krglobal.h"
43 #include "../krservices.h"
44 
45 #define VIRTUALFILESYSTEM_DB "virtualfilesystem.db"
46 
47 QHash<QString, QList<QUrl> *> VirtualFileSystem::_virtFilesystemDict;
48 QHash<QString, QString> VirtualFileSystem::_metaInfoDict;
49 
VirtualFileSystem()50 VirtualFileSystem::VirtualFileSystem() : FileSystem()
51 {
52     if (_virtFilesystemDict.isEmpty()) {
53         restore();
54     }
55 
56     _type = FS_VIRTUAL;
57 }
58 
copyFiles(const QList<QUrl> & urls,const QUrl & destination,KIO::CopyJob::CopyMode,bool,JobMan::StartMode)59 void VirtualFileSystem::copyFiles(const QList<QUrl> &urls, const QUrl &destination,
60                          KIO::CopyJob::CopyMode /*mode*/, bool /*showProgressInfo*/,
61                          JobMan::StartMode /*startMode*/)
62 {
63     const QString dir = QDir(destination.path()).absolutePath().remove('/');
64 
65     if (dir.isEmpty()) {
66         showError(i18n("You cannot copy files directly to the 'virt:/' folder.\n"
67                        "You can create a sub folder and copy your files into it."));
68         return;
69     }
70 
71     if (!_virtFilesystemDict.contains(dir)) {
72         mkDirInternal(dir);
73     }
74 
75     QList<QUrl> *urlList = _virtFilesystemDict[dir];
76     for (const QUrl &fileUrl : urls) {
77         if (!urlList->contains(fileUrl)) {
78             urlList->push_back(fileUrl);
79         }
80     }
81 
82     emit fileSystemChanged(QUrl("virt:///" + dir), false); // may call refresh()
83 }
84 
dropFiles(const QUrl & destination,QDropEvent * event)85 void VirtualFileSystem::dropFiles(const QUrl &destination, QDropEvent *event)
86 {
87     const QList<QUrl> &urls = KUrlMimeData::urlsFromMimeData(event->mimeData());
88     // dropping on virtual filesystem is always copy operation
89     copyFiles(urls, destination);
90 }
91 
addFiles(const QList<QUrl> & fileUrls,KIO::CopyJob::CopyMode,const QString & dir)92 void VirtualFileSystem::addFiles(const QList<QUrl> &fileUrls, KIO::CopyJob::CopyMode /*mode*/,
93                                  const QString &dir)
94 {
95     QUrl destination(_currentDirectory);
96     if (!dir.isEmpty()) {
97         destination.setPath(QDir::cleanPath(destination.path() + '/' + dir));
98     }
99     copyFiles(fileUrls, destination);
100 }
101 
remove(const QStringList & fileNames)102 void VirtualFileSystem::remove(const QStringList &fileNames)
103 {
104     const QString parentDir = currentDir();
105     if (parentDir == "/") { // remove virtual directory
106         for (const QString &filename : fileNames) {
107             _virtFilesystemDict["/"]->removeAll(QUrl(QStringLiteral("virt:/") + filename));
108             delete _virtFilesystemDict[filename];
109             _virtFilesystemDict.remove(filename);
110             _metaInfoDict.remove(filename);
111         }
112     } else {
113         // remove the URLs from the collection
114         for (const QString name : fileNames) {
115             if (_virtFilesystemDict.find(parentDir) != _virtFilesystemDict.end()) {
116                 QList<QUrl> *urlList = _virtFilesystemDict[parentDir];
117                 urlList->removeAll(getUrl(name));
118             }
119         }
120     }
121 
122     emit fileSystemChanged(currentDirectory(), true); // will call refresh()
123 }
124 
getUrl(const QString & name) const125 QUrl VirtualFileSystem::getUrl(const QString &name) const
126 {
127     FileItem *item = getFileItem(name);
128     if (!item) {
129         return QUrl(); // not found
130     }
131 
132     return item->getUrl();
133 }
134 
mkDir(const QString & name)135 void VirtualFileSystem::mkDir(const QString &name)
136 {
137     if (currentDir() != "/") {
138         showError(i18n("Creating new folders is allowed only in the 'virt:/' folder."));
139         return;
140     }
141 
142     mkDirInternal(name);
143 
144     emit fileSystemChanged(currentDirectory(), false); // will call refresh()
145 }
146 
rename(const QString & fileName,const QString & newName)147 void VirtualFileSystem::rename(const QString &fileName, const QString &newName)
148 {
149     FileItem *item = getFileItem(fileName);
150     if (!item)
151         return; // not found
152 
153     if (currentDir() == "/") { // rename virtual directory
154         _virtFilesystemDict["/"]->append(QUrl(QStringLiteral("virt:/") + newName));
155         _virtFilesystemDict["/"]->removeAll(QUrl(QStringLiteral("virt:/") + fileName));
156         _virtFilesystemDict.insert(newName, _virtFilesystemDict.take(fileName));
157         refresh();
158         return;
159     }
160 
161     // newName can be a (local) path or a full url
162     QUrl dest(newName);
163     if (dest.scheme().isEmpty())
164         dest.setScheme("file");
165 
166     // add the new url to the list
167     // the list is refreshed, only existing files remain -
168     // so we don't have to worry if the job was successful
169     _virtFilesystemDict[currentDir()]->append(dest);
170 
171     KIO::Job *job = KIO::moveAs(item->getUrl(), dest, KIO::HideProgressInfo);
172     connect(job, &KIO::Job::result, this, [=](KJob* job) { slotJobResult(job, false); });
173     connect(job, &KIO::Job::result, this, [=]() { emit fileSystemChanged(currentDirectory(), false); });
174 }
175 
canMoveToTrash(const QStringList & fileNames) const176 bool VirtualFileSystem::canMoveToTrash(const QStringList &fileNames) const
177 {
178     if (isRoot())
179         return false;
180 
181     for (const QString fileName : fileNames) {
182         if (!getUrl(fileName).isLocalFile()) {
183             return false;
184         }
185     }
186     return true;
187 }
188 
setMetaInformation(const QString & info)189 void VirtualFileSystem::setMetaInformation(const QString &info)
190 {
191     _metaInfoDict[currentDir()] = info;
192 }
193 
194 // ==== protected ====
195 
refreshInternal(const QUrl & directory,bool onlyScan)196 bool VirtualFileSystem::refreshInternal(const QUrl &directory, bool onlyScan)
197 {
198     _currentDirectory = cleanUrl(directory);
199     _currentDirectory.setHost("");
200     // remove invalid subdirectories
201     _currentDirectory.setPath('/' + _currentDirectory.path().remove('/'));
202 
203     if (!_virtFilesystemDict.contains(currentDir())) {
204         if (onlyScan) {
205             return false; // virtual dir does not exist
206         } else {
207             // Silently creating non-existing directories here. The search and locate tools
208             // expect this. And the user can enter some directory and it will be created.
209             mkDirInternal(currentDir());
210             save();
211             // infinite loop possible
212             // emit fileSystemChanged(currentDirectory());
213             return true;
214         }
215     }
216 
217     QList<QUrl> *urlList = _virtFilesystemDict[currentDir()];
218 
219     if (!onlyScan) {
220         const QString metaInfo = _metaInfoDict[currentDir()];
221         emit fileSystemInfoChanged(metaInfo.isEmpty() ? i18n("Virtual filesystem") : metaInfo,
222                                    "", 0, 0);
223     }
224 
225     QMutableListIterator<QUrl> it(*urlList);
226     while (it.hasNext()) {
227         const QUrl url = it.next();
228         FileItem *item = createFileItem(url);
229         if (!item) { // remove URL from the list for a file that no longer exists
230             it.remove();
231         } else {
232             addFileItem(item);
233         }
234     }
235 
236     save();
237     return true;
238 }
239 
240 // ==== private ====
241 
mkDirInternal(const QString & name)242 void VirtualFileSystem::mkDirInternal(const QString &name)
243 {
244     // clean path, consistent with currentDir()
245     QString dirName = name;
246     dirName = dirName.remove('/');
247     if (dirName.isEmpty())
248         dirName = '/';
249 
250     _virtFilesystemDict.insert(dirName, new QList<QUrl>());
251     _virtFilesystemDict["/"]->append(QUrl(QStringLiteral("virt:/") + dirName));
252 }
253 
save()254 void VirtualFileSystem::save()
255 {
256     KConfig *db = &VirtualFileSystem::getVirtDB();
257     db->deleteGroup("virt_db");
258     KConfigGroup group(db, "virt_db");
259 
260     QHashIterator<QString, QList<QUrl> *> it(_virtFilesystemDict);
261     while (it.hasNext()) {
262         it.next();
263         QList<QUrl> *urlList = it.value();
264 
265         QList<QUrl>::iterator url;
266         QStringList entry;
267         for (url = urlList->begin(); url != urlList->end(); ++url) {
268             entry.append((*url).toDisplayString());
269         }
270         // KDE 4.0 workaround: 'Item_' prefix is added as KConfig fails on 1 char names (such as /)
271         group.writeEntry("Item_" + it.key(), entry);
272         group.writeEntry("MetaInfo_" + it.key(), _metaInfoDict[it.key()]);
273     }
274 
275     db->sync();
276 }
277 
restore()278 void VirtualFileSystem::restore()
279 {
280     KConfig *db = &VirtualFileSystem::getVirtDB();
281     const KConfigGroup dbGrp(db, "virt_db");
282 
283     const QMap<QString, QString> map = db->entryMap("virt_db");
284     QMapIterator<QString, QString> it(map);
285     while (it.hasNext()) {
286         it.next();
287 
288         // KDE 4.0 workaround: check and remove 'Item_' prefix
289         if (!it.key().startsWith(QLatin1String("Item_")))
290             continue;
291         const QString key = it.key().mid(5);
292 
293         const QList<QUrl> urlList = KrServices::toUrlList(dbGrp.readEntry(it.key(), QStringList()));
294         _virtFilesystemDict.insert(key, new QList<QUrl>(urlList));
295         _metaInfoDict.insert(key, dbGrp.readEntry("MetaInfo_" + key, QString()));
296     }
297 
298     if (!_virtFilesystemDict["/"]) { // insert root element if missing for some reason
299         _virtFilesystemDict.insert("/", new QList<QUrl>());
300     }
301 }
302 
createFileItem(const QUrl & url)303 FileItem *VirtualFileSystem::createFileItem(const QUrl &url)
304 {
305     if (url.scheme() == "virt") { // return a virtual directory in root
306         QString path = url.path().mid(1);
307         if (path.isEmpty())
308             path = '/';
309         return FileItem::createVirtualDir(path, url);
310     }
311 
312     const QUrl directory = url.adjusted(QUrl::RemoveFilename);
313 
314     if (url.isLocalFile()) {
315         QFileInfo file(url.path());
316         return file.exists() ? FileSystem::createLocalFileItem(url.fileName(), directory.path(), true) : 0;
317     }
318 
319     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
320     connect(statJob, &KIO::Job::result, this, &VirtualFileSystem::slotStatResult);
321 
322     // ugly: we have to wait here until the stat job is finished
323     QEventLoop eventLoop;
324     connect(statJob, &KJob::finished, &eventLoop, &QEventLoop::quit);
325     eventLoop.exec(); // blocking until quit()
326 
327     if (_fileEntry.count() == 0) {
328         return 0; // stat job failed
329     }
330 
331     if (!_fileEntry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME)) {
332         // TODO this also happens for FTP directories
333         return 0; // file not found
334     }
335 
336     return FileSystem::createFileItemFromKIO(_fileEntry, directory, true);
337 }
338 
getVirtDB()339 KConfig &VirtualFileSystem::getVirtDB()
340 {
341     //virtualfilesystem_db = new KConfig("data",VIRTUALFILESYSTEM_DB,KConfig::NoGlobals);
342     static KConfig db(VIRTUALFILESYSTEM_DB, KConfig::CascadeConfig, QStandardPaths::AppDataLocation);
343     return db;
344 }
345 
slotStatResult(KJob * job)346 void VirtualFileSystem::slotStatResult(KJob *job)
347 {
348     _fileEntry = job->error() ? KIO::UDSEntry() : static_cast<KIO::StatJob *>(job)->statResult();
349 }
350 
showError(const QString & error)351 void VirtualFileSystem::showError(const QString &error)
352 {
353     QWidget *window = QApplication::activeWindow();
354     KMessageBox::sorry(window, error); // window can be null, is allowed
355 }
356