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 org::kde::KDirNotify::emitFileRenamed(src: QUrl::fromLocalFile(localfile: dest), dst: QUrl::fromLocalFile(localfile: dest_orig));
728 }
729
730 // set final permissions
731 if (_mode != -1 && !(_flags & KIO::Resume)) {
732 if (!QFile::setPermissions(filename: dest_orig, permissionSpec: modeToQFilePermissions(mode: _mode))) {
733 // couldn't chmod. Eat the error if the filesystem apparently doesn't support it.
734 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: dest_orig);
735 if (mp && mp->testFileSystemFlag(flag: KMountPoint::SupportsChmod)) {
736 if (!tryChangeFileAttr(action: CHMOD, args: {dest_orig, _mode}, errno).success()) {
737 warning(i18n("Could not change permissions for\n%1", dest_orig));
738 }
739 }
740 }
741 }
742
743 // set original owner and group
744#if !defined(Q_OS_WIN)
745 if (bOrigExists) {
746 if (::chown(qUtf8Printable(dest_orig), owner: owner, group: group) < 0) {
747 warning(i18nc("@info", "Could not change owner and group for\n%1", dest_orig));
748 }
749 }
750#endif
751
752 // set modification time
753 const QString mtimeStr = metaData(QStringLiteral("modified"));
754 if (!mtimeStr.isEmpty()) {
755 QDateTime dt = QDateTime::fromString(string: mtimeStr, format: Qt::ISODate);
756 if (dt.isValid()) {
757 QT_STATBUF dest_statbuf;
758 if (QT_STAT(file: QFile::encodeName(fileName: dest_orig).constData(), buf: &dest_statbuf) == 0) {
759#ifndef Q_OS_WIN
760 struct timeval utbuf[2];
761 // access time
762 utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec
763 utbuf[0].tv_usec = 0;
764 // modification time
765 utbuf[1].tv_sec = dt.toSecsSinceEpoch();
766 utbuf[1].tv_usec = dt.time().msec() * 1000;
767 utimes(file: QFile::encodeName(fileName: dest_orig).constData(), tvp: utbuf);
768#else
769 struct utimbuf utbuf;
770 utbuf.actime = dest_statbuf.st_atime;
771 utbuf.modtime = dt.toSecsSinceEpoch();
772 if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) {
773 tryChangeFileAttr(UTIME, {dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno);
774 }
775#endif
776 }
777 }
778 }
779
780 // We have done our job => finish
781 return WorkerResult::pass();
782}
783
784WorkerResult FileProtocol::special(const QByteArray &data)
785{
786 int tmp;
787 QDataStream stream(data);
788
789 stream >> tmp;
790 switch (tmp) {
791 case 1: {
792 QString fstype;
793 QString dev;
794 QString point;
795 qint8 iRo;
796
797 stream >> iRo >> fstype >> dev >> point;
798
799 bool ro = (iRo != 0);
800
801 // qDebug() << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro;
802 return mount(ro: ro, fstype: fstype.toLatin1().constData(), dev, point);
803 }
804 case 2: {
805 QString point;
806 stream >> point;
807 return unmount(point);
808 }
809 default:
810 break;
811 }
812 return WorkerResult::pass();
813}
814
815static QStringList fallbackSystemPath()
816{
817 return QStringList{
818 QStringLiteral("/sbin"),
819 QStringLiteral("/bin"),
820 };
821}
822
823WorkerResult FileProtocol::mount(bool _ro, const char *_fstype, const QString &_dev, const QString &_point)
824{
825 // qDebug() << "fstype=" << _fstype;
826
827 const QLatin1String label("LABEL=");
828 const QLatin1String uuid("UUID=");
829 QTemporaryFile tmpFile;
830 tmpFile.setAutoRemove(false);
831 tmpFile.open();
832 QByteArray tmpFileName = QFile::encodeName(fileName: tmpFile.fileName());
833 QByteArray dev;
834 if (_dev.startsWith(s: label)) { // turn LABEL=foo into -L foo (#71430)
835 QString labelName = _dev.mid(position: label.size());
836 dev = "-L " + QFile::encodeName(fileName: KShell::quoteArg(arg: labelName)); // is it correct to assume same encoding as filesystem?
837 } else if (_dev.startsWith(s: uuid)) { // and UUID=bar into -U bar
838 QString uuidName = _dev.mid(position: uuid.size());
839 dev = "-U " + QFile::encodeName(fileName: KShell::quoteArg(arg: uuidName));
840 } else {
841 dev = QFile::encodeName(fileName: KShell::quoteArg(arg: _dev)); // get those ready to be given to a shell
842 }
843
844 QByteArray point = QFile::encodeName(fileName: KShell::quoteArg(arg: _point));
845 bool fstype_empty = !_fstype || !*_fstype;
846 QByteArray fstype = KShell::quoteArg(arg: QString::fromLatin1(ba: _fstype)).toLatin1(); // good guess
847 QByteArray readonly = _ro ? "-r" : "";
848 QByteArray mountProg = QStandardPaths::findExecutable(QStringLiteral("mount")).toLocal8Bit();
849 if (mountProg.isEmpty()) {
850 mountProg = QStandardPaths::findExecutable(QStringLiteral("mount"), paths: fallbackSystemPath()).toLocal8Bit();
851 }
852 if (mountProg.isEmpty()) {
853 return WorkerResult::fail(error: KIO::ERR_CANNOT_MOUNT, i18n("Could not find program \"mount\""));
854 }
855
856 // Two steps, in case mount doesn't like it when we pass all options
857 for (int step = 0; step <= 1; step++) {
858 QByteArray buffer = mountProg + ' ';
859 // Mount using device only if no fstype nor mountpoint (KDE-1.x like)
860 if (!dev.isEmpty() && _point.isEmpty() && fstype_empty) {
861 buffer += dev;
862 } else if (!_point.isEmpty() && dev.isEmpty() && fstype_empty) {
863 // Mount using the mountpoint, if no fstype nor device (impossible in first step)
864 buffer += point;
865 } else if (!_point.isEmpty() && !dev.isEmpty() && fstype_empty) { // mount giving device + mountpoint but no fstype
866 buffer += readonly + ' ' + dev + ' ' + point;
867 } else { // mount giving device + mountpoint + fstype
868 buffer += readonly + " -t " + fstype + ' ' + dev + ' ' + point;
869 }
870 if (fstype == "ext2" || fstype == "ext3" || fstype == "ext4") {
871 buffer += " -o errors=remount-ro";
872 }
873
874 buffer += " 2>" + tmpFileName;
875 // qDebug() << buffer;
876
877 int mount_ret = system(command: buffer.constData());
878
879 QString err = readLogFile(filename: tmpFileName);
880 if (err.isEmpty() && mount_ret == 0) {
881 return WorkerResult::pass();
882 } else {
883 // Didn't work - or maybe we just got a warning
884 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(device: _dev);
885 // Is the device mounted ?
886 if (mp && mount_ret == 0) {
887 // qDebug() << "mount got a warning:" << err;
888 warning(msg: err);
889 return WorkerResult::pass();
890 } else {
891 if ((step == 0) && !_point.isEmpty()) {
892 // qDebug() << err;
893 // qDebug() << "Mounting with those options didn't work, trying with only mountpoint";
894 fstype = "";
895 fstype_empty = true;
896 dev = "";
897 // The reason for trying with only mountpoint (instead of
898 // only device) is that some people (hi Malte!) have the
899 // same device associated with two mountpoints
900 // for different fstypes, like /dev/fd0 /mnt/e2floppy and
901 // /dev/fd0 /mnt/dosfloppy.
902 // If the user has the same mountpoint associated with two
903 // different devices, well they shouldn't specify the
904 // mountpoint but just the device.
905 } else {
906 return WorkerResult::fail(error: KIO::ERR_CANNOT_MOUNT, errorString: err);
907 }
908 }
909 }
910 }
911 return WorkerResult::pass();
912}
913
914WorkerResult FileProtocol::unmount(const QString &_point)
915{
916 QByteArray buffer;
917
918 QTemporaryFile tmpFile;
919 tmpFile.setAutoRemove(false);
920 tmpFile.open();
921
922 QByteArray umountProg = QStandardPaths::findExecutable(QStringLiteral("umount")).toLocal8Bit();
923 if (umountProg.isEmpty()) {
924 umountProg = QStandardPaths::findExecutable(QStringLiteral("umount"), paths: fallbackSystemPath()).toLocal8Bit();
925 }
926 if (umountProg.isEmpty()) {
927 return WorkerResult::fail(error: KIO::ERR_CANNOT_UNMOUNT, i18n("Could not find program \"umount\""));
928 }
929
930 QByteArray tmpFileName = QFile::encodeName(fileName: tmpFile.fileName());
931
932 buffer = umountProg + ' ' + QFile::encodeName(fileName: KShell::quoteArg(arg: _point)) + " 2>" + tmpFileName;
933 system(command: buffer.constData());
934
935 QString err = readLogFile(filename: tmpFileName);
936 if (err.isEmpty()) {
937 return WorkerResult::pass();
938 } else {
939 return WorkerResult::fail(error: KIO::ERR_CANNOT_UNMOUNT, errorString: err);
940 }
941}
942
943/*************************************
944 *
945 * Utilities
946 *
947 *************************************/
948
949static QString readLogFile(const QByteArray &_filename)
950{
951 QString result;
952 QFile file(QFile::decodeName(localFileName: _filename));
953 if (file.open(flags: QIODevice::ReadOnly)) {
954 result = QString::fromLocal8Bit(ba: file.readAll());
955 }
956 (void)file.remove();
957 return result;
958}
959
960// We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user
961// where exactly the deletion failed, in case of errors.
962WorkerResult FileProtocol::deleteRecursive(const QString &path)
963{
964 // qDebug() << path;
965 QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
966 QStringList dirsToDelete;
967 while (it.hasNext()) {
968 const QString itemPath = it.next();
969 // qDebug() << "itemPath=" << itemPath;
970 const QFileInfo info = it.fileInfo();
971 if (info.isDir() && !info.isSymLink()) {
972 dirsToDelete.prepend(t: itemPath);
973 } else {
974 // qDebug() << "QFile::remove" << itemPath;
975 if (!QFile::remove(fileName: itemPath)) {
976 auto result = execWithElevatedPrivilege(action: DEL, args: {itemPath}, errno);
977 if (!result.success()) {
978 if (!resultWasCancelled(result)) {
979 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: itemPath);
980 }
981 return result;
982 }
983 }
984 }
985 }
986 QDir dir;
987 for (const QString &itemPath : std::as_const(t&: dirsToDelete)) {
988 // qDebug() << "QDir::rmdir" << itemPath;
989 if (!dir.rmdir(dirName: itemPath)) {
990 auto result = execWithElevatedPrivilege(action: RMDIR, args: {itemPath}, errno);
991 if (!result.success()) {
992 if (!resultWasCancelled(result)) {
993 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: itemPath);
994 }
995 return result;
996 }
997 }
998 }
999 return WorkerResult::pass();
1000}
1001
1002WorkerResult FileProtocol::fileSystemFreeSpace(const QUrl &url)
1003{
1004 if (url.isLocalFile()) {
1005 QStorageInfo storageInfo(url.toLocalFile());
1006 if (storageInfo.isValid() && storageInfo.isReady()) {
1007 setMetaData(QStringLiteral("total"), value: QString::number(storageInfo.bytesTotal()));
1008 setMetaData(QStringLiteral("available"), value: QString::number(storageInfo.bytesAvailable()));
1009
1010 return WorkerResult::pass();
1011 } else {
1012 return WorkerResult::fail(error: KIO::ERR_CANNOT_STAT, errorString: url.url());
1013 }
1014 } else {
1015 return WorkerResult::fail(error: KIO::ERR_UNSUPPORTED_PROTOCOL, errorString: url.url());
1016 }
1017}
1018
1019// needed for JSON file embedding
1020#include "file.moc"
1021
1022#include "moc_file.cpp"
1023

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