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

0001 /*
0002  * This file is part of LibKGAPI library
0003  *
0004  * SPDX-FileCopyrightText: 2020 David Barchiesi <david@barchie.si>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007  */
0008 
0009 #include "fileabstractresumablejob.h"
0010 #include "debug.h"
0011 #include "utils.h"
0012 
0013 #include <QMimeDatabase>
0014 #include <QNetworkReply>
0015 #include <QNetworkRequest>
0016 #include <QUrlQuery>
0017 
0018 using namespace KGAPI2;
0019 using namespace KGAPI2::Drive;
0020 
0021 namespace
0022 {
0023 static const int ChunkSize = 262144;
0024 }
0025 
0026 class Q_DECL_HIDDEN FileAbstractResumableJob::Private
0027 {
0028 public:
0029     Private(FileAbstractResumableJob *parent);
0030     void startUploadSession();
0031     void uploadChunk(bool lastChunk);
0032     void processNext();
0033     void readFromDevice();
0034     bool isTotalSizeKnown() const;
0035 
0036     void _k_uploadProgress(qint64 bytesSent, qint64 totalBytes);
0037 
0038     FilePtr metaData;
0039     QIODevice *device = nullptr;
0040 
0041     QString sessionPath;
0042     QList<QByteArray> chunks;
0043     int uploadedSize = 0;
0044     int totalUploadSize = 0;
0045 
0046     enum SessionState { ReadyStart, Started, ClientEnough, Completed };
0047 
0048     SessionState sessionState = ReadyStart;
0049 
0050 private:
0051     FileAbstractResumableJob *const q;
0052 };
0053 
0054 FileAbstractResumableJob::Private::Private(FileAbstractResumableJob *parent)
0055     : q(parent)
0056 {
0057 }
0058 
0059 void FileAbstractResumableJob::Private::startUploadSession()
0060 {
0061     qCDebug(KGAPIDebug) << "Opening resumable upload session";
0062 
0063     // Setup job url and generic params
0064     QUrl url = q->createUrl();
0065     q->updateUrl(url);
0066 
0067     QUrlQuery query(url);
0068     query.removeQueryItem(QStringLiteral("uploadType"));
0069     query.addQueryItem(QStringLiteral("uploadType"), QStringLiteral("resumable"));
0070     url.setQuery(query);
0071 
0072     QNetworkRequest request(url);
0073     QByteArray rawData;
0074     if (!metaData.isNull()) {
0075         if (metaData->mimeType().isEmpty() && !chunks.isEmpty()) {
0076             // No mimeType set, determine from title and first chunk
0077             const QMimeDatabase db;
0078             const QMimeType mime = db.mimeTypeForFileNameAndData(metaData->title(), chunks.first());
0079             const QString contentType = mime.name();
0080             metaData->setMimeType(contentType);
0081             qCDebug(KGAPIDebug) << "Metadata mimeType was missing, determined" << contentType;
0082         }
0083         qCDebug(KGAPIDebug) << "Metadata has mimeType" << metaData->mimeType();
0084 
0085         rawData = File::toJSON(metaData);
0086     }
0087 
0088     QString contentType = QStringLiteral("application/json");
0089 
0090     request.setHeader(QNetworkRequest::ContentLengthHeader, rawData.length());
0091     request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);
0092 
0093     q->enqueueRequest(request, rawData, contentType);
0094 }
0095 
0096 void FileAbstractResumableJob::Private::uploadChunk(bool lastChunk)
0097 {
0098     QString rangeHeader;
0099     QByteArray partData;
0100     if (chunks.isEmpty()) {
0101         // We have consumed everything but must send one last request with total file size
0102         qCDebug(KGAPIDebug) << "Chunks is empty, sending only final size" << uploadedSize;
0103         rangeHeader = QStringLiteral("bytes */%1").arg(uploadedSize);
0104     } else {
0105         partData = chunks.takeFirst();
0106         // Build range header from saved upload size and new
0107         QString tempRangeHeader = QStringLiteral("bytes %1-%2/%3").arg(uploadedSize).arg(uploadedSize + partData.size() - 1);
0108         if (lastChunk) {
0109             // Need to send last chunk, therefore final file size is known now
0110             tempRangeHeader = tempRangeHeader.arg(uploadedSize + partData.size());
0111         } else {
0112             // Use star in the case that total upload size in unknown
0113             QString totalSymbol = isTotalSizeKnown() ? QString::number(totalUploadSize) : QStringLiteral("*");
0114             tempRangeHeader = tempRangeHeader.arg(totalSymbol);
0115         }
0116         rangeHeader = tempRangeHeader;
0117     }
0118 
0119     qCDebug(KGAPIDebug) << "Sending chunk of" << partData.size() << "bytes with Content-Range header" << rangeHeader;
0120 
0121     QUrl url = QUrl(sessionPath);
0122     QNetworkRequest request(url);
0123     request.setRawHeader(QByteArray("Content-Range"), rangeHeader.toUtf8());
0124     request.setHeader(QNetworkRequest::ContentLengthHeader, partData.length());
0125     q->enqueueRequest(request, partData);
0126     uploadedSize += partData.size();
0127 }
0128 
0129 void FileAbstractResumableJob::Private::processNext()
0130 {
0131     qCDebug(KGAPIDebug) << "Processing next";
0132 
0133     switch (sessionState) {
0134     case ReadyStart:
0135         startUploadSession();
0136         return;
0137     case Started: {
0138         if (chunks.isEmpty() || chunks.first().size() < ChunkSize) {
0139             qCDebug(KGAPIDebug) << "Chunks empty or not big enough to process, asking for more";
0140 
0141             if (device) {
0142                 readFromDevice();
0143             } else {
0144                 // Warning: an endless loop could be started here if the signal receiver isn't using
0145                 // a direct connection.
0146                 q->emitReadyWrite();
0147             }
0148             processNext();
0149             return;
0150         }
0151         uploadChunk(false);
0152         return;
0153     }
0154     case ClientEnough: {
0155         uploadChunk(true);
0156         sessionState = Completed;
0157         return;
0158     }
0159     case Completed:
0160         qCDebug(KGAPIDebug) << "Nothing left to process, done";
0161         q->emitFinished();
0162         return;
0163     }
0164 }
0165 
0166 void KGAPI2::Drive::FileAbstractResumableJob::Private::readFromDevice()
0167 {
0168     char buf[ChunkSize];
0169     int read = device->read(buf, ChunkSize);
0170     if (read == -1) {
0171         qCWarning(KGAPIDebug) << "Failed reading from device" << device->errorString();
0172         return;
0173     }
0174     qCDebug(KGAPIDebug) << "Read from device bytes" << read;
0175     q->write(QByteArray(buf, read));
0176 }
0177 
0178 bool FileAbstractResumableJob::Private::isTotalSizeKnown() const
0179 {
0180     return totalUploadSize != 0;
0181 }
0182 
0183 void FileAbstractResumableJob::Private::_k_uploadProgress(qint64 bytesSent, qint64 totalBytes)
0184 {
0185     // uploadedSize corresponds to total bytes enqueued (including current chunk upload)
0186     qint64 totalUploaded = uploadedSize - totalBytes + bytesSent;
0187     q->emitProgress(totalUploaded, totalUploadSize);
0188 }
0189 
0190 FileAbstractResumableJob::FileAbstractResumableJob(const AccountPtr &account, QObject *parent)
0191     : FileAbstractDataJob(account, parent)
0192     , d(new Private(this))
0193 {
0194 }
0195 
0196 FileAbstractResumableJob::FileAbstractResumableJob(const FilePtr &metadata, const AccountPtr &account, QObject *parent)
0197     : FileAbstractDataJob(account, parent)
0198     , d(new Private(this))
0199 {
0200     d->metaData = metadata;
0201 }
0202 
0203 FileAbstractResumableJob::FileAbstractResumableJob(QIODevice *device, const AccountPtr &account, QObject *parent)
0204     : FileAbstractDataJob(account, parent)
0205     , d(new Private(this))
0206 {
0207     d->device = device;
0208 }
0209 
0210 FileAbstractResumableJob::FileAbstractResumableJob(QIODevice *device, const FilePtr &metadata, const AccountPtr &account, QObject *parent)
0211     : FileAbstractDataJob(account, parent)
0212     , d(new Private(this))
0213 {
0214     d->device = device;
0215     d->metaData = metadata;
0216 }
0217 
0218 FileAbstractResumableJob::~FileAbstractResumableJob() = default;
0219 
0220 FilePtr FileAbstractResumableJob::metadata() const
0221 {
0222     return d->metaData;
0223 }
0224 
0225 void FileAbstractResumableJob::setUploadSize(int size)
0226 {
0227     if (isRunning()) {
0228         qCWarning(KGAPIDebug) << "Can't set upload size when the job is already running";
0229         return;
0230     }
0231 
0232     d->totalUploadSize = size;
0233 }
0234 
0235 void FileAbstractResumableJob::write(const QByteArray &data)
0236 {
0237     qCDebug(KGAPIDebug) << "Received" << data.size() << "bytes to upload";
0238 
0239     if (data.isEmpty()) {
0240         qCDebug(KGAPIDebug) << "Data empty, won't receive any more data from client";
0241         d->sessionState = Private::ClientEnough;
0242         return;
0243     }
0244 
0245     int pos = 0;
0246     // Might need to add to last chunk
0247     if (!d->chunks.isEmpty() && d->chunks.last().size() < ChunkSize) {
0248         QByteArray lastChunk = d->chunks.takeLast();
0249         int missing = ChunkSize - lastChunk.size();
0250         qCDebug(KGAPIDebug) << "Previous last chunk was" << lastChunk.size() << "bytes and could use" << missing << "bytes more, adding to it";
0251         lastChunk.append(data.mid(0, missing));
0252         pos = missing;
0253         d->chunks << lastChunk;
0254     }
0255 
0256     int dataSize = data.size();
0257     QList<QByteArray> chunks;
0258     while (pos < dataSize) {
0259         QByteArray chunk = data.mid(pos, ChunkSize);
0260         chunks << chunk;
0261         pos += chunk.size();
0262     }
0263 
0264     qCDebug(KGAPIDebug) << "Added" << chunks.size() << "new chunks";
0265     d->chunks << chunks;
0266 }
0267 
0268 void FileAbstractResumableJob::start()
0269 {
0270     if (d->device) {
0271         d->readFromDevice();
0272     }
0273     // Ask for more chunks right away in case
0274     // write() wasn't called before starting
0275     if (d->chunks.isEmpty()) {
0276         emitReadyWrite();
0277     }
0278     d->processNext();
0279 }
0280 
0281 void FileAbstractResumableJob::dispatchRequest(QNetworkAccessManager *accessManager,
0282                                                const QNetworkRequest &request,
0283                                                const QByteArray &data,
0284                                                const QString &contentType)
0285 {
0286     Q_UNUSED(contentType)
0287 
0288     QNetworkReply *reply;
0289     if (d->sessionState == Private::ReadyStart) {
0290         reply = accessManager->post(request, data);
0291     } else {
0292         reply = accessManager->put(request, data);
0293     }
0294 
0295     if (d->isTotalSizeKnown()) {
0296         connect(reply, &QNetworkReply::uploadProgress, this, [this](qint64 bytesSent, qint64 totalBytes) {
0297             d->_k_uploadProgress(bytesSent, totalBytes);
0298         });
0299     }
0300 }
0301 
0302 void FileAbstractResumableJob::handleReply(const QNetworkReply *reply, const QByteArray &rawData)
0303 {
0304     Q_UNUSED(rawData)
0305 
0306     int replyCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0307 
0308     switch (d->sessionState) {
0309     case Private::ReadyStart: {
0310         if (replyCode != KGAPI2::OK) {
0311             qCWarning(KGAPIDebug) << "Failed opening upload session" << replyCode;
0312             setError(KGAPI2::UnknownError);
0313             setErrorString(tr("Failed opening upload session"));
0314             emitFinished();
0315             return;
0316         }
0317 
0318         const QString uploadLocation = reply->header(QNetworkRequest::LocationHeader).toString();
0319         qCDebug(KGAPIDebug) << "Got upload session location" << uploadLocation;
0320         d->sessionPath = uploadLocation;
0321         d->sessionState = Private::Started;
0322         break;
0323     }
0324     case Private::Started: {
0325         // If during upload total size is declared via Content-Range header, Google will
0326         // respond with 200 on the last chunk upload. The job is complete in that case.
0327         if (d->isTotalSizeKnown() && replyCode == KGAPI2::OK) {
0328             d->sessionState = Private::Completed;
0329             const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0330             ContentType ct = Utils::stringToContentType(contentType);
0331             if (ct == KGAPI2::JSON) {
0332                 d->metaData = File::fromJSON(rawData);
0333             }
0334             return;
0335         }
0336 
0337         // Google will continue answering ResumeIncomplete until the total upload size is declared
0338         // in the Content-Range header or until last upload range not total upload size.
0339         if (replyCode != KGAPI2::ResumeIncomplete) {
0340             qCWarning(KGAPIDebug) << "Failed uploading chunk" << replyCode;
0341             setError(KGAPI2::UnknownError);
0342             setErrorString(tr("Failed uploading chunk"));
0343             emitFinished();
0344             return;
0345         }
0346 
0347         // Server could send us a new upload session location any time, use it if present
0348         const QString newUploadLocation = reply->header(QNetworkRequest::LocationHeader).toString();
0349         if (!newUploadLocation.isEmpty()) {
0350             qCDebug(KGAPIDebug) << "Got new location" << newUploadLocation;
0351             d->sessionPath = newUploadLocation;
0352         }
0353 
0354         const QString readRange = QString::fromUtf8(reply->rawHeader(QStringLiteral("Range").toUtf8()));
0355         qCDebug(KGAPIDebug) << "Server confirms range" << readRange;
0356         break;
0357     }
0358     case Private::ClientEnough:
0359     case Private::Completed:
0360         if (replyCode != KGAPI2::OK) {
0361             qCWarning(KGAPIDebug) << "Failed completing upload session" << replyCode;
0362             setError(KGAPI2::UnknownError);
0363             setErrorString(tr("Failed completing upload session"));
0364             emitFinished();
0365             return;
0366         }
0367         const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0368         ContentType ct = Utils::stringToContentType(contentType);
0369         if (ct == KGAPI2::JSON) {
0370             d->metaData = File::fromJSON(rawData);
0371         }
0372         break;
0373     }
0374 
0375     d->processNext();
0376 }
0377 
0378 void FileAbstractResumableJob::emitReadyWrite()
0379 {
0380     Q_EMIT readyWrite(this);
0381 }
0382 
0383 #include "moc_fileabstractresumablejob.cpp"