1/*
2 SPDX-FileCopyrightText: 2000-2002 Stephan Kulow <coolo@kde.org>
3 SPDX-FileCopyrightText: 2000-2002 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2000-2002 Waldo Bastian <bastian@kde.org>
5 SPDX-FileCopyrightText: 2006 Allan Sandfeld Jensen <sandfeld@kde.org>
6 SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
7 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
8
9 SPDX-License-Identifier: LGPL-2.0-or-later
10*/
11
12#include "file.h"
13
14#include <QDirIterator>
15
16#include <QStorageInfo>
17
18#include "../../utils_p.h"
19#include "kioglobal_p.h"
20#include "statjob.h"
21
22#include <assert.h>
23#include <cerrno>
24#ifdef Q_OS_WIN
25#include <qt_windows.h>
26#include <sys/utime.h>
27#include <winsock2.h> //struct timeval
28#else
29#include <utime.h>
30#endif
31
32#include <QCoreApplication>
33#include <QDate>
34#include <QTemporaryFile>
35#include <QVarLengthArray>
36#ifdef Q_OS_WIN
37#include <QDir>
38#include <QFileInfo>
39#endif
40
41#include <KConfigGroup>
42#include <KLocalizedString>
43#include <KShell>
44#include <QDataStream>
45#include <QDebug>
46#include <QMimeDatabase>
47#include <QStandardPaths>
48#include <kmountpoint.h>
49
50#include <ioworker_defaults.h>
51#include <kdirnotify.h>
52#include <workerfactory.h>
53
54Q_LOGGING_CATEGORY(KIO_FILE, "kf.kio.workers.file")
55
56class KIOPluginFactory : public KIO::WorkerFactory
57{
58 Q_OBJECT
59 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.file" FILE "file.json")
60
61public:
62 std::unique_ptr<KIO::WorkerBase> createWorker(const QByteArray &pool, const QByteArray &app) override
63 {
64 return std::make_unique<FileProtocol>(args: pool, args: app);
65 }
66};
67
68using namespace KIO;
69
70static constexpr int s_maxIPCSize = 1024 * 32;
71
72static QString readLogFile(const QByteArray &_filename);
73
74extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
75{
76 QCoreApplication app(argc, argv); // needed for QSocketNotifier
77 app.setApplicationName(QStringLiteral("kio_file"));
78
79 if (argc != 4) {
80 fprintf(stderr, format: "Usage: kio_file protocol domain-socket1 domain-socket2\n");
81 exit(status: -1);
82 }
83
84 FileProtocol worker(argv[2], argv[3]);
85
86 // Make sure the first qDebug is after the worker ctor (which sets a SIGPIPE handler)
87 // This is useful in case kdeinit was autostarted by another app, which then exited and closed fd2
88 // (e.g. ctest does that, or closing the terminal window would do that)
89 // qDebug() << "Starting" << getpid();
90
91 worker.dispatchLoop();
92
93 // qDebug() << "Done";
94 return 0;
95}
96
97static QFile::Permissions modeToQFilePermissions(int mode)
98{
99 QFile::Permissions perms;
100 if (mode & S_IRUSR) {
101 perms |= QFile::ReadOwner;
102 }
103 if (mode & S_IWUSR) {
104 perms |= QFile::WriteOwner;
105 }
106 if (mode & S_IXUSR) {
107 perms |= QFile::ExeOwner;
108 }
109 if (mode & S_IRGRP) {
110 perms |= QFile::ReadGroup;
111 }
112 if (mode & S_IWGRP) {
113 perms |= QFile::WriteGroup;
114 }
115 if (mode & S_IXGRP) {
116 perms |= QFile::ExeGroup;
117 }
118 if (mode & S_IROTH) {
119 perms |= QFile::ReadOther;
120 }
121 if (mode & S_IWOTH) {
122 perms |= QFile::WriteOther;
123 }
124 if (mode & S_IXOTH) {
125 perms |= QFile::ExeOther;
126 }
127
128 return perms;
129}
130
131FileProtocol::FileProtocol(const QByteArray &pool, const QByteArray &app)
132 : KIO::WorkerBase(QByteArrayLiteral("file"), pool, app)
133 , mFile(nullptr)
134{
135 testMode = !qEnvironmentVariableIsEmpty(varName: "KIOWORKER_FILE_ENABLE_TESTMODE");
136}
137
138FileProtocol::~FileProtocol()
139{
140}
141
142WorkerResult FileProtocol::chmod(const QUrl &url, int permissions)
143{
144 const QString path(url.toLocalFile());
145 const QByteArray _path(QFile::encodeName(fileName: path));
146 /* FIXME: Should be atomic */
147#ifdef Q_OS_UNIX
148 // QFile::Permissions does not support special attributes like sticky
149 if (::chmod(file: _path.constData(), mode: permissions) == -1 ||
150#else
151 if (!QFile::setPermissions(path, modeToQFilePermissions(permissions)) ||
152#endif
153 (setACL(path: _path.data(), perm: permissions, directoryDefault: false) == -1) ||
154 /* if not a directory, cannot set default ACLs */
155 (setACL(path: _path.data(), perm: permissions, directoryDefault: true) == -1 && errno != ENOTDIR)) {
156 auto result = execWithElevatedPrivilege(action: CHMOD, args: {_path, permissions}, errno);
157 if (!result.success()) {
158 if (!resultWasCancelled(result)) {
159 switch (result.error()) {
160 case EPERM:
161 case EACCES:
162 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: path);
163 break;
164#if defined(ENOTSUP)
165 case ENOTSUP: // from setACL since chmod can't return ENOTSUP
166 return WorkerResult::fail(error: KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path));
167 break;
168#endif
169 case ENOSPC:
170 return WorkerResult::fail(error: KIO::ERR_DISK_FULL, errorString: path);
171 break;
172 default:
173 return WorkerResult::fail(error: KIO::ERR_CANNOT_CHMOD, errorString: path);
174 }
175 }
176 }
177 }
178
179 return WorkerResult::pass();
180}
181
182WorkerResult FileProtocol::setModificationTime(const QUrl &url, const QDateTime &mtime)
183{
184 const QString path(url.toLocalFile());
185 QT_STATBUF statbuf;
186 if (QT_LSTAT(file: QFile::encodeName(fileName: path).constData(), buf: &statbuf) == 0) {
187 struct utimbuf utbuf;
188 utbuf.actime = statbuf.st_atime; // access time, unchanged
189 utbuf.modtime = mtime.toSecsSinceEpoch(); // modification time
190 if (::utime(file: QFile::encodeName(fileName: path).constData(), file_times: &utbuf) != 0) {
191 auto result = execWithElevatedPrivilege(action: UTIME, args: {path, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno);
192 if (!result.success()) {
193 if (!resultWasCancelled(result)) {
194 // TODO: errno could be EACCES, EPERM, EROFS
195 return WorkerResult::fail(error: KIO::ERR_CANNOT_SETTIME, errorString: path);
196 }
197 }
198 }
199 return WorkerResult::pass();
200 } else {
201 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: path);
202 }
203}
204
205WorkerResult FileProtocol::mkdir(const QUrl &url, int permissions)
206{
207 const QString path(url.toLocalFile());
208
209 // qDebug() << path << "permission=" << permissions;
210
211 // Remove existing file or symlink, if requested (#151851)
212 if (metaData(QStringLiteral("overwrite")) == QLatin1String("true")) {
213 if (!QFile::remove(fileName: path)) {
214 execWithElevatedPrivilege(action: DEL, args: {path}, errno);
215 }
216 }
217
218 QT_STATBUF buff;
219 if (QT_LSTAT(file: QFile::encodeName(fileName: path).constData(), buf: &buff) == -1) {
220 bool dirCreated = QDir().mkdir(dirName: path);
221 if (!dirCreated) {
222 auto result = execWithElevatedPrivilege(action: MKDIR, args: {path}, errno);
223 if (!result.success()) {
224 if (!resultWasCancelled(result)) {
225 // TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly)
226 return WorkerResult::fail(error: KIO::ERR_CANNOT_MKDIR, errorString: path);
227 }
228 return WorkerResult::pass();
229 }
230 dirCreated = true;
231 }
232
233 if (dirCreated) {
234 if (permissions != -1) {
235 return chmod(url, permissions);
236 }
237 return WorkerResult::pass();
238 }
239 }
240
241 if (Utils::isDirMask(mode: buff.st_mode)) {
242 // qDebug() << "ERR_DIR_ALREADY_EXIST";
243 return WorkerResult::fail(error: KIO::ERR_DIR_ALREADY_EXIST, errorString: path);
244 }
245 return WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: path);
246}
247
248WorkerResult FileProtocol::redirect(const QUrl &url)
249{
250 QUrl redir(url);
251 redir.setScheme(configValue(QStringLiteral("DefaultRemoteProtocol"), QStringLiteral("smb")));
252
253 // if we would redirect into the Windows world, let's also check for the
254 // DavWWWRoot "token" which in the Windows world tells win explorer to access
255 // a webdav url
256 // https://www.webdavsystem.com/server/access/windows
257 const QLatin1String davRoot("/DavWWWRoot/");
258 if ((redir.scheme() == QLatin1String("smb")) && redir.path().startsWith(s: davRoot)) {
259 redir.setPath(path: redir.path().mid(position: davRoot.size() - 1)); // remove /DavWWWRoot
260 redir.setScheme(QStringLiteral("webdav"));
261 }
262
263 redirection(url: redir);
264 return WorkerResult::pass();
265}
266
267WorkerResult FileProtocol::get(const QUrl &url)
268{
269 if (!url.isLocalFile()) {
270 return redirect(url);
271 }
272
273 const QString path(url.toLocalFile());
274 QT_STATBUF buff;
275 if (QT_STAT(file: QFile::encodeName(fileName: path).constData(), buf: &buff) == -1) {
276 if (errno == EACCES) {
277 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: path);
278 } else {
279 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: path);
280 }
281 }
282
283 if (Utils::isDirMask(mode: buff.st_mode)) {
284 return WorkerResult::fail(error: KIO::ERR_IS_DIRECTORY, errorString: path);
285 }
286 if (!Utils::isRegFileMask(mode: buff.st_mode)) {
287 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: path);
288 }
289
290 QFile f(path);
291 if (!f.open(flags: QIODevice::ReadOnly)) {
292 auto result = tryOpen(f, path: QFile::encodeName(fileName: path), O_RDONLY, S_IRUSR, errno);
293 if (!result.success()) {
294 if (!resultWasCancelled(result)) {
295 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: path);
296 }
297 return WorkerResult::pass();
298 }
299 }
300
301#if HAVE_FADVISE
302 // TODO check return code
303 posix_fadvise(fd: f.handle(), offset: 0, len: 0, POSIX_FADV_SEQUENTIAL);
304#endif
305
306 // Determine the MIME type of the file to be retrieved, and emit it.
307 // This is mandatory in all workers (for KRun/BrowserRun to work)
308 // In real "remote" workers, this is usually done using mimeTypeForFileNameAndData
309 // after receiving some data. But we don't know how much data the mimemagic rules
310 // need, so for local files, better use mimeTypeForFile.
311 QMimeDatabase db;
312 QMimeType mt = db.mimeTypeForFile(fileName: url.toLocalFile());
313 mimeType(type: mt.name());
314 // Emit total size AFTER the MIME type
315 totalSize(bytes: buff.st_size);
316
317 KIO::filesize_t processed_size = 0;
318
319 QString resumeOffset = metaData(QStringLiteral("range-start"));
320 if (resumeOffset.isEmpty()) {
321 resumeOffset = metaData(QStringLiteral("resume")); // old name
322 }
323 if (!resumeOffset.isEmpty()) {
324 bool ok;
325 KIO::fileoffset_t offset = resumeOffset.toLongLong(ok: &ok);
326 if (ok && (offset > 0) && (offset < buff.st_size)) {
327 if (f.seek(offset)) {
328 canResume();
329 processed_size = offset;
330 // qDebug() << "Resume offset:" << KIO::number(offset);
331 }
332 }
333 }
334
335 char buffer[s_maxIPCSize];
336 QByteArray array;
337
338 while (1) {
339 if (wasKilled()) {
340 return WorkerResult::pass();
341 }
342 int n = f.read(data: buffer, maxlen: s_maxIPCSize);
343 if (n == -1) {
344 if (errno == EINTR) {
345 continue;
346 }
347 f.close();
348 return WorkerResult::fail(error: ERR_CANNOT_READ, errorString: path);
349 }
350 if (n == 0) {
351 break; // Finished
352 }
353
354 array = QByteArray::fromRawData(data: buffer, size: n);
355 data(data: array);
356 array.clear();
357
358 processed_size += n;
359 processedSize(bytes: processed_size);
360
361 // qDebug() << "Processed: " << KIO::number (processed_size);
362 }
363
364 data(data: QByteArray());
365
366 f.close();
367
368 processedSize(bytes: buff.st_size);
369 return WorkerResult::pass();
370}
371
372KIO::StatDetails FileProtocol::getStatDetails()
373{
374 const QString statDetails = metaData(QStringLiteral("details"));
375 return statDetails.isEmpty() ? KIO::StatDefaultDetails : static_cast<KIO::StatDetails>(statDetails.toInt());
376}
377
378WorkerResult FileProtocol::open(const QUrl &url, QIODevice::OpenMode mode)
379{
380 // qDebug() << url;
381
382 QString openPath = url.toLocalFile();
383 QT_STATBUF buff;
384 if (QT_STAT(file: QFile::encodeName(fileName: openPath).constData(), buf: &buff) == -1) {
385 if (errno == EACCES) {
386 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: openPath);
387 } else {
388 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: openPath);
389 }
390 }
391
392 if (Utils::isDirMask(mode: buff.st_mode)) {
393 return WorkerResult::fail(error: KIO::ERR_IS_DIRECTORY, errorString: openPath);
394 }
395 if (!Utils::isRegFileMask(mode: buff.st_mode)) {
396 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: openPath);
397 }
398
399 mFile = new QFile(openPath);
400 if (!mFile->open(flags: mode)) {
401 if (mode & QIODevice::ReadOnly) {
402 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: openPath);
403 } else {
404 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_WRITING, errorString: openPath);
405 }
406 }
407 // Determine the MIME type of the file to be retrieved, and emit it.
408 // This is mandatory in all workers (for KRun/BrowserRun to work).
409 // If we're not opening the file ReadOnly or ReadWrite, don't attempt to
410 // read the file and send the MIME type.
411 if (mode & QIODevice::ReadOnly) {
412 QMimeDatabase db;
413 QMimeType mt = db.mimeTypeForFile(fileName: url.toLocalFile());
414 mimeType(type: mt.name());
415 }
416
417 totalSize(bytes: buff.st_size);
418 position(pos: 0);
419
420 return WorkerResult::pass();
421}
422
423WorkerResult FileProtocol::read(KIO::filesize_t bytes)
424{
425 // qDebug() << "File::open -- read";
426 Q_ASSERT(mFile && mFile->isOpen());
427
428 QVarLengthArray<char> buffer(bytes);
429
430 qint64 bytesRead = mFile->read(data: buffer.data(), maxlen: bytes);
431
432 if (bytesRead == -1) {
433 const auto fileName = mFile->fileName();
434 qCWarning(KIO_FILE) << "Couldn't read. Error:" << mFile->errorString();
435 closeWithoutFinish();
436 return WorkerResult::fail(error: KIO::ERR_CANNOT_READ, errorString: fileName);
437 } else {
438 const QByteArray fileData = QByteArray::fromRawData(data: buffer.data(), size: bytesRead);
439 data(data: fileData);
440 return WorkerResult::pass();
441 }
442}
443
444WorkerResult FileProtocol::write(const QByteArray &data)
445{
446 // qDebug() << "File::open -- write";
447 Q_ASSERT(mFile && mFile->isWritable());
448
449 qint64 bytesWritten = mFile->write(data);
450
451 if (bytesWritten == -1) {
452 if (mFile->error() == QFileDevice::ResourceError) { // disk full
453 const auto fileName = mFile->fileName();
454 closeWithoutFinish();
455 return WorkerResult::fail(error: KIO::ERR_DISK_FULL, errorString: fileName);
456 } else {
457 const auto fileName = mFile->fileName();
458 qCWarning(KIO_FILE) << "Couldn't write. Error:" << mFile->errorString();
459 closeWithoutFinish();
460 return WorkerResult::fail(error: KIO::ERR_CANNOT_WRITE, errorString: fileName);
461 }
462 } else {
463 mFile->flush();
464 written(bytes: bytesWritten);
465
466 return WorkerResult::pass();
467 }
468}
469
470KIO::WorkerResult FileProtocol::seek(KIO::filesize_t offset)
471{
472 // qDebug() << "File::open -- seek";
473 Q_ASSERT(mFile && mFile->isOpen());
474
475 if (mFile->seek(offset)) {
476 position(pos: offset);
477 return WorkerResult::pass();
478 } else {
479 const auto fileName = mFile->fileName();
480 closeWithoutFinish();
481 return WorkerResult::fail(error: KIO::ERR_CANNOT_SEEK, errorString: fileName);
482 }
483}
484
485KIO::WorkerResult FileProtocol::truncate(KIO::filesize_t length)
486{
487 Q_ASSERT(mFile && mFile->isOpen());
488
489 if (mFile->resize(sz: length)) {
490 truncated(length: length);
491 return WorkerResult::pass();
492 } else {
493 const auto fileName = mFile->fileName();
494 closeWithoutFinish();
495 return WorkerResult::fail(error: KIO::ERR_CANNOT_TRUNCATE, errorString: fileName);
496 }
497}
498
499void FileProtocol::closeWithoutFinish()
500{
501 Q_ASSERT(mFile);
502
503 delete mFile;
504 mFile = nullptr;
505}
506
507bool FileProtocol::resultWasCancelled(KIO::WorkerResult result)
508{
509 int err = result.error();
510 return err == KIO::ERR_USER_CANCELED || err == KIO::ERR_PRIVILEGE_NOT_REQUIRED;
511}
512
513KIO::WorkerResult FileProtocol::close()
514{
515 // qDebug() << "File::open -- close ";
516 closeWithoutFinish();
517 return WorkerResult::pass();
518}
519
520KIO::WorkerResult FileProtocol::put(const QUrl &url, int _mode, KIO::JobFlags _flags)
521{
522 if (privilegeOperationUnitTestMode()) {
523 return WorkerResult::pass();
524 }
525
526 const QString dest_orig = url.toLocalFile();
527
528 // qDebug() << dest_orig << "mode=" << _mode;
529
530 QString dest_part(dest_orig + QLatin1String(".part"));
531
532 QT_STATBUF buff_orig;
533 const bool bOrigExists = (QT_LSTAT(file: QFile::encodeName(fileName: dest_orig).constData(), buf: &buff_orig) != -1);
534 bool bPartExists = false;
535 const bool bMarkPartial = configValue(QStringLiteral("MarkPartial"), defaultValue: true);
536
537 if (bMarkPartial) {
538 QT_STATBUF buff_part;
539 bPartExists = (QT_LSTAT(file: QFile::encodeName(fileName: dest_part).constData(), buf: &buff_part) != -1);
540
541 if (bPartExists //
542 && !(_flags & KIO::Resume) //
543 && !(_flags & KIO::Overwrite) //
544 && buff_part.st_size > 0 //
545 && Utils::isRegFileMask(mode: buff_part.st_mode) //
546 ) {
547 // qDebug() << "calling canResume with" << KIO::number(buff_part.st_size);
548
549 // Maybe we can use this partial file for resuming
550 // Tell about the size we have, and the app will tell us
551 // if it's ok to resume or not.
552 _flags |= canResume(offset: buff_part.st_size) ? KIO::Resume : KIO::DefaultFlags;
553
554 // qDebug() << "got answer" << (_flags & KIO::Resume);
555 }
556 }
557
558 if (bOrigExists && !(_flags & KIO::Overwrite) && !(_flags & KIO::Resume)) {
559 if (Utils::isDirMask(mode: buff_orig.st_mode)) {
560 return WorkerResult::fail(error: KIO::ERR_DIR_ALREADY_EXIST, errorString: dest_orig);
561 } else {
562 return WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: dest_orig);
563 }
564 return WorkerResult::pass();
565 }
566
567 // Don't change permissions of the original file
568 if (bOrigExists && _mode == -1) {
569 _mode = static_cast<int>(buff_orig.st_mode);
570 // Make sure the value fit by casting it back. mode_t is possibly larger than int
571 Q_ASSERT(static_cast<decltype(buff_orig.st_mode)>(_mode) == buff_orig.st_mode);
572 }
573#if !defined(Q_OS_WIN)
574 uid_t owner = -1;
575 gid_t group = -1;
576 if (bOrigExists) {
577 owner = buff_orig.st_uid;
578 group = buff_orig.st_gid;
579 }
580#endif
581
582 int result;
583 int error = 0;
584 QString dest;
585 QFile f;
586
587 // Loop until we got 0 (end of data)
588 do {
589 QByteArray buffer;
590 dataReq(); // Request for data
591 result = readData(buffer);
592
593 if (result >= 0) {
594 if (dest.isEmpty()) {
595 if (bMarkPartial) {
596 // qDebug() << "Appending .part extension to" << dest_orig;
597 dest = dest_part;
598 if (bPartExists && !(_flags & KIO::Resume)) {
599 // qDebug() << "Deleting partial file" << dest_part;
600 QFile::remove(fileName: dest_part);
601 // Catch errors when we try to open the file.
602 }
603 } else {
604 dest = dest_orig;
605 if (bOrigExists && !(_flags & KIO::Resume)) {
606 // qDebug() << "Deleting destination file" << dest_orig;
607 QFile::remove(fileName: dest_orig);
608 // Catch errors when we try to open the file.
609 }
610 }
611
612 f.setFileName(dest);
613
614 if ((_flags & KIO::Resume)) {
615 f.open(flags: QIODevice::ReadWrite | QIODevice::Append);
616 } else {
617 f.open(flags: QIODevice::Truncate | QIODevice::WriteOnly);
618 if (_mode != -1) {
619 // WABA: Make sure that we keep writing permissions ourselves,
620 // otherwise we can be in for a surprise on NFS.
621 mode_t initialMode = _mode | S_IWUSR | S_IRUSR;
622 f.setPermissions(modeToQFilePermissions(mode: initialMode));
623 }
624 }
625
626 if (!f.isOpen()) {
627 int oflags = 0;
628 int filemode = _mode;
629
630 if ((_flags & KIO::Resume)) {
631 oflags = O_RDWR | O_APPEND;
632 } else {
633 oflags = O_WRONLY | O_TRUNC | O_CREAT;
634 if (_mode != -1) {
635 filemode = _mode | S_IWUSR | S_IRUSR;
636 }
637 }
638
639 auto result = tryOpen(f, path: QFile::encodeName(fileName: dest), flags: oflags, mode: filemode, errno);
640 if (!result.success()) {
641 if (!resultWasCancelled(result)) {
642 // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode;
643 // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")";
644
645 if (f.error() == QFileDevice::PermissionsError) {
646 return WorkerResult::fail(error: KIO::ERR_WRITE_ACCESS_DENIED, errorString: dest);
647 } else {
648 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_WRITING, errorString: dest);
649 }
650 }
651 return WorkerResult::pass();
652 } else {
653#ifndef Q_OS_WIN
654 if ((_flags & KIO::Resume)) {
655 execWithElevatedPrivilege(action: CHOWN, args: {dest, getuid(), getgid()}, errno);
656 QFile::setPermissions(filename: dest, permissionSpec: modeToQFilePermissions(mode: filemode));
657 }
658#endif
659 }
660 }
661 }
662
663 if (f.write(data: buffer) == -1) {
664 if (f.error() == QFile::ResourceError) { // disk full
665 error = KIO::ERR_DISK_FULL;
666 result = -2; // means: remove dest file
667 } else {
668 qCWarning(KIO_FILE) << "Couldn't write. Error:" << f.errorString();
669 error = KIO::ERR_CANNOT_WRITE;
670 }
671 }
672 } else {
673 qCWarning(KIO_FILE) << "readData() returned" << result;
674 error = KIO::ERR_CANNOT_WRITE;
675 }
676 } while (result > 0);
677
678 // An error occurred deal with it.
679 if (result < 0) {
680 // qDebug() << "Error during 'put'. Aborting.";
681
682 if (f.isOpen()) {
683 f.close();
684
685 QT_STATBUF buff;
686 if (QT_STAT(file: QFile::encodeName(fileName: dest).constData(), buf: &buff) == 0) {
687 int size = configValue(QStringLiteral("MinimumKeepSize"), defaultValue: DEFAULT_MINIMUM_KEEP_SIZE);
688 if (buff.st_size < size) {
689 QFile::remove(fileName: dest);
690 }
691 }
692 }
693 return WorkerResult::fail(error: error, errorString: dest_orig);
694 }
695
696 if (!f.isOpen()) { // we got nothing to write out, so we never opened the file
697 return WorkerResult::pass();
698 }
699
700 f.close();
701
702 if (f.error() != QFile::NoError) {
703 qCWarning(KIO_FILE) << "Error when closing file descriptor:" << f.errorString();
704 return WorkerResult::fail(error: KIO::ERR_CANNOT_WRITE, errorString: dest_orig);
705 }
706
707 // after full download rename the file back to original name
708 if (bMarkPartial) {
709 // QFile::rename() never overwrites the destination file unlike ::remove,
710 // so we must remove it manually first
711 if (_flags & KIO::Overwrite) {
712 if (!QFile::remove(fileName: dest_orig)) {
713 execWithElevatedPrivilege(action: DEL, args: {dest_orig}, errno);
714 }
715 }
716
717 if (!QFile::rename(oldName: dest, newName: dest_orig)) {
718 auto result = execWithElevatedPrivilege(action: RENAME, args: {dest, dest_orig}, errno);
719 if (!result.success()) {
720 if (!resultWasCancelled(result)) {
721 qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig;
722 return WorkerResult::fail(error: KIO::ERR_CANNOT_RENAME_PARTIAL, errorString: dest_orig);
723 }
724 return WorkerResult::pass();
725 }
726 }
727#ifdef WITH_QTDBUS
728 org::kde::KDirNotify::emitFileRenamed(src: QUrl::fromLocalFile(localfile: dest), dst: QUrl::fromLocalFile(localfile: dest_orig));
729#endif
730 }
731
732 // set final permissions
733 if (_mode != -1 && !(_flags & KIO::Resume)) {
734 if (!QFile::setPermissions(filename: dest_orig, permissionSpec: modeToQFilePermissions(mode: _mode))) {
735 // couldn't chmod. Eat the error if the filesystem apparently doesn't support it.
736 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: dest_orig);
737 if (mp && mp->testFileSystemFlag(flag: KMountPoint::SupportsChmod)) {
738 if (!tryChangeFileAttr(action: CHMOD, args: {dest_orig, _mode}, errno).success()) {
739 warning(i18n("Could not change permissions for\n%1", dest_orig));
740 }
741 }
742 }
743 }
744
745 // set original owner and group
746#if !defined(Q_OS_WIN)
747 if (bOrigExists) {
748 if (::chown(qUtf8Printable(dest_orig), owner: owner, group: group) < 0) {
749 warning(i18nc("@info", "Could not change owner and group for\n%1", dest_orig));
750 }
751 }
752#endif
753
754 // set modification time
755 const QString mtimeStr = metaData(QStringLiteral("modified"));
756 if (!mtimeStr.isEmpty()) {
757 QDateTime dt = QDateTime::fromString(string: mtimeStr, format: Qt::ISODate);
758 if (dt.isValid()) {
759 QT_STATBUF dest_statbuf;
760 if (QT_STAT(file: QFile::encodeName(fileName: dest_orig).constData(), buf: &dest_statbuf) == 0) {
761#ifndef Q_OS_WIN
762 struct timeval utbuf[2];
763 // access time
764 utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec
765 utbuf[0].tv_usec = 0;
766 // modification time
767 utbuf[1].tv_sec = dt.toSecsSinceEpoch();
768 utbuf[1].tv_usec = dt.time().msec() * 1000;
769 utimes(file: QFile::encodeName(fileName: dest_orig).constData(), tvp: utbuf);
770#else
771 struct utimbuf utbuf;
772 utbuf.actime = dest_statbuf.st_atime;
773 utbuf.modtime = dt.toSecsSinceEpoch();
774 if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) {
775 tryChangeFileAttr(UTIME, {dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno);
776 }
777#endif
778 }
779 }
780 }
781
782 // We have done our job => finish
783 return WorkerResult::pass();
784}
785
786WorkerResult FileProtocol::special(const QByteArray &data)
787{
788 int tmp;
789 QDataStream stream(data);
790
791 stream >> tmp;
792 switch (tmp) {
793 case 1: {
794 QString fstype;
795 QString dev;
796 QString point;
797 qint8 iRo;
798
799 stream >> iRo >> fstype >> dev >> point;
800
801 bool ro = (iRo != 0);
802
803 // qDebug() << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro;
804 return mount(ro: ro, fstype: fstype.toLatin1().constData(), dev, point);
805 }
806 case 2: {
807 QString point;
808 stream >> point;
809 return unmount(point);
810 }
811 default:
812 break;
813 }
814 return WorkerResult::pass();
815}
816
817static QStringList fallbackSystemPath()
818{
819 return QStringList{
820 QStringLiteral("/sbin"),
821 QStringLiteral("/bin"),
822 };
823}
824
825WorkerResult FileProtocol::mount(bool _ro, const char *_fstype, const QString &_dev, const QString &_point)
826{
827 // qDebug() << "fstype=" << _fstype;
828
829 const QLatin1String label("LABEL=");
830 const QLatin1String uuid("UUID=");
831 QTemporaryFile tmpFile;
832 tmpFile.setAutoRemove(false);
833 tmpFile.open();
834 QByteArray tmpFileName = QFile::encodeName(fileName: tmpFile.fileName());
835 QByteArray dev;
836 if (_dev.startsWith(s: label)) { // turn LABEL=foo into -L foo (#71430)
837 QString labelName = _dev.mid(position: label.size());
838 dev = "-L " + QFile::encodeName(fileName: KShell::quoteArg(arg: labelName)); // is it correct to assume same encoding as filesystem?
839 } else if (_dev.startsWith(s: uuid)) { // and UUID=bar into -U bar
840 QString uuidName = _dev.mid(position: uuid.size());
841 dev = "-U " + QFile::encodeName(fileName: KShell::quoteArg(arg: uuidName));
842 } else {
843 dev = QFile::encodeName(fileName: KShell::quoteArg(arg: _dev)); // get those ready to be given to a shell
844 }
845
846 QByteArray point = QFile::encodeName(fileName: KShell::quoteArg(arg: _point));
847 bool fstype_empty = !_fstype || !*_fstype;
848 QByteArray fstype = KShell::quoteArg(arg: QString::fromLatin1(ba: _fstype)).toLatin1(); // good guess
849 QByteArray readonly = _ro ? "-r" : "";
850 QByteArray mountProg = QStandardPaths::findExecutable(QStringLiteral("mount")).toLocal8Bit();
851 if (mountProg.isEmpty()) {
852 mountProg = QStandardPaths::findExecutable(QStringLiteral("mount"), paths: fallbackSystemPath()).toLocal8Bit();
853 }
854 if (mountProg.isEmpty()) {
855 return WorkerResult::fail(error: KIO::ERR_CANNOT_MOUNT, i18n("Could not find program \"mount\""));
856 }
857
858 // Two steps, in case mount doesn't like it when we pass all options
859 for (int step = 0; step <= 1; step++) {
860 QByteArray buffer = mountProg + ' ';
861 // Mount using device only if no fstype nor mountpoint (KDE-1.x like)
862 if (!dev.isEmpty() && _point.isEmpty() && fstype_empty) {
863 buffer += dev;
864 } else if (!_point.isEmpty() && dev.isEmpty() && fstype_empty) {
865 // Mount using the mountpoint, if no fstype nor device (impossible in first step)
866 buffer += point;
867 } else if (!_point.isEmpty() && !dev.isEmpty() && fstype_empty) { // mount giving device + mountpoint but no fstype
868 buffer += readonly + ' ' + dev + ' ' + point;
869 } else { // mount giving device + mountpoint + fstype
870 buffer += readonly + " -t " + fstype + ' ' + dev + ' ' + point;
871 }
872 if (fstype == "ext2" || fstype == "ext3" || fstype == "ext4") {
873 buffer += " -o errors=remount-ro";
874 }
875
876 buffer += " 2>" + tmpFileName;
877 // qDebug() << buffer;
878
879 int mount_ret = system(command: buffer.constData());
880
881 QString err = readLogFile(filename: tmpFileName);
882 if (err.isEmpty() && mount_ret == 0) {
883 return WorkerResult::pass();
884 } else {
885 // Didn't work - or maybe we just got a warning
886 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(device: _dev);
887 // Is the device mounted ?
888 if (mp && mount_ret == 0) {
889 // qDebug() << "mount got a warning:" << err;
890 warning(msg: err);
891 return WorkerResult::pass();
892 } else {
893 if ((step == 0) && !_point.isEmpty()) {
894 // qDebug() << err;
895 // qDebug() << "Mounting with those options didn't work, trying with only mountpoint";
896 fstype = "";
897 fstype_empty = true;
898 dev = "";
899 // The reason for trying with only mountpoint (instead of
900 // only device) is that some people (hi Malte!) have the
901 // same device associated with two mountpoints
902 // for different fstypes, like /dev/fd0 /mnt/e2floppy and
903 // /dev/fd0 /mnt/dosfloppy.
904 // If the user has the same mountpoint associated with two
905 // different devices, well they shouldn't specify the
906 // mountpoint but just the device.
907 } else {
908 return WorkerResult::fail(error: KIO::ERR_CANNOT_MOUNT, errorString: err);
909 }
910 }
911 }
912 }
913 return WorkerResult::pass();
914}
915
916WorkerResult FileProtocol::unmount(const QString &_point)
917{
918 QByteArray buffer;
919
920 QTemporaryFile tmpFile;
921 tmpFile.setAutoRemove(false);
922 tmpFile.open();
923
924 QByteArray umountProg = QStandardPaths::findExecutable(QStringLiteral("umount")).toLocal8Bit();
925 if (umountProg.isEmpty()) {
926 umountProg = QStandardPaths::findExecutable(QStringLiteral("umount"), paths: fallbackSystemPath()).toLocal8Bit();
927 }
928 if (umountProg.isEmpty()) {
929 return WorkerResult::fail(error: KIO::ERR_CANNOT_UNMOUNT, i18n("Could not find program \"umount\""));
930 }
931
932 QByteArray tmpFileName = QFile::encodeName(fileName: tmpFile.fileName());
933
934 buffer = umountProg + ' ' + QFile::encodeName(fileName: KShell::quoteArg(arg: _point)) + " 2>" + tmpFileName;
935 system(command: buffer.constData());
936
937 QString err = readLogFile(filename: tmpFileName);
938 if (err.isEmpty()) {
939 return WorkerResult::pass();
940 } else {
941 return WorkerResult::fail(error: KIO::ERR_CANNOT_UNMOUNT, errorString: err);
942 }
943}
944
945/*!***********************************
946 *
947 * Utilities
948 *
949 *************************************/
950
951static QString readLogFile(const QByteArray &_filename)
952{
953 QString result;
954 QFile file(QFile::decodeName(localFileName: _filename));
955 if (file.open(flags: QIODevice::ReadOnly)) {
956 result = QString::fromLocal8Bit(ba: file.readAll());
957 }
958 (void)file.remove();
959 return result;
960}
961
962// We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user
963// where exactly the deletion failed, in case of errors.
964WorkerResult FileProtocol::deleteRecursive(const QString &path)
965{
966 // qDebug() << path;
967 QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
968 QStringList dirsToDelete;
969 while (it.hasNext()) {
970 const QString itemPath = it.next();
971 // qDebug() << "itemPath=" << itemPath;
972 const QFileInfo info = it.fileInfo();
973 if (info.isDir() && !info.isSymLink()) {
974 dirsToDelete.prepend(t: itemPath);
975 } else {
976 // qDebug() << "QFile::remove" << itemPath;
977 if (!QFile::remove(fileName: itemPath)) {
978 auto result = execWithElevatedPrivilege(action: DEL, args: {itemPath}, errno);
979 if (!result.success()) {
980 if (!resultWasCancelled(result)) {
981 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: itemPath);
982 }
983 return result;
984 }
985 }
986 }
987 }
988 QDir dir;
989 for (const QString &itemPath : std::as_const(t&: dirsToDelete)) {
990 // qDebug() << "QDir::rmdir" << itemPath;
991 if (!dir.rmdir(dirName: itemPath)) {
992 auto result = execWithElevatedPrivilege(action: RMDIR, args: {itemPath}, errno);
993 if (!result.success()) {
994 if (!resultWasCancelled(result)) {
995 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: itemPath);
996 }
997 return result;
998 }
999 }
1000 }
1001 return WorkerResult::pass();
1002}
1003
1004WorkerResult FileProtocol::fileSystemFreeSpace(const QUrl &url)
1005{
1006 if (url.isLocalFile()) {
1007 QStorageInfo storageInfo(url.toLocalFile());
1008 if (storageInfo.isValid() && storageInfo.isReady()) {
1009 setMetaData(QStringLiteral("total"), value: QString::number(storageInfo.bytesTotal()));
1010 setMetaData(QStringLiteral("available"), value: QString::number(storageInfo.bytesAvailable()));
1011
1012 return WorkerResult::pass();
1013 } else {
1014 return WorkerResult::fail(error: KIO::ERR_CANNOT_STAT, errorString: url.url());
1015 }
1016 } else {
1017 return WorkerResult::fail(error: KIO::ERR_UNSUPPORTED_PROTOCOL, errorString: url.url());
1018 }
1019}
1020
1021// needed for JSON file embedding
1022#include "file.moc"
1023
1024#include "moc_file.cpp"
1025

source code of kio/src/kioworkers/file/file.cpp