File indexing completed on 2024-05-05 17:56:55

0001 /*
0002     SPDX-FileCopyrightText: 2000 Shie Erlich <krusader@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2000 Rafi Yanai <krusader@users.sourceforge.net>
0004     SPDX-FileCopyrightText: 2004-2022 Krusader Krew <https://krusader.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "filesystem.h"
0010 
0011 #include <memory>
0012 
0013 // QtCore
0014 #include <QDebug>
0015 #include <QDir>
0016 #include <QList>
0017 // QtWidgets
0018 #include <qplatformdefs.h>
0019 
0020 #include <KConfigCore/KSharedConfig>
0021 #include <KI18n/KLocalizedString>
0022 #include <KIO/JobUiDelegate>
0023 
0024 #include "../JobMan/jobman.h"
0025 #include "../JobMan/krjob.h"
0026 #include "../defaults.h"
0027 #include "../krglobal.h"
0028 #include "fileitem.h"
0029 #include "krpermhandler.h"
0030 
0031 FileSystem::FileSystem()
0032     : DirListerInterface(nullptr)
0033     , _isRefreshing(false)
0034 {
0035 }
0036 
0037 FileSystem::~FileSystem()
0038 {
0039     clear(_fileItems);
0040     // please don't remove this line. This informs the view about deleting the file items.
0041     emit cleared();
0042 }
0043 
0044 QList<QUrl> FileSystem::getUrls(const QStringList &names) const
0045 {
0046     QList<QUrl> urls;
0047     for (const QString &name : names) {
0048         urls.append(getUrl(name));
0049     }
0050     return urls;
0051 }
0052 
0053 FileItem *FileSystem::getFileItem(const QString &name) const
0054 {
0055     return _fileItems.contains(name) ? _fileItems.value(name) : nullptr;
0056 }
0057 
0058 KIO::filesize_t FileSystem::totalSize() const
0059 {
0060     KIO::filesize_t temp = 0;
0061     for (FileItem *item : _fileItems.values()) {
0062         if (!item->isDir() && item->getName() != "." && item->getName() != "..") {
0063             temp += item->getSize();
0064         }
0065     }
0066 
0067     return temp;
0068 }
0069 
0070 QUrl FileSystem::ensureTrailingSlash(const QUrl &url)
0071 {
0072     if (url.path().endsWith('/')) {
0073         return url;
0074     }
0075 
0076     QUrl adjustedUrl(url);
0077     adjustedUrl.setPath(adjustedUrl.path() + '/');
0078     return adjustedUrl;
0079 }
0080 
0081 QUrl FileSystem::preferLocalUrl(const QUrl &url)
0082 {
0083     if (url.isEmpty() || !url.scheme().isEmpty())
0084         return url;
0085 
0086     QUrl adjustedUrl = url;
0087     adjustedUrl.setScheme("file");
0088     return adjustedUrl;
0089 }
0090 
0091 bool FileSystem::scanOrRefresh(const QUrl &directory, bool onlyScan)
0092 {
0093     qDebug() << "from current dir=" << _currentDirectory.toDisplayString() << "; to=" << directory.toDisplayString();
0094     if (_isRefreshing) {
0095         // NOTE: this does not happen (unless async)";
0096         return false;
0097     }
0098 
0099     // workaround for krarc: find out if transition to local fs is wanted and adjust URL manually
0100     QUrl url = directory;
0101     if (_currentDirectory.scheme() == "krarc" && url.scheme() == "krarc" && QDir(url.path()).exists()) {
0102         url.setScheme("file");
0103     }
0104 
0105     const bool dirChange = !url.isEmpty() && cleanUrl(url) != _currentDirectory;
0106 
0107     const QUrl toRefresh = dirChange ? url.adjusted(QUrl::NormalizePathSegments) : _currentDirectory;
0108     if (!toRefresh.isValid()) {
0109         emit error(i18n("Malformed URL:\n%1", toRefresh.toDisplayString()));
0110         return false;
0111     }
0112 
0113     _isRefreshing = true;
0114 
0115     FileItemDict tempFileItems(_fileItems); // old file items are still used during refresh
0116     _fileItems.clear();
0117     if (dirChange)
0118         // show an empty directory while loading the new one and clear selection
0119         emit cleared();
0120 
0121     const bool refreshed = refreshInternal(toRefresh, onlyScan);
0122     _isRefreshing = false;
0123 
0124     if (!refreshed) {
0125         // cleanup and abort
0126         if (!dirChange)
0127             emit cleared();
0128         clear(tempFileItems);
0129         return false;
0130     }
0131 
0132     emit scanDone(dirChange);
0133 
0134     clear(tempFileItems);
0135 
0136     updateFilesystemInfo();
0137 
0138     return true;
0139 }
0140 
0141 void FileSystem::deleteFiles(const QList<QUrl> &urls, bool moveToTrash)
0142 {
0143     KrJob *krJob = KrJob::createDeleteJob(urls, moveToTrash);
0144     connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
0145         connectJobToSources(job, urls);
0146     });
0147 
0148     if (moveToTrash) {
0149         // update destination: the trash bin (in case a panel/tab is showing it)
0150         connect(krJob, &KrJob::started, this, [=](KIO::Job *job) {
0151             // Note: the "trash" protocol should always have only one "/" after the "scheme:" part
0152             connect(job, &KIO::Job::result, this, [=]() {
0153                 emit fileSystemChanged(QUrl("trash:/"), false);
0154             });
0155         });
0156     }
0157 
0158     krJobMan->manageJob(krJob);
0159 }
0160 
0161 void FileSystem::connectJobToSources(KJob *job, const QList<QUrl> &urls)
0162 {
0163     if (!urls.isEmpty()) {
0164         // TODO we assume that all files were in the same directory and only emit one signal for
0165         // the directory of the first file URL (all subdirectories of parent are notified)
0166         const QUrl url = urls.first().adjusted(QUrl::RemoveFilename);
0167         connect(job, &KIO::Job::result, this, [=]() {
0168             emit fileSystemChanged(url, true);
0169         });
0170     }
0171 }
0172 
0173 void FileSystem::connectJobToDestination(KJob *job, const QUrl &destination)
0174 {
0175     connect(job, &KIO::Job::result, this, [=]() {
0176         emit fileSystemChanged(destination, false);
0177     });
0178     // (additional) direct refresh if on local fs because watcher is too slow
0179     const bool refresh = cleanUrl(destination) == _currentDirectory && isLocal();
0180     connect(job, &KIO::Job::result, this, [=](KJob *job) {
0181         slotJobResult(job, refresh);
0182     });
0183 }
0184 
0185 bool FileSystem::showHiddenFiles()
0186 {
0187     const KConfigGroup gl(krConfig, "Look&Feel");
0188     return gl.readEntry("Show Hidden", _ShowHidden);
0189 }
0190 
0191 void FileSystem::addFileItem(FileItem *item)
0192 {
0193     _fileItems.insert(item->getName(), item);
0194 }
0195 
0196 FileItem *FileSystem::createLocalFileItem(const QString &name, const QString &directory, bool virt)
0197 {
0198     const QDir dir = QDir(directory);
0199     const QString path = dir.filePath(name);
0200     const QByteArray pathByteArray = path.toLocal8Bit();
0201     const QString fileItemName = virt ? path : name;
0202     const QUrl fileItemUrl = QUrl::fromLocalFile(path);
0203 
0204     // read file status; in case of error create a "broken" file item
0205     QT_STATBUF stat_p;
0206     memset(&stat_p, 0, sizeof(stat_p));
0207     if (QT_LSTAT(pathByteArray.data(), &stat_p) < 0)
0208         return FileItem::createBroken(fileItemName, fileItemUrl);
0209 
0210     const KIO::filesize_t size = stat_p.st_size;
0211     bool isDir = S_ISDIR(stat_p.st_mode);
0212     const bool isLink = S_ISLNK(stat_p.st_mode);
0213 
0214     // for links, read link destination and determine whether it's broken or not
0215     QString linkDestination;
0216     bool brokenLink = false;
0217     if (isLink) {
0218         linkDestination = readLinkSafely(pathByteArray.data());
0219 
0220         if (linkDestination.isNull()) {
0221             brokenLink = true;
0222         } else {
0223             const QFileInfo linkFile(dir, linkDestination);
0224             if (!linkFile.exists())
0225                 brokenLink = true;
0226             else if (linkFile.isDir())
0227                 isDir = true;
0228         }
0229     }
0230 
0231     // TODO use statx available in glibc >= 2.28 supporting creation time (btime) and more
0232 
0233     // create normal file item
0234     return new FileItem(fileItemName,
0235                         fileItemUrl,
0236                         isDir,
0237                         size,
0238                         stat_p.st_mode,
0239                         stat_p.st_mtime,
0240                         stat_p.st_ctime,
0241                         stat_p.st_atime,
0242                         -1,
0243                         stat_p.st_uid,
0244                         stat_p.st_gid,
0245                         QString(),
0246                         QString(),
0247                         isLink,
0248                         linkDestination,
0249                         brokenLink);
0250 }
0251 
0252 QString FileSystem::readLinkSafely(const char *path)
0253 {
0254     // inspired by the areadlink_with_size function from gnulib, which is used for coreutils
0255     // idea: start with a small buffer and gradually increase it as we discover it wasn't enough
0256 
0257     QT_OFF_T bufferSize = 1024; // start with 1 KiB
0258     QT_OFF_T maxBufferSize = std::numeric_limits<QT_OFF_T>::max();
0259 
0260     while (true) {
0261         // try to read the link
0262         std::unique_ptr<char[]> buffer(new char[bufferSize]);
0263         auto nBytesRead = readlink(path, buffer.get(), bufferSize);
0264 
0265         // should never happen, asserted by the readlink
0266         if (nBytesRead > bufferSize) {
0267             return QString();
0268         }
0269 
0270         // read failure
0271         if (nBytesRead < 0) {
0272             qDebug() << "Failed to read the link " << path;
0273             return QString();
0274         }
0275 
0276         // read success
0277         if (nBytesRead < bufferSize || nBytesRead == maxBufferSize) {
0278             return QString::fromLocal8Bit(buffer.get(), static_cast<int>(nBytesRead));
0279         }
0280 
0281         // increase the buffer and retry again
0282         // bufferSize < maxBufferSize is implied from previous checks
0283         if (bufferSize <= maxBufferSize / 2) {
0284             bufferSize *= 2;
0285         } else {
0286             bufferSize = maxBufferSize;
0287         }
0288     }
0289 }
0290 
0291 FileItem *FileSystem::createFileItemFromKIO(const KIO::UDSEntry &entry, const QUrl &directory, bool virt)
0292 {
0293     const KFileItem kfi(entry, directory, true, true);
0294 
0295     const QString name = kfi.name();
0296     // ignore un-needed entries
0297     if (name.isEmpty() || name == "." || name == "..") {
0298         return nullptr;
0299     }
0300 
0301     const QString localPath = kfi.localPath();
0302     const QUrl url = !localPath.isEmpty() ? QUrl::fromLocalFile(localPath) : kfi.url();
0303     const QString fname = virt ? url.toDisplayString() : name;
0304 
0305     // get file statistics...
0306     const time_t mtime = kfi.time(KFileItem::ModificationTime).toTime_t();
0307     const time_t atime = kfi.time(KFileItem::AccessTime).toTime_t();
0308     const mode_t mode = kfi.mode() | kfi.permissions();
0309     const QDateTime creationTime = kfi.time(KFileItem::CreationTime);
0310     const time_t btime = creationTime.isValid() ? creationTime.toTime_t() : (time_t)-1;
0311 
0312     // NOTE: we could get the mimetype (and file icon) from the kfileitem here but this is very
0313     // slow. Instead, the file item class has it's own (faster) way to determine the file type.
0314 
0315     // NOTE: "broken link" flag is always false, checking link destination existence is
0316     // considered to be too expensive
0317     return new FileItem(fname,
0318                         url,
0319                         kfi.isDir(),
0320                         kfi.size(),
0321                         mode,
0322                         mtime,
0323                         -1,
0324                         atime,
0325                         btime,
0326                         (uid_t)-1,
0327                         (gid_t)-1,
0328                         kfi.user(),
0329                         kfi.group(),
0330                         kfi.isLink(),
0331                         kfi.linkDest(),
0332                         false,
0333                         kfi.ACL().asString(),
0334                         kfi.defaultACL().asString());
0335 }
0336 
0337 void FileSystem::slotJobResult(KJob *job, bool refresh)
0338 {
0339     if (job->error() && job->uiDelegate()) {
0340         // show errors for modifying operations as popup (works always)
0341         job->uiDelegate()->showErrorMessage();
0342     }
0343 
0344     if (refresh) {
0345         FileSystem::refresh();
0346     }
0347 }
0348 
0349 void FileSystem::clear(FileItemDict &fileItems)
0350 {
0351     QHashIterator<QString, FileItem *> lit(fileItems);
0352     while (lit.hasNext()) {
0353         delete lit.next().value();
0354     }
0355     fileItems.clear();
0356 }