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;
};