diff options
| author | Jonah Brüchert <jbb@kaidan.im> | 2022-09-09 23:15:10 +0200 |
|---|---|---|
| committer | Linus Jahn <lnj@kaidan.im> | 2022-09-29 23:46:36 +0200 |
| commit | 68f167995e7ba71a6f2e556a7a0eab3d234e2d1a (patch) | |
| tree | de645b606d96f01da7ea6db63e90224519a89de3 /src/client | |
| parent | 7b02df3ef42ccb2d8c40eea901c5c6dd4b140204 (diff) | |
| download | qxmpp-68f167995e7ba71a6f2e556a7a0eab3d234e2d1a.tar.gz | |
Implement XEP-0448: Stateless File Sharing
This adds a file sharing manager that is capable of using multiple back
ends. Currently implemented are a normal HTTP File Upload backend and an
encrypted HTTP File Upload. Jingle File Transfer could be implemented
later.
Co-authored-by: Linus Jahn <lnj@kaidan.im>
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/QXmppDownload.cpp | 26 | ||||
| -rw-r--r-- | src/client/QXmppDownload.h | 23 | ||||
| -rw-r--r-- | src/client/QXmppEncryptedHttpFileSharingProvider.cpp | 123 | ||||
| -rw-r--r-- | src/client/QXmppEncryptedHttpFileSharingProvider.h | 33 | ||||
| -rw-r--r-- | src/client/QXmppFileSharingManager.cpp | 210 | ||||
| -rw-r--r-- | src/client/QXmppFileSharingManager.h | 77 | ||||
| -rw-r--r-- | src/client/QXmppFileSharingProvider.h | 52 | ||||
| -rw-r--r-- | src/client/QXmppFileTransfer.cpp | 43 | ||||
| -rw-r--r-- | src/client/QXmppFileTransfer.h | 31 | ||||
| -rw-r--r-- | src/client/QXmppHttpFileSharingProvider.cpp | 207 | ||||
| -rw-r--r-- | src/client/QXmppHttpFileSharingProvider.h | 40 | ||||
| -rw-r--r-- | src/client/QXmppHttpUploadManager.cpp | 9 | ||||
| -rw-r--r-- | src/client/QXmppUpload.cpp | 47 | ||||
| -rw-r--r-- | src/client/QXmppUpload.h | 47 |
14 files changed, 961 insertions, 7 deletions
diff --git a/src/client/QXmppDownload.cpp b/src/client/QXmppDownload.cpp new file mode 100644 index 00000000..0ece1906 --- /dev/null +++ b/src/client/QXmppDownload.cpp @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppDownload.h" + +/// +/// \class QXmppDownload +/// +/// \brief Provides progress of stateless file sharing uploads. +/// +/// \since QXmpp 1.5 +/// + +/// +/// \typedef QXmppDownload::Result +/// +/// \brief Contains QXmpp::Success (successfully finished), QXmpp::Cancelled (manually cancelled) +/// or QXmppError (an error occured while downloading). +/// + +/// +/// \fn QXmppDownload::finished +/// +/// Emitted when the download has finished. +/// diff --git a/src/client/QXmppDownload.h b/src/client/QXmppDownload.h new file mode 100644 index 00000000..3256aaea --- /dev/null +++ b/src/client/QXmppDownload.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPDOWNLOAD_H +#define QXMPPDOWNLOAD_H + +#include "QXmppError.h" +#include "QXmppFileTransfer.h" + +#include <variant> + +class QXMPP_EXPORT QXmppDownload : public QXmppFileTransfer +{ + Q_OBJECT +public: + using Result = std::variant<QXmpp::Success, QXmpp::Cancelled, QXmppError>; + Q_SIGNAL void finished(QXmppDownload::Result); +}; + +Q_DECLARE_METATYPE(QXmppDownload::Result); + +#endif // QXMPPDOWNLOAD_H diff --git a/src/client/QXmppEncryptedHttpFileSharingProvider.cpp b/src/client/QXmppEncryptedHttpFileSharingProvider.cpp new file mode 100644 index 00000000..b6e82621 --- /dev/null +++ b/src/client/QXmppEncryptedHttpFileSharingProvider.cpp @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppEncryptedHttpFileSharingProvider.h" + +#include "QXmppClient.h" +#include "QXmppFileEncryption.h" +#include "QXmppHttpUploadManager.h" +#include "QXmppUpload.h" +#include "QXmppUtils.h" + +#include "QcaInitializer_p.h" +#include <QMimeDatabase> + +using namespace QXmpp; +using namespace QXmpp::Private; + +class QXmppSfsEncryptedHttpUpload : public QXmppUpload +{ + Q_OBJECT + +public: + QXmppSfsEncryptedHttpUpload(std::shared_ptr<QXmppHttpUpload> &&httpUpload, const QByteArray &key, const QByteArray &iv) + : inner(std::move(httpUpload)) + { + connect(inner.get(), &QXmppHttpUpload::finished, this, [=](const QXmppHttpUpload::Result &result) { + Q_EMIT uploadFinished(std::visit([=](auto &&value) -> QXmpp::Private::UploadResult { + using T = std::decay_t<decltype(value)>; + if constexpr (std::is_same_v<T, QUrl>) { + QXmppEncryptedFileSource encryptedSource; + encryptedSource.setKey(key); + encryptedSource.setIv(iv); + encryptedSource.setHttpSources({ QXmppHttpFileSource(value) }); + return std::any(encryptedSource); + } else if constexpr (std::is_same_v<T, Cancelled>) { + return Cancelled {}; + } else if constexpr (std::is_same_v<T, QXmppError>) { + return value; + } + }, + result)); + }); + } + + float progress() override { return inner->progress(); } + void cancel() override { inner->cancel(); } + bool isFinished() override { return inner->isFinished(); } + quint64 bytesTransferred() override { return inner->bytesSent(); } + quint64 bytesTotal() override { return inner->bytesTotal(); } + +private: + std::shared_ptr<QXmppHttpUpload> inner; +}; + +/// +/// \class QXmppEncryptedHttpFileSharingProvider +/// +/// Support for storing files encrypted on an HTTP server +/// +/// \since QXmpp 1.5 +/// + +class QXmppEncryptedHttpFileSharingProviderPrivate +{ +public: + QXmpp::Private::QcaInitializer init; + QXmppHttpUploadManager *manager; + QXmppHttpFileSharingProvider *httpProvider; +}; + +/// +/// \brief Create a new QXmppEncryptedHttpFileSharingProvider +/// \param client +/// \param netManager QNetworkAccessManager that can be reused all over your application. +/// +QXmppEncryptedHttpFileSharingProvider::QXmppEncryptedHttpFileSharingProvider(QXmppClient *client, QNetworkAccessManager *netManager) + : d(std::make_unique<QXmppEncryptedHttpFileSharingProviderPrivate>()) +{ + qRegisterMetaType<QXmpp::Private::UploadResult>(); + Q_ASSERT(client); + d->manager = client->findExtension<QXmppHttpUploadManager>(); + Q_ASSERT(d->manager); + d->httpProvider = new QXmppHttpFileSharingProvider(client, netManager); +} + +QXmppEncryptedHttpFileSharingProvider::~QXmppEncryptedHttpFileSharingProvider() = default; + +std::shared_ptr<QXmppDownload> QXmppEncryptedHttpFileSharingProvider::downloadFile(const std::any &source, std::unique_ptr<QIODevice> &&target) +{ + QXmppEncryptedFileSource encryptedSource; + try { + encryptedSource = std::any_cast<QXmppEncryptedFileSource>(source); + } catch (const std::bad_any_cast &) { + qFatal("QXmppEncryptedHttpFileSharingProvider::downloadFile can only handle QXmppEncryptedFileSource sources"); + } + + auto httpSource = encryptedSource.httpSources().front(); + auto output = std::make_unique<Encryption::DecryptionDevice>(std::move(target), encryptedSource.cipher(), encryptedSource.iv(), encryptedSource.key()); + return d->httpProvider->downloadFile(httpSource, std::move(output)); +} + +std::shared_ptr<QXmppUpload> QXmppEncryptedHttpFileSharingProvider::uploadFile( + std::unique_ptr<QIODevice> data, + const QXmppFileMetadata &info) +{ + auto cipher = Aes256CbcPkcs7; + auto key = Encryption::generateKey(cipher); + auto iv = Encryption::generateInitializationVector(cipher); + + auto encDevice = std::make_unique<Encryption::EncryptionDevice>(std::move(data), cipher, key, iv); + auto encryptedSize = encDevice->size(); + + auto upload = d->manager->uploadFile( + std::move(encDevice), + QXmppUtils::generateStanzaHash(10), + QMimeDatabase().mimeTypeForName("application/octet-stream"), + encryptedSize); + + return std::make_shared<QXmppSfsEncryptedHttpUpload>(std::move(upload), key, iv); +} + +#include "QXmppEncryptedHttpFileSharingProvider.moc" diff --git a/src/client/QXmppEncryptedHttpFileSharingProvider.h b/src/client/QXmppEncryptedHttpFileSharingProvider.h new file mode 100644 index 00000000..6c43d8cd --- /dev/null +++ b/src/client/QXmppEncryptedHttpFileSharingProvider.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPENCRYPTEDHTTPFILESHARINGPROVIDER_H +#define QXMPPENCRYPTEDHTTPFILESHARINGPROVIDER_H + +#include "QXmppEncryptedFileSource.h" +#include "QXmppHttpFileSharingProvider.h" + +class QXmppEncryptedHttpFileSharingProviderPrivate; + +class QXMPP_EXPORT QXmppEncryptedHttpFileSharingProvider : public QXmppFileSharingProvider +{ +public: + /// \cond + using SourceType = QXmppEncryptedFileSource; + /// \endcond + + QXmppEncryptedHttpFileSharingProvider(QXmppClient *client, QNetworkAccessManager *netManager); + ~QXmppEncryptedHttpFileSharingProvider() override; + + auto downloadFile(const std::any &source, + std::unique_ptr<QIODevice> &&target) -> std::shared_ptr<QXmppDownload> override; + auto uploadFile( + std::unique_ptr<QIODevice> data, + const QXmppFileMetadata &info) -> std::shared_ptr<QXmppUpload> override; + +private: + std::unique_ptr<QXmppEncryptedHttpFileSharingProviderPrivate> d; +}; + +#endif // QXMPPENCRYPTEDHTTPFILESHARINGPROVIDER_H diff --git a/src/client/QXmppFileSharingManager.cpp b/src/client/QXmppFileSharingManager.cpp new file mode 100644 index 00000000..2daf980a --- /dev/null +++ b/src/client/QXmppFileSharingManager.cpp @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppFileSharingManager.h" + +#include "QXmppBitsOfBinaryContentId.h" +#include "QXmppBitsOfBinaryData.h" +#include "QXmppClient.h" +#include "QXmppDownload.h" +#include "QXmppFileMetadata.h" +#include "QXmppFileShare.h" +#include "QXmppFutureUtils_p.h" +#include "QXmppHashing_p.h" +#include "QXmppThumbnail.h" +#include "QXmppUpload.h" +#include "QXmppUploadRequestManager.h" + +#include <any> + +#include <QFile> +#include <QFileInfo> +#include <QFutureInterface> +#include <QMimeDatabase> +#include <QNetworkAccessManager> +#include <QNetworkReply> + +using namespace QXmpp; +using namespace QXmpp::Private; + +/// +/// \class QXmppFileSharingProvider +/// +/// Base class for Stateless File Sharing providers +/// +/// A provider defines the way that files can be uploaded and downloaded. +/// +/// An example is the QXmppHttpFileSharingProvider, which uses HTTP File Upload. +/// +/// \since QXmpp 1.5 +/// + +/// +/// \class QXmppFileSharingManager +/// +/// The file sharing manager allows to easily send and retrieve files in a chat. +/// +/// \since QXmpp 1.5 +/// + +QXmppFileSharingManager::QXmppFileSharingManager() + : m_metadataGenerator([](std::unique_ptr<QIODevice> &&) -> QFuture<std::shared_ptr<MetadataGeneratorResult>> { + return makeReadyFuture(std::make_shared<MetadataGeneratorResult>()); + }) +{ +} + +/// +/// \typedef QXmppFileSharingManager::MetadataGenerator +/// +/// The function signature of a metadata generator function +/// + +/// +/// \brief Register a function that is called when metadata needs to be gererated for a file. +/// +/// The function is passed a QIODevice, so if you need the path of the file on disk, +/// you can dynamically cast it to a QFile and access the fileName. +/// When doing that, make sure to check the result, +/// as in the future this function might be passed other QIODevices than QFile. +/// +void QXmppFileSharingManager::setMetadataGenerator(MetadataGenerator &&generator) +{ + m_metadataGenerator = std::move(generator); +} + +/// +/// \brief Upload a file in a way that it can be attached to a message. +/// \param provider The provider class decides how the file is uploaded +/// \param filePath Path to a file that should be uploaded +/// \param description Optional description of the file +/// \return An object that allows to track the progress of the upload. +/// Once the upload is finished, the finished signal is emitted on it. +/// +std::shared_ptr<QXmppUpload> QXmppFileSharingManager::sendFile(std::shared_ptr<QXmppFileSharingProvider> provider, + const QString &filePath, + const std::optional<QString> &description) +{ + std::shared_ptr<QXmppUpload> upload; + + QFileInfo fileInfo(filePath); + auto metadata = QXmppFileMetadata::fromFileInfo(fileInfo); + if (description) { + metadata.setDescription(description); + } + + auto openFile = [=]() -> std::unique_ptr<QIODevice> { + auto device = std::make_unique<QFile>(fileInfo.absoluteFilePath()); + if (!device->open(QIODevice::ReadOnly)) { + Q_EMIT upload->finished(QXmppError::fromIoDevice(*device)); + } + return device; + }; + + auto metadataFuture = m_metadataGenerator(openFile()); + auto hashesFuture = calculateHashes(openFile(), { HashAlgorithm::Sha256 }); + + upload = provider->uploadFile(openFile(), metadata); + upload->metadata = metadata; + + connect(upload.get(), &QXmppUpload::uploadFinished, this, [=](auto &&uploadResult) { + if (std::holds_alternative<std::any>(uploadResult)) { + auto source = std::get<std::any>(uploadResult); + await(metadataFuture, this, [=](auto &&result) mutable { + if (result->dimensions) { + upload->metadata.setWidth(result->dimensions->width()); + upload->metadata.setHeight(result->dimensions->height()); + } + if (result->length) { + upload->metadata.setLength(*result->length); + } + + QVector<QXmppThumbnail> thumbnails; + thumbnails.reserve(result->thumbnails.size()); + QXmppBitsOfBinaryDataList dataBlobs; + dataBlobs.reserve(result->thumbnails.size()); + + for (const auto &metadataThumb : result->thumbnails) { + auto bobData = QXmppBitsOfBinaryData::fromByteArray(metadataThumb.data); + bobData.setContentType(metadataThumb.mimeType); + + QXmppThumbnail thumbnail; + thumbnail.setHeight(metadataThumb.height); + thumbnail.setWidth(metadataThumb.width); + thumbnail.setMediaType(metadataThumb.mimeType); + thumbnail.setUri(bobData.cid().toCidUrl()); + + thumbnails.append(std::move(thumbnail)); + dataBlobs.append(std::move(bobData)); + } + upload->metadata.setThumbnails(thumbnails); + + await(hashesFuture, this, [=, dataBlobs = std::move(dataBlobs)](const auto &hashResult) mutable { + QVector<QXmppHash> hashes; + const auto hashValue = hashResult->result; + if (std::holds_alternative<Cancelled>(hashValue)) { + Q_EMIT upload->finished(Cancelled {}); + } else if (std::holds_alternative<std::vector<QXmppHash>>(hashValue)) { + auto hashesVector = std::get<std::vector<QXmppHash>>(hashValue); + std::transform(hashesVector.begin(), hashesVector.end(), + std::back_inserter(hashes), [](auto &&hash) { + return hash; + }); + + upload->metadata.setHashes(hashes); + + QXmppFileShare fs; + fs.setMetadata(upload->metadata); + fs.addSource(std::move(source)); + + Q_EMIT upload->finished(QXmppUpload::FileResult { fs, std::move(dataBlobs) }); + } else if (std::holds_alternative<QXmppError>(hashValue)) { + Q_EMIT upload->finished(std::get<QXmppError>(hashValue)); + } + }); + }); + } else if (std::holds_alternative<QXmppError>(uploadResult)) { + Q_EMIT upload->finished(std::get<QXmppError>(uploadResult)); + } + }); + + return upload; +} + +/// +/// \brief Download a file from a QXmppFileShare +/// +/// \warning This function currently does not check the hash of the downloaded file. +/// +/// Make sure to register the provider +/// that handles the sources used in this file share before calling this function. +/// +/// \param fileShare The file share object which you want to download +/// \param output An open QIODevice that the data should be written into. +/// In most cases, a QFile +/// \return An object that allows to track the progress of the download. +/// +std::shared_ptr<QXmppDownload> QXmppFileSharingManager::downloadFile( + const QXmppFileShare &fileShare, + std::unique_ptr<QIODevice> &&output) +{ + std::shared_ptr<QXmppDownload> download; + fileShare.visitSources([&](const std::any &source) { + std::type_index index(source.type()); + try { + auto provider = m_providers.at(index); + download = provider->downloadFile(source, std::move(output)); + return true; + } catch (const std::out_of_range &) { + return false; + } + }); + + return download; +} + +void QXmppFileSharingManager::internalRegisterProvider(std::type_index index, std::shared_ptr<QXmppFileSharingProvider> provider) +{ + m_providers.insert_or_assign(index, provider); +} diff --git a/src/client/QXmppFileSharingManager.h b/src/client/QXmppFileSharingManager.h new file mode 100644 index 00000000..a7f4c5dd --- /dev/null +++ b/src/client/QXmppFileSharingManager.h @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPFILESHARINGMANAGER_H +#define QXMPPFILESHARINGMANAGER_H + +#include "QXmppClientExtension.h" +#include "QXmppFileShare.h" +#include "QXmppFileSharingProvider.h" +#include "QXmppGlobal.h" + +#include <any> +#include <functional> +#include <memory> +#include <typeindex> +#include <variant> + +#include <QFuture> +#include <QMimeType> +#include <QObject> +#include <QSize> + +class QIODevice; +class QXmppFileMetadata; + +class QXMPP_EXPORT QXmppFileSharingManager : public QXmppClientExtension +{ +public: + struct MetadataThumbnail + { + uint32_t width; + uint32_t height; + QByteArray data; + QMimeType mimeType; + }; + + struct MetadataGeneratorResult + { + std::optional<QSize> dimensions; + std::optional<uint32_t> length; + QVector<MetadataThumbnail> thumbnails; + std::unique_ptr<QIODevice> dataDevice; + }; + + using MetadataGenerator = std::function<QFuture<std::shared_ptr<MetadataGeneratorResult>>(std::unique_ptr<QIODevice>)>; + + QXmppFileSharingManager(); + + void setMetadataGenerator(MetadataGenerator &&generator); + + /// + /// \brief Register a provider for automatic downloads + /// \param manager A shared_ptr to a QXmppFileSharingProvider subclass + /// + template<typename ProviderType> + void registerProivder(std::shared_ptr<ProviderType> manager) + { + std::type_index index(typeid(typename ProviderType::SourceType)); + internalRegisterProvider(index, manager); + } + + std::shared_ptr<QXmppUpload> sendFile(std::shared_ptr<QXmppFileSharingProvider> provider, + const QString &filePath, + const std::optional<QString> &description = {}); + + std::shared_ptr<QXmppDownload> downloadFile(const QXmppFileShare &fileShare, + std::unique_ptr<QIODevice> &&output); + +private: + void internalRegisterProvider(std::type_index, std::shared_ptr<QXmppFileSharingProvider> provider); + + MetadataGenerator m_metadataGenerator; + std::unordered_map<std::type_index, std::shared_ptr<QXmppFileSharingProvider>> m_providers; +}; + +#endif // QXMPPFILESHARINGMANAGER_H diff --git a/src/client/QXmppFileSharingProvider.h b/src/client/QXmppFileSharingProvider.h new file mode 100644 index 00000000..cde719db --- /dev/null +++ b/src/client/QXmppFileSharingProvider.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPFILESHARINGPROVIDER_H +#define QXMPPFILESHARINGPROVIDER_H + +#include "QXmppGlobal.h" + +#include <any> +#include <memory> + +class QIODevice; +class QXmppFileMetadata; +class QXmppUpload; +class QXmppDownload; + +/// +/// \brief The interface of a provider for the FileSharingManager +/// +/// To use it, implement all the pure virtual functions, +/// and add a using declaration for the type of source you want to handle. +/// ``` +/// using SourceType = QXmppHttpFileSource; +/// ``` +/// +class QXMPP_EXPORT QXmppFileSharingProvider +{ +public: + /// \cond + virtual ~QXmppFileSharingProvider() = default; + /// \endcond + + /// + /// \brief Handles the download of files for this provider + /// \param source A type-erased source object. The provider will only ever have to handle its own sources, + /// so this can be safely casted to a concrete type. + /// \param target QIODevice into which the received data should be written + /// \return A subclass of QXmppDownload + /// + virtual auto downloadFile(const std::any &source, std::unique_ptr<QIODevice> &&target) -> std::shared_ptr<QXmppDownload> = 0; + + /// + /// \brief Handles the upload of a file for this provider + /// \param source A QIODevice from which data for uploading should be read. + /// \param info Metadata of the file + /// \return A subclass of QXmppUpload + /// + virtual auto uploadFile(std::unique_ptr<QIODevice> source, const QXmppFileMetadata &info) -> std::shared_ptr<QXmppUpload> = 0; +}; + +#endif // QXMPPFILESHARINGPROVIDER_H diff --git a/src/client/QXmppFileTransfer.cpp b/src/client/QXmppFileTransfer.cpp new file mode 100644 index 00000000..f26c3a89 --- /dev/null +++ b/src/client/QXmppFileTransfer.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppFileTransfer.h" + +/// +/// \class QXmppFileTransfer +/// +/// \brief Provides progress information about ongoing file uploads and downloads. +/// +/// \since QXmpp 1.5 +/// + +/// +/// \fn QXmppFileTransfer::cancel() +/// +/// \brief Cancels the file transfer. finished() will be emitted. +/// + +/// +/// \fn QXmppFileTransfer::isFinished() +/// +/// \brief Returns whether the file transfer is finished. +/// + +/// +/// \fn QXmppFileTransfer::bytesTransferred() +/// +/// \brief Returns the number of bytes that have been uploaded or downloaded. +/// + +/// +/// \fn QXmppFileTransfer::bytesTotal() +/// +/// \brief Returns the number of bytes that totally need to be transferred. +/// + +/// +/// \fn QXmppFileTransfer::progressChanged() +/// +/// \brief Emitted when new bytes have been transferred. +/// diff --git a/src/client/QXmppFileTransfer.h b/src/client/QXmppFileTransfer.h new file mode 100644 index 00000000..30a91aa9 --- /dev/null +++ b/src/client/QXmppFileTransfer.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPFILETRANSFER_H +#define QXMPPFILETRANSFER_H + +#include "QXmppGlobal.h" + +#include <QObject> + +class QXMPP_EXPORT QXmppFileTransfer : public QObject +{ + Q_OBJECT + /// Progress of the file transfer between 0.0 and 1.0. + Q_PROPERTY(float progress READ progress NOTIFY progressChanged) + +public: + /// Returns the current progress between 0.0 and 1.0. + virtual float progress() = 0; + virtual void cancel() = 0; + virtual bool isFinished() = 0; + virtual quint64 bytesTransferred() = 0; + virtual quint64 bytesTotal() = 0; + + // TODO consider adding speed getter + + Q_SIGNAL void progressChanged(); +}; + +#endif // QXMPPFILETRANSFER_H diff --git a/src/client/QXmppHttpFileSharingProvider.cpp b/src/client/QXmppHttpFileSharingProvider.cpp new file mode 100644 index 00000000..5c3e356c --- /dev/null +++ b/src/client/QXmppHttpFileSharingProvider.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppHttpFileSharingProvider.h" + +#include "QXmppClient.h" +#include "QXmppDownload.h" +#include "QXmppHttpUploadManager.h" +#include "QXmppUpload.h" +#include "QXmppUtils.h" +#include "QXmppUtils_p.h" + +#include <QMimeDatabase> +#include <QNetworkReply> + +using namespace QXmpp; +using namespace QXmpp::Private; + +/// +/// \class QXmppHttpFileSharingProvider +/// +/// A file sharing provider that uses HTTP File Upload to upload the file. +/// +/// \since QXmpp 1.5 +/// + +class QXmppSfsHttpUpload : public QXmppUpload +{ + Q_OBJECT + +public: + QXmppSfsHttpUpload(std::shared_ptr<QXmppHttpUpload> &&httpUpload) + : inner(std::move(httpUpload)) + { + connect(inner.get(), &QXmppHttpUpload::finished, this, [this](const QXmppHttpUpload::Result &result) { + Q_EMIT uploadFinished(std::visit([](auto &&value) -> QXmpp::Private::UploadResult { + using T = std::decay_t<decltype(value)>; + if constexpr (std::is_same_v<T, QUrl>) { + return std::any(QXmppHttpFileSource(value)); + } else if constexpr (std::is_same_v<T, Cancelled>) { + return Cancelled {}; + } else if constexpr (std::is_same_v<T, QXmppError>) { + return value; + } + }, + result)); + }); + connect(inner.get(), &QXmppHttpUpload::progressChanged, + this, &QXmppSfsHttpUpload::progressChanged); + } + + float progress() override { return inner->progress(); } + void cancel() override { inner->cancel(); } + bool isFinished() override { return inner->isFinished(); } + quint64 bytesTransferred() override { return inner->bytesSent(); } + quint64 bytesTotal() override { return inner->bytesTotal(); } + +private: + std::shared_ptr<QXmppHttpUpload> inner; +}; + +class QXmppHttpDownload : public QXmppDownload +{ + float progress() override + { + return calculateProgress(m_bytesSent, m_bytesTotal); + } + + void cancel() override + { + m_aborted = true; + reply->abort(); + } + + bool isFinished() override + { + return m_isFinished; + } + + quint64 bytesTransferred() override + { + return m_bytesSent; + } + + quint64 bytesTotal() override + { + return m_bytesTotal; + } + +public: + void reportProgress(quint64 bytesSent, quint64 bytesTotal) + { + m_bytesSent = bytesSent; + m_bytesTotal = bytesTotal; + Q_EMIT progressChanged(); + } + + void reportFinished(Result &&result) + { + Q_EMIT finished(result); + m_isFinished = true; + } + + [[nodiscard]] bool aborted() const + { + return m_aborted; + } + +private: + QNetworkReply *reply = nullptr; + quint64 m_bytesSent = 0; + quint64 m_bytesTotal = 0; + bool m_isFinished = false; + bool m_aborted = false; +}; + +class QXmppHttpFileSharingProviderPrivate +{ +public: + QXmppHttpUploadManager *manager; + QNetworkAccessManager *netManager; +}; + +/// +/// \brief Create a QXmppHttpFileSharingProvider +/// \param client +/// \param netManager QNetworkAccessManager that can be reused all over your application. +/// +QXmppHttpFileSharingProvider::QXmppHttpFileSharingProvider(QXmppClient *client, QNetworkAccessManager *netManager) + : d(std::make_unique<QXmppHttpFileSharingProviderPrivate>()) +{ + qRegisterMetaType<QXmpp::Private::UploadResult>(); + Q_ASSERT(client); + d->manager = client->findExtension<QXmppHttpUploadManager>(); + Q_ASSERT(d->manager); + d->netManager = netManager; +} + +QXmppHttpFileSharingProvider::~QXmppHttpFileSharingProvider() = default; + +auto QXmppHttpFileSharingProvider::downloadFile(const std::any &source, std::unique_ptr<QIODevice> &&target) + -> std::shared_ptr<QXmppDownload> +{ + QXmppHttpFileSource httpSource; + try { + httpSource = std::any_cast<QXmppHttpFileSource>(source); + } catch (const std::bad_any_cast &) { + qFatal("QXmppHttpFileSharingProvider::downloadFile can only handle QXmppHttpFileSharingProvider sources"); + } + + auto *reply = d->netManager->get(QNetworkRequest(httpSource.url())); + + auto download = std::make_shared<QXmppHttpDownload>(); + + QObject::connect(reply, &QNetworkReply::finished, [=]() { + if (reply->error() != QNetworkReply::NoError) { + download->reportFinished(QXmppError::fromIoDevice(*reply)); + } else if (download->aborted()) { + download->reportFinished(Cancelled()); + } else { + download->reportFinished(Success()); + } + reply->deleteLater(); + }); + + QObject::connect(reply, &QNetworkReply::readyRead, [file = std::move(target), reply]() { + file->write(reply->readAll()); + }); + + QObject::connect(reply, &QNetworkReply::downloadProgress, [=](qint64 bytesReceived, qint64 bytesTotal) { + download->reportProgress(bytesReceived, bytesTotal); + }); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QObject::connect(reply, &QNetworkReply::errorOccurred, +#else + QObject::connect(reply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), +#endif + [download, reply]() { + download->reportFinished(QXmppError::fromNetworkReply(*reply)); + }); + + QObject::connect(reply, &QNetworkReply::uploadProgress, [download](qint64 sent, qint64 total) { + quint64 sentBytes = sent < 0 ? 0 : quint64(sent); + quint64 totalBytes = total < 0 ? 0 : quint64(total); + download->reportProgress(sentBytes, totalBytes); + }); + + return download; +} + +auto QXmppHttpFileSharingProvider::uploadFile(std::unique_ptr<QIODevice> data, + const QXmppFileMetadata &info) + -> std::shared_ptr<QXmppUpload> +{ + Q_ASSERT(d->manager); + auto upload = d->manager->uploadFile( + std::move(data), + QXmppUtils::generateStanzaHash(10), + info.mediaType().value_or(QMimeDatabase().mimeTypeForName("application/octet-stream")), + info.size() ? info.size().value() : -1); + + return std::make_shared<QXmppSfsHttpUpload>(std::move(upload)); +} + +#include "QXmppHttpFileSharingProvider.moc" diff --git a/src/client/QXmppHttpFileSharingProvider.h b/src/client/QXmppHttpFileSharingProvider.h new file mode 100644 index 00000000..748c1277 --- /dev/null +++ b/src/client/QXmppHttpFileSharingProvider.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPHTTPFILESHARINGPROVIDER_H +#define QXMPPHTTPFILESHARINGPROVIDER_H + +#include "QXmppFileSharingProvider.h" +#include "QXmppHttpFileSource.h" + +#include <any> +#include <memory> + +class QXmppClient; +class QIODevice; +class QXmppHttpUploadManager; +class QNetworkAccessManager; + +class QXmppHttpFileSharingProviderPrivate; + +class QXMPP_EXPORT QXmppHttpFileSharingProvider : public QXmppFileSharingProvider +{ +public: + /// \cond + using SourceType = QXmppHttpFileSource; + /// \endcond + + QXmppHttpFileSharingProvider(QXmppClient *client, QNetworkAccessManager *netManager); + ~QXmppHttpFileSharingProvider() override; + + auto downloadFile(const std::any &source, + std::unique_ptr<QIODevice> &&target) -> std::shared_ptr<QXmppDownload> override; + auto uploadFile(std::unique_ptr<QIODevice> data, + const QXmppFileMetadata &info) -> std::shared_ptr<QXmppUpload> override; + +private: + std::unique_ptr<QXmppHttpFileSharingProviderPrivate> d; +}; + +#endif // QXMPPHTTPFILESHARINGPROVIDER_H diff --git a/src/client/QXmppHttpUploadManager.cpp b/src/client/QXmppHttpUploadManager.cpp index f6261773..7feac738 100644 --- a/src/client/QXmppHttpUploadManager.cpp +++ b/src/client/QXmppHttpUploadManager.cpp @@ -8,6 +8,7 @@ #include "QXmppFutureUtils_p.h" #include "QXmppHttpUploadIq.h" #include "QXmppUploadRequestManager.h" +#include "QXmppUtils_p.h" #include <QFile> #include <QFileInfo> @@ -149,13 +150,7 @@ QXmppHttpUpload::~QXmppHttpUpload() = default; /// float QXmppHttpUpload::progress() const { - if (d->bytesTotal > 0) { - if (d->bytesSent > d->bytesTotal) { - return 1; - } - return float(d->bytesSent) / d->bytesTotal; - } - return 0; + return calculateProgress(d->bytesSent, d->bytesTotal); } /// diff --git a/src/client/QXmppUpload.cpp b/src/client/QXmppUpload.cpp new file mode 100644 index 00000000..e7b876e5 --- /dev/null +++ b/src/client/QXmppUpload.cpp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppUpload.h" + +/// +/// \class QXmppUpload +/// +/// \brief Provides progress of stateless file sharing uploads. +/// +/// \since QXmpp 1.5 +/// + +/// +/// \class QXmppUpload::FileResult +/// +/// \brief Contains QXmppFileShare of the uploaded file and possible data blobs containing +/// referenced thumbnails. +/// + +/// +/// \var QXmppUpload::FileResult::fileShare +/// +/// \brief File share with file metadata and file shares of the uploaded file. +/// + +/// +/// \var QXmppUpload::FileResult::dataBlobs +/// +/// \brief Data blobs of possibly in the metadata referenced thumbnails. +/// +/// The QXmppFileSharingManager may generate file thumbnails. +/// + +/// +/// \typedef QXmppUpload::Result +/// +/// \brief Contains FileResult (successfully finished), QXmpp::Cancelled (manually cancelled) +/// or QXmppError (an error occured while downloading). +/// + +/// +/// \fn QXmppUpload::finished +/// +/// Emitted when the upload has finished. +/// diff --git a/src/client/QXmppUpload.h b/src/client/QXmppUpload.h new file mode 100644 index 00000000..c948496f --- /dev/null +++ b/src/client/QXmppUpload.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPUPLOAD_H +#define QXMPPUPLOAD_H + +#include "QXmppBitsOfBinaryDataList.h" +#include "QXmppError.h" +#include "QXmppFileMetadata.h" +#include "QXmppFileShare.h" +#include "QXmppFileTransfer.h" + +#include <variant> + +namespace QXmpp::Private { +using UploadResult = std::variant<std::any /* source */, QXmpp::Cancelled, QXmppError>; +} + +class QXMPP_EXPORT QXmppUpload : public QXmppFileTransfer +{ + Q_OBJECT +public: + struct FileResult + { + QXmppFileShare fileShare; + QXmppBitsOfBinaryDataList dataBlobs; + }; + + using Result = std::variant<FileResult, QXmpp::Cancelled, QXmppError>; + Q_SIGNAL void finished(QXmppUpload::Result); + +protected: + /// \cond + Q_SIGNAL void uploadFinished(QXmpp::Private::UploadResult); + /// \endcond + +private: + QXmppFileMetadata metadata; + + friend class QXmppFileSharingManager; +}; + +Q_DECLARE_METATYPE(QXmppUpload::Result); +Q_DECLARE_METATYPE(QXmpp::Private::UploadResult); + +#endif // QXMPPUPLOAD_H |
