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"