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 | |
54 | Q_LOGGING_CATEGORY(KIO_FILE, "kf.kio.workers.file" ) |
55 | |
56 | class KIOPluginFactory : public KIO::WorkerFactory |
57 | { |
58 | Q_OBJECT |
59 | Q_PLUGIN_METADATA(IID "org.kde.kio.worker.file" FILE "file.json" ) |
60 | |
61 | public: |
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 | |
68 | using namespace KIO; |
69 | |
70 | static constexpr int s_maxIPCSize = 1024 * 32; |
71 | |
72 | static QString readLogFile(const QByteArray &_filename); |
73 | |
74 | extern "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 | |
97 | static 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 | |
131 | FileProtocol::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 | |
138 | FileProtocol::~FileProtocol() |
139 | { |
140 | } |
141 | |
142 | WorkerResult 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 | |
182 | WorkerResult 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 | |
205 | WorkerResult 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 | |
248 | WorkerResult 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 | |
267 | WorkerResult 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 | |
372 | KIO::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 | |
378 | WorkerResult 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 | |
423 | WorkerResult 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 | |
444 | WorkerResult 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 | |
470 | KIO::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 | |
485 | KIO::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 | |
499 | void FileProtocol::closeWithoutFinish() |
500 | { |
501 | Q_ASSERT(mFile); |
502 | |
503 | delete mFile; |
504 | mFile = nullptr; |
505 | } |
506 | |
507 | bool 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 | |
513 | KIO::WorkerResult FileProtocol::close() |
514 | { |
515 | // qDebug() << "File::open -- close "; |
516 | closeWithoutFinish(); |
517 | return WorkerResult::pass(); |
518 | } |
519 | |
520 | KIO::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 | |
784 | WorkerResult 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 | |
815 | static QStringList fallbackSystemPath() |
816 | { |
817 | return QStringList{ |
818 | QStringLiteral("/sbin" ), |
819 | QStringLiteral("/bin" ), |
820 | }; |
821 | } |
822 | |
823 | WorkerResult 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 | |
914 | WorkerResult 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 | |
949 | static 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. |
962 | WorkerResult 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 | |
1002 | WorkerResult 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 | |