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