mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
api sync
This commit is contained in:
parent
999a2c5ef2
commit
a409f994cd
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ QtClient/data/
|
||||
app/data/uploads/
|
||||
!app/data/uploads/.gitkeep
|
||||
sparse_test.php
|
||||
INFO.md
|
@ -1,4 +1,4 @@
|
||||
QT += qml quick sql quickcontrols2
|
||||
QT += qml quick sql quickcontrols2 network
|
||||
|
||||
CONFIG += c++11
|
||||
|
||||
@ -16,7 +16,10 @@ SOURCES += \
|
||||
application.cpp \
|
||||
models/notecollection.cpp \
|
||||
services/notecache.cpp \
|
||||
models/qmlnote.cpp
|
||||
models/qmlnote.cpp \
|
||||
webapi.cpp \
|
||||
synchronizer.cpp \
|
||||
settings.cpp
|
||||
|
||||
RESOURCES += qml.qrc \
|
||||
database.qrc
|
||||
@ -44,7 +47,10 @@ HEADERS += \
|
||||
models/notecollection.h \
|
||||
services/notecache.h \
|
||||
sparsevector.hpp \
|
||||
models/qmlnote.h
|
||||
models/qmlnote.h \
|
||||
webapi.h \
|
||||
synchronizer.h \
|
||||
settings.h
|
||||
|
||||
DISTFILES +=
|
||||
|
||||
|
@ -4,11 +4,20 @@
|
||||
#include "database.h"
|
||||
#include "models/foldermodel.h"
|
||||
#include "services/folderservice.h"
|
||||
#include "settings.h"
|
||||
|
||||
using namespace jop;
|
||||
|
||||
Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
|
||||
db_ = Database("D:/Web/www/joplin/QtClient/data/notes.sqlite");
|
||||
Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv), db_("D:/Web/www/joplin/QtClient/data/notes.sqlite"), api_("http://joplin.local"), synchronizer_(api_, db_) {
|
||||
// This is linked to where the QSettings will be saved. In other words,
|
||||
// if these values are changed, the settings will be reset and saved
|
||||
// somewhere else.
|
||||
QCoreApplication::setOrganizationName("Cozic");
|
||||
QCoreApplication::setOrganizationDomain("cozic.net");
|
||||
QCoreApplication::setApplicationName("Joplin");
|
||||
|
||||
Settings settings;
|
||||
|
||||
folderService_ = FolderService(db_);
|
||||
folderModel_.setService(folderService_);
|
||||
|
||||
@ -29,6 +38,31 @@ Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
|
||||
connect(rootObject, SIGNAL(currentNoteChanged()), this, SLOT(view_currentNoteChanged()));
|
||||
|
||||
view_.show();
|
||||
|
||||
connect(&api_, SIGNAL(requestDone(const QJsonObject&, const QString&)), this, SLOT(api_requestDone(const QJsonObject&, const QString&)));
|
||||
|
||||
QString sessionId = settings.value("sessionId").toString();
|
||||
if (sessionId == "") {
|
||||
QUrlQuery postData;
|
||||
postData.addQueryItem("email", "laurent@cozic.net");
|
||||
postData.addQueryItem("password", "12345678");
|
||||
postData.addQueryItem("client_id", "B6E12222B6E12222");
|
||||
api_.post("sessions", QUrlQuery(), postData, "getSession");
|
||||
} else {
|
||||
afterSessionInitialization();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::api_requestDone(const QJsonObject& response, const QString& tag) {
|
||||
// TODO: handle errors
|
||||
|
||||
if (tag == "getSession") {
|
||||
QString sessionId = response.value("id").toString();
|
||||
Settings settings;
|
||||
settings.setValue("sessionId", sessionId);
|
||||
afterSessionInitialization();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QString Application::selectedFolderId() const {
|
||||
@ -47,6 +81,17 @@ QString Application::selectedNoteId() const {
|
||||
return noteModel_.data(modelIndex, NoteModel::IdRole).toString();
|
||||
}
|
||||
|
||||
void Application::afterSessionInitialization() {
|
||||
// TODO: rather than saving the session id, save the username/password and
|
||||
// request a new session everytime on startup.
|
||||
|
||||
Settings settings;
|
||||
QString sessionId = settings.value("sessionId").toString();
|
||||
qDebug() << "Session:" << sessionId;
|
||||
api_.setSessionId(sessionId);
|
||||
synchronizer_.start();
|
||||
}
|
||||
|
||||
void Application::view_currentFolderChanged() {
|
||||
QString folderId = selectedFolderId();
|
||||
noteCollection_ = NoteCollection(db_, folderId, "title ASC");
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include "services/notecache.h"
|
||||
#include "models/notemodel.h"
|
||||
#include "models/qmlnote.h"
|
||||
#include "webapi.h"
|
||||
#include "synchronizer.h"
|
||||
|
||||
namespace jop {
|
||||
|
||||
@ -35,11 +37,16 @@ private:
|
||||
QString selectedNoteId() const;
|
||||
NoteCache noteCache_;
|
||||
QmlNote selectedQmlNote_;
|
||||
WebApi api_;
|
||||
Synchronizer synchronizer_;
|
||||
|
||||
void afterSessionInitialization();
|
||||
|
||||
public slots:
|
||||
|
||||
void view_currentFolderChanged();
|
||||
void view_currentNoteChanged();
|
||||
void api_requestDone(const QJsonObject& response, const QString& tag);
|
||||
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ using namespace jop;
|
||||
Database::Database(const QString &path) {
|
||||
version_ = -1;
|
||||
|
||||
// QFile::remove(path);
|
||||
//QFile::remove(path);
|
||||
|
||||
db_ = QSqlDatabase::addDatabase("QSQLITE");
|
||||
db_.setDatabaseName(path);
|
||||
|
@ -12,7 +12,6 @@ public:
|
||||
Database(const QString& path);
|
||||
Database();
|
||||
QSqlQuery query(const QString& sql) const;
|
||||
//QSqlQuery exec(const QString& sql, const QMap<QString, QVariant> ¶meters);
|
||||
|
||||
private:
|
||||
|
||||
|
@ -14,8 +14,6 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -5,16 +5,7 @@ using namespace jop;
|
||||
|
||||
Item::Item() {
|
||||
isPartial_ = true;
|
||||
}
|
||||
|
||||
void Item::fromSqlQuery(const QSqlQuery &q) {
|
||||
int i_id = q.record().indexOf("id");
|
||||
int i_title = q.record().indexOf("title");
|
||||
int i_created_time = q.record().indexOf("created_time");
|
||||
|
||||
id_ = q.value(i_id).toString();
|
||||
title_ = q.value(i_title).toString();
|
||||
createdTime_ = q.value(i_created_time).toInt();
|
||||
synced_ = false;
|
||||
}
|
||||
|
||||
QString Item::id() const {
|
||||
@ -29,6 +20,10 @@ int Item::createdTime() const {
|
||||
return createdTime_;
|
||||
}
|
||||
|
||||
int Item::updatedTime() const {
|
||||
return updatedTime_;
|
||||
}
|
||||
|
||||
void Item::setId(const QString& v) {
|
||||
id_ = v;
|
||||
}
|
||||
@ -48,3 +43,17 @@ void Item::setIsPartial(bool v) {
|
||||
bool Item::isPartial() const {
|
||||
return isPartial_;
|
||||
}
|
||||
|
||||
QStringList Item::dbFields() {
|
||||
QStringList output;
|
||||
output << "id" << "title" << "created_time" << "updated_time" << "synced";
|
||||
return output;
|
||||
}
|
||||
|
||||
void Item::fromSqlQuery(const QSqlQuery &q) {
|
||||
id_ = q.value(0).toString();
|
||||
title_ = q.value(1).toString();
|
||||
createdTime_ = q.value(2).toInt();
|
||||
updatedTime_ = q.value(3).toInt();
|
||||
synced_ = q.value(4).toBool();
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ public:
|
||||
QString id() const;
|
||||
QString title() const;
|
||||
int createdTime() const;
|
||||
int updatedTime() const;
|
||||
bool isPartial() const;
|
||||
static QStringList dbFields();
|
||||
|
||||
void setId(const QString &v);
|
||||
void setTitle(const QString& v);
|
||||
@ -27,7 +29,10 @@ private:
|
||||
|
||||
QString id_;
|
||||
QString title_;
|
||||
int createdTime_;
|
||||
time_t createdTime_;
|
||||
time_t updatedTime_;
|
||||
bool synced_;
|
||||
|
||||
bool isPartial_;
|
||||
|
||||
};
|
||||
|
@ -14,3 +14,15 @@ QString Note::body() const {
|
||||
void Note::setBody(const QString &v) {
|
||||
body_ = v;
|
||||
}
|
||||
|
||||
QStringList Note::dbFields() {
|
||||
QStringList output = Item::dbFields();
|
||||
output << "body";
|
||||
return output;
|
||||
}
|
||||
|
||||
void Note::fromSqlQuery(const QSqlQuery &q) {
|
||||
Item::fromSqlQuery(q);
|
||||
int idx = Item::dbFields().size();
|
||||
body_ = q.value(idx).toString();
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ public:
|
||||
Note();
|
||||
QString body() const;
|
||||
void setBody(const QString& v);
|
||||
static QStringList dbFields();
|
||||
void fromSqlQuery(const QSqlQuery &q);
|
||||
|
||||
private:
|
||||
|
||||
|
@ -2,7 +2,8 @@ CREATE TABLE folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
created_time INT,
|
||||
updated_time INT
|
||||
updated_time INT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE notes (
|
||||
@ -23,20 +24,23 @@ CREATE TABLE notes (
|
||||
todo_completed INT,
|
||||
source_application TEXT,
|
||||
application_data TEXT,
|
||||
`order` INT
|
||||
`order` INT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
created_time INT,
|
||||
updated_time INT
|
||||
updated_time INT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE note_tags (
|
||||
id INTEGER PRIMARY KEY,
|
||||
note_id TEXT,
|
||||
tag_id TEXT
|
||||
tag_id TEXT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE resources (
|
||||
@ -45,13 +49,15 @@ CREATE TABLE resources (
|
||||
mime TEXT,
|
||||
filename TEXT,
|
||||
created_time INT,
|
||||
updated_time INT
|
||||
updated_time INT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE note_resources (
|
||||
id INTEGER PRIMARY KEY,
|
||||
note_id TEXT,
|
||||
resource_id TEXT
|
||||
resource_id TEXT,
|
||||
synced BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE version (
|
||||
|
3
QtClient/JoplinQtClient/settings.cpp
Executable file
3
QtClient/JoplinQtClient/settings.cpp
Executable file
@ -0,0 +1,3 @@
|
||||
#include "settings.h"
|
||||
|
||||
using namespace jop;
|
19
QtClient/JoplinQtClient/settings.h
Executable file
19
QtClient/JoplinQtClient/settings.h
Executable file
@ -0,0 +1,19 @@
|
||||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <stable.h>
|
||||
|
||||
namespace jop {
|
||||
|
||||
class Settings : public QSettings {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
//public:
|
||||
// Settings();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SETTINGS_H
|
@ -10,8 +10,6 @@
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
//#include <QUuid>
|
||||
//#include <vector>
|
||||
#include <QList>
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
@ -20,6 +18,13 @@
|
||||
#include <QQmlContext>
|
||||
#include <QQmlProperty>
|
||||
#include <QSqlError>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
|
83
QtClient/JoplinQtClient/synchronizer.cpp
Executable file
83
QtClient/JoplinQtClient/synchronizer.cpp
Executable file
@ -0,0 +1,83 @@
|
||||
#include "synchronizer.h"
|
||||
#include "models/folder.h"
|
||||
#include "models/note.h"
|
||||
|
||||
using namespace jop;
|
||||
|
||||
Synchronizer::Synchronizer(WebApi& api, Database &database) : api_(api), db_(database) {
|
||||
qDebug() << api_.baseUrl();
|
||||
connect(&api_, SIGNAL(requestDone(QJsonObject,QString)), this, SLOT(api_requestDone(QJsonObject,QString)));
|
||||
}
|
||||
|
||||
void Synchronizer::start() {
|
||||
qDebug() << "Starting synchronizer...";
|
||||
|
||||
QSqlQuery query;
|
||||
|
||||
std::vector<Folder> folders;
|
||||
query = db_.query("SELECT " + Folder::dbFields().join(',') + " FROM folders WHERE synced = 0");
|
||||
query.exec();
|
||||
|
||||
while (query.next()) {
|
||||
Folder folder;
|
||||
folder.fromSqlQuery(query);
|
||||
folders.push_back(folder);
|
||||
}
|
||||
|
||||
QList<Note> notes;
|
||||
query = db_.query("SELECT " + Note::dbFields().join(',') + " FROM notes WHERE synced = 0");
|
||||
query.exec();
|
||||
|
||||
while (query.next()) {
|
||||
Note note;
|
||||
note.fromSqlQuery(query);
|
||||
notes << note;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < folders.size(); i++) {
|
||||
Folder folder = folders[i];
|
||||
QUrlQuery data;
|
||||
data.addQueryItem("id", folder.id());
|
||||
data.addQueryItem("title", folder.title());
|
||||
data.addQueryItem("created_time", QString::number(folder.createdTime()));
|
||||
data.addQueryItem("updated_time", QString::number(folder.updatedTime()));
|
||||
api_.put("folders/" + folder.id(), QUrlQuery(), data, "putFolder:" + folder.id());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < notes.size(); i++) {
|
||||
Note note = notes[i];
|
||||
QUrlQuery data;
|
||||
data.addQueryItem("id", note.id());
|
||||
data.addQueryItem("title", note.title());
|
||||
data.addQueryItem("body", note.body());
|
||||
data.addQueryItem("created_time", QString::number(note.createdTime()));
|
||||
data.addQueryItem("updated_time", QString::number(note.updatedTime()));
|
||||
api_.put("notes/" + note.id(), QUrlQuery(), data, "putNote:" + note.id());
|
||||
}
|
||||
}
|
||||
|
||||
void Synchronizer::api_requestDone(const QJsonObject& response, const QString& tag) {
|
||||
QSqlQuery query;
|
||||
QStringList parts = tag.split(':');
|
||||
QString action = tag;
|
||||
QString id = "";
|
||||
|
||||
if (parts.size() == 2) {
|
||||
action = parts[0];
|
||||
id = parts[1];
|
||||
}
|
||||
|
||||
if (action == "putFolder") {
|
||||
// qDebug() << "Done folder" << id;
|
||||
// query = db_.query("UPDATE folders SET synced = 1 WHERE id = ?");
|
||||
// query.addBindValue(id);
|
||||
// query.exec();
|
||||
}
|
||||
|
||||
if (action == "putNote") {
|
||||
// qDebug() << "Done note" << id;
|
||||
// query = db_.query("UPDATE notes SET synced = 1 WHERE id = ?");
|
||||
// query.addBindValue(id);
|
||||
// query.exec();
|
||||
}
|
||||
}
|
32
QtClient/JoplinQtClient/synchronizer.h
Executable file
32
QtClient/JoplinQtClient/synchronizer.h
Executable file
@ -0,0 +1,32 @@
|
||||
#ifndef SYNCHRONIZER_H
|
||||
#define SYNCHRONIZER_H
|
||||
|
||||
#include <stable.h>
|
||||
#include "webapi.h"
|
||||
#include "database.h"
|
||||
|
||||
namespace jop {
|
||||
|
||||
class Synchronizer : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
Synchronizer(WebApi &api, Database& database);
|
||||
void start();
|
||||
|
||||
private:
|
||||
|
||||
WebApi& api_;
|
||||
Database& db_;
|
||||
|
||||
public slots:
|
||||
|
||||
void api_requestDone(const QJsonObject& response, const QString& tag);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SYNCHRONIZER_H
|
110
QtClient/JoplinQtClient/webapi.cpp
Executable file
110
QtClient/JoplinQtClient/webapi.cpp
Executable file
@ -0,0 +1,110 @@
|
||||
#include <stable.h>
|
||||
|
||||
#include "webapi.h"
|
||||
|
||||
using namespace jop;
|
||||
|
||||
WebApi::WebApi(const QString &baseUrl) {
|
||||
baseUrl_ = baseUrl;
|
||||
sessionId_ = "";
|
||||
connect(&manager_, SIGNAL(finished(QNetworkReply*)), this, SLOT(request_finished(QNetworkReply*)));
|
||||
}
|
||||
|
||||
QString WebApi::baseUrl() const {
|
||||
return baseUrl_;
|
||||
}
|
||||
|
||||
void WebApi::execRequest(QNetworkAccessManager::Operation method, const QString &path, const QUrlQuery &query, const QUrlQuery &data, const QString& tag) {
|
||||
QueuedRequest r;
|
||||
r.method = method;
|
||||
r.path = path;
|
||||
r.query = query;
|
||||
r.data = data;
|
||||
r.tag = tag;
|
||||
queuedRequests_ << r;
|
||||
|
||||
processQueue();
|
||||
}
|
||||
|
||||
void WebApi::post(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PostOperation, path, query, data, tag); }
|
||||
void WebApi::get(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::GetOperation, path, query, data, tag); }
|
||||
void WebApi::put(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PutOperation, path, query, data, tag); }
|
||||
//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "") { execRequest(QNetworkAccessManager::PatchOperation, query, data, tag); }
|
||||
|
||||
void WebApi::setSessionId(const QString &v) {
|
||||
sessionId_ = v;
|
||||
}
|
||||
|
||||
void WebApi::processQueue() {
|
||||
if (!queuedRequests_.size() || inProgressRequests_.size() >= 50) return;
|
||||
QueuedRequest& r = queuedRequests_.takeFirst();
|
||||
|
||||
QString url = baseUrl_ + "/" + r.path;
|
||||
|
||||
QNetworkRequest* request = new QNetworkRequest(url);
|
||||
request->setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
|
||||
QNetworkReply* reply = NULL;
|
||||
|
||||
if (r.method == QNetworkAccessManager::GetOperation) {
|
||||
// TODO
|
||||
//manager->get(QNetworkRequest(QUrl("http://qt-project.org")));
|
||||
}
|
||||
|
||||
if (r.method == QNetworkAccessManager::PostOperation) {
|
||||
reply = manager_.post(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
|
||||
}
|
||||
|
||||
if (r.method == QNetworkAccessManager::PutOperation) {
|
||||
reply = manager_.put(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
|
||||
}
|
||||
|
||||
if (!reply) {
|
||||
qWarning() << "WebApi::processQueue(): reply object was not created - invalid request method";
|
||||
return;
|
||||
}
|
||||
|
||||
r.reply = reply;
|
||||
r.request = request;
|
||||
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(request_error(QNetworkReply::NetworkError)));
|
||||
|
||||
QStringList cmd;
|
||||
cmd << "curl";
|
||||
if (r.method == QNetworkAccessManager::PutOperation) {
|
||||
cmd << "-X" << "PUT";
|
||||
cmd << "--data" << "'" + r.data.toString(QUrl::FullyEncoded) + "'";
|
||||
cmd << url;
|
||||
}
|
||||
|
||||
//qDebug().noquote() << cmd.join(" ");
|
||||
|
||||
inProgressRequests_.push_back(r);
|
||||
}
|
||||
|
||||
void WebApi::request_finished(QNetworkReply *reply) {
|
||||
QByteArray responseBodyBA = reply->readAll();
|
||||
QJsonObject response;
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseBodyBA, &err);
|
||||
if (err.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Could not parse JSON:" << err.errorString();
|
||||
qWarning().noquote() << QString(responseBodyBA);
|
||||
} else {
|
||||
response = doc.object();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < inProgressRequests_.size(); i++) {
|
||||
QueuedRequest r = inProgressRequests_[i];
|
||||
if (r.reply == reply) {
|
||||
inProgressRequests_.erase(inProgressRequests_.begin() + i);
|
||||
emit requestDone(response, r.tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
processQueue();
|
||||
}
|
||||
|
||||
void WebApi::request_error(QNetworkReply::NetworkError e) {
|
||||
qDebug() << "Network error" << e;
|
||||
}
|
55
QtClient/JoplinQtClient/webapi.h
Executable file
55
QtClient/JoplinQtClient/webapi.h
Executable file
@ -0,0 +1,55 @@
|
||||
#ifndef WEBAPI_H
|
||||
#define WEBAPI_H
|
||||
|
||||
#include <stable.h>
|
||||
|
||||
namespace jop {
|
||||
|
||||
class WebApi : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
struct QueuedRequest {
|
||||
QNetworkAccessManager::Operation method;
|
||||
QString path;
|
||||
QUrlQuery query;
|
||||
QUrlQuery data;
|
||||
QNetworkReply* reply;
|
||||
QNetworkRequest* request;
|
||||
QString tag;
|
||||
};
|
||||
|
||||
WebApi(const QString& baseUrl);
|
||||
QString baseUrl() const;
|
||||
void execRequest(QNetworkAccessManager::Operation method, const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
|
||||
void post(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
|
||||
void get(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
|
||||
void put(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
|
||||
//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
|
||||
void setSessionId(const QString& v);
|
||||
|
||||
private:
|
||||
|
||||
QString baseUrl_;
|
||||
QList<QueuedRequest> queuedRequests_;
|
||||
QList<QueuedRequest> inProgressRequests_;
|
||||
void processQueue();
|
||||
QString sessionId_;
|
||||
QNetworkAccessManager manager_;
|
||||
|
||||
public slots:
|
||||
|
||||
void request_finished(QNetworkReply* reply);
|
||||
void request_error(QNetworkReply::NetworkError e);
|
||||
|
||||
signals:
|
||||
|
||||
void requestDone(const QJsonObject& response, const QString& tag);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // WEBAPI_H
|
0
app/data/uploads/.gitkeep
Normal file → Executable file
0
app/data/uploads/.gitkeep
Normal file → Executable file
@ -66,7 +66,11 @@ abstract class ApiController extends Controller {
|
||||
protected function session() {
|
||||
if ($this->useTestUserAndSession) {
|
||||
$session = Session::find(Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'));
|
||||
if ($session) $session->delete();
|
||||
if ($session) return $session;
|
||||
// if ($session) {
|
||||
// $ok = $session->delete();
|
||||
// if (!$ok) throw new \Exception("Cannot delete session");
|
||||
// }
|
||||
$session = new Session();
|
||||
$session->id = Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB');
|
||||
$session->owner_id = Session::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
||||
@ -75,7 +79,6 @@ abstract class ApiController extends Controller {
|
||||
return $session;
|
||||
}
|
||||
|
||||
|
||||
if ($this->session) return $this->session;
|
||||
$request = $this->container->get('request_stack')->getCurrentRequest();
|
||||
$this->session = Session::find(BaseModel::unhex($request->query->get('session')));
|
||||
@ -142,37 +145,49 @@ abstract class ApiController extends Controller {
|
||||
protected function patchParameters() {
|
||||
$output = array();
|
||||
$input = file_get_contents('php://input');
|
||||
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
|
||||
$boundary = $matches[1];
|
||||
$blocks = preg_split("/-+$boundary/", $input);
|
||||
array_pop($blocks);
|
||||
foreach ($blocks as $id => $block) {
|
||||
if (empty($block)) continue;
|
||||
|
||||
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
|
||||
// Two content types are supported:
|
||||
//
|
||||
// multipart/form-data; boundary=------------------------68670b1a1565e787
|
||||
// application/x-www-form-urlencoded
|
||||
|
||||
// parse uploaded files
|
||||
if (strpos($block, 'application/octet-stream') !== FALSE) {
|
||||
// match "name", then everything after "stream" (optional) except for prepending newlines
|
||||
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
|
||||
} else {
|
||||
// match "name" and optional value in between newline sequences
|
||||
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
|
||||
}
|
||||
if (!isset($matches[2])) {
|
||||
// Regex above will not find anything if the parameter has not value. For example
|
||||
// "parent_id" below:
|
||||
if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded') {
|
||||
parse_str($input, $output);
|
||||
} else {
|
||||
if (!isset($_SERVER['CONTENT_TYPE'])) throw new \Exception("Cannot decode input data");
|
||||
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
|
||||
if (!isset($matches[1])) throw new \Exception("Cannot decode input data");
|
||||
$boundary = $matches[1];
|
||||
$blocks = preg_split("/-+$boundary/", $input);
|
||||
array_pop($blocks);
|
||||
foreach ($blocks as $id => $block) {
|
||||
if (empty($block)) continue;
|
||||
|
||||
// Content-Disposition: form-data; name="parent_id"
|
||||
//
|
||||
//
|
||||
// Content-Disposition: form-data; name="id"
|
||||
//
|
||||
// 54ad197be333c98778c7d6f49506efcb
|
||||
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
|
||||
|
||||
$output[$matches[1]] = '';
|
||||
} else {
|
||||
$output[$matches[1]] = $matches[2];
|
||||
// parse uploaded files
|
||||
if (strpos($block, 'application/octet-stream') !== FALSE) {
|
||||
// match "name", then everything after "stream" (optional) except for prepending newlines
|
||||
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
|
||||
} else {
|
||||
// match "name" and optional value in between newline sequences
|
||||
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
|
||||
}
|
||||
if (!isset($matches[2])) {
|
||||
// Regex above will not find anything if the parameter has no value. For example
|
||||
// "parent_id" below:
|
||||
|
||||
// Content-Disposition: form-data; name="parent_id"
|
||||
//
|
||||
//
|
||||
// Content-Disposition: form-data; name="id"
|
||||
//
|
||||
// 54ad197be333c98778c7d6f49506efcb
|
||||
|
||||
$output[$matches[1]] = '';
|
||||
} else {
|
||||
$output[$matches[1]] = $matches[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,18 +36,10 @@ class FoldersController extends ApiController {
|
||||
}
|
||||
|
||||
if ($request->isMethod('PUT')) {
|
||||
// TODO: call fromPublicArray() - handles unhex conversion
|
||||
|
||||
$data = $this->putParameters();
|
||||
$isNew = !$folder;
|
||||
if ($isNew) $folder = new Folder();
|
||||
foreach ($data as $n => $v) {
|
||||
if ($n == 'parent_id') $v = Folder::unhex($v);
|
||||
$folder->{$n} = $v;
|
||||
}
|
||||
$folder->owner_id = $this->user()->id;
|
||||
if (!$folder) $folder = new Folder();
|
||||
$folder->fromPublicArray($this->putParameters());
|
||||
$folder->id = Folder::unhex($id);
|
||||
$folder->setIsNew($isNew);
|
||||
$folder->owner_id = $this->user()->id;
|
||||
$folder->save();
|
||||
return static::successResponse($folder);
|
||||
}
|
||||
|
@ -37,16 +37,10 @@ class NotesController extends ApiController {
|
||||
}
|
||||
|
||||
if ($request->isMethod('PUT')) {
|
||||
$data = $this->putParameters();
|
||||
$isNew = !$note;
|
||||
if ($isNew) $note = new Note();
|
||||
foreach ($data as $n => $v) {
|
||||
if ($n == 'parent_id') $v = Note::unhex($v);
|
||||
$note->{$n} = $v;
|
||||
}
|
||||
if (!$note) $note = new Note();
|
||||
$note->fromPublicArray($this->putParameters());
|
||||
$note->id = Note::unhex($id);
|
||||
$note->owner_id = $this->user()->id;
|
||||
$note->setIsNew($isNew);
|
||||
$note->save();
|
||||
return static::successResponse($note);
|
||||
}
|
||||
|
@ -25,161 +25,6 @@ class UsersController extends ApiController {
|
||||
* @Route("/users")
|
||||
*/
|
||||
public function allAction(Request $request) {
|
||||
|
||||
|
||||
|
||||
$source = "This is the first line.\n\nThis is the second line.";
|
||||
$target1 = "This is the first line XXX.\n\nThis is the second line.";
|
||||
$target2 = "This is the first line.\n\nThis is the second line YYY.";
|
||||
|
||||
$r = Diff::merge3($source, $target1, $target2);
|
||||
var_dump($r);die();
|
||||
|
||||
|
||||
// $dmp = new DiffMatchPatch();
|
||||
// $patches = $dmp->patch_make($source, $target1);
|
||||
// // @@ -1,11 +1,12 @@
|
||||
// // Th
|
||||
// // -e
|
||||
// // +at
|
||||
// // quick b
|
||||
// // @@ -22,18 +22,17 @@
|
||||
// // jump
|
||||
// // -s
|
||||
// // +ed
|
||||
// // over
|
||||
// // -the
|
||||
// // +a
|
||||
// // laz
|
||||
// $result = $dmp->patch_apply($patches, $target2);
|
||||
// var_dump($result);
|
||||
// die();
|
||||
|
||||
// $dmp = new DiffMatchPatch();
|
||||
|
||||
// $source = "This is the first line.\n\nThis is the second line.";
|
||||
// $target1 = "This is the first line XXX.\n\nThis is the second line.";
|
||||
// $target2 = "edsùfrklq lkzerlmk zemlkrmzlkerm lze.";
|
||||
|
||||
|
||||
// $diff1 = $dmp->patch_make($source, $target1);
|
||||
// $diff2 = $dmp->patch_make($source, $target2);
|
||||
|
||||
// //var_dump($dmp->patch_toText($diff1));
|
||||
// // //var_dump($diff1[0]->patch_toText());
|
||||
|
||||
// $r = $dmp->patch_apply($diff1, $source);
|
||||
// $r = $dmp->patch_apply($diff1, $target2);
|
||||
// var_dump($r);die();
|
||||
|
||||
// $r = $dmp->patch_apply($diff2, $r[0]);
|
||||
|
||||
// var_dump($r);
|
||||
|
||||
|
||||
// $dmp = new DiffMatchPatch();
|
||||
// $patches = $dmp->patch_make($source, $target1);
|
||||
// // @@ -1,11 +1,12 @@
|
||||
// // Th
|
||||
// // -e
|
||||
// // +at
|
||||
// // quick b
|
||||
// // @@ -22,18 +22,17 @@
|
||||
// // jump
|
||||
// // -s
|
||||
// // +ed
|
||||
// // over
|
||||
// // -the
|
||||
// // +a
|
||||
// // laz
|
||||
// $result = $dmp->patch_apply($patches, $target2);
|
||||
// var_dump($result);
|
||||
|
||||
// die();
|
||||
|
||||
// $r = Diff::merge($source, $target1, $target2);
|
||||
// var_dump($r);die();
|
||||
|
||||
// $diff1 = xdiff_string_diff($source, $target1);
|
||||
// $diff2 = xdiff_string_diff($source, $target2);
|
||||
|
||||
// $errors = array();
|
||||
// $t = xdiff_string_merge3($source , $target1, $target2, $errors);
|
||||
// var_dump($errors);
|
||||
// var_dump($t);die();
|
||||
|
||||
// var_dump($diff1);
|
||||
// var_dump($diff2);
|
||||
|
||||
// $errors = array();
|
||||
// $t = xdiff_string_patch($source, $diff1, XDIFF_PATCH_NORMAL, $errors);
|
||||
// var_dump($t);
|
||||
// var_dump($errors);
|
||||
|
||||
// $errors = array();
|
||||
// $t = xdiff_string_patch($t, $diff2, XDIFF_PATCH_NORMAL, $errors);
|
||||
// var_dump($t);
|
||||
// var_dump($errors);
|
||||
|
||||
|
||||
|
||||
// var_dump($diff1);
|
||||
// var_dump($diff2);
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->delta = 'salut ca va';
|
||||
// $change->save();
|
||||
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut, ça va ? oui très bien');
|
||||
// $change->save();
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut - oui très bien');
|
||||
// $change->save();
|
||||
|
||||
// $change = new Change();
|
||||
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||
// $change->createDelta('salut, ça va ? oui bien');
|
||||
// $change->save();
|
||||
|
||||
|
||||
|
||||
$d = Change::fullFieldText(BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), BaseItem::enumId('field', 'title'));
|
||||
var_dump($d);die();
|
||||
|
||||
|
||||
|
||||
die();
|
||||
|
||||
|
||||
// $fineDiff = $this->get('app.fine_diff');
|
||||
// $opcodes = $fineDiff->getDiffOpcodes('salut ca va', 'salut va?');
|
||||
// var_dump($opcodes);
|
||||
// $merged = $fineDiff->renderToTextFromOpcodes('salut ca va', $opcodes);
|
||||
// var_dump($merged);
|
||||
// die();
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$user = new User();
|
||||
$data = $request->request->all();
|
||||
|
@ -14,7 +14,7 @@ class Eloquent {
|
||||
'host' => 'localhost',
|
||||
'database' => 'notes',
|
||||
'username' => 'root',
|
||||
'password' => 'pass',
|
||||
'password' => '',
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
@ -8,7 +8,7 @@ class BaseItem extends BaseModel {
|
||||
public $incrementing = false;
|
||||
|
||||
static protected $enums = array(
|
||||
'type' => array('folder', 'note', 'todo', 'tag'),
|
||||
'type' => array('folder', 'note', 'tag'),
|
||||
);
|
||||
|
||||
public function itemTypeId() {
|
||||
|
@ -322,7 +322,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
|
||||
if ($isNew) $this->created_time = time();
|
||||
|
||||
parent::save($options);
|
||||
$output = parent::save($options);
|
||||
|
||||
$this->isNew = null;
|
||||
|
||||
@ -330,14 +330,18 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
|
||||
}
|
||||
$this->changedVersionedFieldValues = array();
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
parent::delete();
|
||||
$output = parent::delete();
|
||||
|
||||
if (count($this->versionedFields)) {
|
||||
$this->recordChanges('delete');
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function recordChanges($type, $versionedData = array()) {
|
||||
|
@ -80,12 +80,14 @@ class Change extends BaseModel {
|
||||
$revId = 0;
|
||||
for ($i = 0; $i < count($changes); $i++) {
|
||||
$change = $changes[$i];
|
||||
$result = Diff::patch($output, $change->delta);
|
||||
if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
|
||||
if (!$result[1][0]) {
|
||||
// Could not patch the string. TODO: handle conflict
|
||||
if (!empty($change->delta)) {
|
||||
$result = Diff::patch($output, $change->delta);
|
||||
if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
|
||||
if (!$result[1][0]) {
|
||||
// Could not patch the string. TODO: handle conflict
|
||||
}
|
||||
$output = $result[0];
|
||||
}
|
||||
$output = $result[0];
|
||||
|
||||
$revId = $change->id;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ class FolderItem extends BaseModel {
|
||||
public $incrementing = false;
|
||||
|
||||
static protected $enums = array(
|
||||
'type' => array('folder', 'note', 'todo'),
|
||||
'type' => array('folder', 'note'),
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class User extends BaseModel {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function byEmail($email) {
|
||||
static public function byEmail($email) {
|
||||
return self::where('email', '=', $email)->first();
|
||||
}
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\AppBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
public function testIndex()
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/');
|
||||
|
||||
$this->assertEquals(200, $client->getResponse()->getStatusCode());
|
||||
$this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
|
||||
}
|
||||
}
|
@ -48,6 +48,27 @@ class ChangeTest extends BaseTestCase {
|
||||
$this->assertEquals($r, $text2);
|
||||
}
|
||||
|
||||
public function testSame() {
|
||||
$note = new Note();
|
||||
$note->fromPublicArray(array('body' => 'test'));
|
||||
$note->owner_id = $this->userId();
|
||||
$note->save();
|
||||
|
||||
$noteId = $note->id;
|
||||
|
||||
$note = Note::find($noteId);
|
||||
|
||||
$this->assertEquals('test', $note->versionedFieldValue('body'));
|
||||
|
||||
$note->fromPublicArray(array('body' => 'test'));
|
||||
$note->owner_id = $this->userId();
|
||||
$note->save();
|
||||
|
||||
$note = Note::find($noteId);
|
||||
|
||||
$this->assertEquals('test', $note->versionedFieldValue('body'));
|
||||
}
|
||||
|
||||
public function testDiff3Ways() {
|
||||
// Scenario where two different clients change the same note at the same time.
|
||||
//
|
||||
|
@ -2,23 +2,29 @@
|
||||
|
||||
require_once dirname(__FILE__) . '/BaseTestCase.php';
|
||||
|
||||
$dbName = 'notes_test';
|
||||
$dbConfig = array(
|
||||
'dbName' => 'notes_test',
|
||||
'user' => 'root',
|
||||
'password' => '',
|
||||
'host' => '127.0.0.1',
|
||||
);
|
||||
|
||||
$structureFile = dirname(dirname(__FILE__)) . '/structure.sql';
|
||||
|
||||
$cmd = sprintf("mysql -u root -ppass -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbName, $dbName);
|
||||
$cmd = sprintf("mysql -u %s %s -h %s -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $dbConfig['dbName']);
|
||||
exec($cmd);
|
||||
|
||||
$cmd = sprintf("mysql -u root -ppass %s < '%s'", $dbName, $structureFile);
|
||||
$cmd = sprintf("mysql -u %s %s -h %s %s < '%s'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $structureFile);
|
||||
exec($cmd);
|
||||
|
||||
$capsule = new \Illuminate\Database\Capsule\Manager();
|
||||
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'database' => $dbName,
|
||||
'username' => 'root',
|
||||
'password' => 'pass',
|
||||
'host' => $dbConfig['host'],
|
||||
'database' => $dbConfig['dbName'],
|
||||
'username' => $dbConfig['user'],
|
||||
'password' => $dbConfig['password'],
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
Loading…
Reference in New Issue
Block a user