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"