File indexing completed on 2024-05-12 05:22:19

0001 /*
0002  * This file is part of LibKGAPI library
0003  *
0004  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007  */
0008 
0009 #include "fileabstractuploadjob.h"
0010 #include "debug.h"
0011 #include "utils.h"
0012 
0013 #include <QNetworkReply>
0014 #include <QNetworkRequest>
0015 
0016 #include <QCryptographicHash>
0017 #include <QFile>
0018 #include <QFileInfo>
0019 #include <QMimeDatabase>
0020 #include <QMimeType>
0021 #include <QUrlQuery>
0022 
0023 using namespace KGAPI2;
0024 using namespace KGAPI2::Drive;
0025 
0026 class Q_DECL_HIDDEN FileAbstractUploadJob::Private
0027 {
0028 public:
0029     Private(FileAbstractUploadJob *parent);
0030     void processNext();
0031     QByteArray buildMultipart(const QString &filePath, const FilePtr &metaData, QString &boundary);
0032     QByteArray readFile(const QString &filePath, QString &contentType);
0033 
0034     void _k_uploadProgress(qint64 bytesSent, qint64 totalBytes);
0035 
0036     int originalFilesCount = 0;
0037     QMap<QString, FilePtr> files;
0038 
0039     QMap<QString, FilePtr> uploadedFiles;
0040 
0041     File::SerializationOptions serializationOptions = File::NoOptions;
0042 
0043 private:
0044     FileAbstractUploadJob *const q;
0045 };
0046 
0047 FileAbstractUploadJob::Private::Private(FileAbstractUploadJob *parent)
0048     : q(parent)
0049 {
0050 }
0051 
0052 QByteArray FileAbstractUploadJob::Private::readFile(const QString &filePath, QString &contentType)
0053 {
0054     QFile file(filePath);
0055     if (!file.open(QIODevice::ReadOnly)) {
0056         qCWarning(KGAPIDebug) << "Failed to access" << filePath;
0057         return QByteArray();
0058     }
0059 
0060     if (contentType.isEmpty()) {
0061         const QMimeDatabase db;
0062         const QMimeType mime = db.mimeTypeForFileNameAndData(filePath, &file);
0063         contentType = mime.name();
0064         qCDebug(KGAPIDebug) << "Determined content type" << contentType << "for" << filePath;
0065     }
0066 
0067     file.reset();
0068     QByteArray output = file.readAll();
0069 
0070     file.close();
0071 
0072     return output;
0073 }
0074 
0075 QByteArray FileAbstractUploadJob::Private::buildMultipart(const QString &filePath, const FilePtr &metaData, QString &boundary)
0076 {
0077     QString fileContentType = metaData->mimeType();
0078     QByteArray fileContent;
0079 
0080     fileContent = readFile(filePath, fileContentType);
0081     if (fileContent.isEmpty()) {
0082         return QByteArray();
0083     }
0084 
0085     qCDebug(KGAPIDebug) << "Setting content type" << fileContentType << "for" << filePath;
0086 
0087     // Wannabe implementation of RFC2387, i.e. multipart/related
0088     QByteArray body;
0089     QFileInfo finfo(filePath);
0090     const QByteArray md5 = QCryptographicHash::hash(finfo.fileName().toLatin1(), QCryptographicHash::Md5);
0091     boundary = QString::fromLatin1(md5.toHex());
0092 
0093     body += "--" + boundary.toLatin1() + '\n';
0094     body += "Content-Type: application/json; charset=UTF-8\n";
0095     body += '\n';
0096     body += File::toJSON(metaData, q->serializationOptions());
0097     body += '\n';
0098     body += '\n';
0099     body += "--" + boundary.toLatin1() + '\n';
0100     body += "Content-Type: " + fileContentType.toLatin1() + '\n';
0101     body += '\n';
0102     body += fileContent;
0103     body += '\n';
0104     body += "--" + boundary.toLatin1() + "--";
0105 
0106     return body;
0107 }
0108 
0109 void FileAbstractUploadJob::Private::processNext()
0110 {
0111     if (files.isEmpty()) {
0112         q->emitFinished();
0113         return;
0114     }
0115 
0116     const QString filePath = files.cbegin().key();
0117     if (!filePath.startsWith(QLatin1StringView("?=")) && !QFile::exists(filePath)) {
0118         qCWarning(KGAPIDebug) << filePath << "is not a valid file path";
0119         processNext();
0120         return;
0121     }
0122 
0123     const FilePtr metaData = files.take(filePath);
0124 
0125     QUrl url;
0126     if (filePath.startsWith(QLatin1StringView("?="))) {
0127         url = q->createUrl(QString(), metaData);
0128     } else {
0129         url = q->createUrl(filePath, metaData);
0130     }
0131 
0132     q->updateUrl(url);
0133     QUrlQuery query(url);
0134 
0135     QByteArray rawData;
0136     QString contentType;
0137 
0138     // just to be sure
0139     query.removeQueryItem(QStringLiteral("uploadType"));
0140     if (metaData.isNull()) {
0141         query.addQueryItem(QStringLiteral("uploadType"), QStringLiteral("media"));
0142 
0143         rawData = readFile(filePath, contentType);
0144         if (rawData.isEmpty()) {
0145             processNext();
0146             return;
0147         }
0148 
0149     } else if (!filePath.startsWith(QLatin1StringView("?="))) {
0150         query.addQueryItem(QStringLiteral("uploadType"), QStringLiteral("multipart"));
0151 
0152         QString boundary;
0153         rawData = buildMultipart(filePath, metaData, boundary);
0154 
0155         contentType = QStringLiteral("multipart/related; boundary=%1").arg(boundary);
0156         if (rawData.isEmpty()) {
0157             processNext();
0158             return;
0159         }
0160     } else {
0161         rawData = File::toJSON(metaData, q->serializationOptions());
0162         contentType = QStringLiteral("application/json");
0163     }
0164 
0165     url.setQuery(query);
0166 
0167     QNetworkRequest request(url);
0168     request.setHeader(QNetworkRequest::ContentLengthHeader, rawData.length());
0169     request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);
0170     request.setAttribute(QNetworkRequest::User, filePath);
0171 
0172     q->enqueueRequest(request, rawData, contentType);
0173 }
0174 
0175 void FileAbstractUploadJob::Private::_k_uploadProgress(qint64 bytesSent, qint64 totalBytes)
0176 {
0177     // Each file consists of 100 units, so if we have two files, one already
0178     // uploaded and the other one uploaded from 50%, the values are (150, 200)
0179 
0180     int processedParts = (originalFilesCount - files.count() - 1) * 100;
0181     int currentFileParts = 100.0 * ((qreal)bytesSent / (qreal)totalBytes);
0182 
0183     q->emitProgress(processedParts + currentFileParts, originalFilesCount * 100);
0184 }
0185 
0186 FileAbstractUploadJob::FileAbstractUploadJob(const FilePtr &metadata, const AccountPtr &account, QObject *parent)
0187     : FileAbstractDataJob(account, parent)
0188     , d(new Private(this))
0189 {
0190     d->files.insert(QStringLiteral("?=0"), metadata);
0191     d->originalFilesCount = 1;
0192 }
0193 
0194 FileAbstractUploadJob::FileAbstractUploadJob(const FilesList &metadata, const AccountPtr &account, QObject *parent)
0195     : FileAbstractDataJob(account, parent)
0196     , d(new Private(this))
0197 {
0198     int i = 0;
0199     for (const FilePtr &file : metadata) {
0200         d->files.insert(QStringLiteral("?=%1").arg(i), file);
0201         ++i;
0202     }
0203     d->originalFilesCount = d->files.count();
0204 }
0205 
0206 FileAbstractUploadJob::FileAbstractUploadJob(const QString &filePath, const AccountPtr &account, QObject *parent)
0207     : FileAbstractDataJob(account, parent)
0208     , d(new Private(this))
0209 {
0210     d->files.insert(filePath, FilePtr());
0211     d->originalFilesCount = 1;
0212 }
0213 
0214 FileAbstractUploadJob::FileAbstractUploadJob(const QString &filePath, const FilePtr &metaData, const AccountPtr &account, QObject *parent)
0215     : FileAbstractDataJob(account, parent)
0216     , d(new Private(this))
0217 {
0218     d->files.insert(filePath, metaData);
0219     d->originalFilesCount = 1;
0220 }
0221 
0222 FileAbstractUploadJob::FileAbstractUploadJob(const QStringList &filePaths, const AccountPtr &account, QObject *parent)
0223     : FileAbstractDataJob(account, parent)
0224     , d(new Private(this))
0225 {
0226     for (const QString &filePath : filePaths) {
0227         d->files.insert(filePath, FilePtr());
0228     }
0229     d->originalFilesCount = d->files.count();
0230 }
0231 
0232 FileAbstractUploadJob::FileAbstractUploadJob(const QMap<QString, FilePtr> &files, const AccountPtr &account, QObject *parent)
0233     : FileAbstractDataJob(account, parent)
0234     , d(new Private(this))
0235 {
0236     d->files = files;
0237     d->originalFilesCount = d->files.count();
0238 }
0239 
0240 FileAbstractUploadJob::~FileAbstractUploadJob()
0241 {
0242     delete d;
0243 }
0244 
0245 void FileAbstractUploadJob::start()
0246 {
0247     d->processNext();
0248 }
0249 
0250 QMap<QString, FilePtr> FileAbstractUploadJob::files() const
0251 {
0252     return d->uploadedFiles;
0253 }
0254 
0255 void FileAbstractUploadJob::dispatchRequest(QNetworkAccessManager *accessManager,
0256                                             const QNetworkRequest &request,
0257                                             const QByteArray &data,
0258                                             const QString &contentType)
0259 {
0260     Q_UNUSED(contentType)
0261 
0262     QNetworkReply *reply = dispatch(accessManager, request, data);
0263 
0264     connect(reply, &QNetworkReply::uploadProgress, this, [this](qint64 bytesSent, qint64 totalBytes) {
0265         d->_k_uploadProgress(bytesSent, totalBytes);
0266     });
0267 }
0268 
0269 void FileAbstractUploadJob::handleReply(const QNetworkReply *reply, const QByteArray &rawData)
0270 {
0271     const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0272     ContentType ct = Utils::stringToContentType(contentType);
0273     if (ct == KGAPI2::JSON) {
0274         const QNetworkRequest request = reply->request();
0275         const QString filePath = request.attribute(QNetworkRequest::User).toString();
0276 
0277         FilePtr file = File::fromJSON(rawData);
0278 
0279         d->uploadedFiles.insert(filePath, file);
0280     } else {
0281         setError(KGAPI2::InvalidResponse);
0282         setErrorString(tr("Invalid response content type"));
0283         emitFinished();
0284         return;
0285     }
0286 
0287     d->processNext();
0288 }
0289 
0290 void FileAbstractUploadJob::setSerializationOptions(File::SerializationOptions options)
0291 {
0292     d->serializationOptions = options;
0293 }
0294 
0295 File::SerializationOptions FileAbstractUploadJob::serializationOptions() const
0296 {
0297     return d->serializationOptions;
0298 }
0299 
0300 #include "moc_fileabstractuploadjob.cpp"