File indexing completed on 2024-04-14 04:52:22
0001 /* 0002 * SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de> 0003 * SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "kio_afc.h" 0007 0008 #include "afc_debug.h" 0009 0010 #include "afcapp.h" 0011 #include "afcdevice.h" 0012 #include "afcfile.h" 0013 #include "afcfilereader.h" 0014 #include "afcurl.h" 0015 #include "afcutils.h" 0016 0017 #include <QCoreApplication> 0018 #include <QDateTime> 0019 #include <QMimeDatabase> 0020 #include <QMimeType> 0021 #include <QMutexLocker> 0022 #include <QScopeGuard> 0023 0024 #include <KFileUtils> 0025 #include <KLocalizedString> 0026 0027 #include <algorithm> 0028 0029 using namespace KIO; 0030 0031 // Pseudo plugin class to embed meta data 0032 class KIOPluginForMetaData : public QObject 0033 { 0034 Q_OBJECT 0035 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.afc" FILE "afc.json") 0036 }; 0037 0038 using namespace KIO; 0039 extern "C" { 0040 int Q_DECL_EXPORT kdemain(int argc, char **argv) 0041 { 0042 QCoreApplication app(argc, argv); 0043 app.setApplicationName(QStringLiteral("kio_afc")); 0044 0045 qCDebug(KIO_AFC_LOG) << "*** Starting kio_afc"; 0046 0047 if (argc != 4) { 0048 qCDebug(KIO_AFC_LOG) << "Usage: kio_afc protocol domain-socket1 domain-socket2"; 0049 exit(-1); 0050 } 0051 0052 AfcWorker worker(argv[2], argv[3]); 0053 worker.dispatchLoop(); 0054 return 0; 0055 } 0056 } 0057 0058 AfcWorker::AfcWorker(const QByteArray &poolSocket, const QByteArray &appSocket) 0059 : WorkerBase(QByteArrayLiteral("kio_afc"), poolSocket, appSocket) 0060 { 0061 const auto result = init(); 0062 Q_ASSERT(result.success()); 0063 } 0064 0065 AfcWorker::~AfcWorker() 0066 { 0067 idevice_event_unsubscribe(); 0068 0069 qDeleteAll(m_devices); 0070 m_devices.clear(); 0071 } 0072 0073 Result AfcWorker::init() 0074 { 0075 bool ok; 0076 const int logLevel = qEnvironmentVariableIntValue("KIO_AFC_LOG_VERBOSITY", &ok); 0077 if (ok) { 0078 idevice_set_debug_level(logLevel); 0079 } 0080 0081 idevice_event_subscribe( 0082 [](const idevice_event_t *event, void *user_data) { 0083 // NOTE this is executed in a different thread! 0084 static_cast<AfcWorker *>(user_data)->onDeviceEvent(event); 0085 }, 0086 this); 0087 0088 updateDeviceList(); 0089 0090 return Result::pass(); 0091 } 0092 0093 void AfcWorker::onDeviceEvent(const idevice_event_t *event) 0094 { 0095 // NOTE this is executed in a different thread! 0096 0097 switch (event->event) { 0098 case IDEVICE_DEVICE_ADD: 0099 qCDebug(KIO_AFC_LOG) << "idevice event ADD for" << event->udid; 0100 addDevice(QString::fromLatin1(event->udid)); 0101 return; 0102 case IDEVICE_DEVICE_REMOVE: 0103 qCDebug(KIO_AFC_LOG) << "idevice event REMOVE for" << event->udid; 0104 removeDevice(QString::fromLatin1(event->udid)); 0105 return; 0106 #if IMOBILEDEVICE_API >= QT_VERSION_CHECK(1, 3, 0) 0107 case IDEVICE_DEVICE_PAIRED: 0108 qCDebug(KIO_AFC_LOG) << "idevice event PAIRED for" << event->udid; 0109 return; 0110 #endif 0111 } 0112 0113 qCWarning(KIO_AFC_LOG) << "Unhandled idevice event" << event->event << "for" << event->udid; 0114 } 0115 0116 Result AfcWorker::clientForUrl(const AfcUrl &afcUrl, AfcClient::Ptr &client) const 0117 { 0118 AfcDevice *device = m_devices.value(deviceIdForFriendlyUrl(afcUrl)); 0119 if (!device) { 0120 return Result::fail(ERR_DOES_NOT_EXIST, afcUrl.url().toDisplayString()); 0121 } 0122 0123 return device->client(afcUrl.appId(), client); 0124 } 0125 0126 QString AfcWorker::deviceIdForFriendlyUrl(const AfcUrl &afcUrl) const 0127 { 0128 QString deviceId = m_friendlyNames.value(afcUrl.device()); 0129 if (deviceId.isEmpty()) { 0130 deviceId = afcUrl.device(); 0131 } 0132 return deviceId; 0133 } 0134 0135 QUrl AfcWorker::resolveSolidUrl(const QUrl &url) const 0136 { 0137 const QString path = url.path(); 0138 0139 const QString prefix = QStringLiteral("udi=/org/kde/solid/imobile/"); 0140 if (!path.startsWith(prefix)) { 0141 return {}; 0142 } 0143 0144 QString deviceId = path.mid(prefix.length()); 0145 const int slashIdx = deviceId.indexOf(QLatin1Char('/')); 0146 if (slashIdx > -1) { 0147 deviceId = deviceId.left(slashIdx); 0148 } 0149 0150 const QString friendlyName = m_friendlyNames.key(deviceId); 0151 0152 QUrl newUrl; 0153 newUrl.setScheme(QStringLiteral("afc")); 0154 newUrl.setHost(!friendlyName.isEmpty() ? friendlyName : deviceId); 0155 // TODO would be nice to preserve subdirectories 0156 newUrl.setPath(QStringLiteral("/")); 0157 0158 return newUrl; 0159 } 0160 0161 bool AfcWorker::redirectIfSolidUrl(const QUrl &url) 0162 { 0163 const QUrl redirectUrl = resolveSolidUrl(url); 0164 if (!redirectUrl.isValid()) { 0165 return false; 0166 } 0167 0168 redirection(redirectUrl); 0169 return true; 0170 } 0171 0172 UDSEntry AfcWorker::overviewEntry(const QString &fileName) const 0173 { 0174 UDSEntry entry; 0175 entry.fastInsert(UDSEntry::UDS_NAME, !fileName.isEmpty() ? fileName : i18n("Apple Devices")); 0176 entry.fastInsert(UDSEntry::UDS_ICON_NAME, QStringLiteral("phone-apple-iphone")); 0177 entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0178 entry.fastInsert(UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 0179 return entry; 0180 } 0181 0182 UDSEntry AfcWorker::deviceEntry(const AfcDevice *device, const QString &fileName, bool asLink) const 0183 { 0184 QString deviceId = m_friendlyNames.key(device->id()); 0185 if (deviceId.isEmpty()) { 0186 deviceId = device->id(); 0187 } 0188 const QString deviceClass = device->deviceClass(); 0189 0190 UDSEntry entry; 0191 entry.fastInsert(UDSEntry::UDS_NAME, !fileName.isEmpty() ? fileName : deviceId); 0192 if (!device->name().isEmpty()) { 0193 entry.fastInsert(UDSEntry::UDS_DISPLAY_NAME, device->name()); 0194 } 0195 // TODO prettier 0196 entry.fastInsert(UDSEntry::UDS_DISPLAY_TYPE, deviceClass); 0197 entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0198 entry.fastInsert(UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 0199 0200 QString iconName; 0201 // We can assume iPod running iOS/supporting imobiledevice is an iPod touch? 0202 if (deviceClass.contains(QLatin1String("iPad"))) { 0203 iconName = QStringLiteral("computer-apple-ipad"); 0204 } else if (deviceClass.contains(QLatin1String("iPod"))) { 0205 iconName = QStringLiteral("multimedia-player-apple-ipod-touch"); 0206 } else { 0207 iconName = QStringLiteral("phone-apple-iphone"); 0208 } 0209 0210 entry.fastInsert(UDSEntry::UDS_ICON_NAME, iconName); 0211 0212 if (asLink) { 0213 const QString contentsUrl = QStringLiteral("afc://%1/").arg(deviceId); 0214 entry.fastInsert(UDSEntry::UDS_LINK_DEST, contentsUrl); 0215 entry.fastInsert(UDSEntry::UDS_TARGET_URL, contentsUrl); 0216 } 0217 0218 return entry; 0219 } 0220 0221 UDSEntry AfcWorker::appsOverviewEntry(const AfcDevice *device, const QString &fileName) const 0222 { 0223 QString deviceId = m_friendlyNames.key(device->id()); 0224 if (deviceId.isEmpty()) { 0225 deviceId = device->id(); 0226 } 0227 0228 UDSEntry entry; 0229 entry.fastInsert(UDSEntry::UDS_NAME, !fileName.isEmpty() ? fileName : QStringLiteral("@apps")); 0230 entry.fastInsert(UDSEntry::UDS_DISPLAY_NAME, i18nc("Link to folder with files stored inside apps", "Apps")); 0231 entry.fastInsert(UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-documents")); 0232 entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0233 0234 const QString appsUrl = QStringLiteral("afc://%1:%2/").arg(deviceId).arg(static_cast<int>(AfcUrl::BrowseMode::Apps)); 0235 entry.fastInsert(UDSEntry::UDS_LINK_DEST, appsUrl); 0236 entry.fastInsert(UDSEntry::UDS_TARGET_URL, appsUrl); 0237 0238 return entry; 0239 } 0240 0241 void AfcWorker::updateDeviceList() 0242 { 0243 char **devices = nullptr; 0244 int count = 0; 0245 0246 idevice_get_device_list(&devices, &count); 0247 for (int i = 0; i < count; ++i) { 0248 const QString id = QString::fromLatin1(devices[i]); 0249 addDevice(id); 0250 } 0251 0252 if (devices) { 0253 idevice_device_list_free(devices); 0254 } 0255 } 0256 0257 bool AfcWorker::addDevice(const QString &id) 0258 { 0259 // NOTE this may be executed in a different thread when called from device idevice_event callback 0260 QMutexLocker locker(&m_mutex); 0261 0262 if (m_devices.contains(id)) { 0263 return false; 0264 } 0265 0266 auto *device = new AfcDevice(id); 0267 if (!device->isValid()) { 0268 delete device; 0269 return false; 0270 } 0271 0272 m_devices.insert(id, device); 0273 0274 Q_ASSERT(!device->name().isEmpty()); 0275 0276 // HACK URL host cannot contain spaces or non-ascii, and has to be lowercase. 0277 auto normalizeHost = [](const QString &name) { 0278 return QString::fromLatin1(name.toLatin1()).toLower().replace(QLatin1Char(' '), QLatin1Char('_')); 0279 }; 0280 0281 QString friendlyName = normalizeHost(device->name()); 0282 // FIXME FIXME FIXME (my KCoreAddons is too old for KFileUtils::makeSuggestedName :) 0283 while (m_friendlyNames.contains(friendlyName)) { 0284 friendlyName = normalizeHost(KFileUtils::makeSuggestedName(friendlyName)); 0285 } 0286 0287 QUrl checkUrl; 0288 checkUrl.setHost(friendlyName); 0289 0290 if (checkUrl.host().isEmpty()) { 0291 qCCritical(KIO_AFC_LOG) << "Failed to normalize" << device->name() << "into a valid URL host, this is a bug!"; 0292 } else { 0293 m_friendlyNames.insert(friendlyName, id); 0294 } 0295 0296 return true; 0297 } 0298 0299 void AfcWorker::removeDevice(const QString &id) 0300 { 0301 // NOTE this may be executed in a different thread when called from device idevice_event callback 0302 QMutexLocker locker(&m_mutex); 0303 0304 auto *device = m_devices.take(id); 0305 if (device) { 0306 if (m_openFile && m_openFile->client()->device() == device) { 0307 m_openFile.reset(); 0308 } 0309 delete device; 0310 0311 auto it = std::find_if(m_friendlyNames.begin(), m_friendlyNames.end(), [&id](const QString &deviceId) { 0312 return deviceId == id; 0313 }); 0314 if (it != m_friendlyNames.end()) { 0315 m_friendlyNames.erase(it); 0316 } 0317 } 0318 } 0319 0320 Result AfcWorker::listDir(const QUrl &url) 0321 { 0322 qCDebug(KIO_AFC_LOG) << "list directory:" << url; 0323 0324 if (redirectIfSolidUrl(url)) { 0325 return Result::pass(); 0326 } 0327 0328 const AfcUrl afcUrl(url); 0329 if (!afcUrl.isValid()) { 0330 return Result::fail(ERR_MALFORMED_URL, url.toDisplayString()); 0331 } 0332 0333 // Don't have an empty path. 0334 // Otherwise we get "invalid URL" errors when trying to enter a subfolder. 0335 if (!url.host().isEmpty() && url.path().isEmpty()) { 0336 QUrl newUrl = url; 0337 newUrl.setPath(QStringLiteral("/")); 0338 redirection(newUrl); 0339 return Result::pass(); 0340 } 0341 0342 if (afcUrl.device().isEmpty()) { 0343 updateDeviceList(); 0344 0345 for (AfcDevice *device : m_devices) { 0346 UDSEntry entry = deviceEntry(device, QString(), true /*asLink*/); 0347 0348 // When there is only one device, redirect to it right away 0349 if (m_devices.count() == 1) { 0350 redirection(QUrl(entry.stringValue(UDSEntry::UDS_TARGET_URL))); 0351 return Result::pass(); 0352 } 0353 0354 listEntry(entry); 0355 } 0356 0357 // We cannot just list that at the beginning because we might do a redirect 0358 listEntry(overviewEntry(QStringLiteral("."))); 0359 0360 return Result::pass(); 0361 } 0362 0363 AfcDevice *device = m_devices.value(deviceIdForFriendlyUrl(afcUrl)); 0364 if (!device) { 0365 return Result::fail(ERR_DOES_NOT_EXIST, afcUrl.device()); 0366 } 0367 0368 const QString appId = afcUrl.appId(); 0369 if (afcUrl.browseMode() == AfcUrl::BrowseMode::Apps && appId.isEmpty()) { 0370 QVector<AfcApp> apps; 0371 const auto result = device->apps(apps); 0372 if (!result.success()) { 0373 return result; 0374 } 0375 0376 // Cannot browse apps without sharing enabled, don't list them. 0377 apps.erase(std::remove_if(apps.begin(), 0378 apps.end(), 0379 [](const AfcApp &app) { 0380 return !app.sharingEnabled(); 0381 }), 0382 apps.end()); 0383 0384 device->fetchAppIcons(apps); 0385 0386 for (const auto &app : apps) { 0387 listEntry(app.entry()); 0388 } 0389 0390 listEntry(appsOverviewEntry(device, QStringLiteral("."))); 0391 return Result::pass(); 0392 } 0393 0394 AfcClient::Ptr client; 0395 Result result = device->client(appId, client); 0396 if (!result.success()) { 0397 return result; 0398 } 0399 0400 // Ourself must be "." 0401 UDSEntry rootEntry; 0402 result = client->entry(afcUrl.path(), rootEntry); 0403 if (!result.success()) { 0404 return result; 0405 } 0406 0407 // NOTE this must not be "fastInsert" as AfcDevice::entry already sets a name 0408 rootEntry.replace(UDSEntry::UDS_NAME, QStringLiteral(".")); 0409 listEntry(rootEntry); 0410 0411 QStringList files; 0412 result = client->entryList(afcUrl.path(), files); 0413 if (!result.success()) { 0414 // One can only access the "Documents" folder within an app, redirect to it if applicable 0415 if (result.error() == KIO::ERR_ACCESS_DENIED && !afcUrl.appId().isEmpty() && afcUrl.path().isEmpty()) { 0416 QUrl newUrl = url; 0417 newUrl.setPath(newUrl.path() + QLatin1String("/Documents")); 0418 qCDebug(KIO_AFC_LOG) << "Got access denied on app root folder, redirecting to Documents folder"; 0419 0420 redirection(newUrl); 0421 return Result::pass(); 0422 } 0423 0424 return result; 0425 } 0426 0427 for (const QString &file : files) { 0428 QString absolutePath = afcUrl.path(); 0429 if (!absolutePath.endsWith(QLatin1Char('/')) && !file.startsWith(QLatin1Char('/'))) { 0430 absolutePath.append(QLatin1Char('/')); 0431 } 0432 absolutePath.append(file); 0433 0434 UDSEntry entry; 0435 result = client->entry(absolutePath, entry); 0436 if (!result.success()) { 0437 qCWarning(KIO_AFC_LOG) << "Failed to list" << absolutePath << result.error() << result.errorString(); 0438 continue; 0439 } 0440 0441 listEntry(entry); 0442 } 0443 0444 // Add link to "Apps documents" to device root folder 0445 if (afcUrl.path().isEmpty()) { 0446 listEntry(appsOverviewEntry(device)); 0447 } 0448 0449 return Result::pass(); 0450 } 0451 0452 Result AfcWorker::stat(const QUrl &url) 0453 { 0454 if (redirectIfSolidUrl(url)) { 0455 return Result::pass(); 0456 } 0457 0458 const AfcUrl afcUrl(url); 0459 if (!afcUrl.isValid()) { 0460 return Result::fail(ERR_MALFORMED_URL, url.toDisplayString()); 0461 } 0462 0463 // Device overview page afc:/ 0464 if (afcUrl.device().isEmpty()) { 0465 statEntry(overviewEntry()); 0466 return Result::pass(); 0467 } 0468 0469 AfcDevice *device = m_devices.value(deviceIdForFriendlyUrl(afcUrl)); 0470 if (!device) { 0471 return Result::fail(ERR_DOES_NOT_EXIST, url.toDisplayString()); 0472 } 0473 0474 if (afcUrl.path().isEmpty()) { 0475 // Device file system or device app overview 0476 if (afcUrl.appId().isEmpty()) { 0477 UDSEntry rootEntry = deviceEntry(device); 0478 if (afcUrl.browseMode() == AfcUrl::BrowseMode::Apps) { 0479 rootEntry.replace(UDSEntry::UDS_DISPLAY_NAME, i18nc("Placeholder is device name", "%1 (Apps)", device->name())); 0480 } 0481 statEntry(rootEntry); 0482 return Result::pass(); 0483 } 0484 0485 // App folder 0486 AfcApp app = device->app(afcUrl.appId()); 0487 if (!app.isValid()) { 0488 return Result::fail(KIO::ERR_DOES_NOT_EXIST, afcUrl.appId()); 0489 } 0490 0491 device->fetchAppIcon(app); 0492 0493 UDSEntry appEntry = app.entry(); 0494 statEntry(appEntry); 0495 return Result::pass(); 0496 } 0497 0498 AfcClient::Ptr client; 0499 auto result = device->client(afcUrl.appId(), client); 0500 if (!result.success()) { 0501 return result; 0502 } 0503 0504 UDSEntry entry; 0505 result = client->entry(afcUrl.path(), entry); 0506 if (!result.success()) { 0507 return result; 0508 } 0509 0510 statEntry(entry); 0511 return Result::pass(); 0512 } 0513 0514 void AfcWorker::guessMimeType(AfcFile &file, const QString &path) 0515 { 0516 // Determine the mimetype of the file to be retrieved, and emit it. 0517 // This is mandatory in all workers... 0518 0519 AfcFileReader reader = file.reader(); 0520 reader.setSize(1024); 0521 const Result result = reader.read(); 0522 if (result.success()) { 0523 QMimeDatabase db; 0524 const QString fileName = path.section(QLatin1Char('/'), -1, -1); 0525 QMimeType mime = db.mimeTypeForFileNameAndData(fileName, reader.data()); 0526 mimeType(mime.name()); 0527 } 0528 0529 file.seek(0); 0530 } 0531 0532 Result AfcWorker::get(const QUrl &url) 0533 { 0534 if (redirectIfSolidUrl(url)) { 0535 return Result::pass(); 0536 } 0537 0538 const AfcUrl afcUrl(url); 0539 0540 AfcClient::Ptr client; 0541 auto result = clientForUrl(afcUrl, client); 0542 if (!result.success()) { 0543 return result; 0544 } 0545 0546 UDSEntry entry; 0547 result = client->entry(afcUrl.path(), entry); 0548 if (!result.success()) { 0549 return result; 0550 } 0551 0552 AfcFile file(client, afcUrl.path()); 0553 0554 result = file.open(QIODevice::ReadOnly); 0555 if (!result.success()) { 0556 return result; 0557 } 0558 0559 const auto size = entry.numberValue(UDSEntry::UDS_SIZE, 0); 0560 totalSize(size); 0561 0562 guessMimeType(file, afcUrl.path()); 0563 0564 position(0); 0565 0566 AfcFileReader reader = file.reader(); 0567 reader.setSize(size); 0568 0569 while (reader.hasMore()) { 0570 const auto result = reader.read(); 0571 if (!result.success()) { 0572 return result; 0573 } 0574 data(reader.data()); 0575 } 0576 0577 return Result::pass(); 0578 } 0579 0580 Result AfcWorker::put(const QUrl &url, int permissions, JobFlags flags) 0581 { 0582 Q_UNUSED(permissions); 0583 const AfcUrl afcUrl(url); 0584 0585 AfcClient::Ptr client; 0586 Result result = clientForUrl(afcUrl, client); 0587 if (!result.success()) { 0588 return result; 0589 } 0590 0591 UDSEntry entry; 0592 result = client->entry(afcUrl.path(), entry); 0593 0594 const bool exists = result.error() != ERR_DOES_NOT_EXIST; 0595 if (exists && !flags.testFlag(Overwrite) && !flags.testFlag(Resume)) { 0596 if (S_ISDIR(entry.numberValue(UDSEntry::UDS_FILE_TYPE))) { 0597 return Result::fail(ERR_DIR_ALREADY_EXIST, afcUrl.path()); 0598 } 0599 return Result::fail(ERR_FILE_ALREADY_EXIST, afcUrl.path()); 0600 } 0601 0602 AfcFile file(client, afcUrl.path()); 0603 0604 if (flags.testFlag(Resume)) { 0605 result = file.open(QIODevice::Append); 0606 } else { 0607 result = file.open(QIODevice::WriteOnly); 0608 } 0609 0610 if (!result.success()) { 0611 return result; 0612 } 0613 0614 int readDataResult = 0; 0615 0616 do { 0617 QByteArray buffer; 0618 dataReq(); 0619 0620 readDataResult = readData(buffer); 0621 0622 if (readDataResult < 0) { 0623 return Result::fail(ERR_CANNOT_READ, QStringLiteral("readData result was %1").arg(readDataResult)); 0624 } 0625 0626 uint32_t bytesWritten = 0; 0627 const auto result = file.write(buffer, bytesWritten); 0628 0629 if (!result.success()) { 0630 return result; 0631 } 0632 } while (readDataResult > 0); 0633 0634 const QString modifiedMeta = metaData(QStringLiteral("modified")); 0635 0636 if (!modifiedMeta.isEmpty()) { 0637 const QDateTime mtime = QDateTime::fromString(modifiedMeta, Qt::ISODate); 0638 0639 if (mtime.isValid() && !client->setModificationTime(afcUrl.path(), mtime).success()) { 0640 qCWarning(KIO_AFC_LOG) << "Failed to set mtime for" << afcUrl.path() << "in put"; 0641 } 0642 } 0643 0644 return Result::pass(); 0645 } 0646 0647 Result AfcWorker::open(const QUrl &url, QIODevice::OpenMode mode) 0648 { 0649 // TODO fail if already open? 0650 0651 const AfcUrl afcUrl(url); 0652 0653 AfcClient::Ptr client; 0654 Result result = clientForUrl(afcUrl, client); 0655 if (!result.success()) { 0656 return result; 0657 } 0658 0659 UDSEntry entry; 0660 result = client->entry(afcUrl.path(), entry); 0661 if (!result.success()) { 0662 return result; 0663 } 0664 0665 auto file = std::make_unique<AfcFile>(client, afcUrl.path()); 0666 0667 result = file->open(mode); 0668 if (!result.success()) { 0669 return result; 0670 } 0671 0672 if (mode.testFlag(QIODevice::ReadOnly) && !mode.testFlag(QIODevice::Append)) { 0673 guessMimeType(*file, afcUrl.path()); 0674 } 0675 0676 m_openFile = std::move(file); 0677 0678 totalSize(entry.numberValue(UDSEntry::UDS_SIZE, 0)); 0679 position(0); 0680 0681 return Result::pass(); 0682 } 0683 0684 Result AfcWorker::read(filesize_t bytesRequested) 0685 { 0686 if (!m_openFile) { 0687 return Result::fail(ERR_CANNOT_READ, i18n("Cannot read without opening file first")); 0688 } 0689 0690 AfcFileReader reader = m_openFile->reader(); 0691 reader.setSize(bytesRequested); 0692 0693 while (reader.hasMore()) { 0694 const Result result = reader.read(); 0695 if (!result.success()) { 0696 return result; 0697 } 0698 data(reader.data()); 0699 } 0700 0701 return Result::pass(); 0702 } 0703 0704 Result AfcWorker::seek(filesize_t offset) 0705 { 0706 if (!m_openFile) { 0707 return Result::fail(ERR_CANNOT_SEEK, i18n("Cannot seek without opening file first")); 0708 } 0709 0710 const Result result = m_openFile->seek(offset); 0711 if (result.success()) { 0712 position(offset); 0713 } 0714 0715 return result; 0716 } 0717 0718 Result AfcWorker::truncate(filesize_t length) 0719 { 0720 if (!m_openFile) { 0721 return Result::fail(ERR_CANNOT_TRUNCATE, QStringLiteral("Cannot truncate without opening file first")); 0722 } 0723 0724 Result result = m_openFile->truncate(length); 0725 if (result.success()) { 0726 truncated(length); 0727 } 0728 0729 return result; 0730 } 0731 0732 Result AfcWorker::write(const QByteArray &data) 0733 { 0734 if (!m_openFile) { 0735 return Result::fail(ERR_CANNOT_WRITE, i18n("Cannot write without opening file first")); 0736 } 0737 0738 uint32_t bytesWritten = 0; 0739 const Result result = m_openFile->write(data, bytesWritten); 0740 if (result.success()) { 0741 written(bytesWritten); 0742 } 0743 0744 return result; 0745 } 0746 0747 Result AfcWorker::close() 0748 { 0749 if (!m_openFile) { 0750 return Result::fail(ERR_INTERNAL, QStringLiteral("Cannot close what is not open")); 0751 } 0752 0753 const Result result = m_openFile->close(); 0754 if (result.success()) { 0755 m_openFile.reset(); 0756 } 0757 0758 return result; 0759 } 0760 0761 Result AfcWorker::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags) 0762 { 0763 Q_UNUSED(permissions); 0764 0765 const AfcUrl srcAfcUrl(src); 0766 const AfcUrl destAfcUrl(dest); 0767 0768 if (deviceIdForFriendlyUrl(srcAfcUrl) != deviceIdForFriendlyUrl(destAfcUrl)) { 0769 // Let KIO handle copying onto, off the, and between devices 0770 return Result::fail(ERR_UNSUPPORTED_ACTION); 0771 } 0772 0773 AfcClient::Ptr client; 0774 Result result = clientForUrl(srcAfcUrl, client); 0775 if (!result.success()) { 0776 return result; 0777 } 0778 0779 UDSEntry srcEntry; 0780 result = client->entry(srcAfcUrl.path(), srcEntry); 0781 if (!result.success()) { 0782 return result; 0783 } 0784 0785 UDSEntry destEntry; 0786 result = client->entry(destAfcUrl.path(), destEntry); 0787 0788 const bool exists = result.error() != ERR_DOES_NOT_EXIST; 0789 if (exists && !flags.testFlag(Overwrite)) { 0790 if (S_ISDIR(destEntry.numberValue(UDSEntry::UDS_FILE_TYPE))) { 0791 return Result::fail(ERR_DIR_ALREADY_EXIST, destAfcUrl.path()); 0792 } 0793 return Result::fail(ERR_FILE_ALREADY_EXIST, destAfcUrl.path()); 0794 } 0795 0796 AfcFile srcFile(client, srcAfcUrl.path()); 0797 result = srcFile.open(QIODevice::ReadOnly); 0798 if (!result.success()) { 0799 return result; 0800 } 0801 0802 AfcFile destFile(client, destAfcUrl.path()); 0803 0804 if (flags.testFlag(Resume)) { 0805 result = destFile.open(QIODevice::Append); 0806 } else { 0807 result = destFile.open(QIODevice::WriteOnly); 0808 } 0809 0810 if (!result.success()) { 0811 return result; 0812 } 0813 0814 auto cleanup = qScopeGuard([&client, &destAfcUrl] { 0815 qCInfo(KIO_AFC_LOG) << "Cleaning up leftover file" << destAfcUrl.path(); 0816 // NOTE cannot emit failure here because emitResult 0817 // will already have been called before 0818 auto result = client->del(destAfcUrl.path()); 0819 if (!result.success()) { 0820 qCWarning(KIO_AFC_LOG) << "Failed to clean up" << result.error() << result.errorString(); 0821 } 0822 }); 0823 0824 const auto size = srcEntry.numberValue(UDSEntry::UDS_SIZE, 0); 0825 totalSize(size); 0826 0827 AfcFileReader reader = srcFile.reader(); 0828 reader.setSize(size); 0829 0830 KIO::filesize_t copied = 0; 0831 0832 while (!wasKilled() && reader.hasMore()) { 0833 auto result = reader.read(); 0834 if (!result.success()) { 0835 return result; 0836 } 0837 0838 const QByteArray chunk = reader.data(); 0839 0840 uint32_t bytesWritten = 0; 0841 result = destFile.write(chunk, bytesWritten); 0842 if (!result.success()) { 0843 return result; 0844 } 0845 0846 // TODO check if bytesWritten matches reader.data().size()? 0847 0848 copied += chunk.size(); 0849 processedSize(copied); 0850 } 0851 0852 cleanup.dismiss(); 0853 destFile.close(); 0854 0855 // TODO check if conversion back and forth QDateTime is too expensive when copying many files 0856 const QDateTime mtime = QDateTime::fromSecsSinceEpoch(srcEntry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0)); 0857 if (mtime.isValid()) { 0858 client->setModificationTime(destAfcUrl.path(), mtime); 0859 } 0860 0861 return Result::pass(); 0862 } 0863 0864 Result AfcWorker::del(const QUrl &url, bool isFile) 0865 { 0866 const AfcUrl afcUrl(url); 0867 0868 AfcClient::Ptr client; 0869 Result result = clientForUrl(afcUrl, client); 0870 if (result.success()) { 0871 if (isFile) { 0872 result = client->del(afcUrl.path()); 0873 } else { 0874 result = client->delRecursively(afcUrl.path()); 0875 } 0876 } 0877 0878 return result; 0879 } 0880 0881 Result AfcWorker::rename(const QUrl &url, const QUrl &dest, JobFlags flags) 0882 { 0883 const AfcUrl srcAfcUrl(url); 0884 const AfcUrl destAfcUrl(dest); 0885 0886 if (deviceIdForFriendlyUrl(srcAfcUrl) != deviceIdForFriendlyUrl(destAfcUrl)) { 0887 return Result::fail(ERR_CANNOT_RENAME, i18n("Cannot rename between devices.")); 0888 } 0889 0890 AfcClient::Ptr client; 0891 Result result = clientForUrl(srcAfcUrl, client); 0892 if (result.success()) { 0893 result = client->rename(srcAfcUrl.path(), destAfcUrl.path(), flags); 0894 } 0895 0896 return result; 0897 } 0898 0899 Result AfcWorker::symlink(const QString &target, const QUrl &dest, JobFlags flags) 0900 { 0901 const AfcUrl destAfcUrl(dest); 0902 0903 AfcClient::Ptr client; 0904 Result result = clientForUrl(destAfcUrl, client); 0905 if (result.success()) { 0906 result = client->symlink(target, destAfcUrl.path(), flags); 0907 } 0908 0909 return result; 0910 } 0911 0912 Result AfcWorker::mkdir(const QUrl &url, int permissions) 0913 { 0914 Q_UNUSED(permissions) 0915 0916 const AfcUrl afcUrl(url); 0917 0918 AfcClient::Ptr client; 0919 Result result = clientForUrl(afcUrl, client); 0920 if (result.success()) { 0921 result = client->mkdir(afcUrl.path()); 0922 } 0923 0924 return result; 0925 } 0926 0927 Result AfcWorker::setModificationTime(const QUrl &url, const QDateTime &mtime) 0928 { 0929 const AfcUrl afcUrl(url); 0930 0931 AfcClient::Ptr client; 0932 Result result = clientForUrl(afcUrl, client); 0933 if (result.success()) { 0934 result = client->setModificationTime(afcUrl.path(), mtime); 0935 } 0936 0937 return result; 0938 } 0939 0940 Result AfcWorker::fileSystemFreeSpace(const QUrl &url) 0941 { 0942 // TODO FileSystemFreeSpaceJob does not follow redirects! 0943 const QUrl redirectUrl = resolveSolidUrl(url); 0944 if (redirectUrl.isValid()) { 0945 return fileSystemFreeSpace(redirectUrl); 0946 } 0947 0948 // TODO FileSystemFreeSpaceJob does not follow redirects! 0949 const AfcUrl afcUrl(url); 0950 if (afcUrl.device().isEmpty() && m_devices.count() == 1) { 0951 return fileSystemFreeSpace(QUrl(QStringLiteral("afc://%1/").arg((*m_devices.constBegin())->id()))); 0952 } 0953 0954 AfcClient::Ptr client; 0955 const Result result = clientForUrl(afcUrl, client); 0956 if (!result.success()) { 0957 return result; 0958 } 0959 0960 const AfcDiskUsage diskUsage(client); 0961 if (!diskUsage.isValid()) { 0962 return Result::fail(ERR_CANNOT_STAT, url.toDisplayString()); 0963 } 0964 0965 setMetaData(QStringLiteral("total"), QString::number(diskUsage.total())); 0966 setMetaData(QStringLiteral("available"), QString::number(diskUsage.free())); 0967 return Result::pass(); 0968 } 0969 0970 #include "kio_afc.moc"