From 9a4da6c794609df7397e04747f44851e8bbfb584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Bourgeois?= Date: Fri, 7 Apr 2023 16:46:35 +0200 Subject: [PATCH] Google Drive --- CHANGELOG.md | 5 +- README.md | 84 +++- .../kleiner-brauhelfer-app.pro | 4 +- kleiner-brauhelfer-app/qml/main.qml | 8 + .../qml/pagesOthers/PageSettings.qml | 129 ++++- kleiner-brauhelfer-app/src/syncservice.cpp | 3 +- kleiner-brauhelfer-app/src/syncservice.h | 4 +- .../src/syncservicedropbox.cpp | 34 +- .../src/syncservicedropbox.h | 2 +- .../src/syncservicegoogle.cpp | 469 ++++++++++++++++++ .../src/syncservicegoogle.h | 100 ++++ .../src/syncservicemanager.cpp | 4 + .../src/syncservicemanager.h | 5 +- 13 files changed, 788 insertions(+), 63 deletions(-) create mode 100644 kleiner-brauhelfer-app/src/syncservicegoogle.cpp create mode 100644 kleiner-brauhelfer-app/src/syncservicegoogle.h diff --git a/CHANGELOG.md b/CHANGELOG.md index bd04013..307502b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Version 2.5.2 (07.04.2023) +- Neu: Google Drive Unterstützung + ## Version 2.5.1 (02.04.2023) - Fix: Aktualisieren des Dropbox Access Tokens - Fix: Berücksichtigung der Temperatur bei Spindelmessung @@ -48,4 +51,4 @@ - Fix: WebDav ## Version 2.0.0beta1 (29.10.2019) -- Neu: Unterstüzung für kleiner-brauhelfer Version 2.x.x +- Neu: Unterstützung für kleiner-brauhelfer Version 2.x.x diff --git a/README.md b/README.md index f8e5312..7487cc3 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ [![GitHub Release Date](https://img.shields.io/github/release-date/kleiner-brauhelfer/kleiner-brauhelfer-app)](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-app/releases/latest/) [![GitHub Downlaods](https://img.shields.io/github/downloads/kleiner-brauhelfer/kleiner-brauhelfer-app/total)](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-app/releases/latest/) -Die kleiner-brauhelfer-app ist eine App, welche die Software [kleiner-brauhelfer-2](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-2) ergänzt. Die App wird nur für Android Geräte kompiliert, sollte aber auch mit anderen Betriebssysteme kompatibel sein. +Die Android App *kleiner-brauhelfer-app* ergänzt das Desktopprogramm [kleiner-brauhelfer-2](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-2). -**Diskussionsthread:** +**Diskussion auf Hobbybrauer.de:** https://hobbybrauer.de/forum/viewtopic.php?f=3&t=17466 -## Download letzte Version +## Download - [Version 2.x.x](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-app/releases/latest) passend zum [kleinen-brauhelfer-2](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-2) - [Version 1.0.0](https://github.com/kleiner-brauhelfer/kleiner-brauhelfer-app/releases/tag/v1.0.0) passend zum [kleinen-brauhelfer bis 1.4.4.6](https://github.com/Gremmel/kleiner-brauhelfer) @@ -22,29 +22,63 @@ Siehe [Changelog](CHANGELOG.md). ![Screenshot 03](doc/Screenshot_03.png) ![Screenshot 04](doc/Screenshot_04.png) ![Screenshot 05](doc/Screenshot_05.png) -![Screenshot 06](doc/Screenshot_06.png) ![Screenshot 07](doc/Screenshot_07.png) ## Setup ### Synchronization mit Dropbox -1. *Dropbox developer area* aufrufen https://www.dropbox.com/developers. -2. Oben rechts auf *App Console* klicken. -3. Auf *Create your app* klicken. -4. *Scoped access* auswählen. -5. *App folder* auswählen. -6. App Name wählen. -7. Auf *Create app* klicken. -8. Oben auf den *Permissions* Reiter wechseln. -9. Folgende Berechtigungen aktivieren: *files.metadata.write*, *files.metadata.read*, *files.content.write* und *files.content.read* -10. Einstellungen mit *Submit* bestätigen. -11. Zurück zum *Settings* Reiter wechseln. -12. Bei *Redirect URIs* *http://127.0.0.1:5476/* eintragen. -13. Die Dropbox Seite aufrufen https://www.dropbox.com und dabei die *Dropbox developer area* offen lassen. -14. Bei den Dateien sollte es jetzt einen Unterordner *App* und darin einen weiteren Unterordner mit dem App Name geben. -15. Datenbankdatei (*kb_daten.sqlite*) dorthin platzieren. -16. *kleiner-brauhelfer-app* starten und zu den Einstellungen wechseln. -17. *App key* und *App secret* aus der *Dropbox developer area* kopieren. -18. Unter *Pfad* den Pfad zur Datenbank inklusive Dateiname eingeben. Befindet sich die Datei direkt im Hauptordner des erstellten Dropboxordner, dann lautet der Pfad */kb_daten.sqlite*. -19. Auf *Zugriff erlauben* klicken und die Berechtigung erlauben. Die App sollte mit *Access granted!* die Zugriffberechtigung bestätigen. -20. Die App sollte sich dann mit Dropbox verbinden. Möglicherweise ist ein Neustart der App erforderlich. -21. Nicht vergessen im Desktopprogramm *kleiner-brauhelfer* die Datenbank vom entsprechenden Dropbox Ordner auszuwählen. +1. *Dropbox developer area* aufrufen (http://www.dropbox.com/developers) +2. Auf *App Console* klicken +3. Auf *Create app* klicken +4. *Scoped access* auswählen +5. *App folder* auswählen +6. App Name wählen +7. Auf *Create app* klicken +8. Zum *Permissions* Reiter wechseln +9. Folgende Berechtigungen aktivieren: + - *files.metadata.write* + - *files.metadata.read* + - *files.content.write* + - *files.content.read* +10. Einstellungen mit *Submit* bestätigen +11. Zurück zum *Settings* Reiter wechseln +12. Bei *Redirect URIs* "*http://127.0.0.1:5476/*" eintragen +13. Dropbox Seite aufrufen http://www.dropbox.com und dabei die *Dropbox developer area* offen lassen +14. Ein Ordner *Apps* und ein Unterordner mit dem App Name sollten automatisch erstellt worden sein +15. Die Datenbankdatei (*kb_daten.sqlite*) im Unterordner hochladen +16. *kleiner-brauhelfer-app* starten und zu den Einstellungen wechseln +17. *App key* und *App secret* aus der *Dropbox developer area* kopieren +18. Unter *Pfad* "*/kb_daten.sqlite*" eingeben. Eintrag entsprechend anpassen, falls die Datenbank in einem Unterordner platziert oder anders benannt wurde. +19. Auf *Zugriff erlauben* klicken und die Berechtigung erlauben. Die App sollte mit *Zugang gewährt.* die Zugriffberechtigung bestätigen. +20. Die App sollte sich nun mit Dropbox verbinden. Möglicherweise ist ein Neustart der App erforderlich. +21. Achten, dass das Desktopprogramm *kleiner-brauhelfer* auch auf die Datenbank aus dem Dropbox Ordner zugreift + +### Synchronization mit Google Drive +1. *Google Cloud Platform Console* aufrufen (http://console.cloud.google.com) +2. Neues Projekt mit beliebigem Name anlegen und selektieren +3. Linkes Panel öffnen und *APIs & services* selektieren +4. Auf *Enable APIs and Services* (oder *Library*) klicken +5. *Google Drive API* auswählen und auf *Enable* klicken +6. Auf der linken Seite auf *OAuth consent screen* klicken +7. User Type *External* auswählen und auf *Create* klicken +8. Formular ausfüllen (App name, User support email & developer contact information) und auf *Save and continue* klicken +9. *Scopes* und *Test users* leer lassen und mit Klick auf *Save and continue* bestätigen +10. *Summary* mit Klick auf *Back to dashboard* bestätigen +11. *Publishing status* mit Klick auf *Publish App* ändern +12. Auf der linken Seite auf *Credentials* klicken +13. Auf *Create credentials* klicken und *OAuth client ID* wählen +14. Als *Application type* *Web application* wählen +15. Unter *Authorised redirect URIs* "*http://127.0.0.1:5477/*" eintragen +16. Auf *Create* klicken +17. *Client ID* und *Client secret* in der *kleiner-brauhelfer-app* eintragen +18. Auf *Zugriff erlauben* klicken und die Berechtigung erlauben. Die App sollte mit *Zugang gewährt.* die Zugriffberechtigung bestätigen. +19. Die Datenbankdatei (*kb_daten.sqlite*) im Google Drive (https://www.google.com/drive) hochladen +20. Dateiname in der *kleiner-brauhelfer-app* eintragen und auf *Datei ID ermitteln* klicken +21. Kann die richtige ID nicht ermittelt werden kann so vorgegangen werden: + 1. Google Drive aufrufen (https://www.google.com/drive) + 2. Rechtsklick auf die Datenbankdatei und *Get Link* wählen + 3. Link mit *Copy link* kopieren und in einem Texteditor einfügen + 4. Die ID ist der Teil zwischen "*../d/*" und "*/view...*" + Z.B. Link: https://drive.google.com/file/d/1eXmIGOU9Wzo7qtqYTOaUV-TTpEDaL-ON/view?usp=share_link + ID: 1eXmIGOU9Wzo7qtqYTOaUV-TTpEDaL-ON + 5. ID in der *kleiner-brauhelfer-app* eintragen +22. Noch einmal auf *Zugriff erlauben* klicken. Dieses Mal sollte der Zugang gewährt werden und die Datenbank heruntergeladen werden. diff --git a/kleiner-brauhelfer-app/kleiner-brauhelfer-app.pro b/kleiner-brauhelfer-app/kleiner-brauhelfer-app.pro index 7c46fb1..0ff94da 100644 --- a/kleiner-brauhelfer-app/kleiner-brauhelfer-app.pro +++ b/kleiner-brauhelfer-app/kleiner-brauhelfer-app.pro @@ -9,7 +9,7 @@ ORGANIZATION = kleiner-brauhelfer TARGET = kleiner-brauhelfer-app VER_MAJ = 2 VER_MIN = 5 -VER_PAT = 1 +VER_PAT = 2 VERSION = $$sprintf("%1.%2.%3",$$VER_MAJ,$$VER_MIN,$$VER_PAT) DEFINES += ORGANIZATION=\\\"$$ORGANIZATION\\\" TARGET=\\\"$$TARGET\\\" VERSION=\\\"$$VERSION\\\" DEFINES += VER_MAJ=\"$$VER_MAJ\" VER_MIN=\"$$VER_MIN\" VER_PAT=\"$$VER_PAT\" @@ -55,6 +55,7 @@ INCLUDEPATH += src ../kleiner-brauhelfer-core/src HEADERS += \ src/qmlutils.h \ src/syncservice.h \ + src/syncservicegoogle.h \ src/syncservicelocal.h \ src/syncservicemanager.h \ src/syncservicedropbox.h \ @@ -66,6 +67,7 @@ SOURCES += \ src/main.cpp \ src/qmlutils.cpp \ src/syncservice.cpp \ + src/syncservicegoogle.cpp \ src/syncservicelocal.cpp \ src/syncservicemanager.cpp \ src/syncservicedropbox.cpp \ diff --git a/kleiner-brauhelfer-app/qml/main.qml b/kleiner-brauhelfer-app/qml/main.qml index fe78784..c3b4f5a 100644 --- a/kleiner-brauhelfer-app/qml/main.qml +++ b/kleiner-brauhelfer-app/qml/main.qml @@ -66,6 +66,14 @@ ApplicationWindow { messageDialog.open() } } + Connections { + target: SyncService.syncServiceGoogle + function onAccessGranted() { + connect() + messageDialog.text = qsTr("Zugang gewährt.") + messageDialog.open() + } + } // scheduler to do stuff in the background, use run() or runExt() Timer { diff --git a/kleiner-brauhelfer-app/qml/pagesOthers/PageSettings.qml b/kleiner-brauhelfer-app/qml/pagesOthers/PageSettings.qml index c0abc50..82bc904 100644 --- a/kleiner-brauhelfer-app/qml/pagesOthers/PageSettings.qml +++ b/kleiner-brauhelfer-app/qml/pagesOthers/PageSettings.qml @@ -55,6 +55,12 @@ PageBase { app.connect() } break + case SyncService.Google: + if (SyncService.syncServiceGoogle.clientId !== "" && + SyncService.syncServiceGoogle.clientSecret !== "" && + SyncService.syncServiceGoogle.fileId !== "") + app.connect() + break } } @@ -70,7 +76,7 @@ PageBase { ComboBoxBase { Layout.fillWidth: true Layout.preferredHeight: height - model: [qsTr("Lokal"), qsTr("Dropbox"), qsTr("WebDav")] + model: [qsTr("Lokal"), qsTr("Dropbox"), qsTr("WebDav"), qsTr("Google Drive")] currentIndex: SyncService.serviceId onCurrentIndexChanged: { if (activeFocus) { @@ -155,7 +161,7 @@ PageBase { Layout.topMargin: 8 Layout.bottomMargin: 8 font.italic: true - text: qsTr("Benötigt eine Dropbox App.") + text: qsTr("Benötigt eine Dropbox App.") onLinkActivated: (link) => Qt.openUrlExternally(link) } LabelSubheader { @@ -318,6 +324,125 @@ PageBase { } } } + ColumnLayout { + Layout.fillWidth: true + visible: SyncService.serviceId === SyncService.Google + LabelPrim { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 8 + font.italic: true + text: qsTr("Benötigt ein Google Cloud-Projekt.") + onLinkActivated: (link) => Qt.openUrlExternally(link) + } + LabelSubheader { + Layout.fillWidth: true + text: qsTr("Client ID") + } + TextFieldBase { + property bool wasEdited: false + Layout.fillWidth: true + placeholderText: "Client ID" + inputMethodHints: Qt.ImhNoAutoUppercase + text: SyncService.syncServiceGoogle.clientId + selectByMouse: true + onTextChanged: { + if (activeFocus) + wasEdited = true + } + onEditingFinished: { + if (wasEdited) + { + SyncService.syncServiceGoogle.clientId = text + wasEdited = false + } + } + } + LabelSubheader { + Layout.fillWidth: true + text: qsTr("Client secret") + } + TextFieldBase { + property bool wasEdited: false + Layout.fillWidth: true + placeholderText: "Client secret" + inputMethodHints: Qt.ImhNoAutoUppercase + echoMode: TextInput.Password + text: SyncService.syncServiceGoogle.clientSecret + selectByMouse: true + onTextChanged: { + if (activeFocus) + wasEdited = true + } + onEditingFinished: { + if (wasEdited) + { + SyncService.syncServiceGoogle.clientSecret = text + wasEdited = false + } + } + } + ButtonBase { + Layout.fillWidth: true + text: qsTr("Zugriff erlauben") + onClicked: { + Brauhelfer.disconnectDatabase() + SyncService.syncServiceGoogle.grantAccess() + } + } + LabelSubheader { + Layout.fillWidth: true + text: qsTr("Dateiname") + } + TextFieldBase { + property bool wasEdited: false + Layout.fillWidth: true + placeholderText: "kb_daten.sqlite" + text: SyncService.syncServiceGoogle.fileName + selectByMouse: true + onTextChanged: { + if (activeFocus) + wasEdited = true + } + onEditingFinished: { + if (wasEdited) + { + SyncService.syncServiceGoogle.fileName = text + wasEdited = false + } + } + } + LabelSubheader { + Layout.fillWidth: true + text: qsTr("Datei ID") + } + TextFieldBase { + property bool wasEdited: false + Layout.fillWidth: true + placeholderText: "Datei ID" + text: SyncService.syncServiceGoogle.fileId + selectByMouse: true + onTextChanged: { + if (activeFocus) + wasEdited = true + } + onEditingFinished: { + if (wasEdited) + { + SyncService.syncServiceGoogle.fileId = text + wasEdited = false + } + } + } + ButtonBase { + Layout.fillWidth: true + text: qsTr("Datei ID ermitteln") + onClicked: { + Brauhelfer.disconnectDatabase() + SyncService.syncServiceGoogle.retrieveFileId() + } + } + } CheckBoxBase { Layout.fillWidth: true text: qsTr("Schreibgeschützt") diff --git a/kleiner-brauhelfer-app/src/syncservice.cpp b/kleiner-brauhelfer-app/src/syncservice.cpp index 7c3dabe..bead6ba 100644 --- a/kleiner-brauhelfer-app/src/syncservice.cpp +++ b/kleiner-brauhelfer-app/src/syncservice.cpp @@ -2,9 +2,8 @@ #include -SyncService::SyncService(QSettings *settings, const QString &urlServerCheck) : +SyncService::SyncService(QSettings *settings) : _settings(settings), - _urlServerCheck(urlServerCheck), _filePath(""), _state(SyncState::Failed) { diff --git a/kleiner-brauhelfer-app/src/syncservice.h b/kleiner-brauhelfer-app/src/syncservice.h index ee97a04..b24bca3 100644 --- a/kleiner-brauhelfer-app/src/syncservice.h +++ b/kleiner-brauhelfer-app/src/syncservice.h @@ -45,9 +45,8 @@ class SyncService : public QObject /** * @brief Abstract class for synchronization service * @param settings Settings - * @param urlServerCheck URL to check availability if synchronization service */ - SyncService(QSettings *settings, const QString &urlServerCheck = ""); + SyncService(QSettings *settings); virtual ~SyncService(); /** @@ -124,7 +123,6 @@ class SyncService : public QObject static QString cacheFilePath(const QString filePath); QSettings* _settings; - QString _urlServerCheck; private: QString _filePath; diff --git a/kleiner-brauhelfer-app/src/syncservicedropbox.cpp b/kleiner-brauhelfer-app/src/syncservicedropbox.cpp index f4c6d43..774b6bf 100644 --- a/kleiner-brauhelfer-app/src/syncservicedropbox.cpp +++ b/kleiner-brauhelfer-app/src/syncservicedropbox.cpp @@ -9,21 +9,16 @@ #include #include -static const char* DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize"; -static const char* DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token"; -static const char* DROPBOX_API_URL = "https://api.dropboxapi.com"; -static const char* DROPBOX_CONTENT_URL = "https://content.dropboxapi.com"; - SyncServiceDropbox::SyncServiceDropbox(QSettings *settings) : - SyncService(settings, DROPBOX_API_URL), + SyncService(settings), _mightNeedToRefreshToken(false) { setFilePath(cacheFilePath(filePathServer())); _oauth2 = new QOAuth2AuthorizationCodeFlow(this); _netManager = new QNetworkAccessManager(this); - _oauth2->setAuthorizationUrl(QUrl(DROPBOX_AUTH_URL)); - _oauth2->setAccessTokenUrl(QUrl(DROPBOX_TOKEN_URL)); + _oauth2->setAuthorizationUrl(QUrl("https://www.dropbox.com/oauth2/authorize")); + _oauth2->setAccessTokenUrl(QUrl("https://api.dropboxapi.com/oauth2/token")); _oauth2->setClientIdentifier(appKey()); _oauth2->setClientIdentifierSharedKey(appSecret()); _oauth2->setRefreshToken(refreshToken()); @@ -62,18 +57,12 @@ SyncServiceDropbox::~SyncServiceDropbox() delete _netManager; } -bool SyncServiceDropbox::grantAccess() +void SyncServiceDropbox::grantAccess() { if (_oauth2->refreshToken().isEmpty()) - { _oauth2->grant(); - return false; - } else - { _oauth2->refreshAccessToken(); - } - return true; } void SyncServiceDropbox::refreshAccess() @@ -85,11 +74,8 @@ bool SyncServiceDropbox::downloadFile() { bool ret = false; - QUrl url; - url.setUrl(DROPBOX_CONTENT_URL); - url.setPath(QString("/2/files/download")); - QNetworkRequest req; + QUrl url("https://content.dropboxapi.com/2/files/download"); req.setUrl(url); req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); QString json = QString("{\"path\": \"%1\"}").arg((filePathServer().compare("/") == 0) ? "" : filePathServer()); @@ -130,11 +116,8 @@ bool SyncServiceDropbox::uploadFile() QFile srcFile(getFilePath()); if (srcFile.open(QIODevice::ReadOnly)) { - QUrl url; - url.setUrl(DROPBOX_CONTENT_URL); - url.setPath(QString("/2/files/upload")); - QNetworkRequest req; + QUrl url("https://content.dropboxapi.com/2/files/upload"); req.setUrl(url); req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); @@ -163,11 +146,8 @@ bool SyncServiceDropbox::uploadFile() QString SyncServiceDropbox::getServerRevision(QNetworkReply::NetworkError* replyCode) { - QUrl url; - url.setUrl(DROPBOX_API_URL); - url.setPath(QString("/2/files/get_metadata")); - QNetworkRequest req; + QUrl url("https://api.dropboxapi.com/2/files/get_metadata"); req.setUrl(url); req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); diff --git a/kleiner-brauhelfer-app/src/syncservicedropbox.h b/kleiner-brauhelfer-app/src/syncservicedropbox.h index caff31c..227de1d 100644 --- a/kleiner-brauhelfer-app/src/syncservicedropbox.h +++ b/kleiner-brauhelfer-app/src/syncservicedropbox.h @@ -32,7 +32,7 @@ class SyncServiceDropbox : public SyncService * @brief Grant or refresh access * @return */ - Q_INVOKABLE bool grantAccess(); + Q_INVOKABLE void grantAccess(); /** * @brief Refresh the access with the refresh token diff --git a/kleiner-brauhelfer-app/src/syncservicegoogle.cpp b/kleiner-brauhelfer-app/src/syncservicegoogle.cpp new file mode 100644 index 0000000..bb0b3aa --- /dev/null +++ b/kleiner-brauhelfer-app/src/syncservicegoogle.cpp @@ -0,0 +1,469 @@ +#include "syncservicegoogle.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +SyncServiceGoogle::SyncServiceGoogle(QSettings *settings) : + SyncService(settings), + _mightNeedToRefreshToken(false) +{ + setFilePath(cacheFilePath(fileId())); + _oauth2 = new QOAuth2AuthorizationCodeFlow(this); + _netManager = new QNetworkAccessManager(this); + + _oauth2->setAuthorizationUrl(QUrl("https://accounts.google.com/o/oauth2/v2/auth")); + _oauth2->setAccessTokenUrl(QUrl("https://oauth2.googleapis.com/token")); + _oauth2->setScope("https://www.googleapis.com/auth/drive"); + _oauth2->setClientIdentifier(clientId()); + _oauth2->setClientIdentifierSharedKey(clientSecret()); + _oauth2->setRefreshToken(refreshToken()); + _oauth2->setToken(accessToken()); + _oauth2->setReplyHandler(new QOAuthHttpServerReplyHandler(5477, this)); + #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + _oauth2->setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap* parameters) + #else + _oauth2->setModifyParametersFunction([](QAbstractOAuth::Stage stage, QVariantMap* parameters) + #endif + { + QByteArray code = parameters->value("code").toByteArray(); + (*parameters)["code"] = QUrl::fromPercentEncoding(code); + switch (stage) + { + case QAbstractOAuth::Stage::RequestingAuthorization: + parameters->insert("access_type", "offline"); + parameters->insert("prompt", "consent"); + break; + case QAbstractOAuth::Stage::RefreshingAccessToken: + parameters->remove("redirect_uri"); + break; + default: + break; + } + }); + connect(_oauth2, SIGNAL(error(QString,QString,QUrl)), this, SLOT(authError(QString,QString,QUrl))); + connect(_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl); + connect(_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this]() { + setRefreshToken(_oauth2->refreshToken()); + setAccessToken(_oauth2->token()); + if (!_mightNeedToRefreshToken) + emit accessGranted(); + }); +} + +SyncServiceGoogle::~SyncServiceGoogle() +{ + delete _netManager; +} + +void SyncServiceGoogle::grantAccess() +{ + if (_oauth2->refreshToken().isEmpty()) + _oauth2->grant(); + else + _oauth2->refreshAccessToken(); +} + +void SyncServiceGoogle::refreshAccess() +{ + _oauth2->refreshAccessToken(); +} + +bool SyncServiceGoogle::retrieveFileId() +{ + bool ret = false; + + if (fileName().isEmpty()) + return false; + + QNetworkRequest req; + QUrl url(QString("https://www.googleapis.com/drive/v3/files?q=name='%1'&fields=files(id)").arg(fileName())); + req.setUrl(url); + req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); + + QEventLoop loop; + _netReply = _netManager->get(req); + connect(_netReply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + connect(_netReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError))); + connect(_netReply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QNetworkReply::NetworkError code = _netReply->error(); + if (code == QNetworkReply::NoError) + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(_netReply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) + { + QJsonObject json = jsonDoc.object(); + QJsonArray files = json.value("files").toArray(); + if (files.count() == 0) + { + setFileId(""); + emit message(QtMsgType::QtWarningMsg, "File not found."); + } + else if (files.count() == 1) + { + setFileId(files[0].toObject().value("id").toString()); + ret = true; + } + else + { + setFileId(files[0].toObject().value("id").toString()); + emit message(QtMsgType::QtWarningMsg, "Multiple files not found. Verify ID manually."); + ret = true; + } + } + else + { + emit message(QtMsgType::QtCriticalMsg, jsonError.errorString()); + } + } + + return ret; +} + +bool SyncServiceGoogle::downloadFile() +{ + bool ret = false; + + QNetworkRequest req; + QUrl url(QString("https://www.googleapis.com/drive/v3/files/%1?alt=media").arg(fileId())); + req.setUrl(url); + req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); + + QEventLoop loop; + _netReply = _netManager->get(req); + connect(_netReply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + connect(_netReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError))); + connect(_netReply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QNetworkReply::NetworkError code = _netReply->error(); + if (code == QNetworkReply::NoError) + { + QFile dstFile(getFilePath()); + QFileInfo finfo(dstFile); + QDir dir(finfo.absolutePath()); + if (!dir.exists()) + { + dir.mkpath("."); + } + if (dstFile.open(QIODevice::WriteOnly)) + { + if (dstFile.write(_netReply->readAll()) != -1) + ret = true; + dstFile.close(); + } + } + + return ret; +} + +bool SyncServiceGoogle::uploadFile() +{ + bool ret = false; + + QFile srcFile(getFilePath()); + if (srcFile.open(QIODevice::ReadOnly)) + { + QNetworkRequest req; + QUrl url(QString("https://www.googleapis.com/upload/drive/v3/files/%1?uploadType=media").arg(fileId())); + req.setUrl(url); + req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); + + QEventLoop loop; + _netReply =_netManager->sendCustomRequest(req, "PATCH", srcFile.readAll()); + connect(_netReply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + connect(_netReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError))); + connect(_netReply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QNetworkReply::NetworkError code = _netReply->error(); + if (code == QNetworkReply::NoError) + { + ret = true; + } + + srcFile.close(); + } + + return ret; +} + +QString SyncServiceGoogle::getServerRevision(QNetworkReply::NetworkError* replyCode) +{ + QNetworkRequest req; + QUrl url(QString("https://www.googleapis.com/drive/v3/files/%1?fields=headRevisionId").arg(fileId())); + req.setUrl(url); + req.setRawHeader("Authorization", QString("Bearer %1").arg(accessToken()).toUtf8()); + + QEventLoop loop; + _netReply = _netManager->get(req); + connect(_netReply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + connect(_netReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError))); + connect(_netReply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QNetworkReply::NetworkError code = _netReply->error(); + if (replyCode) + *replyCode = code; + if (code == QNetworkReply::NoError) + { + QJsonParseError jsonError; + QJsonDocument json = QJsonDocument::fromJson(_netReply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) + { + QJsonObject jsonData = json.object(); + return jsonData.value("headRevisionId").toString(); + } + else + { + emit message(QtMsgType::QtCriticalMsg, jsonError.errorString()); + } + } + + return QString(); +} + +void SyncServiceGoogle::networkError(QNetworkReply::NetworkError error) +{ + if (_mightNeedToRefreshToken && error == QNetworkReply::AuthenticationRequiredError) + return; + + QString msg = _netReply->errorString(); + QJsonParseError jsonError; + QJsonDocument json = QJsonDocument::fromJson(_netReply->readAll(), &jsonError); + if(jsonError.error == QJsonParseError::NoError) + { + QJsonObject jsonData = json.object(); + QString error_summary = jsonData.value("error_summary").toString(); + if (!error_summary.isEmpty()) + msg += "\n" + error_summary; + } + emit message(QtMsgType::QtCriticalMsg, msg); +} + +void SyncServiceGoogle::authError(const QString &error, const QString &errorDescription, const QUrl &uri) +{ + Q_UNUSED(uri) + emit message(QtMsgType::QtCriticalMsg, error + "\n" + errorDescription); +} + +void SyncServiceGoogle::sslErrors(const QList &errors) +{ + if (errors.count() > 0) + emit message(QtMsgType::QtWarningMsg, errors[0].errorString()); + else + emit message(QtMsgType::QtWarningMsg, "SSL error."); + _netReply->ignoreSslErrors(); +} + +QString SyncServiceGoogle::getLocalRevision() const +{ + return _settings->value("SyncService/google/revisions/" + fileId(), "").toString(); +} + +void SyncServiceGoogle::setLocalRevision(const QString &revision) +{ + _settings->setValue("SyncService/google/revisions/" + fileId(), revision); +} + +bool SyncServiceGoogle::synchronize(SyncDirection direction) +{ + if (fileId().isEmpty()) + { + setState(SyncState::Failed); + return false; + } + + if (refreshToken().isEmpty() || accessToken().isEmpty()) { + setState(SyncState::Failed); + emit message(QtMsgType::QtCriticalMsg, "Grant access first."); + return false; + } + + QNetworkReply::NetworkError replyCode; + _mightNeedToRefreshToken = true; + QString revision = getServerRevision(&replyCode); + if (replyCode == QNetworkReply::AuthenticationRequiredError) + { + QEventLoop loop; + _oauth2->refreshAccessToken(); + connect(_oauth2, SIGNAL(granted()), &loop, SLOT(quit())); + connect(_oauth2, SIGNAL(error(QString,QString,QUrl)), &loop, SLOT(quit())); + loop.exec(); + revision = getServerRevision(); + } + _mightNeedToRefreshToken = false; + if (revision.isEmpty()) + { + setState(SyncState::Failed); + return false; + } + + if (QFile::exists(getFilePath())) + { + if (revision == getLocalRevision()) + { + if (direction == SyncDirection::Download) + { + setState(SyncState::UpToDate); + return true; + } + else + { + if (uploadFile()) + { + setLocalRevision(getServerRevision()); + setState( SyncState::Updated); + return true; + } + else + { + setState(SyncState::Failed); + return false; + } + } + } + else + { + if (direction == SyncDirection::Download) + { + if (downloadFile()) + { + setLocalRevision(revision); + setState(SyncState::Updated); + return true; + } + else + { + setState(SyncState::Failed); + return false; + } + } + else + { + setState(SyncState::OutOfSync); + return false; + } + } + } + else + { + if (direction == SyncDirection::Download) + { + if (downloadFile()) + { + setLocalRevision(getServerRevision()); + setState(SyncState::Updated); + return true; + } + else + { + setState(SyncState::Failed); + return false; + } + } + else + { + setState(SyncState::NotFound); + return false; + } + } +} + +QString SyncServiceGoogle::clientId() const +{ + return _settings->value("SyncService/google/ClientId").toString(); +} + +void SyncServiceGoogle::setClientId(const QString &id) +{ + if (clientId() != id) + { + _settings->setValue("SyncService/google/ClientId", id); + _oauth2->setClientIdentifier(id); + clearCache(); + emit clientIdChanged(id); + } +} + +QString SyncServiceGoogle::clientSecret() const +{ + return _settings->value("SyncService/google/ClientSecret").toString(); +} + +void SyncServiceGoogle::setClientSecret(const QString &secret) +{ + if (clientSecret() != secret) + { + _settings->setValue("SyncService/google/ClientSecret", secret); + _oauth2->setClientIdentifierSharedKey(secret); + clearCache(); + emit clientSecretChanged(secret); + } +} + +QString SyncServiceGoogle::fileId() const +{ + return _settings->value("SyncService/google/fileId").toString(); +} + +void SyncServiceGoogle::setFileId(const QString &id) +{ + if (fileId() != id) + { + _settings->setValue("SyncService/google/fileId", id); + setFilePath(cacheFilePath(id)); + emit fileIdChanged(id); + } +} + +QString SyncServiceGoogle::fileName() const +{ + return _settings->value("SyncService/google/fileName").toString(); +} + +void SyncServiceGoogle::setFileName(const QString &name) +{ + if (fileName() != name) + { + _settings->setValue("SyncService/google/fileName", name); + emit fileNameChanged(name); + } +} + +QString SyncServiceGoogle::refreshToken() const +{ + return _settings->value("SyncService/google/RefreshToken").toString(); +} + +void SyncServiceGoogle::setRefreshToken(const QString &token) +{ + _settings->setValue("SyncService/google/RefreshToken", token); +} + +QString SyncServiceGoogle::accessToken() const +{ + return _settings->value("SyncService/google/AccessToken").toString(); +} + +void SyncServiceGoogle::setAccessToken(const QString &token) +{ + _settings->setValue("SyncService/google/AccessToken", token); +} + +void SyncServiceGoogle::clearCachedSettings() +{ + _settings->remove("SyncService/google/RefreshToken"); + _settings->remove("SyncService/google/AccessToken"); + _oauth2->setRefreshToken(""); + _oauth2->setToken(""); +} diff --git a/kleiner-brauhelfer-app/src/syncservicegoogle.h b/kleiner-brauhelfer-app/src/syncservicegoogle.h new file mode 100644 index 0000000..cc79d8d --- /dev/null +++ b/kleiner-brauhelfer-app/src/syncservicegoogle.h @@ -0,0 +1,100 @@ +#ifndef SYNCSERVICEGOOGLE_H +#define SYNCSERVICEGOOGLE_H + +#include "syncservice.h" +#include +#include +#include +#include +#include + +/** + * @brief Dropbox synchronization service + */ +class SyncServiceGoogle : public SyncService +{ + Q_OBJECT + + Q_PROPERTY(QString clientId READ clientId WRITE setClientId NOTIFY clientIdChanged) + Q_PROPERTY(QString clientSecret READ clientSecret WRITE setClientSecret NOTIFY clientSecretChanged) + Q_PROPERTY(QString fileId READ fileId WRITE setFileId NOTIFY fileIdChanged) + Q_PROPERTY(QString fileName READ fileName WRITE setFileName NOTIFY fileNameChanged) + +public: + + /** + * @brief Dropbox synchronization service + * @param settings Settings + */ + SyncServiceGoogle(QSettings *settings); + ~SyncServiceGoogle(); + + /** + * @brief Grant or refresh access + * @return + */ + Q_INVOKABLE void grantAccess(); + + /** + * @brief Refresh the access with the refresh token + */ + Q_INVOKABLE void refreshAccess(); + + /** + * @brief Grant or refresh access + * @return + */ + Q_INVOKABLE bool retrieveFileId(); + + /** + * @brief Synchronizes the file + * @param direction Direction to synchronize + * @note See getState() for details about the synchronization state + * @return True on success + */ + bool synchronize(SyncDirection direction) Q_DECL_OVERRIDE; + + QString clientId() const; + void setClientId(const QString &id); + + QString clientSecret() const; + void setClientSecret(const QString &secret); + + QString fileId() const; + void setFileId(const QString &id); + + QString fileName() const; + void setFileName(const QString &name); + +signals: + void accessGranted(); + void clientIdChanged(const QString &id); + void clientSecretChanged(const QString &secret); + void fileIdChanged(const QString &id); + void fileNameChanged(const QString &name); + +private slots: + void networkError(QNetworkReply::NetworkError error); + void authError(const QString &error, const QString &errorDescription, const QUrl &uri); + void sslErrors(const QList& errors); + +private: + bool downloadFile(); + bool uploadFile(); + QString getServerRevision(QNetworkReply::NetworkError* replyCode = nullptr); + QString getLocalRevision() const; + void setLocalRevision(const QString &revision); + QString refreshToken() const; + void setRefreshToken(const QString &token); + QString accessToken() const; + void setAccessToken(const QString &token); + void clearCachedSettings() Q_DECL_OVERRIDE; + +private: + bool _mightNeedToRefreshToken; + QOAuth2AuthorizationCodeFlow *_oauth2; + QNetworkAccessManager* _netManager; + QNetworkReply* _netReply; +}; + +#endif // SYNCSERVICEGOOGLE_H diff --git a/kleiner-brauhelfer-app/src/syncservicemanager.cpp b/kleiner-brauhelfer-app/src/syncservicemanager.cpp index 47af95a..4211437 100644 --- a/kleiner-brauhelfer-app/src/syncservicemanager.cpp +++ b/kleiner-brauhelfer-app/src/syncservicemanager.cpp @@ -2,6 +2,7 @@ #include "syncservicelocal.h" #include "syncservicedropbox.h" +#include "syncservicegoogle.h" #include "syncservicewebdav.h" bool SyncServiceManager::supportsSsl() @@ -32,6 +33,9 @@ SyncServiceManager::SyncServiceManager(QSettings *settings, QObject *parent) : mSyncServiceWebDav = new SyncServiceWebDav(mSettings); connect(mSyncServiceWebDav, SIGNAL(message(int,QString)), this, SIGNAL(message(int,QString))); mServices.append(mSyncServiceWebDav); + mSyncServiceGoogle = new SyncServiceGoogle(mSettings); + connect(mSyncServiceGoogle, SIGNAL(message(int,QString)), this, SIGNAL(message(int,QString))); + mServices.append(mSyncServiceGoogle); setServiceId((SyncServiceId)mSettings->value("SyncService/Id", 0).toInt()); } diff --git a/kleiner-brauhelfer-app/src/syncservicemanager.h b/kleiner-brauhelfer-app/src/syncservicemanager.h index c96b857..eeb9551 100644 --- a/kleiner-brauhelfer-app/src/syncservicemanager.h +++ b/kleiner-brauhelfer-app/src/syncservicemanager.h @@ -17,6 +17,7 @@ class SyncServiceManager : public QObject Q_PROPERTY(SyncService::SyncState syncState READ syncState NOTIFY syncStateChanged) Q_PROPERTY(SyncService* syncServiceLocal MEMBER mSyncServiceLocal CONSTANT) Q_PROPERTY(SyncService* syncServiceDropbox MEMBER mSyncServiceDropbox CONSTANT) + Q_PROPERTY(SyncService* syncServiceGoogle MEMBER mSyncServiceGoogle CONSTANT) Q_PROPERTY(SyncService* syncServiceWebDav MEMBER mSyncServiceWebDav CONSTANT) public: @@ -46,7 +47,8 @@ class SyncServiceManager : public QObject { Local, Dropbox, - WebDav + WebDav, + Google }; Q_ENUM(SyncServiceId) @@ -151,6 +153,7 @@ class SyncServiceManager : public QObject QList mServices; SyncService* mSyncServiceLocal; SyncService* mSyncServiceDropbox; + SyncService* mSyncServiceGoogle; SyncService* mSyncServiceWebDav; };