Commit 7ed26c8e authored by David Barchiesi's avatar David Barchiesi
Browse files

Add FileResumableCreateJob and FileResumableModifyJob for uploading files in a chunked manner.

parent fd66c654
......@@ -3,4 +3,5 @@ add_subdirectory(drives)
add_subdirectory(teamdrive)
add_subdirectory(permissions)
add_subdirectory(tasks)
add_subdirectory(files)
#add_subdirectory(staticmaps)
kde_enable_exceptions()
include_directories(
${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}
)
set(files_example_SRCS main.cpp mainwindow.cpp)
set(files_example_HDRS mainwindow.h)
qt5_wrap_ui(files_example_SRCS ui/main.ui)
add_executable(files-example
${files_example_SRCS}
${files_example_HDRS_MOC}
)
target_link_libraries(files-example
Qt5::Widgets
Qt5::Core
KF5::GAPICore
KF5::GAPIDrive
)
/*
SPDX-FileCopyrightText: 2020 David Barchiesi <david@barchie.si>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow ex;
ex.show();
return app.exec();
}
/*
SPDX-FileCopyrightText: 2020 David Barchiesi <david@barchie.si>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "mainwindow.h"
#include "ui_main.h"
#include <drive/file.h>
#include <authjob.h>
#include <account.h>
#include <QFileDialog>
#include <QDebug>
MainWindow::MainWindow(QWidget * parent):
QMainWindow(parent),
m_ui(new Ui::MainWindow)
{
/* Initialize GUI */
m_ui->setupUi(this);
connect(m_ui->authButton, &QAbstractButton::clicked,
this, &MainWindow::authenticate);
connect(m_ui->browseButton, &QAbstractButton::clicked,
this, &MainWindow::browseFiles);
connect(m_ui->uploadButton, &QAbstractButton::clicked,
this, &MainWindow::uploadFile);
setInputsEnabled(false);
}
MainWindow::~MainWindow()
{
delete m_ui;
}
void MainWindow::authenticate()
{
KGAPI2::AccountPtr account(new KGAPI2::Account);
account->setScopes( QList<QUrl>() << KGAPI2::Account::driveScopeUrl() );
/* Create AuthJob to retrieve OAuth tokens for the account */
KGAPI2::AuthJob *authJob = new KGAPI2::AuthJob(
account,
QStringLiteral("554041944266.apps.googleusercontent.com"),
QStringLiteral("mdT1DjzohxN3npUUzkENT0gO"));
connect(authJob, &KGAPI2::Job::finished,
this, &MainWindow::slotAuthJobFinished);
}
void MainWindow::slotAuthJobFinished(KGAPI2::Job *job)
{
KGAPI2::AuthJob *authJob = qobject_cast<KGAPI2::AuthJob*>(job);
Q_ASSERT(authJob);
/* Always remember to delete the jobs, otherwise your application will
* leak memory. */
authJob->deleteLater();
if (authJob->error() != KGAPI2::NoError) {
m_ui->statusbar->showMessage(QStringLiteral("Error: %1").arg(authJob->errorString()));
return;
}
m_account = authJob->account();
m_ui->authStatusLabel->setText(QStringLiteral("Authenticated"));
m_ui->authButton->setEnabled(false);
setInputsEnabled(true);
}
void MainWindow::browseFiles()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("Select file"));
m_ui->sourceLineEdit->setText(fileName);
}
void MainWindow::uploadFile()
{
if (m_ui->sourceLineEdit->text().isEmpty()) {
m_ui->statusbar->showMessage(QStringLiteral("Error: must specify source file."));
return;
}
uploadingFile = new QFile(m_ui->sourceLineEdit->text());
if (!uploadingFile->open(QIODevice::ReadOnly)) {
m_ui->statusbar->showMessage(QStringLiteral("Error: source file not valid."));
return;
}
KGAPI2::Drive::FilePtr uploadFile = KGAPI2::Drive::FilePtr::create();
QFileInfo fileInfo(uploadingFile->fileName());
uploadFile->setTitle(fileInfo.fileName());
KGAPI2::Drive::FileResumableCreateJob *fileCreateJob = new KGAPI2::Drive::FileResumableCreateJob(uploadFile, m_account, this);
connect(fileCreateJob, &KGAPI2::Drive::FileResumableCreateJob::finished,
this, &MainWindow::slotFileCreateJobFinished);
connect(fileCreateJob, &KGAPI2::Drive::FileResumableCreateJob::readyWrite,
this, &MainWindow::slotFileCreateJobReadyWrite);
bytesUploaded = 0;
uploadProgressBar = new QProgressBar(m_ui->statusbar);
uploadProgressBar->setMaximum(uploadingFile->size());
m_ui->statusbar->addWidget(uploadProgressBar);
}
void MainWindow::slotFileCreateJobFinished(KGAPI2::Job *job)
{
qDebug() << "Completed job" << job << "error code:" << job->error() << "- message:" << job->errorString();
KGAPI2::Drive::FileResumableCreateJob *fileCreateJob = qobject_cast<KGAPI2::Drive::FileResumableCreateJob*>(job);
Q_ASSERT(fileCreateJob);
fileCreateJob->deleteLater();
if (fileCreateJob->error() != KGAPI2::NoError) {
m_ui->statusbar->showMessage(QStringLiteral("Error: %1").arg(fileCreateJob->errorString()));
} else {
KGAPI2::Drive::FilePtr file = fileCreateJob->metadata();
m_ui->statusbar->showMessage(QStringLiteral("Upload complete, id %1, size %2 (uploaded %3), mimeType %4").arg(file->id()).arg(file->fileSize()).arg(bytesUploaded).arg(file->mimeType()));
}
m_ui->statusbar->removeWidget(uploadProgressBar);
uploadProgressBar->deleteLater();
}
void MainWindow::slotFileCreateJobReadyWrite(KGAPI2::Drive::FileAbstractResumableJob *job)
{
QByteArray data = uploadingFile->read(50000);
bytesUploaded += data.size();
job->write(data);
uploadProgressBar->setValue(bytesUploaded);
}
void MainWindow::setInputsEnabled(bool enabled)
{
m_ui->sourceLineEdit->setEnabled(enabled);
m_ui->browseButton->setEnabled(enabled);
m_ui->uploadButton->setEnabled(enabled);
}
/*
SPDX-FileCopyrightText: 2020 David Barchiesi <david@barchie.si>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QProgressBar>
#include <QFile>
#include <types.h>
#include <drive/fileresumablecreatejob.h>
namespace Ui {
class MainWindow;
}
namespace KGAPI2 {
class Job;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
private Q_SLOTS:
/**
* Retrieves tokens from Google that we will use to authenticate
* fursther requests
*/
void authenticate();
/**
* Authentication has finished
*/
void slotAuthJobFinished(KGAPI2::Job *job);
/**
* Browses files to select the one to upload
*/
void browseFiles();
/**
* Starts resumable file upload of source file to destination directory
*/
void uploadFile();
/**
* FileCreateJob requests data
*/
void slotFileCreateJobFinished(KGAPI2::Job *job);
/**
* FileCreateJob requests data
*/
void slotFileCreateJobReadyWrite(KGAPI2::Drive::FileAbstractResumableJob *job);
private:
Ui::MainWindow *m_ui;
KGAPI2::AccountPtr m_account;
QFile *uploadingFile;
int bytesUploaded;
QProgressBar *uploadProgressBar;
void setInputsEnabled(bool enabled);
};
#endif // MAINWINDOW_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>905</width>
<height>249</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="authLayout">
<item>
<widget class="QPushButton" name="authButton">
<property name="text">
<string>Authenticate</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="authStatusLabel">
<property name="text">
<string>Not authenticated</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="sourceLabel">
<property name="text">
<string>Source:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="sourceLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="uploadButton">
<property name="text">
<string>Upload to My Drive root</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>905</width>
<height>30</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionQuit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionQuit">
<property name="text">
<string comment="Action that quits the application">Quit</string>
</property>
</action>
</widget>
<resources/>
<connections>
<connection>
<sender>actionQuit</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>319</x>
<y>206</y>
</hint>
</hints>
</connection>
</connections>
</ui>
......@@ -168,6 +168,7 @@ void Job::Private::_k_replyReceived(QNetworkReply* reply)
case KGAPI2::OK: /** << OK status (fetched, updated, removed) */
case KGAPI2::Created: /** << OK status (created) */
case KGAPI2::NoContent: /** << OK status (removed task using Tasks API) */
case KGAPI2::ResumeIncomplete: /** << OK status (partially uploaded a file via resumable upload) */
break;
case KGAPI2::TemporarilyMovedUseSameMethod: /** << Temporarily moved - Google provides a new URL where to send the request which must use the original method */
......
......@@ -187,6 +187,7 @@ enum Error {
OK = 200, ///< Request successfully executed.
Created = 201, ///< Create request successfully executed.
NoContent = 204, ///< Tasks API returns 204 when task is successfully removed.
ResumeIncomplete = 308, ///< Drive Api returns 308 when accepting a partial file upload
TemporarilyMoved = 302, ///< The object is located on a different URL provided in reply.
NotModified = 304, ///< Request was successful, but no data were updated.
TemporarilyMovedUseSameMethod = 307, ///< The object is located at a different URL provided in the reply. The same request method must be used.
......
......@@ -15,12 +15,15 @@ set(kgapidrive_SRCS
fileabstractdatajob.cpp
fileabstractmodifyjob.cpp
fileabstractuploadjob.cpp
fileabstractresumablejob.cpp
filecopyjob.cpp
filecreatejob.cpp
filedeletejob.cpp
filefetchcontentjob.cpp
filefetchjob.cpp
filemodifyjob.cpp
fileresumablecreatejob.cpp
fileresumablemodifyjob.cpp
filesearchquery.cpp
filetouchjob.cpp
filetrashjob.cpp
......@@ -73,12 +76,15 @@ ecm_generate_headers(kgapidrive_CamelCase_HEADERS
FileAbstractDataJob
FileAbstractModifyJob
FileAbstractUploadJob
FileAbstractResumableJob
FileCopyJob
FileCreateJob
FileDeleteJob
FileFetchContentJob
FileFetchJob
FileModifyJob
FileResumableCreateJob
FileResumableModifyJob
FileSearchQuery
FileTouchJob
FileTrashJob
......
/*
* This file is part of LibKGAPI library
*
* SPDX-FileCopyrightText: 2020 David Barchiesi <david@barchie.si>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "fileabstractresumablejob.h"
#include "../debug.h"
#include "utils.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QMimeDatabase>
#include <QUrlQuery>
using namespace KGAPI2;
using namespace KGAPI2::Drive;
namespace {
static const int ChunkSize = 262144;
}
class Q_DECL_HIDDEN FileAbstractResumableJob::Private
{
public:
Private(FileAbstractResumableJob *parent);
void startUploadSession();
void uploadChunk(bool lastChunk);
void processNext();
FilePtr metaData;
QString sessionPath;
QList<QByteArray> chunks;
int uploadedSize = 0;
enum SessionState {
ReadyStart,
Started,
ClientEnough,
Completed
};
SessionState sessionState = ReadyStart;
private:
FileAbstractResumableJob *const q;
};
FileAbstractResumableJob::Private::Private(FileAbstractResumableJob *parent):
q(parent)
{
}
void FileAbstractResumableJob::Private::startUploadSession()
{
qCDebug(KGAPIDebug) << "Opening resumable upload session";
// Setup job url and generic params
QUrl url = q->createUrl();
q->updateUrl(url);
QUrlQuery query(url);
query.removeQueryItem(QStringLiteral("uploadType"));
query.addQueryItem(QStringLiteral("uploadType"), QStringLiteral("resumable"));
url.setQuery(query);
QNetworkRequest request(url);
QByteArray rawData;
if (!metaData.isNull()) {
if (metaData->mimeType().isEmpty() && !chunks.isEmpty()) {
// No mimeType set, determine from title and first chunk
const QMimeDatabase db;
const QMimeType mime = db.mimeTypeForFileNameAndData(metaData->title(), chunks.first());
const QString contentType = mime.name();
metaData->setMimeType(contentType);
qCDebug(KGAPIDebug) << "Metadata mimeType was missing, determined" << contentType;
}
qCDebug(KGAPIDebug) << "Metadata has mimeType" << metaData->mimeType();
rawData = File::toJSON(metaData);
}
QString contentType = QStringLiteral("application/json");
request.setHeader(QNetworkRequest::ContentLengthHeader, rawData.length());
request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);
q->enqueueRequest(request, rawData, contentType);
}
void FileAbstractResumableJob::Private::uploadChunk(bool lastChunk)
{
QString rangeHeader;
QByteArray partData;
if (chunks.isEmpty()) {
// We have consumed everything but must send one last request with total file size
qCDebug(KGAPIDebug) << "Chunks is empty, sending only final size" << uploadedSize;
rangeHeader = QStringLiteral("bytes */%1").arg(uploadedSize);
} else {
partData = chunks.takeFirst();
// Build range header from saved upload size and new
QString tempRangeHeader = QStringLiteral("bytes %1-%2/%3").arg(uploadedSize).arg(uploadedSize + partData.size() - 1);
if (lastChunk) {
// Need to send last chunk, therefore final file size is known now
tempRangeHeader = tempRangeHeader.arg(uploadedSize + partData.size());
} else {
// In the middle of the upload, file size is not yet known so use star
tempRangeHeader = tempRangeHeader.arg(QStringLiteral("*"));
}
rangeHeader = tempRangeHeader;
}
qCDebug(KGAPIDebug) << "Sending chunk of" << partData.size() << "bytes with Content-Range header" << rangeHeader;
QUrl url = QUrl(sessionPath);
QNetworkRequest request(url);
request.setRawHeader(QByteArray("Content-Range"), rangeHeader.toUtf8());
request.setHeader(QNetworkRequest::ContentLengthHeader, partData.length());
q->enqueueRequest(request, partData);
uploadedSize += partData.size();
}
void FileAbstractResumableJob::Private::processNext()
{
qCDebug(KGAPIDebug) << "Processing next";
switch (sessionState) {
case ReadyStart:
startUploadSession();
return;
case Started: {
if (chunks.isEmpty() || chunks.first().size() < ChunkSize) {
qCDebug(KGAPIDebug) << "Chunks empty or not big enough to process, asking for more";
// Warning: an endless loop could be started here is the signal receiver isn't using
// a direct connection.
q->emitReadyWrite();
processNext();
return;
}
uploadChunk(false);
return;
}