1 /*****************************************************************************
2  * Copyright (C) 2000 Shie Erlich <krusader@users.sourceforge.net>           *
3  * Copyright (C) 2000 Rafi Yanai <krusader@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 <memory>
23 
24 #include "filesystem.h"
25 
26 // QtCore
27 #include <QDebug>
28 #include <QDir>
29 #include <QList>
30 // QtWidgets
31 #include <qplatformdefs.h>
32 
33 #include <KConfigCore/KSharedConfig>
34 #include <KI18n/KLocalizedString>
35 #include <KIO/JobUiDelegate>
36 
37 #include "fileitem.h"
38 #include "krpermhandler.h"
39 #include "../defaults.h"
40 #include "../krglobal.h"
41 #include "../JobMan/jobman.h"
42 #include "../JobMan/krjob.h"
43 
FileSystem()44 FileSystem::FileSystem() : DirListerInterface(0), _isRefreshing(false) {}
45 
~FileSystem()46 FileSystem::~FileSystem()
47 {
48     clear(_fileItems);
49     // please don't remove this line. This informs the view about deleting the file items.
50     emit cleared();
51 }
52 
getUrls(const QStringList & names) const53 QList<QUrl> FileSystem::getUrls(const QStringList &names) const
54 {
55     QList<QUrl> urls;
56     for (const QString name : names) {
57         urls.append(getUrl(name));
58     }
59     return urls;
60 }
61 
getFileItem(const QString & name) const62 FileItem *FileSystem::getFileItem(const QString &name) const
63 {
64     return _fileItems.contains(name) ? _fileItems.value(name) : 0;
65 }
66 
totalSize() const67 KIO::filesize_t FileSystem::totalSize() const
68 {
69     KIO::filesize_t temp = 0;
70     for (FileItem *item : _fileItems.values()) {
71         if (!item->isDir() && item->getName() != "." && item->getName() != "..") {
72             temp += item->getSize();
73         }
74     }
75 
76     return temp;
77 }
78 
ensureTrailingSlash(const QUrl & url)79 QUrl FileSystem::ensureTrailingSlash(const QUrl &url)
80 {
81     if (url.path().endsWith('/')) {
82         return url;
83     }
84 
85     QUrl adjustedUrl(url);
86     adjustedUrl.setPath(adjustedUrl.path() + '/');
87     return adjustedUrl;
88 }
89 
preferLocalUrl(const QUrl & url)90 QUrl FileSystem::preferLocalUrl(const QUrl &url){
91     if (url.isEmpty() || !url.scheme().isEmpty())
92         return url;
93 
94     QUrl adjustedUrl = url;
95     adjustedUrl.setScheme("file");
96     return adjustedUrl;
97 }
98 
scanOrRefresh(const QUrl & directory,bool onlyScan)99 bool FileSystem::scanOrRefresh(const QUrl &directory, bool onlyScan)
100 {
101     qDebug() << "from current dir=" << _currentDirectory.toDisplayString()
102              << "; to=" << directory.toDisplayString();
103     if (_isRefreshing) {
104         // NOTE: this does not happen (unless async)";
105         return false;
106     }
107 
108     // workaround for krarc: find out if transition to local fs is wanted and adjust URL manually
109     QUrl url = directory;
110     if (_currentDirectory.scheme() == "krarc" && url.scheme() == "krarc" &&
111         QDir(url.path()).exists()) {
112         url.setScheme("file");
113     }
114 
115     const bool dirChange = !url.isEmpty() && cleanUrl(url) != _currentDirectory;
116 
117     const QUrl toRefresh =
118             dirChange ? url.adjusted(QUrl::NormalizePathSegments) : _currentDirectory;
119     if (!toRefresh.isValid()) {
120         emit error(i18n("Malformed URL:\n%1", toRefresh.toDisplayString()));
121         return false;
122     }
123 
124     _isRefreshing = true;
125 
126     FileItemDict tempFileItems(_fileItems); // old file items are still used during refresh
127     _fileItems.clear();
128     if (dirChange)
129         // show an empty directory while loading the new one and clear selection
130         emit cleared();
131 
132     const bool refreshed = refreshInternal(toRefresh, onlyScan);
133     _isRefreshing = false;
134 
135     if (!refreshed) {
136         // cleanup and abort
137         if (!dirChange)
138             emit cleared();
139         clear(tempFileItems);
140         return false;
141     }
142 
143     emit scanDone(dirChange);
144 
145     clear(tempFileItems);
146 
147     updateFilesystemInfo();
148 
149     return true;
150 }
151 
deleteFiles(const QList<QUrl> & urls,bool moveToTrash)152 void FileSystem::deleteFiles(const QList<QUrl> &urls, bool moveToTrash)
153 {
154     KrJob *krJob = KrJob::createDeleteJob(urls, moveToTrash);
155     connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
156         connectJobToSources(job, urls);
157     });
158 
159     if (moveToTrash) {
160         // update destination: the trash bin (in case a panel/tab is showing it)
161         connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
162             // Note: the "trash" protocol should always have only one "/" after the "scheme:" part
163             connect(job, &KIO::Job::result, this, [=]() { emit fileSystemChanged(QUrl("trash:/"), false); });
164         });
165     }
166 
167     krJobMan->manageJob(krJob);
168 }
169 
connectJobToSources(KJob * job,const QList<QUrl> urls)170 void FileSystem::connectJobToSources(KJob *job, const QList<QUrl> urls)
171 {
172     if (!urls.isEmpty()) {
173         // TODO we assume that all files were in the same directory and only emit one signal for
174         // the directory of the first file URL (all subdirectories of parent are notified)
175         const QUrl url = urls.first().adjusted(QUrl::RemoveFilename);
176         connect(job, &KIO::Job::result, this, [=]() { emit fileSystemChanged(url, true); });
177     }
178 }
179 
connectJobToDestination(KJob * job,const QUrl & destination)180 void FileSystem::connectJobToDestination(KJob *job, const QUrl &destination)
181 {
182     connect(job, &KIO::Job::result, this, [=]() { emit fileSystemChanged(destination, false); });
183     // (additional) direct refresh if on local fs because watcher is too slow
184     const bool refresh = cleanUrl(destination) == _currentDirectory && isLocal();
185     connect(job, &KIO::Job::result, this, [=](KJob* job) { slotJobResult(job, refresh); });
186 }
187 
showHiddenFiles()188 bool FileSystem::showHiddenFiles()
189 {
190     const KConfigGroup gl(krConfig, "Look&Feel");
191     return gl.readEntry("Show Hidden", _ShowHidden);
192 }
193 
addFileItem(FileItem * item)194 void FileSystem::addFileItem(FileItem *item)
195 {
196     _fileItems.insert(item->getName(), item);
197 }
198 
createLocalFileItem(const QString & name,const QString & directory,bool virt)199 FileItem *FileSystem::createLocalFileItem(const QString &name, const QString &directory, bool virt)
200 {
201     const QDir dir = QDir(directory);
202     const QString path = dir.filePath(name);
203     const QByteArray pathByteArray = path.toLocal8Bit();
204     const QString fileItemName = virt ? path : name;
205     const QUrl fileItemUrl = QUrl::fromLocalFile(path);
206 
207     // read file status; in case of error create a "broken" file item
208     QT_STATBUF stat_p;
209     memset(&stat_p, 0, sizeof(stat_p));
210     if (QT_LSTAT(pathByteArray.data(), &stat_p) < 0)
211         return FileItem::createBroken(fileItemName, fileItemUrl);
212 
213     const KIO::filesize_t size = stat_p.st_size;
214     bool isDir = S_ISDIR(stat_p.st_mode);
215     const bool isLink = S_ISLNK(stat_p.st_mode);
216 
217     // for links, read link destination and determine whether it's broken or not
218     QString linkDestination;
219     bool brokenLink = false;
220     if (isLink) {
221         linkDestination = readLinkSafely(pathByteArray.data());
222 
223         if (linkDestination.isNull()) {
224             brokenLink = true;
225         }
226         else {
227             const QFileInfo linkFile(dir, linkDestination);
228             if (!linkFile.exists())
229                 brokenLink = true;
230             else if (linkFile.isDir())
231                 isDir = true;
232         }
233     }
234 
235     // TODO use statx available in glibc >= 2.28 supporting creation time (btime) and more
236 
237     // create normal file item
238     return new FileItem(fileItemName, fileItemUrl, isDir,
239                         size, stat_p.st_mode,
240                         stat_p.st_mtime, stat_p.st_ctime, stat_p.st_atime, -1,
241                         stat_p.st_uid, stat_p.st_gid, QString(), QString(),
242                         isLink, linkDestination, brokenLink);
243 }
244 
readLinkSafely(const char * path)245 QString FileSystem::readLinkSafely(const char *path)
246 {
247     // inspired by the areadlink_with_size function from gnulib, which is used for coreutils
248     // idea: start with a small buffer and gradually increase it as we discover it wasn't enough
249 
250     QT_OFF_T bufferSize = 1024; // start with 1 KiB
251     QT_OFF_T maxBufferSize = std::numeric_limits<QT_OFF_T>::max();
252 
253     while (true) {
254         // try to read the link
255         std::unique_ptr<char[]> buffer(new char[bufferSize]);
256         auto nBytesRead = readlink(path, buffer.get(), bufferSize);
257 
258         // should never happen, asserted by the readlink
259         if (nBytesRead > bufferSize) {
260             return QString();
261         }
262 
263         // read failure
264         if (nBytesRead < 0) {
265             qDebug() << "Failed to read the link " << path;
266             return QString();
267         }
268 
269         // read success
270         if (nBytesRead < bufferSize || nBytesRead == maxBufferSize) {
271             return QString::fromLocal8Bit(buffer.get(), nBytesRead);
272         }
273 
274         // increase the buffer and retry again
275         // bufferSize < maxBufferSize is implied from previous checks
276         if (bufferSize <= maxBufferSize / 2) {
277             bufferSize *= 2;
278         }
279         else {
280             bufferSize = maxBufferSize;
281         }
282     }
283 }
284 
createFileItemFromKIO(const KIO::UDSEntry & entry,const QUrl & directory,bool virt)285 FileItem *FileSystem::createFileItemFromKIO(const KIO::UDSEntry &entry, const QUrl &directory, bool virt)
286 {
287     const KFileItem kfi(entry, directory, true, true);
288 
289     const QString name = kfi.text();
290     // ignore un-needed entries
291     if (name.isEmpty() || name == "." || name == "..") {
292         return 0;
293     }
294 
295     const QString localPath = kfi.localPath();
296     const QUrl url = !localPath.isEmpty() ? QUrl::fromLocalFile(localPath) : kfi.url();
297     const QString fname = virt ? url.toDisplayString() : name;
298 
299     // get file statistics...
300     const time_t mtime = kfi.time(KFileItem::ModificationTime).toTime_t();
301     const time_t atime = kfi.time(KFileItem::AccessTime).toTime_t();
302     const mode_t mode = kfi.mode() | kfi.permissions();
303     const QDateTime creationTime = kfi.time(KFileItem::CreationTime);
304     const time_t btime = creationTime.isValid() ? creationTime.toTime_t() : (time_t) -1;
305 
306     // NOTE: we could get the mimetype (and file icon) from the kfileitem here but this is very
307     // slow. Instead, the file item class has it's own (faster) way to determine the file type.
308 
309     // NOTE: "broken link" flag is always false, checking link destination existence is
310     // considered to be too expensive
311     return new FileItem(fname, url, kfi.isDir(),
312                      kfi.size(), mode,
313                      mtime, -1, atime, btime,
314                      (uid_t) -1, (gid_t) -1, kfi.user(), kfi.group(),
315                      kfi.isLink(), kfi.linkDest(), false,
316                      kfi.ACL().asString(), kfi.defaultACL().asString());
317 }
318 
slotJobResult(KJob * job,bool refresh)319 void FileSystem::slotJobResult(KJob *job, bool refresh)
320 {
321     if (job->error() && job->uiDelegate()) {
322         // show errors for modifying operations as popup (works always)
323         job->uiDelegate()->showErrorMessage();
324     }
325 
326     if (refresh) {
327         FileSystem::refresh();
328     }
329 }
330 
clear(FileItemDict & fileItems)331 void FileSystem::clear(FileItemDict &fileItems)
332 {
333     QHashIterator<QString, FileItem *> lit(fileItems);
334     while (lit.hasNext()) {
335         delete lit.next().value();
336     }
337     fileItems.clear();
338 }
339