Merge lp:~kalikiana/u1db-qt/syncWithU1 into lp:u1db-qt

Proposed by Cris Dywan
Status: Work in progress
Proposed branch: lp:~kalikiana/u1db-qt/syncWithU1
Merge into: lp:u1db-qt
Diff against target: 1664 lines (+934/-295)
13 files modified
debian/control (+2/-0)
examples/notes-cloud/notes-cloud.qml (+267/-148)
modules/U1db/plugin.cpp (+2/-0)
src/CMakeLists.txt (+5/-1)
src/database.cpp (+10/-0)
src/database.h (+1/-1)
src/request.cpp (+87/-0)
src/request.h (+77/-0)
src/server.cpp (+94/-0)
src/server.h (+67/-0)
src/synchronizer.cpp (+204/-137)
src/synchronizer.h (+21/-8)
tests/tst_sync.qml (+97/-0)
To merge this branch: bzr merge lp:~kalikiana/u1db-qt/syncWithU1
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
U1DB Qt developers Pending
Review via email: mp+202508@code.launchpad.net

Commit message

Make sync with U1 and authentication possible

Description of the change

This branch incorporates:
- Improvemed error handling. HTTP status codes, U1Db JSON errors and JSON validation all end up in sync_output as errors now.
- Rewritten example including both test and live server, editable notes and credentials test UI.
- New target syntax: "uri" is sufficient to define local and remote targets with IP or hostname and optional port. Notably it's no longer required to know the IP.
- Code unification between Database and Synchronizer for getting the replica_id.

Progress: 100%
- Caveat: New data isn't pushed to the remote, this is bug 1273688.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
lp:~kalikiana/u1db-qt/syncWithU1 updated
119. By Cris Dywan

Merge lp:u1db-qt

120. By Cris Dywan

Fix synchronize property/ signal/ methods

121. By Cris Dywan

sync_output should be called syncOutput

122. By Cris Dywan

Reset synchronize after success/ error or if nothing happened

123. By Cris Dywan

Add message to syncOutput if no targets were specified

124. By Cris Dywan

Reset synchronize if URI is invalid

125. By Cris Dywan

Add unit tests for Synchronizer target syntax

126. By Cris Dywan

Introduce Server based on Request for sync unit tests

127. By Cris Dywan

Implement 5 minute timeout in Request

Unmerged revisions

127. By Cris Dywan

Implement 5 minute timeout in Request

126. By Cris Dywan

Introduce Server based on Request for sync unit tests

125. By Cris Dywan

Add unit tests for Synchronizer target syntax

124. By Cris Dywan

Reset synchronize if URI is invalid

123. By Cris Dywan

Add message to syncOutput if no targets were specified

122. By Cris Dywan

Reset synchronize after success/ error or if nothing happened

121. By Cris Dywan

sync_output should be called syncOutput

120. By Cris Dywan

Fix synchronize property/ signal/ methods

119. By Cris Dywan

Merge lp:u1db-qt

118. By Cris Dywan

Implement networkRequest signal to handle credentials correctly

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2014-01-22 13:14:22 +0000
3+++ debian/control 2014-02-20 14:57:43 +0000
4@@ -65,6 +65,8 @@
5 Section: doc
6 Architecture: all
7 Depends: libu1db-qt5-3 (>= ${source:Version}),
8+ qtdeclarative5-ubuntuone1.0,
9+ qtdeclarative5-ubuntu-ui-toolkit-plugin,
10 ${misc:Depends},
11 Description: Qt5 binding and QtQuick2 plugin for U1DB - examples
12 Simple Qt5 binding and QtQuick2 plugin for U1DB (https://launchpad.net/u1db).
13
14=== renamed file 'examples/u1db-qt-example-6/u1db-qt-example-6.qdoc' => 'documentation/u1db-qt-example-6.qdoc'
15=== renamed directory 'examples/u1db-qt-example-6' => 'examples/notes-cloud'
16=== renamed file 'examples/u1db-qt-example-6/u1db-qt-example-6.qml' => 'examples/notes-cloud/notes-cloud.qml'
17--- examples/u1db-qt-example-6/u1db-qt-example-6.qml 2013-08-09 10:42:18 +0000
18+++ examples/notes-cloud/notes-cloud.qml 2014-02-20 14:57:43 +0000
19@@ -1,8 +1,8 @@
20 /*
21- * Copyright (C) 2013 Canonical, Ltd.
22+ * Copyright (C) 2014 Canonical, Ltd.
23 *
24 * Authors:
25- * Kevin Wright <kevin.wright@canonical.com>
26+ * Christian Dywan <christian.dywan@canonical.com>
27 *
28 * This program is free software; you can redistribute it and/or modify
29 * it under the terms of the GNU Lesser General Public License as published by
30@@ -20,154 +20,273 @@
31 import QtQuick 2.0
32 import U1db 1.0 as U1db
33 import Ubuntu.Components 0.1
34-
35-
36-Item {
37-
38- width: units.gu(45)
39- height: units.gu(80)
40-
41- U1db.Database {
42- id: aDatabase
43- path: "aDatabase6"
44- }
45-
46- U1db.Document {
47- id: aDocument1
48- database: aDatabase
49- docId: 'helloworld'
50- create: true
51- contents:{"hello": { "world": [ { "message": "Hello World" } ] } }
52-
53- }
54-
55- U1db.Index{
56- database: aDatabase
57- id: by_helloworld
58- expression: ["hello.world.message"]
59- }
60-
61- U1db.Query{
62- id: aQuery
63- index: by_helloworld
64- query: [{"message":"Hel*"}]
65- }
66-
67- U1db.Synchronizer{
68- id: aSynchronizer
69- source: aDatabase
70- targets: [{remote:true},
71- {remote:true,
72- ip:"127.0.0.1",
73- port: 7777,
74- name:"example1.u1db",
75- resolve_to_source:true},
76- {remote:"OK"}]
77- synchronize: false
78- }
79-
80- MainView {
81-
82- id: u1dbView
83- width: units.gu(45)
84- height: units.gu(80)
85- anchors.top: parent.top;
86-
87- Tabs {
88- id: tabs
89+import UbuntuOne 1.0
90+import Ubuntu.Components.Popups 0.1
91+
92+MainView {
93+ applicationName: "com.ubuntu.developer.foobar.notes-cloud"
94+
95+ width: units.gu(50)
96+ height: units.gu(75)
97+
98+ /*
99+ Notes database for both local and remote data
100+ */
101+ U1db.Database {
102+ id: db
103+ path: inMemory.checked ? ":memory:" : filename
104+ property string filename: "notes.db"
105+ }
106+ U1db.Document {
107+ id: doc
108+ database: db
109+ docId: 'text'
110+ create: true
111+ defaults: { "notes": "Lorem ipsum" }
112+ }
113+
114+ /*
115+ Use this to reset switches and indicator to avoid "broken" appearance
116+ when error would be too fast to see anything happen at all
117+ */
118+ Timer {
119+ id: smoothReset
120+ interval: 900
121+
122+ onRunningChanged: {
123+ testSyncActive.enabled = !liveSyncActive.checked
124+ liveSyncActive.enabled = !testSyncActive.checked
125+ syncIndicator.running = testSync.synchronize || liveSync.synchronize || running
126+ }
127+
128+ onTriggered: {
129+ running = false
130+ testSyncActive.checked = testSync.synchronize
131+ liveSyncActive.checked = liveSync.synchronize
132+ }
133+ }
134+
135+ function fillLabel(server, syncOutput) {
136+ var prettyOutput = '%1'.arg(server)
137+ for (var i in syncOutput)
138+ prettyOutput += '\n%1'.arg(syncOutput[i].message_value)
139+ syncLabel.text = prettyOutput
140+ }
141+
142+ /*
143+ U1 Sync
144+ */
145+ U1db.Synchronizer {
146+ id: testSync
147+ source: db
148+ targets: [ {
149+ uri: "http://127.0.0.1:7777/" + db.filename,
150+ resolve_to_source: true,
151+ // Legacy syntax
152+ ip: "127.0.0.1",
153+ port: 7777,
154+ name: db.filename,
155+ remote: true,
156+ }, ]
157+ synchronize: false
158+ onSynchronizeChanged: smoothReset.start()
159+ onSyncOutputChanged: fillLabel(targets[0].uri, syncOutput)
160+ }
161+
162+ U1db.Server {
163+ port: 7777
164+ onNetworkRequest: writeContents('HTTP/1.0 404 Not Found\r\nContent-Type: text/html\r\nContent-Length: 8\r\nNot found\r\n')
165+ }
166+
167+
168+ U1db.Synchronizer {
169+ id: liveSync
170+ source: db
171+ targets: [ {
172+ uri: "https://u1db.one.ubuntu.com/~/notes-cloud-" + db.path,
173+ resolve_to_source: true,
174+ }, ]
175+ synchronize: false
176+ onNetworkRequest: {
177+ request.handled = true
178+ u1credservice.onUrlSigned.connect(onUrlSigned.bind(request, request))
179+ u1credservice.onUrlSigningError.connect(onUrlSigningError.bind(request, request))
180+ u1credservice.signUrl(request.uri, request.method)
181+ }
182+ function onUrlSigned(request, signedUrl) {
183+ u1credservice.onUrlSigned.disconnect(onUrlSigned.bind(request, request))
184+ u1credservice.onUrlSigningError.disconnect(onUrlSigningError.bind(request, request))
185+ request.authorization = signedUrl
186+ request.send()
187+ }
188+ function onUrlSigningError(request, errorMessage) {
189+ u1credservice.onUrlSigned.disconnect(onUrlSigned.bind(request, request))
190+ u1credservice.onUrlSigningError.disconnect(onUrlSigningError.bind(request, request))
191+ request.cancel()
192+ }
193+ onSynchronizeChanged: smoothReset.start()
194+ onSyncOutputChanged: fillLabel(targets[0].uri, syncOutput)
195+ }
196+
197+ UbuntuOneCredentialsService {
198+ id: u1credservice
199+ onCredentialsFound: {
200+ statusLabel.text = i18n.tr('Credentials found')
201+ }
202+ onCredentialsNotFound: {
203+ statusLabel.text = i18n.tr('No account credentials found')
204+ }
205+ onCredentialsDeleted: {
206+ statusLabel.text = i18n.tr('Clear credentials successfully')
207+ }
208+ onUrlSigned: {
209+ statusLabel.text = i18n.tr('Signed URL: %1').arg(signedUrl)
210+ }
211+ onUrlSigningError: {
212+ statusLabel.text = i18n.tr('Error signing: %1').arg(errorMessage)
213+ }
214+
215+ onLoginOrRegisterSuccess: {
216+ statusLabel.text = i18n.tr('Login successful')
217+ }
218+ onLoginOrRegisterError: {
219+ statusLabel.text = i18n.tr('Login failed')
220+ }
221+ onTwoFactorAuthRequired: {
222+ statusLabel.text = i18n.tr('Two Factor Auth required')
223+ }
224+ }
225+
226+ /*
227+ UI: Login credentials
228+ */
229+
230+ Component {
231+ id: loginPopCom
232+ Popover {
233+ id: loginPop
234+ Column {
235+ anchors.centerIn: parent
236+ spacing: units.gu(1)
237+ Label {
238+ text: i18n.tr('Please fill in your credentails')
239+ }
240+ TextField {
241+ id: email
242+ placeholderText: i18n.tr('email')
243+ }
244+ TextField {
245+ id: pass
246+ placeholderText: i18n.tr('password')
247+ }
248+ TextField {
249+ id: two
250+ inputMethodHints: Qt.ImhHiddenText
251+ placeholderText: i18n.tr('two-factor code')
252+ }
253+ Button {
254+ text: i18n.tr('Confirm')
255+ onClicked: u1credservice.login(email.text, pass.text, two.text)
256+ }
257+ }
258+ }
259+ }
260+
261+ /*
262+ UI: Creds/ sync results, notes input
263+ */
264+ Page {
265+
266+ title: i18n.tr("Notes in the Cloud")
267+
268+ Item {
269+ anchors.margins: units.gu(1)
270 anchors.fill: parent
271
272- Tab {
273- objectName: "Tab1"
274-
275- title: i18n.tr("Hello U1Db!")
276-
277- page: Page {
278- id: helloPage
279-
280- Rectangle {
281- width: units.gu(45)
282- height: units.gu(40)
283- anchors.top: parent.top;
284- border.width: 1
285-
286- Text {
287- id: sourceLabel
288- anchors.top: parent.top;
289- font.bold: true
290- text: "aDatabase6 Contents"
291- }
292-
293- ListView {
294- id: sourceListView
295- width: units.gu(45)
296- height: units.gu(35)
297- anchors.top: sourceLabel.bottom;
298- model: aQuery
299-
300- delegate: Text {
301- wrapMode: Text.WordWrap
302- x: 6; y: 77
303- text: {
304- text: "(" + index + ") '" + contents.message + "'"
305- }
306- }
307- }
308-
309-
310- }
311-
312- Rectangle {
313- id: lowerRectangle
314- width: units.gu(45)
315- height: units.gu(35)
316- anchors.bottom: parent.bottom;
317- border.width: 1
318-
319- Text {
320- id: errorsLabel
321- anchors.top: parent.top;
322- font.bold: true
323- text: "Log:"
324- }
325-
326- ListView {
327-
328- parent: lowerRectangle
329- width: units.gu(45)
330- height: units.gu(30)
331- anchors.top: errorsLabel.bottom;
332- model: aSynchronizer
333- delegate:Text {
334- width: units.gu(40)
335- anchors.left: parent.left
336- anchors.right: parent.right
337- wrapMode: Text.WordWrap
338- text: {
339- text: sync_output
340- }
341- }
342- }
343-
344- Button{
345- parent: lowerRectangle
346- anchors.bottom: parent.bottom;
347- text: "Sync"
348- onClicked: aSynchronizer.synchronize = true
349- anchors.left: parent.left
350- anchors.right: parent.right
351-
352- }
353-
354- }
355- }
356-
357- }
358-
359+ Column {
360+ id: col
361+ spacing: units.gu(1)
362+ anchors.top: parent.top
363+ anchors.left: parent.left
364+ anchors.right: parent.right
365+
366+ TextArea {
367+ id: statusLabel
368+ text: i18n.tr('N/A')
369+ readOnly: true
370+ autoSize: true
371+ anchors.left: parent.left
372+ anchors.right: parent.right
373+ }
374+
375+ Row {
376+ spacing: units.gu(1)
377+ Button {
378+ text: i18n.tr('Verify creds')
379+ onClicked: u1credservice.checkCredentials()
380+ }
381+ Button {
382+ text: i18n.tr('Clear creds')
383+ onClicked: {
384+ statusLabel.text = i18n.tr('N/A')
385+ u1credservice.invalidateCredentials()
386+ }
387+ }
388+ Button {
389+ id: loginButton
390+ text: i18n.tr('Login')
391+ onClicked: PopupUtils.open(loginPopCom, loginButton)
392+ }
393+ ActivityIndicator {
394+ id: syncIndicator
395+ }
396+ }
397+
398+ TextArea {
399+ id: syncLabel
400+ text: i18n.tr('No sync target enabled')
401+ readOnly: true
402+ autoSize: true
403+ anchors.left: parent.left
404+ anchors.right: parent.right
405+ }
406+
407+ Row {
408+ spacing: units.gu(1)
409+ Label {
410+ text: i18n.tr('Test')
411+ }
412+
413+ Switch {
414+ id: testSyncActive
415+ onClicked: testSync.synchronize = true
416+ }
417+ Label {
418+ text: i18n.tr('U1')
419+ }
420+ Switch {
421+ id: liveSyncActive
422+ onClicked: liveSync.synchronize = true
423+ }
424+ Label {
425+ text: i18n.tr('Memory')
426+ }
427+ Switch {
428+ id: inMemory
429+ }
430+ }
431+
432+ }
433+
434+ TextArea {
435+ text: doc.contents.notes
436+ onFocusChanged: doc.contents = { "notes": text }
437+ anchors.margins: units.gu(1)
438+ anchors.top: col.bottom
439+ anchors.bottom: parent.bottom
440+ anchors.left: parent.left
441+ anchors.right: parent.right
442+ }
443 }
444-
445 }
446-
447 }
448-
449-
450
451=== modified file 'modules/U1db/plugin.cpp'
452--- modules/U1db/plugin.cpp 2013-05-02 19:52:10 +0000
453+++ modules/U1db/plugin.cpp 2014-02-20 14:57:43 +0000
454@@ -22,6 +22,7 @@
455 #include "index.h"
456 #include "query.h"
457 #include "synchronizer.h"
458+#include "server.h"
459 #include "plugin.h"
460 #include <qqml.h>
461
462@@ -34,5 +35,6 @@
463 qmlRegisterType<Index>(uri, 1, 0, "Index");
464 qmlRegisterType<Query>(uri, 1, 0, "Query");
465 qmlRegisterType<Synchronizer>(uri, 1, 0, "Synchronizer");
466+ qmlRegisterType<Server>(uri, 1, 0, "Server");
467 }
468
469
470=== modified file 'src/CMakeLists.txt'
471--- src/CMakeLists.txt 2013-08-08 10:11:39 +0000
472+++ src/CMakeLists.txt 2014-02-20 14:57:43 +0000
473@@ -7,6 +7,8 @@
474 index.cpp
475 query.cpp
476 synchronizer.cpp
477+ request.cpp
478+ server.cpp
479 )
480
481 # Generated files
482@@ -16,6 +18,8 @@
483 moc_index.cpp
484 moc_query.cpp
485 moc_synchronizer.cpp
486+ moc_request.cpp
487+ moc_server.cpp
488 )
489 set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${U1DB_QT_GENERATED}")
490
491@@ -54,7 +58,7 @@
492 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
493 )
494
495-install(FILES global.h database.h document.h index.h query.h synchronizer.h
496+install(FILES global.h database.h document.h index.h query.h synchronizer.h request.h
497 DESTINATION ${INCLUDE_INSTALL_DIR}
498 )
499
500
501=== modified file 'src/database.cpp'
502--- src/database.cpp 2014-02-17 16:52:42 +0000
503+++ src/database.cpp 2014-02-20 14:57:43 +0000
504@@ -156,6 +156,16 @@
505 return setError(QString("Failed to read internal schema: FileError %1").arg(file.error()));
506 }
507 }
508+
509+ // index_storage is read by the python sqlite implementation
510+ // it wasn't set in in earlier u1db-qt versions so always check it
511+ QSqlQuery query(m_db.exec(
512+ "SELECT value FROM u1db_config WHERE name = 'index_storage'"));
513+ if (!query.next())
514+ query.prepare("INSERT INTO u1db_config VALUES ('index_storage', 'expand referenced')");
515+ if (!query.exec())
516+ return setError(QString("Failed to set index storage: %1\n%2").arg(m_db.lastError().text()).arg(query.lastQuery()));
517+
518 return true;
519 }
520
521
522=== modified file 'src/database.h'
523--- src/database.h 2014-01-24 11:13:35 +0000
524+++ src/database.h 2014-02-20 14:57:43 +0000
525@@ -63,6 +63,7 @@
526 void updateSyncLog(bool insert, QString uid, QString generation, QString transaction_id);
527 QList<QString> listTransactionsSince(int generation);
528 QMap<QString,QVariant> getSyncLogInfo(QMap<QString,QVariant> lastSyncInformation, QString uid, QString prefix);
529+ QString getReplicaUid();
530
531 Q_SIGNALS:
532 void pathChanged(const QString& path);
533@@ -81,7 +82,6 @@
534 QSqlDatabase m_db;
535 QString m_error;
536
537- QString getReplicaUid();
538 bool isInitialized();
539 bool initializeIfNeeded(const QString& path=":memory:");
540 bool setError(const QString& error);
541
542=== added file 'src/request.cpp'
543--- src/request.cpp 1970-01-01 00:00:00 +0000
544+++ src/request.cpp 2014-02-20 14:57:43 +0000
545@@ -0,0 +1,87 @@
546+/*
547+ * Copyright (C) 2014 Canonical, Ltd.
548+ *
549+ * Authors:
550+ * Christian Dywan <christian.dywan@canonical.com>
551+ *
552+ * This program is free software; you can redistribute it and/or modify
553+ * it under the terms of the GNU Lesser General Public License as published by
554+ * the Free Software Foundation; version 3.
555+ *
556+ * This program is distributed in the hope that it will be useful,
557+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
558+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
559+ * GNU Lesser General Public License for more details.
560+ *
561+ * You should have received a copy of the GNU Lesser General Public License
562+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
563+ */
564+
565+#include <QDebug>
566+
567+#include "request.h"
568+#include "private.h"
569+
570+QT_BEGIN_NAMESPACE_U1DB
571+
572+Request::Request(const QString& uri, const QString& method, QObject *parent) :
573+ QObject(parent), m_uri(uri), m_method(method), m_handled(false)
574+{
575+ m_request = QNetworkRequest (QUrl (uri));
576+
577+ /* QNetworkManager doesn't time out in all cases */
578+ m_timer.start(5 * 60000); // 5 * 60 seconds
579+ connect(&m_timer, &QTimer::timeout, this, &Request::timeout);
580+}
581+
582+void Request::timeout()
583+{
584+ disconnect(&m_timer, &QTimer::timeout, this, &Request::timeout);
585+ cancel("Timeout after 5 minutes");
586+}
587+
588+QString Request::getUri()
589+{
590+ return m_uri;
591+}
592+
593+void Request::setUri(const QString& uri)
594+{
595+ m_uri = uri;
596+ m_request.setUrl(uri);
597+}
598+
599+QString Request::getMethod()
600+{
601+ return m_method;
602+}
603+
604+void Request::setMethod(const QString& method)
605+{
606+ m_method = method;
607+}
608+
609+bool Request::getHandled()
610+{
611+ return m_handled;
612+}
613+
614+void Request::setHandled(bool handled)
615+{
616+ m_handled = handled;
617+}
618+
619+QString Request::getAuthorization()
620+{
621+ return m_authorization;
622+}
623+
624+void Request::setAuthorization(const QString& authorization)
625+{
626+ m_authorization = authorization;
627+}
628+
629+QT_END_NAMESPACE_U1DB
630+
631+#include "moc_request.cpp"
632+
633
634=== added file 'src/request.h'
635--- src/request.h 1970-01-01 00:00:00 +0000
636+++ src/request.h 2014-02-20 14:57:43 +0000
637@@ -0,0 +1,77 @@
638+/*
639+ * Copyright (C) 2014 Canonical, Ltd.
640+ *
641+ * Authors:
642+ * Christian Dywan <christian.dywan@canonical.com>
643+ *
644+ * This program is free software; you can redistribute it and/or modify
645+ * it under the terms of the GNU Lesser General Public License as published by
646+ * the Free Software Foundation; version 3.
647+ *
648+ * This program is distributed in the hope that it will be useful,
649+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
650+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
651+ * GNU Lesser General Public License for more details.
652+ *
653+ * You should have received a copy of the GNU Lesser General Public License
654+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
655+ */
656+
657+#ifndef U1DB_REQUEST_H
658+#define U1DB_REQUEST_H
659+
660+#include <QtCore/QObject>
661+#include <QVariant>
662+#include <QMetaType>
663+#include <QtNetwork>
664+#include <QNetworkAccessManager>
665+
666+
667+#include "global.h"
668+
669+QT_BEGIN_NAMESPACE_U1DB
670+
671+class Q_DECL_EXPORT Request : public QObject {
672+ Q_OBJECT
673+ Q_PROPERTY(QString uri READ getUri WRITE setUri)
674+ Q_PROPERTY(QString method READ getMethod WRITE setMethod)
675+ Q_PROPERTY(bool handled READ getHandled WRITE setHandled)
676+ Q_PROPERTY(QString authorization READ getAuthorization WRITE setAuthorization)
677+
678+public:
679+ Request(const QString& uri, const QString& method, QObject* parent = 0);
680+
681+ QString getUri();
682+ void setUri(const QString& uri);
683+ QString getMethod();
684+ void setMethod(const QString& method);
685+ bool getHandled();
686+ void setHandled(bool handled);
687+ QString getAuthorization();
688+ void setAuthorization(const QString& authorization);
689+
690+ // FIXME private
691+ QNetworkRequest m_request;
692+
693+Q_SIGNALS:
694+
695+ void send();
696+ void cancel(const QString& reason=QString());
697+
698+private:
699+ //Q_DISABLE_COPY(Request)
700+ QString m_uri;
701+ QString m_method;
702+ bool m_handled;
703+ QString m_authorization;
704+ QTimer m_timer;
705+
706+ void timeout();
707+};
708+
709+QT_END_NAMESPACE_U1DB
710+
711+
712+#endif // U1DB_REQUEST_H
713+
714+
715
716=== added file 'src/server.cpp'
717--- src/server.cpp 1970-01-01 00:00:00 +0000
718+++ src/server.cpp 2014-02-20 14:57:43 +0000
719@@ -0,0 +1,94 @@
720+/*
721+ * Copyright (C) 2014 Canonical, Ltd.
722+ *
723+ * Authors:
724+ * Christian Dywan <christian.dywan@canonical.com>
725+ *
726+ * This program is free software; you can redistribute it and/or modify
727+ * it under the terms of the GNU Lesser General Public License as published by
728+ * the Free Software Foundation; version 3.
729+ *
730+ * This program is distributed in the hope that it will be useful,
731+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
732+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
733+ * GNU Lesser General Public License for more details.
734+ *
735+ * You should have received a copy of the GNU Lesser General Public License
736+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
737+ */
738+
739+#include <QDebug>
740+
741+#include "server.h"
742+#include "private.h"
743+
744+QT_BEGIN_NAMESPACE_U1DB
745+
746+Server::Server(QTcpServer *parent) :
747+ QTcpServer(parent), m_port(-1)
748+{
749+ connect(this, &QTcpServer::acceptError, this, &Server::acceptError);
750+}
751+
752+void Server::acceptError(QAbstractSocket::SocketError socketError)
753+{
754+ qDebug() << socketError << m_socket->errorString();
755+}
756+
757+int Server::getPort()
758+{
759+ return m_port;
760+}
761+
762+void Server::setPort(int port)
763+{
764+ if (port == m_port)
765+ return;
766+
767+ if (isListening())
768+ close ();
769+
770+ // port 0 means random, that's why we take the value after the call
771+ listen(QHostAddress::Any, port);
772+ m_port = serverPort();
773+}
774+
775+void Server::incomingConnection(qintptr socket)
776+{
777+ m_socket = new QTcpSocket(this);
778+ connect(m_socket, &QIODevice::readyRead, this, &Server::readClient);
779+ connect(m_socket, &QAbstractSocket::disconnected, this, &Server::discardClient);
780+}
781+
782+void Server::readClient()
783+{
784+ if (!m_socket->canReadLine())
785+ return;
786+ QString line(m_socket->readLine());
787+ // GET /foo/bar HTTP/1.1
788+ QStringList tokens(line.split(" "));
789+ QString method(tokens.at(0));
790+ QString uri(tokens.at(1));
791+ Request* request = new Request(uri, method);
792+ Q_EMIT networkRequest(request);
793+}
794+
795+void Server::discardClient()
796+{
797+ m_socket->deleteLater();
798+}
799+
800+void Server::writeContents (const QString& output)
801+{
802+ QTextStream os(m_socket);
803+ os.setAutoDetectUnicode(true);
804+ os << output;
805+ m_socket->close();
806+ if (m_socket->state() == QTcpSocket::UnconnectedState)
807+ delete m_socket;
808+}
809+
810+QT_END_NAMESPACE_U1DB
811+
812+#include "moc_server.cpp"
813+
814
815=== added file 'src/server.h'
816--- src/server.h 1970-01-01 00:00:00 +0000
817+++ src/server.h 2014-02-20 14:57:43 +0000
818@@ -0,0 +1,67 @@
819+/*
820+ * Copyright (C) 2014 Canonical, Ltd.
821+ *
822+ * Authors:
823+ * Christian Dywan <christian.dywan@canonical.com>
824+ *
825+ * This program is free software; you can redistribute it and/or modify
826+ * it under the terms of the GNU Lesser General Public License as published by
827+ * the Free Software Foundation; version 3.
828+ *
829+ * This program is distributed in the hope that it will be useful,
830+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
831+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
832+ * GNU Lesser General Public License for more details.
833+ *
834+ * You should have received a copy of the GNU Lesser General Public License
835+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
836+ */
837+
838+#ifndef U1DB_SERVER_H
839+#define U1DB_SERVER_H
840+
841+#include <QtCore/QObject>
842+#include <QVariant>
843+#include <QMetaType>
844+#include <QtNetwork>
845+#include <QNetworkAccessManager>
846+#include <QTcpServer>
847+
848+
849+#include "global.h"
850+#include "request.h"
851+
852+QT_BEGIN_NAMESPACE_U1DB
853+
854+class Q_DECL_EXPORT Server : public QTcpServer {
855+ Q_OBJECT
856+ Q_PROPERTY(int port READ getPort WRITE setPort)
857+
858+public:
859+ Server(QTcpServer* parent = 0);
860+
861+ int getPort();
862+ void setPort(int port);
863+
864+ Q_INVOKABLE void writeContents(const QString& output);
865+
866+Q_SIGNALS:
867+ void networkRequest(Request* request);
868+
869+private:
870+ //Q_DISABLE_COPY(Server)
871+ int m_port;
872+ QTcpSocket* m_socket;
873+
874+ void acceptError(QAbstractSocket::SocketError socketError);
875+ void incomingConnection(qintptr socket);
876+ void readClient();
877+ void discardClient();
878+};
879+
880+QT_END_NAMESPACE_U1DB
881+
882+
883+#endif // U1DB_SERVER_H
884+
885+
886
887=== modified file 'src/synchronizer.cpp'
888--- src/synchronizer.cpp 2013-11-25 13:22:07 +0000
889+++ src/synchronizer.cpp 2014-02-20 14:57:43 +0000
890@@ -1,8 +1,9 @@
891 /*
892- * Copyright (C) 2013 Canonical, Ltd.
893+ * Copyright (C) 2013-2014 Canonical, Ltd.
894 *
895 * Authors:
896 * Kevin Wright <kevin.wright@canonical.com>
897+ * Christian Dywan <christian.dywan@canonical.com>
898 *
899 * This program is free software; you can redistribute it and/or modify
900 * it under the terms of the GNU Lesser General Public License as published by
901@@ -111,7 +112,8 @@
902 Synchronizer::Synchronizer(QObject *parent) :
903 QAbstractListModel(parent), m_synchronize(false), m_source(NULL)
904 {
905- QObject::connect(this, &Synchronizer::syncChanged, this, &Synchronizer::onSyncChanged);
906+ QObject::connect(this, &Synchronizer::synchronizeChanged, this, &Synchronizer::onSynchronizeChanged);
907+ QObject::connect(this, &Synchronizer::syncOutputChanged, this, &Synchronizer::onSyncOutputChanged);
908 }
909
910 /*!
911@@ -229,16 +231,73 @@
912 * \property Synchronizer::synchronize
913 */
914
915-void Synchronizer::setSync(bool synchronize)
916+void Synchronizer::setSynchronize(bool synchronize)
917 {
918
919 if (m_synchronize == synchronize)
920 return;
921
922 m_synchronize = synchronize;
923- Q_EMIT syncChanged(synchronize);
924-}
925-
926+ Q_EMIT synchronizeChanged(synchronize);
927+}
928+
929+void Synchronizer::sendNetworkRequest(QNetworkAccessManager* manager, const QString& uri, QString method)
930+{
931+ Request* _request = new Request(uri, method);
932+ _request->m_request.setOriginatingObject(manager);
933+
934+ connect(_request, &Request::send, this, &Synchronizer::networkRequestSent);
935+ connect(_request, &Request::cancel, this, &Synchronizer::networkRequestCancelled);
936+ connect(this, &Synchronizer::networkRequest, this, &Synchronizer::networkRequestEmitted);
937+ Q_EMIT networkRequest(_request);
938+}
939+
940+void Synchronizer::networkRequestEmitted(Request* _request)
941+{
942+ if (!_request->getHandled())
943+ Q_EMIT _request->send();
944+}
945+
946+void Synchronizer::networkRequestSent()
947+{
948+ Request* _request = qobject_cast<Request *>(QObject::sender());
949+ QNetworkRequest request = _request->m_request;
950+
951+ if (!_request->getAuthorization().isEmpty())
952+ request.setRawHeader(QStringLiteral("Authorization").toUtf8(),
953+ _request->getAuthorization().toUtf8());
954+
955+ /* Validate here, otherwise QUrl is empty, finished won't be called */
956+ QNetworkAccessManager* manager = qobject_cast<QNetworkAccessManager *>(request.originatingObject());
957+ if (_request->getMethod() == "GET" && _request->m_request.url().isValid())
958+ manager->get(request);
959+ else if (_request->getMethod() == "POST" && _request->m_request.url().isValid()) {
960+ request.setRawHeader("Content-Type", "application/x-u1db-sync-stream");
961+ QByteArray postString(manager->property("postString").toByteArray());
962+ QByteArray postDataSize = QByteArray::number(postString.size());
963+ request.setRawHeader("Content-Length", postDataSize);
964+ manager->post(request, postString);
965+ } else {
966+ QVariantMap output_map;
967+ output_map.insert("message_type", "error");
968+ output_map.insert("message_value", "Invalid Request.method: " + _request->getMethod());
969+ m_sync_output.append(output_map);
970+ Q_EMIT syncOutputChanged(m_sync_output);
971+ setSynchronize(false);
972+ }
973+}
974+
975+void Synchronizer::networkRequestCancelled(const QString& reason)
976+{
977+ disconnect(this, &Synchronizer::networkRequest, this, &Synchronizer::networkRequestEmitted);
978+
979+ QVariantMap output_map;
980+ output_map.insert("message_type", "error");
981+ output_map.insert("message_value", "Request was cancelled: " + reason);
982+ m_sync_output.append(output_map);
983+ Q_EMIT syncOutputChanged(m_sync_output);
984+ setSynchronize(false);
985+}
986
987 /*!
988 * \property Synchronizer::resolve_to_source
989@@ -255,9 +314,9 @@
990
991
992 /*!
993- * \fn void Synchronizer::setSyncOutput(QList<QVariant> sync_output)
994+ * \fn void Synchronizer::setSyncOutput(QList<QVariant> syncOutput)
995 *
996- * Sets the current value for the active session's \a sync_output.
997+ * Sets the current value for the active session's \a syncOutput.
998 *
999 */
1000
1001@@ -296,7 +355,7 @@
1002 }
1003
1004 /*!
1005- * \brief Synchronizer::getSync
1006+ * \brief Synchronizer::getSynchronize
1007 *
1008 *
1009 * Returns the current value of synchronize. If true then the synchronize
1010@@ -308,7 +367,7 @@
1011 *
1012 */
1013
1014-bool Synchronizer::getSync()
1015+bool Synchronizer::getSynchronize()
1016 {
1017 return m_synchronize;
1018 }
1019@@ -329,7 +388,7 @@
1020 }
1021
1022 /*!
1023- * \property Synchronizer::sync_output
1024+ * \property Synchronizer::syncOutput
1025 * \brief Synchronizer::getSyncOutput
1026 *
1027 * Returns the output from a sync session. The list should contain numerous
1028@@ -358,6 +417,10 @@
1029
1030 */
1031
1032+void Synchronizer::onSyncOutputChanged(QList<QVariant> syncOutput) {
1033+ beginResetModel();
1034+ endResetModel();
1035+}
1036
1037 /*!
1038 * \brief Synchronizer::onSyncChanged
1039@@ -366,7 +429,7 @@
1040 *
1041 */
1042
1043-void Synchronizer::onSyncChanged(bool synchronize){
1044+void Synchronizer::onSynchronizeChanged(bool synchronize){
1045
1046 Database* source = getSource();
1047
1048@@ -381,6 +444,7 @@
1049 QMap<QString,QString>validator;
1050
1051 validator.insert("remote","bool");
1052+ validator.insert("uri","QString");
1053 validator.insert("location","QString");
1054 validator.insert("resolve_to_source","bool");
1055
1056@@ -393,10 +457,11 @@
1057
1058 QList<QString>mandatory;
1059
1060- mandatory.append("remote");
1061+ mandatory.append("uri");
1062 mandatory.append("resolve_to_source");
1063
1064 if(synchronize == true){
1065+ m_sync_output.clear();
1066
1067 /*!
1068 A list of valid sync target databases is generated by calling the getValidTargets(validator, mandatory) method, and adding the return value to a QList (sync_targets).
1069@@ -410,34 +475,21 @@
1070 */
1071
1072 synchronizeTargets(source, sync_targets);
1073-
1074- /*!
1075- * After the synchronization is complete the model is reset so that
1076- *log and error messages are available at the application level.
1077- *
1078- */
1079-
1080- beginResetModel();
1081- endResetModel();
1082-
1083- /*!
1084- The convenience signals syncOutputChanged and syncCompleted are
1085-emitted after the model has been reset.
1086- */
1087-
1088- Q_EMIT syncOutputChanged(m_sync_output);
1089- Q_EMIT syncCompleted();
1090-
1091- /*!
1092- * The sync boolean value is reset to its default value (false)
1093- *once all sync activity is complete.
1094- */
1095-
1096- setSync(false);
1097-
1098- }
1099- else{
1100-
1101+
1102+ if (sync_targets.isEmpty()) {
1103+ QVariantMap output_map;
1104+ output_map.insert("concerning_property", "targets");
1105+ output_map.insert("message_type", "no-errors");
1106+ output_map.insert("message_value", "No targets specified");
1107+ m_sync_output.append(output_map);
1108+ Q_EMIT syncOutputChanged(m_sync_output);
1109+ }
1110+
1111+ /* No sync started */
1112+ if (m_sync_output.isEmpty() || sync_targets.isEmpty()) {
1113+ Q_EMIT syncCompleted();
1114+ setSynchronize(false);
1115+ }
1116 }
1117 }
1118
1119@@ -486,6 +538,7 @@
1120 output_map.insert("message_type","error");
1121 output_map.insert("message_value",message_value);
1122 m_sync_output.append(output_map);
1123+ Q_EMIT syncOutputChanged(m_sync_output);
1124
1125 target.insert("sync",false);
1126
1127@@ -542,7 +595,7 @@
1128 target.insert("sync",true);
1129 sync_targets.append(target);
1130
1131- QString message_value = "Mandatory properties were included and their values are valid.";
1132+ QString message_value = "Mandatory properties included and valid.";
1133
1134 QVariantMap output_map;
1135 output_map.insert("concerning_property","targets");
1136@@ -550,6 +603,7 @@
1137 output_map.insert("message_type","no-errors");
1138 output_map.insert("message_value",message_value);
1139 m_sync_output.append(output_map);
1140+ Q_EMIT syncOutputChanged(m_sync_output);
1141
1142 }
1143
1144@@ -588,67 +642,53 @@
1145 if(target.typeName()== QStringLiteral("QVariantMap")){
1146 QMap<QString,QVariant> target_map = target.toMap();
1147
1148- if(target_map.contains("remote")&&target_map["remote"]==false){
1149- if(target_map.contains("sync")&&target_map["sync"]==true){
1150+ QVariantMap output_map;
1151+ output_map.insert("concerning_property", "targets");
1152+ output_map.insert("concerning_index", target_index);
1153
1154+ bool sync = target_map.contains("sync") && target_map["sync"].toBool();
1155+ QUrl url(target_map["uri"].toString());
1156+ if (url.isLocalFile()) {
1157+ if (sync) {
1158 QString message_value = "Valid local target.";
1159-
1160- QVariantMap output_map;
1161- output_map.insert("concerning_property","targets");
1162- output_map.insert("concerning_index",target_index);
1163 output_map.insert("message_type","no-errors");
1164 output_map.insert("message_value",message_value);
1165 m_sync_output.append(output_map);
1166+ Q_EMIT syncOutputChanged(m_sync_output);
1167
1168 syncLocalToLocal(source, target_map);
1169 }
1170 }
1171- else if(target_map.contains("remote")&&target_map["remote"]==true){
1172- if(target_map.contains("sync")&&target_map["sync"]==true){
1173-
1174- //ip
1175- //port
1176- //name
1177+ else if (url.scheme() == "http" || url.scheme() == "https") {
1178+ if (sync) {
1179 //GET /thedb/sync-from/my_replica_uid
1180
1181 QString source_uid = getUidFromLocalDb(source->getPath());
1182- QString get_string = target_map["name"].toString()+"/sync-from/"+source_uid;
1183- QString url_string = "http://"+target_map["ip"].toString();
1184- QString full_get_request = url_string+"/"+get_string;
1185- int port_number = target_map["port"].toInt();
1186+ QString full_get_request = target_map["uri"].toString() + "/sync-from/" + source_uid;
1187
1188 QNetworkAccessManager *manager = new QNetworkAccessManager(source);
1189
1190- QUrl url(full_get_request);
1191- url.setPort(port_number);
1192-
1193- QNetworkRequest request(url);
1194-
1195 connect(manager, &QNetworkAccessManager::finished, this, &Synchronizer::remoteGetSyncInfoFinished);
1196
1197 QString message_value = "Valid remote target.";
1198
1199- QVariantMap output_map;
1200- output_map.insert("concerning_property","targets");
1201- output_map.insert("concerning_index",target_index);
1202 output_map.insert("message_type","no-errors");
1203 output_map.insert("message_value",message_value);
1204 m_sync_output.append(output_map);
1205-
1206- manager->get(QNetworkRequest(request));
1207-
1208+ Q_EMIT syncOutputChanged(m_sync_output);
1209+
1210+ sendNetworkRequest(manager, full_get_request, "GET");
1211 }
1212 }
1213 else{
1214
1215- QString message_value = "Unknown error. Please check properties";
1216+ QString message_value = "Invalid URI, must be file, http or https";
1217
1218- QVariantMap output_map;
1219- output_map.insert("concerning_property","targets");
1220- output_map.insert("concerning_index",target_index);
1221 output_map.insert("message_type","error");
1222 output_map.insert("message_value",message_value);
1223 m_sync_output.append(output_map);
1224+ Q_EMIT syncOutputChanged(m_sync_output);
1225+ setSynchronize(false);
1226
1227 }
1228
1229@@ -671,7 +711,7 @@
1230 void Synchronizer::syncLocalToLocal(Database *sourceDb, QMap<QString,QVariant> target)
1231 {
1232
1233- QString target_db_name = target["location"].toString();
1234+ QString target_db_name = QUrl(target["uri"].toString()).toLocalFile();
1235
1236 Database *targetDb;
1237 Index *targetIndex;
1238@@ -705,6 +745,8 @@
1239 output_map.insert("message_type","error");
1240 output_map.insert("message_value",message_value);
1241 m_sync_output.append(output_map);
1242+ Q_EMIT syncOutputChanged(m_sync_output);
1243+ setSynchronize(false);
1244
1245 return;
1246 }
1247@@ -890,6 +932,8 @@
1248 /* If the source has seen no changes unrelated to the synchronisation during this whole process, it now sends the target what its latest change is, so that the next synchronisation does not have to consider changes that were the result of this one.*/
1249
1250
1251+ Q_EMIT syncOutputChanged(m_sync_output);
1252+ setSynchronize(false);
1253 }
1254
1255 /*!
1256@@ -966,64 +1010,28 @@
1257
1258 QString Synchronizer::getUidFromLocalDb(QString dbFileName)
1259 {
1260-
1261 QString dbUid;
1262-
1263- QSqlDatabase db;
1264-
1265- db = QSqlDatabase::addDatabase("QSQLITE",QUuid::createUuid().toString());
1266-
1267- QFile db_file(dbFileName);
1268-
1269- if(!db_file.exists())
1270- {
1271-
1272- QString message_value = "Database does not exist.";
1273-
1274+ Database localDb;
1275+
1276+ localDb.setPath(dbFileName);
1277+ QString lastError(localDb.lastError());
1278+ if (lastError.isEmpty()) {
1279+ dbUid = localDb.getReplicaUid();
1280+ dbUid = dbUid.replace("{","");
1281+ dbUid = dbUid.replace("}","");
1282+ lastError = localDb.lastError();
1283+ }
1284+
1285+ if (!lastError.isEmpty()) {
1286+ QString message_value = localDb.lastError();
1287 QVariantMap output_map;
1288+
1289 output_map.insert("concerning_property","source|targets");
1290 output_map.insert("concerning_database",dbFileName);
1291 output_map.insert("message_type","error");
1292 output_map.insert("message_value",message_value);
1293 m_sync_output.append(output_map);
1294-
1295- return dbUid;
1296- }
1297- else
1298- {
1299- db.setDatabaseName(dbFileName);
1300- if (!db.open()){
1301-
1302- QString message_value = db.lastError().text();
1303-
1304- QVariantMap output_map;
1305- output_map.insert("concerning_property","source|targets");
1306- output_map.insert("concerning_database",dbFileName);
1307- output_map.insert("message_type","error");
1308- output_map.insert("message_value",message_value);
1309- m_sync_output.append(output_map);
1310-
1311- }
1312- else{
1313- QSqlQuery query (db.exec("SELECT value FROM u1db_config WHERE name = 'replica_uid'"));
1314-
1315- if(!query.lastError().isValid() && query.next()){
1316- dbUid = query.value(0).toString();
1317- db.close();
1318-
1319- dbUid = dbUid.replace("{","");
1320-
1321- dbUid = dbUid.replace("}","");
1322-
1323- return dbUid;
1324- }
1325- else{
1326- qDebug() << query.lastError().text();
1327- db.close();
1328- return dbUid;
1329- }
1330- }
1331-
1332+ Q_EMIT syncOutputChanged(m_sync_output);
1333 }
1334
1335 return dbUid;
1336@@ -1047,6 +1055,21 @@
1337
1338 void Synchronizer::remoteGetSyncInfoFinished(QNetworkReply* reply)
1339 {
1340+ QVariant statusCodeVariant = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
1341+ int statusCode(statusCodeVariant.isValid() ? statusCodeVariant.toInt() : -1);
1342+
1343+ if(!(statusCode == 200 || statusCode == 401)) {
1344+ QVariant reasonVariant = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
1345+ QString reason(reasonVariant.isValid() ? reasonVariant.toString() : "N/A");
1346+ QVariantMap output_map;
1347+ output_map.insert("concerning_property", "targets");
1348+ output_map.insert("message_type", "error");
1349+ output_map.insert("message_value", QString("Pre-sync bad HTTP code: %1 - %2").arg(statusCode).arg(reason));
1350+ m_sync_output.append(output_map);
1351+ Q_EMIT syncOutputChanged(m_sync_output);
1352+ setSynchronize(false);
1353+ return;
1354+ }
1355
1356 QNetworkAccessManager *manager = reply->manager();
1357
1358@@ -1123,15 +1146,8 @@
1359
1360 postString.append("\r\n]");
1361
1362- QByteArray postDataSize = QByteArray::number(postString.size());
1363-
1364- QNetworkRequest request(postUrl);
1365- request.setRawHeader("User-Agent", "U1Db-Qt v1.0");
1366- request.setRawHeader("X-Custom-User-Agent", "U1Db-Qt v1.0");
1367- request.setRawHeader("Content-Type", "application/x-u1db-sync-stream");
1368- request.setRawHeader("Content-Length", postDataSize);
1369-
1370- manager->post(QNetworkRequest(request),postString);
1371+ manager->setProperty("postString", postString);
1372+ sendNetworkRequest(manager, postUrl.toString(), "POST");
1373
1374 }
1375
1376@@ -1150,6 +1166,21 @@
1377
1378 void Synchronizer::remotePostSyncInfoFinished(QNetworkReply* reply)
1379 {
1380+ QVariant statusCodeVariant = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
1381+ int statusCode(statusCodeVariant.isValid() ? statusCodeVariant.toInt() : -1);
1382+
1383+ if(!(statusCode == 200 || statusCode == 409 || statusCode == 401)) {
1384+ QVariant reasonVariant = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
1385+ QString reason(reasonVariant.isValid() ? reasonVariant.toString() : "N/A");
1386+ QVariantMap output_map;
1387+ output_map.insert("concerning_property", "targets");
1388+ output_map.insert("message_type", "error");
1389+ output_map.insert("message_value", QString("Post-sync bad HTTP code: %1 - %2").arg(statusCode).arg(reason));
1390+ m_sync_output.append(output_map);
1391+ Q_EMIT syncOutputChanged(m_sync_output);
1392+ setSynchronize(false);
1393+ return;
1394+ }
1395
1396 QNetworkAccessManager *manager = reply->manager();
1397
1398@@ -1180,9 +1211,32 @@
1399
1400 replyData = replyData.replace("\r\n","");
1401
1402- QJsonDocument replyJson = QJsonDocument::fromJson(replyData.toUtf8());
1403-
1404- QVariant replyVariant = replyJson.toVariant();
1405+ QJsonParseError parseError;
1406+ QJsonDocument replyJson = QJsonDocument::fromJson(replyData.toUtf8(), &parseError);
1407+ QString syncError;
1408+ QVariant replyVariant;
1409+
1410+ if (parseError.error == QJsonParseError::NoError) {
1411+ // Valid JSON: errors may be returned as { "error": "Foo bar" }
1412+ replyVariant = replyJson.toVariant();
1413+ QVariantMap replyMap(replyVariant.toMap());
1414+ if (replyMap.contains("error"))
1415+ syncError = QString("Server - %1: %2").arg(replyMap["error"].toString()).arg(replyMap["message"].toString());
1416+ } else {
1417+ // The response is likely an HTML page but can also be malformed JSON
1418+ syncError = QString("JSON - %1").arg(parseError.errorString());
1419+ }
1420+
1421+ if (!syncError.isEmpty()) {
1422+ QVariantMap output_map;
1423+ output_map.insert("concerning_property", "targets");
1424+ output_map.insert("message_type", "error");
1425+ output_map.insert("message_value", "Error: " + syncError);
1426+ m_sync_output.append(output_map);
1427+ Q_EMIT syncOutputChanged(m_sync_output);
1428+ setSynchronize(false);
1429+ return;
1430+ }
1431
1432 QVariantList replyList = replyVariant.toList();
1433
1434@@ -1190,6 +1244,8 @@
1435
1436 int index = -1;
1437
1438+ QString changelog;
1439+
1440 while(i.hasNext()){
1441
1442 index++;
1443@@ -1240,6 +1296,7 @@
1444 {
1445 source->putDoc(content,id);
1446 source->updateDocRevisionNumber(id,rev);
1447+ changelog += QString(" %1@%2").arg(id).arg(rev);
1448 }
1449
1450 }
1451@@ -1248,6 +1305,16 @@
1452
1453 }
1454
1455+ if (changelog.isEmpty())
1456+ changelog = "No changes";
1457+
1458+ QVariantMap output_map;
1459+ output_map.insert("concerning_property", "source|targets");
1460+ output_map.insert("message_type", "no-errors");
1461+ output_map.insert("message_value", "Sync done:" + changelog);
1462+ m_sync_output.append(output_map);
1463+ Q_EMIT syncOutputChanged(m_sync_output);
1464+ setSynchronize(false);
1465 }
1466
1467 QT_END_NAMESPACE_U1DB
1468
1469=== modified file 'src/synchronizer.h'
1470--- src/synchronizer.h 2013-11-22 15:21:37 +0000
1471+++ src/synchronizer.h 2014-02-20 14:57:43 +0000
1472@@ -31,6 +31,7 @@
1473 #include "database.h"
1474 #include "index.h"
1475 #include "query.h"
1476+#include "request.h"
1477
1478 QT_BEGIN_NAMESPACE_U1DB
1479
1480@@ -41,10 +42,10 @@
1481 #else
1482 Q_PROPERTY(QT_PREPEND_NAMESPACE_U1DB(Database*) source READ getSource WRITE setSource NOTIFY sourceChanged)
1483 #endif
1484- Q_PROPERTY(bool synchronize READ getSync WRITE setSync NOTIFY syncChanged)
1485+ Q_PROPERTY(bool synchronize READ getSynchronize WRITE setSynchronize NOTIFY synchronizeChanged)
1486 Q_PROPERTY(bool resolve_to_source READ getResolveToSource WRITE setResolveToSource NOTIFY resolveToSourceChanged)
1487 Q_PROPERTY(QVariant targets READ getTargets WRITE setTargets NOTIFY targetsChanged)
1488- Q_PROPERTY(QList<QVariant> sync_output READ getSyncOutput NOTIFY syncOutputChanged)
1489+ Q_PROPERTY(QList<QVariant> syncOutput READ getSyncOutput NOTIFY syncOutputChanged)
1490
1491 public:
1492 Synchronizer(QObject* parent = 0);
1493@@ -60,13 +61,13 @@
1494
1495 Database* getSource();
1496 QVariant getTargets();
1497- bool getSync();
1498+ bool getSynchronize();
1499 bool getResolveToSource();
1500 QList<QVariant> getSyncOutput();
1501
1502 void setSource(Database* source);
1503 void setTargets(QVariant targets);
1504- void setSync(bool synchronize);
1505+ void setSynchronize(bool synchronize);
1506 void setResolveToSource(bool resolve_to_source);
1507 void setSyncOutput(QList<QVariant> sync_output);
1508
1509@@ -97,11 +98,11 @@
1510 void targetsChanged(QVariant targets);
1511
1512 /*!
1513- * \brief syncChanged
1514+ * \brief synchronizeChanged
1515 * \param synchronize
1516 */
1517
1518- void syncChanged(bool synchronize);
1519+ void synchronizeChanged(bool synchronize);
1520
1521 /*!
1522 * \brief resolveToSourceChanged
1523@@ -112,7 +113,7 @@
1524
1525 /*!
1526 * \brief syncOutputChanged
1527- * \param sync_output
1528+ * \param syncOutput
1529 */
1530
1531 void syncOutputChanged(QList<QVariant> sync_output);
1532@@ -123,6 +124,12 @@
1533
1534 void syncCompleted();
1535
1536+ /*!
1537+ * \brief networkRequest
1538+ * \param request
1539+ */
1540+ void networkRequest(Request* request);
1541+
1542 private:
1543 //Q_DISABLE_COPY(Synchronizer)
1544 bool m_synchronize;
1545@@ -131,10 +138,16 @@
1546 QVariant m_targets;
1547 QList<QVariant> m_sync_output;
1548
1549- void onSyncChanged(bool synchronize);
1550+ void onSynchronizeChanged(bool synchronize);
1551+ void onSyncOutputChanged(QList<QVariant> syncOutput);
1552 void remoteGetSyncInfoFinished(QNetworkReply* reply);
1553 void remotePostSyncInfoFinished(QNetworkReply* reply);
1554
1555+ void sendNetworkRequest(QNetworkAccessManager* manager, const QString& uri, QString method);
1556+ void networkRequestSent();
1557+ void networkRequestCancelled(const QString& reason);
1558+ void networkRequestEmitted(Request* request);
1559+
1560 };
1561
1562 QT_END_NAMESPACE_U1DB
1563
1564=== added file 'tests/tst_sync.qml'
1565--- tests/tst_sync.qml 1970-01-01 00:00:00 +0000
1566+++ tests/tst_sync.qml 2014-02-20 14:57:43 +0000
1567@@ -0,0 +1,97 @@
1568+/*
1569+ * Copyright (C) 2014 Canonical, Ltd.
1570+ *
1571+ * Authors:
1572+ * Christian Dywan <christian.dywan@canonical.com>
1573+ *
1574+ * This program is free software; you can redistribute it and/or modify
1575+ * it under the terms of the GNU Lesser General Public License as published by
1576+ * the Free Software Foundation; version 3.
1577+ *
1578+ * This program is distributed in the hope that it will be useful,
1579+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1580+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1581+ * GNU Lesser General Public License for more details.
1582+ *
1583+ * You should have received a copy of the GNU Lesser General Public License
1584+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1585+ */
1586+
1587+import QtQuick 2.0
1588+import QtTest 1.0
1589+import U1db 1.0 as U1db
1590+
1591+Item {
1592+ width: 200; height: 200
1593+
1594+ U1db.Database {
1595+ id: stars
1596+ }
1597+
1598+ U1db.Document {
1599+ database: stars
1600+ docId: 'earth'
1601+ contents: { 'habitable': true }
1602+ }
1603+
1604+ U1db.Document {
1605+ database: stars
1606+ docId: 'jupiter'
1607+ contents: { 'moons': [ 'europa' ] }
1608+ }
1609+
1610+ U1db.Synchronizer {
1611+ id: online
1612+ source: stars
1613+ }
1614+
1615+ SignalSpy {
1616+ id: spySynchronizeChanged
1617+ target: online
1618+ signalName: "synchronizeChanged"
1619+ }
1620+
1621+ U1db.Server {
1622+ id: mockServer
1623+ port: 0
1624+ property bool unresponsive: false
1625+ onNetworkRequest: {
1626+ if (!unresponsive)
1627+ write ("HTTP/1.0 404 Ok\nContent-Type: text/html; charset=\"utf-8\"\nNot found!\n")
1628+ }
1629+ }
1630+
1631+TestCase {
1632+ name: "Sync"
1633+
1634+ function test_0_incomplete () {
1635+ online.synchronize = true
1636+ spySynchronizeChanged.wait ()
1637+ compare(online.syncOutput[0].message_value, "No targets specified")
1638+
1639+ online.targets = [ { } ]
1640+ online.synchronize = true
1641+ spySynchronizeChanged.wait ()
1642+ compare(0, online.syncOutput[1].message_value.indexOf("Invalid URI"))
1643+
1644+ online.targets = [ { "uri": "foo://bar.com:9876/rien.db",
1645+ resolve_to_source: true } ]
1646+ online.synchronize = true
1647+ spySynchronizeChanged.wait ()
1648+ compare(0, online.syncOutput[1].message_value.indexOf("Invalid URI"))
1649+
1650+ online.targets = [ { "uri": "http://.invalid:1234/nada.db" } ]
1651+ online.synchronize = true
1652+ spySynchronizeChanged.wait ()
1653+ compare(0, online.syncOutput[0].message_value.indexOf("Expected property `resolve_to_source"))
1654+ }
1655+
1656+ function test_1_faultyServer () {
1657+ online.targets = [ { "uri": "http://.invalid:1234/nada.db",
1658+ resolve_to_source: true } ]
1659+ online.synchronize = true
1660+ spySynchronizeChanged.wait ()
1661+ compare("no-errors", online.syncOutput[0].message_type)
1662+ }
1663+} }
1664+

Subscribers

People subscribed via source and target branches

to all changes: