diff --git a/CMakeLists.txt b/CMakeLists.txt index 370dbb03..aa25d6ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,212 +1,222 @@ cmake_minimum_required(VERSION 3.0) # KDE Applications Version, managed by release script set (RELEASE_SERVICE_VERSION_MAJOR "20") set (RELEASE_SERVICE_VERSION_MINOR "07") set (RELEASE_SERVICE_VERSION_MICRO "70") set (RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") project(kio-extras VERSION ${RELEASE_SERVICE_VERSION}) include(FeatureSummary) set(QT_MIN_VERSION "5.11.0") set(KF5_MIN_VERSION "5.66.0") find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS DBus Network Widgets Svg) find_package(Qt5Test ${QT_MIN_VERSION} CONFIG QUIET) set_package_properties(Qt5Test PROPERTIES PURPOSE "Required for tests" TYPE OPTIONAL ) add_feature_info("Qt5Test" Qt5Test_FOUND "Required for building tests") if (NOT Qt5Test_FOUND) set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.") endif() find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Archive Config ConfigWidgets CoreAddons DBusAddons DocTools DNSSD IconThemes I18n KIO Solid Bookmarks GuiAddons SyntaxHighlighting ) # As this is the check used for linkage, only require it in the same location... if (UNIX) find_package(KF5Pty ${KF5_MIN_VERSION} REQUIRED) endif() include(CheckIncludeFile) include(CMakePackageConfigHelpers) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMMarkNonGuiExecutable) include(ECMMarkAsTest) include(ECMOptionalAddSubdirectory) include(ECMQtDeclareLoggingCategory) add_definitions(-DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_TO_ASCII) find_package(KF5Activities QUIET) set_package_properties(KF5Activities PROPERTIES PURPOSE "Provides the activities:/ kioslave and fileitem plugin." TYPE OPTIONAL ) find_package(Qt5Sql QUIET) set_package_properties(Qt5Sql PROPERTIES PURPOSE "Provides the activities:/ kioslave and fileitem plugin." TYPE OPTIONAL ) find_package(KF5ActivitiesStats 5.62 QUIET) set_package_properties(KF5ActivitiesStats PROPERTIES PURPOSE "Provides the recentlyused:/ kioslave." TYPE OPTIONAL ) find_package(Phonon4Qt5 4.6.60 NO_MODULE) set_package_properties(Phonon4Qt5 PROPERTIES DESCRIPTION "Qt-based audio library" PURPOSE "Required for the audio preview plugin" TYPE OPTIONAL) include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) # we need a version of samba which has already smbc_set_context(), Alex set(SAMBA_REQUIRE_SMBC_SET_CONTEXT TRUE) set(SAMBA_REQUIRE_SMBC_OPTION_SET TRUE) find_package(Samba) set_package_properties(Samba PROPERTIES DESCRIPTION "the SMB client library, a version with smbc_set_context() and smbc_option_set()" URL "https://www.samba.org/" TYPE OPTIONAL PURPOSE "Needed to build the SMB kioslave" ) endif() find_package(libssh 0.7.0 MODULE) set_package_properties(libssh PROPERTIES DESCRIPTION "the SSH library with SFTP support" URL "https://www.libssh.org/" TYPE OPTIONAL PURPOSE "Needed to build the SFTP kioslave" ) find_package(Mtp) set_package_properties(Mtp PROPERTIES DESCRIPTION "the MTP library" URL "http://libmtp.sourceforge.net/" TYPE OPTIONAL PURPOSE "Needed to build the MTP kioslave" ) +find_package(IMobileDevice) +set_package_properties(IMobileDevice PROPERTIES + TYPE OPTIONAL + PURPOSE "Needed to build the AFC (Apple File Conduit) kioslave" + ) + check_include_file(utime.h HAVE_UTIME_H) # ECM's KDECompilerSettings.cmake should take care of enabling supporting on # 32bit architectures. # Thorw a fatal error if off_t isn't >=64bit to ensure that large files are working # as expected. # BUG: 165449 if(UNIX) check_cxx_source_compiles(" #include /* Check that off_t can represent 2**63 - 1 correctly. We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ #define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; int main() { return 0; } " OFFT_IS_64BIT) if(NOT OFFT_IS_64BIT) message(FATAL_ERROR "Large file support is not enabled.") endif() find_package(Gperf) set_package_properties(Gperf PROPERTIES TYPE OPTIONAL PURPOSE "Needed to build the man kioslave" ) find_package(TIRPC) set_package_properties(TIRPC PROPERTIES TYPE OPTIONAL PURPOSE "Needed to build the NFS kioslave" ) else() # FIXME: on windows we ignore support until trash gets integrated endif() add_subdirectory( doc ) add_subdirectory( about ) if(TARGET KF5::Activities AND TARGET Qt5::Sql) add_subdirectory( activities ) endif() if(KF5ActivitiesStats_FOUND) add_subdirectory( recentlyused ) endif() add_subdirectory( bookmarks ) add_subdirectory( filter ) if(Phonon4Qt5_FOUND) add_subdirectory( kfileaudiopreview ) endif() add_subdirectory( info ) add_subdirectory( archive ) if(NOT WIN32) add_subdirectory( network ) endif() add_subdirectory( recentdocuments ) if (NOT WIN32) # does not compile: fish.cpp(41): fatal error C1083: Cannot open include file: 'sys/resource.h': No such file or directory # Used for getting the resource limit for closing all child process FDs. Could be completely replaced by fcloseall() if available for Unix or _fcloseall() for Windows, either conditionally on Q_OS_type or using a configure test. add_subdirectory( fish ) endif() add_subdirectory( thumbnail ) add_subdirectory( docfilter ) if (libssh_FOUND) add_subdirectory(sftp) endif () add_subdirectory(settings) add_subdirectory( filenamesearch ) if (MTP_FOUND) add_subdirectory(mtp) endif() if(NOT WIN32) if(Gperf_FOUND) add_subdirectory( man ) endif() if(TIRPC_FOUND) add_subdirectory( nfs ) endif() endif() # KDNSSD before 5.54 suffers from a race condition in avahi's dbus API and # ideally should not be used in ways that can deadlock a slave. if(${KF5DNSSD_FOUND} AND ${KF5DNSSD_VERSION} VERSION_GREATER "5.53") set(HAVE_KDNSSD_WITH_SIGNAL_RACE_PROTECTION TRUE) endif() if(SAMBA_FOUND) add_subdirectory(smb) endif() +if(IMobileDevice_FOUND) + add_subdirectory(afc) +endif() + configure_file (config-runtime.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-runtime.h ) install(FILES kio-extras.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/afc/CMakeLists.txt b/afc/CMakeLists.txt new file mode 100644 index 00000000..2abececc --- /dev/null +++ b/afc/CMakeLists.txt @@ -0,0 +1,31 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kio5_afc\") + +set(kio_afc_SRCS + kio_afc.cpp + afcdevice.cpp + afcerror.cpp + afcurl.cpp + afcdiskusage.cpp + afcfilereader.cpp +) + +ecm_qt_declare_logging_category(kio_afc_SRCS + HEADER afc-debug.h + IDENTIFIER KIO_AFC + CATEGORY_NAME kio_afc) + +add_library(kio_afc MODULE ${kio_afc_SRCS}) + +target_link_libraries(kio_afc + PUBLIC + KF5::KIOCore + IMobileDevice::IMobileDevice + plist # FIXME do I need to find this lib? + PRIVATE + KF5::I18n +) + +set_target_properties(kio_afc PROPERTIES OUTPUT_NAME "afc") +install(TARGETS kio_afc DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kio) + +install(FILES solid_afc.desktop DESTINATION ${KDE_INSTALL_DATADIR}/solid/actions) diff --git a/afc/afc.json b/afc/afc.json new file mode 100644 index 00000000..508f311b --- /dev/null +++ b/afc/afc.json @@ -0,0 +1,27 @@ +{ + "KDE-KIO-Protocols": { + "afc": { + "Class": ":internet", + "Icon": "smartphone", + "X-DocPath": "kioslave5/afc/index.html", + "deleting": true, + "exec": "kf5/kio/afc", + "input": "none", + "listing": [ + "Name", + "Type", + "Size", + "Date" + ], + "linking": true, + "makedir": true, + "moving": true, + "output": "filesystem", + "protocol": "afc", + "opening": true, + "truncating": true, + "reading": true, + "writing": true + } + } +} diff --git a/afc/afcdevice.cpp b/afc/afcdevice.cpp new file mode 100644 index 00000000..67c4bb2c --- /dev/null +++ b/afc/afcdevice.cpp @@ -0,0 +1,398 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "afcdevice.h" + +#include "afc-debug.h" + +#include "afcerror.h" + +#include + +#include +#include + +using namespace KIO; + +AfcDevice::AfcDevice(const QString &id) + : m_id(id) +{ + idevice_new(&m_device, id.toUtf8().constData()); + if (!m_device) { + qCWarning(KIO_AFC) << "Failed to create idevice for" << id; + return; + } + + auto ret = lockdownd_client_new_with_handshake(m_device, &m_lockdowndClient, "kio_afc"); + if (ret != LOCKDOWN_E_SUCCESS) { + qCWarning(KIO_AFC) << "Failed to create lockdown client for" << id << ret << "make sure the device is unlocked and trusted"; + return; + } + + lockdownd_service_descriptor_t service = nullptr; + ret = lockdownd_start_service(m_lockdowndClient, "com.apple.afc", &service); + if (ret != LOCKDOWN_E_SUCCESS) { + qCWarning(KIO_AFC) << "Failed to start AFC service through lockdownd on" << id; + return; + } + + auto afcRet = afc_client_new(m_device, service, &m_afcClient); + if (afcRet != AFC_E_SUCCESS) { + qCWarning(KIO_AFC) << "Failed to create AFC client for" << id; + return; + } + + char *name = nullptr; + auto lockdownRet = lockdownd_get_device_name(m_lockdowndClient, &name); + if (lockdownRet != LOCKDOWN_E_SUCCESS) { + qCWarning(KIO_AFC) << "Failed to get device name for" << id; + } else { + m_name = QString::fromUtf8(name); + free(name); + } + + char *model = nullptr; + if (afc_get_device_info_key(m_afcClient, "Model", &model) != AFC_E_SUCCESS) { + qCWarning(KIO_AFC) << "Failed to get device model for" << id; + } else { + m_model = QString::fromUtf8(model); + free(model); + } +} + +AfcDevice::~AfcDevice() +{ + if (m_afcClient) { + afc_client_free(m_afcClient); + m_afcClient = nullptr; + } + + if (m_lockdowndClient) { + lockdownd_client_free(m_lockdowndClient); + m_lockdowndClient = nullptr; + } + + if (m_device) { + idevice_free(m_device); + m_device = nullptr; + } +} + +QString AfcDevice::id() const +{ + return m_id; +} + +bool AfcDevice::isValid() const +{ + return m_device && m_afcClient; +} + +QString AfcDevice::name() const +{ + return m_name; +} + +QString AfcDevice::model() const +{ + return m_model; +} + +UDSEntry AfcDevice::rootEntry(const QString &fileName) const +{ + UDSEntry entry; + entry.fastInsert(UDSEntry::UDS_NAME, !fileName.isEmpty() ? fileName : m_id); + entry.fastInsert(UDSEntry::UDS_DISPLAY_NAME, m_name); + // TODO prettier + entry.fastInsert(UDSEntry::UDS_DISPLAY_TYPE, m_model); + entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); + entry.fastInsert(UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); + + QString iconName; + if (m_model.contains(QLatin1String("iPhone"))) { + iconName = QStringLiteral("phone-apple-iphone"); + } else if (m_model.contains(QLatin1String("iPad"))) { + iconName = QStringLiteral("computer-apple-ipad"); + } else if (m_model.contains(QLatin1String("iPod"))) { + // We can assume iPod running iOS/supporting imobiledevice is an iPod touch? + iconName = QStringLiteral("multimedia-player-apple-ipod-touch"); + } + + if (!iconName.isEmpty()) { + entry.fastInsert(UDSEntry::UDS_ICON_NAME, iconName); + } + + return entry; +} + +UDSEntry AfcDevice::entry(const QString &path, AfcError &error) const +{ + UDSEntry entry; + + char **info = nullptr; + const auto ret = afc_get_file_info(m_afcClient, path.toUtf8(), &info); + // may return null https://github.com/libimobiledevice/libimobiledevice/issues/206 + error = AfcError(ret, path); + if (error || !info) { + return entry; + } + + const int lastSlashIdx = path.lastIndexOf(QLatin1Char('/')); + entry.fastInsert(UDSEntry::UDS_NAME, path.mid(lastSlashIdx + 1)); + + // Apply special icons for known locations + static const QHash s_folderIcons = { + {QStringLiteral("/DCIM"), QStringLiteral("camera-photo")}, + {QStringLiteral("/Downloads"), QStringLiteral("folder-downloads")}, + {QStringLiteral("/Photos"), QStringLiteral("folder-pictures")} + }; + const QString iconName = s_folderIcons.value(path); + if (!iconName.isEmpty()) { + entry.fastInsert(UDSEntry::UDS_ICON_NAME, iconName); + } + + for (int i = 0; info[i]; i += 2) { + const auto *key = info[i]; + const auto *value = info[i + 1]; + + if (strcmp(key, "st_size") == 0) { + entry.fastInsert(UDSEntry::UDS_SIZE, atoll(value)); + } else if (strcmp(key, "st_blocks") == 0) { + + } else if (strcmp(key, "st_nlink") == 0) { + + } else if (strcmp(key, "st_ifmt") == 0) { + int type = 0; + if (strcmp(value, "S_IFREG") == 0) { + type = S_IFREG; + } else if (strcmp(value, "S_IFDIR") == 0) { + // TODO also inode/directory mime? + type = S_IFDIR; + } else if (strcmp(value, "S_IFLNK") == 0) { + type = S_IFLNK; + } + // TODO S_IFMT, S_IFCHR, S_IFBLK, S_IFIFO, S_IFSOCK? + + if (type) { + entry.fastInsert(UDSEntry::UDS_FILE_TYPE, type); + } else { + qCWarning(KIO_AFC) << "Encountered unknown" << key << "of" << value << "for" << path; + } + // is returned in nanoseconds + } else if (strcmp(key, "st_mtime") == 0) { + entry.fastInsert(UDSEntry::UDS_MODIFICATION_TIME, atoll(value) / 1000000000); + } else if (strcmp(key, "st_birthtime") == 0) { + entry.fastInsert(UDSEntry::UDS_CREATION_TIME, atoll(value) / 1000000000); + } else { + qCDebug(KIO_AFC) << "Encountered file info key" << key << "for" << path; + } + } + + if (info) { + afc_dictionary_free(info); + } + + return entry; +} + +QStringList AfcDevice::entryList(const QString &path, AfcError &error) const +{ + char **entries = nullptr; + QStringList stringList; + + const auto ret = afc_read_directory(m_afcClient, path.toUtf8(), &entries); + error = AfcError(ret, path); + if (error) { + return stringList; + } + + char **it = entries; + while (*it) { + stringList.append(QString::fromUtf8(*it)); + ++it; + } + + if (entries) { + afc_dictionary_free(entries); + } + + return stringList; +} + +void AfcDevice::open(const QString &path, QIODevice::OpenMode mode, AfcError &error) +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + + afc_file_mode_t fileMode = static_cast(0); + + if (mode == QIODevice::ReadOnly) { + fileMode = AFC_FOPEN_RDONLY; + } else if (mode == QIODevice::WriteOnly) { + fileMode = AFC_FOPEN_WRONLY; + } else if (mode == QIODevice::ReadWrite) { + fileMode = AFC_FOPEN_RW; + } else if (mode == (QIODevice::ReadWrite | QIODevice::Truncate)) { + fileMode = AFC_FOPEN_WR; + } else if (mode == QIODevice::Append || mode == (QIODevice::Append | QIODevice::WriteOnly)) { + fileMode = AFC_FOPEN_APPEND; + } else if (mode == (QIODevice::Append | QIODevice::ReadWrite)) { + fileMode = AFC_FOPEN_RDAPPEND; + } + + if (!fileMode) { + error = AfcError(KIO::ERR_UNSUPPORTED_ACTION, QString::number(mode)); + return; + } + + const auto ret = afc_file_open(m_afcClient, path.toLocal8Bit().constData(), fileMode, &m_currentHandle); + error = AfcError(ret, path); + if (error) { + return; + } +} + +void AfcDevice::seek(filesize_t offset, AfcError &error) +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + const auto ret = afc_file_seek(m_afcClient, m_currentHandle, offset, SEEK_SET); + error = AfcError(ret); +} + +void AfcDevice::truncate(filesize_t length, AfcError &error) +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + const auto ret = afc_file_truncate(m_afcClient, m_currentHandle, length); + error = AfcError(ret); +} + +uint32_t AfcDevice::write(const QByteArray &data, AfcError &error) +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + uint32_t bytesWritten; + const auto ret = afc_file_write(m_afcClient, m_currentHandle, data.constData(), data.size(), &bytesWritten); + error = AfcError(ret); + return bytesWritten; +} + +void AfcDevice::close(AfcError &error) +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + + const auto ret = afc_file_close(m_afcClient, m_currentHandle); + error = AfcError(ret); + if (error) { + return; + } + + m_currentHandle = -1; +} + +void AfcDevice::del(const QString &path, AfcError &error) +{ + const auto ret = afc_remove_path(m_afcClient, path.toUtf8()); + error = AfcError(ret, path); +} + +void AfcDevice::rename(const QString &src, const QString &dest, JobFlags flags, AfcError &error) +{ + UDSEntry srcEntry = this->entry(src, error); + Q_UNUSED(srcEntry) + if (error) { + return; + } + + AfcError destError; + UDSEntry destEntry = this->entry(dest, destError); + const bool exists = destError.errorCode() != ERR_DOES_NOT_EXIST; + if (exists && !flags.testFlag(KIO::Overwrite)) { + if (S_ISDIR(destEntry.numberValue(UDSEntry::UDS_FILE_TYPE))) { + error = AfcError(ERR_DIR_ALREADY_EXIST, dest); + } else { + error = AfcError(ERR_FILE_ALREADY_EXIST, dest); + } + return; + } + + const auto ret = afc_rename_path(m_afcClient, + src.toUtf8().constData(), + dest.toUtf8().constData()); + error = AfcError(ret, dest); +} + +void AfcDevice::symlink(const QString &target, const QString &dest, JobFlags flags, AfcError &error) +{ + UDSEntry targetEntry = this->entry(target, error); + Q_UNUSED(targetEntry) + if (error) { + return; + } + + AfcError destError; + UDSEntry destEntry = this->entry(dest, destError); + const bool exists = destError.errorCode() != ERR_DOES_NOT_EXIST; + if (exists && !flags.testFlag(KIO::Overwrite)) { + if (S_ISDIR(destEntry.numberValue(UDSEntry::UDS_FILE_TYPE))) { + error = AfcError(ERR_DIR_ALREADY_EXIST, dest); + } else { + error = AfcError(ERR_FILE_ALREADY_EXIST, dest); + } + return; + } + + const auto ret = afc_make_link(m_afcClient, + AFC_SYMLINK, + target.toUtf8().constData(), + dest.toUtf8().constData()); + error = AfcError(ret, dest); +} + +void AfcDevice::mkdir(const QString &path, AfcError &error) +{ + AfcError getError; + UDSEntry entry = this->entry(path, getError); + const bool exists = getError.errorCode() != ERR_DOES_NOT_EXIST; + if (exists) { + if (S_ISDIR(entry.numberValue(UDSEntry::UDS_FILE_TYPE))) { + error = AfcError(ERR_DIR_ALREADY_EXIST, path); + } else { + error = AfcError(ERR_FILE_ALREADY_EXIST, path); + } + return; + } + + const auto ret = afc_make_directory(m_afcClient, path.toUtf8().constData()); + error = AfcError(ret, path); +} + +void AfcDevice::setModificationTime(const QString &path, const QDateTime &mtime, AfcError &error) +{ + const auto ret = afc_set_file_time(m_afcClient, + path.toUtf8().constData(), + mtime.toMSecsSinceEpoch() /*ms*/ * 1000000 /*us*/); + error = AfcError(ret, path); +} + +AfcDiskUsage AfcDevice::diskUsage() const +{ + plist_t plist = nullptr; + if (lockdownd_get_value(m_lockdowndClient, + "com.apple.disk_usage", + nullptr /*key*/, + &plist) != LOCKDOWN_E_SUCCESS) { + return AfcDiskUsage(); + } + + AfcDiskUsage usage(plist); + + plist_free(plist); + return usage; +} + +AfcFileReader AfcDevice::fileReader() const +{ + Q_ASSERT(m_currentHandle == static_cast(-1)); + AfcFileReader reader(m_afcClient, m_currentHandle); + return reader; +} diff --git a/afc/afcdevice.h b/afc/afcdevice.h new file mode 100644 index 00000000..2badeb80 --- /dev/null +++ b/afc/afcdevice.h @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +#include +#include + +#include "afcdiskusage.h" +#include "afcfilereader.h" + +#include + +class QDateTime; + +class AfcError; + +class AfcDevice +{ +public: + explicit AfcDevice(const QString &id); + ~AfcDevice(); + + QString id() const; + bool isValid() const; + QString errorText() const; + + QString name() const; + QString model() const; + + KIO::UDSEntry rootEntry(const QString &fileName = QString()) const; + KIO::UDSEntry entry(const QString &path, AfcError &error) const; + QStringList entryList(const QString &path, AfcError &error) const; + + void open(const QString &path, QIODevice::OpenMode mode, AfcError &error); + void seek(KIO::filesize_t offset, AfcError &error); + void truncate(KIO::filesize_t length, AfcError &error); + uint32_t write(const QByteArray &data, AfcError &error); + void close(AfcError &error); + + void del(const QString &path, AfcError &error); + void rename(const QString &src, const QString &dest, KIO::JobFlags flags, AfcError &error); + void symlink(const QString &target, const QString &dest, KIO::JobFlags flags, AfcError &error); + void mkdir(const QString &path, AfcError &error); + void setModificationTime(const QString &path, const QDateTime &mtime, AfcError &error); + + AfcDiskUsage diskUsage() const; + AfcFileReader fileReader() const; + +private: + idevice_t m_device = nullptr; + lockdownd_client_t m_lockdowndClient = nullptr; + afc_client_t m_afcClient = nullptr; + + QString m_id; + QString m_name; + QString m_model; + + uint64_t m_currentHandle = static_cast(-1); + +}; diff --git a/afc/afcdiskusage.cpp b/afc/afcdiskusage.cpp new file mode 100644 index 00000000..5c765383 --- /dev/null +++ b/afc/afcdiskusage.cpp @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "afcdiskusage.h" + +using namespace KIO; + +AfcDiskUsage::AfcDiskUsage() = default; + +AfcDiskUsage::AfcDiskUsage(plist_t plist) +{ + uint64_t value = 0; + if (auto item = plist_dict_get_item(plist, "TotalDiskCapacity")) { + plist_get_uint_val(item, &value); + m_totalDiskCapacity = value; + } else { + return; + } + + if (auto item = plist_dict_get_item(plist, "TotalDataCapacity")) { + plist_get_uint_val(item, &value); + m_totalDataCapacity = value; + } else { + return; + } + + if (auto item = plist_dict_get_item(plist, "TotalDataAvailable")) { + plist_get_uint_val(item, &value); + m_totalDataAvailable = value; + } else { + return; + } + + m_valid = true; +} + +bool AfcDiskUsage::isValid() const +{ + return m_valid; +} + +filesize_t AfcDiskUsage::totalDiskCapacity() const +{ + return m_totalDiskCapacity; +} + +filesize_t AfcDiskUsage::totalDataCapacity() const +{ + return m_totalDataCapacity; +} + +filesize_t AfcDiskUsage::totalDataAvailable() const +{ + return m_totalDataAvailable; +} diff --git a/afc/afcdiskusage.h b/afc/afcdiskusage.h new file mode 100644 index 00000000..958d8340 --- /dev/null +++ b/afc/afcdiskusage.h @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +class AfcDiskUsage +{ +public: + AfcDiskUsage(); + explicit AfcDiskUsage(plist_t plist); + + bool isValid() const; + + KIO::filesize_t totalDiskCapacity() const; + KIO::filesize_t totalDataCapacity() const; + KIO::filesize_t totalDataAvailable() const; + // There's also CameraUsage and MobileApplicationUsage + +private: + bool m_valid = false; + + KIO::filesize_t m_totalDiskCapacity = 0; + KIO::filesize_t m_totalDataCapacity = 0; + KIO::filesize_t m_totalDataAvailable = 0; +}; diff --git a/afc/afcerror.cpp b/afc/afcerror.cpp new file mode 100644 index 00000000..81c4689f --- /dev/null +++ b/afc/afcerror.cpp @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "afcerror.h" + +#include "afc-debug.h" + +#include + +AfcError::AfcError() +{ + +} + +AfcError::AfcError(afc_error_t afcError, const QString &errorText) +{ + switch (afcError) { + case AFC_E_SUCCESS: + case AFC_E_END_OF_DATA: + m_errorCode = static_cast(0); // KJob::NoError + break; + + case AFC_E_UNKNOWN_ERROR: + m_errorCode = KIO::ERR_UNKNOWN; + break; + + case AFC_E_NO_RESOURCES: + case AFC_E_NO_MEM: + m_errorCode = KIO::ERR_OUT_OF_MEMORY; + break; + + case AFC_E_READ_ERROR: + m_errorCode = KIO::ERR_COULD_NOT_READ; + break; + case AFC_E_WRITE_ERROR: + m_errorCode = KIO::ERR_COULD_NOT_WRITE; + break; + case AFC_E_OBJECT_NOT_FOUND: + m_errorCode = KIO::ERR_DOES_NOT_EXIST; + break; + case AFC_E_OBJECT_IS_DIR: + m_errorCode = KIO::ERR_IS_DIRECTORY; + break; + case AFC_E_PERM_DENIED: + m_errorCode = KIO::ERR_ACCESS_DENIED; + break; + case AFC_E_SERVICE_NOT_CONNECTED : + m_errorCode = KIO::ERR_CONNECTION_BROKEN; + break; + case AFC_E_OP_TIMEOUT: + m_errorCode = KIO::ERR_SERVER_TIMEOUT; + break; + case AFC_E_OP_NOT_SUPPORTED: + m_errorCode = KIO::ERR_UNSUPPORTED_ACTION; + break; + case AFC_E_OBJECT_EXISTS: + m_errorCode = KIO::ERR_FILE_ALREADY_EXIST; + break; + case AFC_E_NO_SPACE_LEFT: + m_errorCode = KIO::ERR_DISK_FULL; + break; + case AFC_E_IO_ERROR: + m_errorCode = KIO::ERR_CONNECTION_BROKEN; + break; + case AFC_E_INTERNAL_ERROR: + m_errorCode = KIO::ERR_INTERNAL_SERVER; + break; + case AFC_E_DIR_NOT_EMPTY: + m_errorCode = KIO::ERR_COULD_NOT_RMDIR; + break; + + case AFC_E_OP_HEADER_INVALID: + case AFC_E_UNKNOWN_PACKET_TYPE: + case AFC_E_INVALID_ARG: + case AFC_E_TOO_MUCH_DATA: + case AFC_E_OBJECT_BUSY: + case AFC_E_FORCE_SIGNED_TYPE: + case AFC_E_OP_INTERRUPTED: // UNKNOWN_INTERRUPT? + case AFC_E_OP_IN_PROGRESS: + case AFC_E_OP_WOULD_BLOCK: + case AFC_E_MUX_ERROR: + case AFC_E_NOT_ENOUGH_DATA: + m_errorCode = KIO::ERR_INTERNAL; + break; + default: + qCWarning(KIO_AFC) << "Unhandled afc_error_t" << afcError; + } + + if (m_errorCode) { + m_errorText = errorText; + } +} + +AfcError::AfcError(KIO::Error kioError, const QString &errorText) + : m_errorCode(kioError) + , m_errorText(errorText) +{ + +} + +bool AfcError::isValid() const +{ + return m_errorCode != -1; +} + +KIO::Error AfcError::errorCode() const +{ + return m_errorCode; +} + +QString AfcError::errorText() const +{ + return m_errorText; +} + +AfcError::operator bool() const +{ + return isValid() && m_errorCode != 0; +} + +QDebug operator<<(QDebug debug, const AfcError &error) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "AfcError("; + if (!error.isValid()) { + debug << "[Invalid]"; + } else if (!error) { + debug << "[No error]"; + } else { + debug << error.errorCode(); + } + debug << ")"; + return debug; +} diff --git a/afc/afcerror.h b/afc/afcerror.h new file mode 100644 index 00000000..8b9a224a --- /dev/null +++ b/afc/afcerror.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +#include + +class AfcError +{ +public: + AfcError(); + explicit AfcError(afc_error_t afcError, const QString &errorText = QString()); + explicit AfcError(KIO::Error kioError, const QString &errorText = QString()); + + bool isValid() const; + KIO::Error errorCode() const; + QString errorText() const; + + operator bool() const; + +private: + KIO::Error m_errorCode = static_cast(-1); + QString m_errorText; + +}; + +QDebug operator<<(QDebug debug, const AfcError &error); diff --git a/afc/afcfilereader.cpp b/afc/afcfilereader.cpp new file mode 100644 index 00000000..8736615d --- /dev/null +++ b/afc/afcfilereader.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "afcfilereader.h" + +#include "afc-debug.h" + +using namespace KIO; + +AfcFileReader::AfcFileReader(afc_client_t afcClient, uint64_t handle) + : m_afcClient(afcClient) + , m_handle(handle) +{ + +} + +AfcFileReader::~AfcFileReader() +{ + delete[] m_buffer; +} + +filesize_t AfcFileReader::size() const +{ + return m_size; +} + +void AfcFileReader::setSize(filesize_t size) +{ + m_size = size; + m_remainingSize = size; + + // TODO just realloc or something? + if (m_buffer) { + delete[] m_buffer; + m_buffer = nullptr; + } +} + +void AfcFileReader::read() +{ + m_data.clear(); + m_error = AfcError(); + + if (m_remainingSize == 0) { + return; + } + + // TODO check if size is right or zero it out? + if (!m_buffer) { + m_buffer = new char[m_remainingSize]; + } + + if (!m_buffer) { + m_error = AfcError(ERR_OUT_OF_MEMORY); + return; + } + + uint32_t bytesRead = 0; + afc_error_t ret = AFC_E_SUCCESS; + + ret = afc_file_read(m_afcClient, m_handle, m_buffer, m_remainingSize, &bytesRead); + + if (ret != AFC_E_SUCCESS && ret != AFC_E_END_OF_DATA) { + m_error = AfcError(ret); + return; + } + + m_data = QByteArray::fromRawData(m_buffer, bytesRead); + m_remainingSize -= bytesRead; +} + +AfcError AfcFileReader::error() const +{ + return m_error; +} + +QByteArray AfcFileReader::data() const +{ + return m_data; +} + +bool AfcFileReader::hasMore() const +{ + return m_remainingSize > 0; +} diff --git a/afc/afcfilereader.h b/afc/afcfilereader.h new file mode 100644 index 00000000..9e0fd840 --- /dev/null +++ b/afc/afcfilereader.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +#include "afcerror.h" + +class AfcFileReader +{ +public: + ~AfcFileReader(); + + KIO::filesize_t size() const; + void setSize(KIO::filesize_t size); + + void read(); + bool hasMore() const; + + AfcError error() const; + QByteArray data() const; + +private: + friend class AfcDevice; + + AfcFileReader(afc_client_t afcClient, uint64_t handle); + + afc_client_t m_afcClient; + uint64_t m_handle; + KIO::filesize_t m_size = 0; + KIO::filesize_t m_remainingSize = 0; + + AfcError m_error; + char *m_buffer = nullptr; + QByteArray m_data; + +}; diff --git a/afc/afcurl.cpp b/afc/afcurl.cpp new file mode 100644 index 00000000..f0344b81 --- /dev/null +++ b/afc/afcurl.cpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "afcurl.h" + +AfcUrl::AfcUrl(const QUrl &url) +{ + if (!url.isValid() || url.scheme() != QLatin1String("afc")) { + return; + } + + const QString path = url.path(); + if (path.length() <= 1) { + // too short to have any device name + return; + } + + Q_ASSERT(path.startsWith(QLatin1Char('/'))); + + const int slashIdx = path.indexOf(QLatin1Char('/'), 1); + if (slashIdx == -1) { + m_deviceId = path.mid(1); + return; + } + + m_deviceId = path.mid(1, slashIdx - 1); + m_path = path.mid(slashIdx + 1); +} + +QString AfcUrl::deviceId() const +{ + return m_deviceId; +} + +void AfcUrl::setDeviceId(const QString &deviceId) +{ + m_deviceId = deviceId; +} + +QString AfcUrl::path() const +{ + return m_path; +} + +void AfcUrl::setPath(const QString &path) +{ + m_path = path; +} diff --git a/afc/afcurl.h b/afc/afcurl.h new file mode 100644 index 00000000..a59f2d65 --- /dev/null +++ b/afc/afcurl.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include + +class AfcUrl +{ +public: + explicit AfcUrl(const QUrl &url); + + QString deviceId() const; + void setDeviceId(const QString &deviceId); + + QString path() const; + void setPath(const QString &path); + +private: + QString m_deviceId; + QString m_path; + +}; diff --git a/afc/kio_afc.cpp b/afc/kio_afc.cpp new file mode 100644 index 00000000..48a226e3 --- /dev/null +++ b/afc/kio_afc.cpp @@ -0,0 +1,755 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "kio_afc.h" + +#include "afc-debug.h" + +#include "afcdevice.h" +#include "afcerror.h" +#include "afcfilereader.h" + +#include +#include +#include + +#include + +using namespace KIO; + +// Pseudo plugin class to embed meta data +class KIOPluginForMetaData : public QObject +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kio.slave.afc" FILE "afc.json") +}; + +AfcSlave::AfcSlave(const QByteArray &pool, const QByteArray &app) + : SlaveBase("afc", pool, app) +{ + idevice_event_subscribe([](const idevice_event_t *event, void *user_data) { + static_cast(user_data)->onDeviceEvent(event); + }, this); + + updateDeviceList(); +} + +AfcSlave::~AfcSlave() +{ + idevice_event_unsubscribe(); + + // Should we close it first? + m_openDevice = nullptr; + + qDeleteAll(m_devices); + m_devices.clear(); +} + +void AfcSlave::onDeviceEvent(const idevice_event_t *event) +{ + switch (event->event) { + case IDEVICE_DEVICE_ADD: + addDevice(QString::fromLatin1(event->udid)); + return; + case IDEVICE_DEVICE_REMOVE: { + removeDevice(QString::fromLatin1(event->udid)); + return; + } + case IDEVICE_DEVICE_PAIRED: + return; + } + + qCWarning(KIO_AFC) << "Unhandled idevice event" << event->event << "for" << event->udid; +} + +AfcUrl AfcSlave::afcUrl(const QUrl &url) const +{ + AfcUrl afcUrl(url); + // Resolve pretty URL + const QString deviceIdFromUniqueName = m_deviceUniqueNames.value(afcUrl.deviceId()); + if (!deviceIdFromUniqueName.isEmpty()) { + afcUrl.setDeviceId(deviceIdFromUniqueName); + } + return afcUrl; +} + +void AfcSlave::emitError(const AfcError &afcError) +{ + error(afcError.errorCode(), afcError.errorText()); +} + +QUrl AfcSlave::resolveSolidUrl(const QUrl &url) const +{ + const QString path = url.path(); + + const QString prefix = QStringLiteral("udi=/org/kde/solid/imobile/"); + if (!path.startsWith(prefix)) { + return {}; + } + + QString deviceId = path.mid(prefix.length()); + const int slashIdx = deviceId.indexOf(QLatin1Char('/')); + if (slashIdx > -1) { + deviceId = deviceId.left(slashIdx); + } + + QString newPath = m_deviceUniqueNames.key(deviceId); + if (newPath.isEmpty()) { + newPath = deviceId; + } + + // TODO would be nice to preserve subdirectories + const QUrl newUrl(QStringLiteral("afc:/%1").arg(newPath)); + return newUrl; +} + +bool AfcSlave::redirectIfSolidUrl(const QUrl &url) +{ + const QUrl redirectUrl = resolveSolidUrl(url); + if (!redirectUrl.isValid()) { + return false; + } + + redirection(redirectUrl); + finished(); + return true; +} + +void AfcSlave::updateDeviceList(bool *foundInvalidDevices) +{ + char **devices = nullptr; + int count = 0; + + idevice_get_device_list(&devices, &count); + for (int i = 0; i < count; ++i) { + const QString id = QString::fromLatin1(devices[i]); + if (!addDevice(id)) { + if (foundInvalidDevices) { + *foundInvalidDevices = true; + } + } + } + + if (devices) { + idevice_device_list_free(devices); + } +} + +bool AfcSlave::addDevice(const QString &id) +{ + if (m_devices.contains(id)) { + return true; + } + + auto *device = new AfcDevice(id); + if (!device->isValid()) { + delete device; + return false; + } + + m_devices.insert(id, device); + + if (device->name().isEmpty()) { + return true; // Without a name isn't pretty but still valid + } + + // FIXME append (2) or wahtever instead of not doing it + if (!m_deviceUniqueNames.contains(device->name())) { + m_deviceUniqueNames.insert(device->name(), id); + } + return true; +} + +void AfcSlave::removeDevice(const QString &id) +{ + auto *device = m_devices.take(id); + if (device) { + const QString uniquePrettyName = m_deviceUniqueNames.key(id); + if (!uniquePrettyName.isEmpty()) { + m_deviceUniqueNames.remove(uniquePrettyName); + } + + // Should we close it first? + if (m_openDevice == device) { + m_openDevice = nullptr; + } + + delete device; + } +} + +void AfcSlave::listDir(const QUrl &url) +{ + if (redirectIfSolidUrl(url)) { + return; + } + + const auto afcUrl = this->afcUrl(url); + + if (afcUrl.deviceId().isEmpty()) { + UDSEntry entry; + entry.fastInsert(UDSEntry::UDS_NAME, QStringLiteral(".")); + entry.fastInsert(UDSEntry::UDS_DISPLAY_NAME, i18n("Apple Devices")); + //entry.fastInsert(UDSEntry::UDS_ICON_NAME, + listEntry(entry); + + // Make sure refreshing the view will show devices that were trusted in the meantime + bool foundInvalidDevices = false; + updateDeviceList(&foundInvalidDevices); + + for (AfcDevice *device : m_devices) { + const QString prettyName = m_deviceUniqueNames.key(device->id()); + UDSEntry entry = device->rootEntry(prettyName); + + // When there is only one device, redirect to it right away + // We just read UDS_NAME so we get pretty name or ID without + // FIXME also asserts in KCoreDirLister... + /*if (m_devices.count() == 1) { + redirection(QUrl(QStringLiteral("afc:/%1").arg(entry.stringValue(UDSEntry::UDS_NAME)))); + finished(); + return; + }*/ + + listEntry(entry); + } + + if (m_devices.isEmpty() && foundInvalidDevices) { + error(ERR_SLAVE_DEFINED, i18n("An Apple device was found but it could not be accessed. Make sure that your device is unlocked and that it trusts this computer.")); + return; + } + + finished(); + return; + } + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + error(ERR_DOES_NOT_EXIST, afcUrl.deviceId()); + return; + } + + // Ourself must be "." + AfcError error; + + UDSEntry rootEntry = device->entry(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + // NOTE this must not be "fastInsert" as AfcDevice::entry alreday sets a name + rootEntry.replace(UDSEntry::UDS_NAME, QStringLiteral(".")); + // FIXME when listing "." KCoreDirLister asserts in slotEntries + // Q_ASSERT(dir->rootItem.isNull()); + //listEntry(rootEntry); + + const QStringList files = device->entryList(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + for (const QString &file : files) { + QString absolutePath = afcUrl.path(); + if (!absolutePath.endsWith(QLatin1Char('/')) + && !file.startsWith(QLatin1Char('/'))) { + absolutePath.append(QLatin1Char('/')); + } + absolutePath.append(file); + + UDSEntry entry = device->entry(absolutePath, error); + if (error) { + continue; + } + + listEntry(entry); + } + + finished(); +} + +void AfcSlave::stat(const QUrl &url) +{ + if (redirectIfSolidUrl(url)) { + return; + } + + const auto afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + UDSEntry entry = device->entry(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + statEntry(entry); + finished(); +} + +void AfcSlave::get(const QUrl &url) +{ + if (redirectIfSolidUrl(url)) { + return; + } + + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + UDSEntry entry = device->entry(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + device->open(afcUrl.path(), QIODevice::ReadOnly, error); + if (error) { + emitError(error); + return; + } + + auto cleanup = qScopeGuard([device, url] { + AfcError error; + device->close(error); + if (error) { + qCWarning(KIO_AFC) << "Failed to close file after get" << url; + } + }); + + const auto size = entry.numberValue(UDSEntry::UDS_SIZE, 0); + totalSize(size); + position(0); + + AfcFileReader reader = device->fileReader(); + reader.setSize(size); + + while (!reader.error() && reader.hasMore()) { + reader.read(); + data(reader.data()); + } + + if (reader.error()) { + emitError(reader.error()); + return; + } + + finished(); +} + +void AfcSlave::put(const QUrl &url, int permissions, JobFlags flags) +{ + Q_UNUSED(permissions); + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + bool needToCloseFile = false; + + auto cleanup = qScopeGuard([&needToCloseFile, device] { + if (needToCloseFile) { + AfcError error; + device->close(error); + if (error) { + qCWarning(KIO_AFC) << "Failed to close file after put"; + } + } + }); + + AfcError getError; + UDSEntry entry = device->entry(afcUrl.path(), getError); + + const bool exists = getError.errorCode() != ERR_DOES_NOT_EXIST; + if (exists && !flags.testFlag(KIO::Overwrite) && !flags.testFlag(KIO::Resume)) { + if (S_ISDIR(entry.numberValue(UDSEntry::UDS_FILE_TYPE))) { + error = AfcError(ERR_DIR_ALREADY_EXIST, afcUrl.path()); + } else { + error = AfcError(ERR_FILE_ALREADY_EXIST, afcUrl.path()); + } + emitError(error); + return; + } + + if (!afcUrl.path().isEmpty()) { + if (flags.testFlag(KIO::Resume)) { + device->open(afcUrl.path(), QIODevice::Append, error); + if (error) { + emitError(error); + return; + } + } else { + device->open(afcUrl.path(), QIODevice::WriteOnly, error); + if (error) { + emitError(error); + return; + } + } + + needToCloseFile = true; + } + + int result = 0; + + do { + QByteArray buffer; + dataReq(); + // FIXME blocks when copying files within the same device + result = readData(buffer); + + if (result < 0) { + error = AfcError(ERR_CANNOT_READ, QStringLiteral("readData result was %1").arg(result)); + emitError(error); + return; + } + + device->write(buffer, error); + if (error) { + emitError(error); + return; + } + } while (result > 0); + + if (!afcUrl.path().isEmpty()) { + const QString modifiedMeta = metaData(QStringLiteral("modified")); + + if (!modifiedMeta.isEmpty()) { + const QDateTime mtime = QDateTime::fromString(modifiedMeta, Qt::ISODate); + + if (mtime.isValid()) { + AfcError error; + device->setModificationTime(afcUrl.path(), mtime, error); + if (error) { + qCWarning(KIO_AFC) << "Failed to set mtime for" << afcUrl.path() << "in put"; + } + } + } + } + + finished(); +} + +void AfcSlave::open(const QUrl &url, QIODevice::OpenMode mode) +{ + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + UDSEntry entry = device->entry(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + device->open(afcUrl.path(), mode, error); + if (error) { + emitError(error); + return; + } + + m_openDevice = device; + + totalSize(entry.numberValue(UDSEntry::UDS_SIZE, 0)); + position(0); + + finished(); +} + +void AfcSlave::read(filesize_t bytesRequested) +{ + if (!m_openDevice) { + error(KIO::ERR_CANNOT_SEEK, i18n("Cannot read without opening file first")); + return; + } + + AfcFileReader reader = m_openDevice->fileReader(); + reader.setSize(bytesRequested); + + while (!reader.error() && reader.hasMore()) { + reader.read(); + data(reader.data()); + } + + if (reader.error()) { + emitError(reader.error()); + return; + } + + finished(); +} + +void AfcSlave::seek(filesize_t offset) +{ + if (!m_openDevice) { + error(KIO::ERR_CANNOT_SEEK, i18n("Cannot seek without opening file first")); + return; + } + + AfcError error; + m_openDevice->seek(offset, error); + + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::write(const QByteArray &data) +{ + if (!m_openDevice) { + error(KIO::ERR_CANNOT_SEEK, i18n("Cannot write without opening file first")); + return; + } + + AfcError error; + const auto bytesWritten = m_openDevice->write(data, error); + + if (error) { + emitError(error); + return; + } + + written(bytesWritten); + finished(); +} + +void AfcSlave::close() +{ + if (!m_openDevice) { + error(KIO::ERR_INTERNAL, i18n("Cannot close what is not open")); + return; + } + + AfcError error; + m_openDevice->close(error); + + if (error) { + emitError(error); + return; + } + + m_openDevice = nullptr; +} + +void AfcSlave::del(const QUrl &url, bool isFile) +{ + Q_UNUSED(isFile) + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + device->del(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::rename(const QUrl &url, const QUrl &dest, JobFlags flags) +{ + const AfcUrl srcAfcUrl = afcUrl(url); + const AfcUrl destAfcUrl = afcUrl(dest); + + if (srcAfcUrl.deviceId() != destAfcUrl.deviceId()) { + error(ERR_CANNOT_RENAME, i18n("Cannot rename between devices.")); + return; + } + + AfcDevice *device = m_devices.value(srcAfcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + device->rename(srcAfcUrl.path(), destAfcUrl.path(), flags, error); + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::symlink(const QString &target, const QUrl &dest, JobFlags flags) +{ + QUrl targetUrl; + targetUrl.setScheme(QStringLiteral("afc")); + targetUrl.setPath(target); + + // Turning it into a QUrl so we can resolve both device ID and unique name + const AfcUrl targetAfcUrl = afcUrl(targetUrl); + const AfcUrl destAfcUrl = afcUrl(dest); + + if (targetAfcUrl.deviceId() != destAfcUrl.deviceId()) { + error(ERR_CANNOT_SYMLINK, i18n("Cannot symlink between devices.")); + return; + } + + AfcDevice *device = m_devices.value(destAfcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, dest.toDisplayString()); + return; + } + + AfcError error; + device->symlink(targetAfcUrl.path(), destAfcUrl.path(), flags, error); + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::mkdir(const QUrl &url, int permissions) +{ + Q_UNUSED(permissions) + + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + device->mkdir(afcUrl.path(), error); + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::setModificationTime(const QUrl &url, const QDateTime &mtime) +{ + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + AfcError error; + device->setModificationTime(afcUrl.path(), mtime, error); + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::fileSystemFreeSpace(const QUrl &url) +{ + const QUrl redirectUrl = resolveSolidUrl(url); + // TODO FileSystemFreeSpaceJob does not follow redirects + if (redirectUrl.isValid()) { + fileSystemFreeSpace(redirectUrl); + return; + } + + const AfcUrl afcUrl = this->afcUrl(url); + + AfcDevice *device = m_devices.value(afcUrl.deviceId()); + if (!device) { + this->error(ERR_DOES_NOT_EXIST, url.toDisplayString()); + return; + } + + const AfcDiskUsage diskUsage = device->diskUsage(); + if (!diskUsage.isValid()) { + this->error(ERR_CANNOT_STAT, url.toDisplayString()); + return; + } + + setMetaData(QStringLiteral("total"), QString::number(diskUsage.totalDiskCapacity())); + setMetaData(QStringLiteral("available"), QString::number(diskUsage.totalDataAvailable())); + finished(); +} + +void AfcSlave::cotruncate(filesize_t length) +{ + if (!m_openDevice) { + error(KIO::ERR_CANNOT_TRUNCATE, QStringLiteral("Cannot truncate without opening file first")); + return; + } + + AfcError error; + m_openDevice->truncate(length, error); + + if (error) { + emitError(error); + return; + } + + finished(); +} + +void AfcSlave::virtual_hook(int id, void *data) +{ + switch(id) { + case SlaveBase::GetFileSystemFreeSpace: { + QUrl *url = static_cast(data); + fileSystemFreeSpace(*url); + } + break; + case SlaveBase::Truncate: { + const auto length = static_cast(data); + cotruncate(*length); + } + break; + default: + SlaveBase::virtual_hook(id, data); + } +} + +extern "C" +{ + int Q_DECL_EXPORT kdemain(int argc, char **argv) + { + QCoreApplication app(argc, argv); + app.setApplicationName(QStringLiteral("kio_afc")); + + AfcSlave slave(argv[2], argv[3]); + slave.dispatchLoop(); + return 0; + } +} + +#include "kio_afc.moc" diff --git a/afc/kio_afc.h b/afc/kio_afc.h new file mode 100644 index 00000000..99910d3c --- /dev/null +++ b/afc/kio_afc.h @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + * SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +#include +#include + +#include "afcurl.h" + +using namespace KIO; + +class AfcDevice; +class AfcError; + +class AfcSlave : public QObject, public KIO::SlaveBase +{ + Q_OBJECT + +public: + AfcSlave(const QByteArray &pool, const QByteArray &app); + ~AfcSlave() override; + + void onDeviceEvent(const idevice_event_t *event); + + void listDir(const QUrl &url) override; + + void stat(const QUrl &url) override; + + void get(const QUrl &url) override; + void put(const QUrl &url, int permissions, KIO::JobFlags flags) override; + + void open(const QUrl &url, QIODevice::OpenMode mode) override; + void read(KIO::filesize_t bytesRequested) override; + void seek(KIO::filesize_t offset) override; + void write(const QByteArray &data) override; + void close() override; + + void del(const QUrl &url, bool isFile) override; + // direct copy not supported by afc + void rename(const QUrl &url, const QUrl &dest, KIO::JobFlags flags) override; + void symlink(const QString &target, const QUrl &dest, KIO::JobFlags flags) override; + void mkdir(const QUrl &url, int permissions) override; + void setModificationTime(const QUrl &url, const QDateTime &mtime) override; + + void fileSystemFreeSpace(const QUrl &url); + void cotruncate(KIO::filesize_t length); + + void virtual_hook(int id, void *data) override; + +private: + AfcUrl afcUrl(const QUrl &url) const; + void emitError(const AfcError &afcError); + + void updateDeviceList(bool *foundInvalidDevices = nullptr); + bool addDevice(const QString &id); + void removeDevice(const QString &id); + + QUrl resolveSolidUrl(const QUrl &url) const; + bool redirectIfSolidUrl(const QUrl &url); + + QHash m_devices; + QHash m_deviceUniqueNames; + + AfcDevice *m_openDevice = nullptr; + +}; diff --git a/afc/solid_afc.desktop b/afc/solid_afc.desktop new file mode 100644 index 00000000..8045b3e8 --- /dev/null +++ b/afc/solid_afc.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +X-KDE-Solid-Predicate=PortableMediaPlayer.supportedProtocols == 'afc' +Type=Service +Actions=open; + +[Desktop Action open] +Exec=kioclient5 exec afc:udi=%i/ +Icon=system-file-manager +Name=Browse Files with File Manager diff --git a/cmake/FindIMobileDevice.cmake b/cmake/FindIMobileDevice.cmake new file mode 100644 index 00000000..4dcf9573 --- /dev/null +++ b/cmake/FindIMobileDevice.cmake @@ -0,0 +1,61 @@ +#.rst: +# FindIMobileDevice +# -------- +# +# Try to find the imobiledevice library, once done this will define: +# +# ``IMobileDevice_FOUND`` +# System has libimobiledevice. +# +# ``IMobileDevice_INCLUDE_DIRS`` +# The libimobiledevice include directory. +# +# ``IMobileDevice_LIBRARIES`` +# The libimobiledevice libraries. +# +# ``IMobileDevice_VERSION`` +# The libimobiledevice version. +# +# If ``IMobileDevice_FOUND`` is TRUE, the following imported target +# will be available: +# +# ``IMobileDevice::IMobileDevice`` +# The libimobiledevice library + +#============================================================================= +# SPDX-FileCopyrightText: 2020 Kai Uwe Broulik +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +find_package(PkgConfig QUIET) +pkg_check_modules(PC_libimobiledevice QUIET libimobiledevice) + +find_path(IMobileDevice_INCLUDE_DIRS NAMES libimobiledevice/libimobiledevice.h HINTS ${PC_libimobiledevice_INCLUDE_DIRS}) +find_library(IMobileDevice_LIBRARIES NAMES imobiledevice HINTS ${PC_libimobiledevice_LIBRARY_DIRS}) + +set(IMobileDevice_VERSION ${PC_libimobiledevice_VERSION}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(IMobileDevice + FOUND_VAR IMobileDevice_FOUND + REQUIRED_VARS IMobileDevice_INCLUDE_DIRS IMobileDevice_LIBRARIES + VERSION_VAR IMobileDevice_VERSION +) + +mark_as_advanced(IMobileDevice_INCLUDE_DIRS IMobileDevice_LIBRARIES) + +if(IMobileDevice_FOUND AND NOT TARGET IMobileDevice::IMobileDevice) + add_library(IMobileDevice::IMobileDevice UNKNOWN IMPORTED) + set_target_properties(IMobileDevice::IMobileDevice PROPERTIES + IMPORTED_LOCATION "${IMobileDevice_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${IMobileDevice_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "${PC_libimobiledevice_CFLAGS_OTHER}" + ) +endif() + +include(FeatureSummary) +set_package_properties(IMobileDevice PROPERTIES + DESCRIPTION "library to communicate with iOS devices" + URL "https://www.libimobiledevice.org/" +)