Marble

OwncloudSyncBackend.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com>
4//
5
6#include "OwncloudSyncBackend.h"
7
8#include "CloudRouteModel.h"
9#include "CloudSyncManager.h"
10#include "GeoDataData.h"
11#include "GeoDataDocument.h"
12#include "GeoDataExtendedData.h"
13#include "GeoDataFolder.h"
14#include "GeoDataParser.h"
15#include "GeoDataPlacemark.h"
16#include "GeoDocument.h"
17#include "MarbleDebug.h"
18#include "MarbleDirs.h"
19#include "MarbleModel.h"
20#include "MarbleWidget.h"
21#include "RenderPlugin.h"
22#include "Route.h"
23#include "RouteItem.h"
24#include "RoutingManager.h"
25#include "RoutingModel.h"
26
27#include <QBuffer>
28#include <QDir>
29#include <QFileInfo>
30#include <QJsonArray>
31#include <QJsonDocument>
32#include <QJsonObject>
33#include <QNetworkAccessManager>
34#include <QNetworkRequest>
35
36namespace Marble
37{
38
39class Q_DECL_HIDDEN OwncloudSyncBackend::Private
40{
41public:
42 Private(CloudSyncManager *cloudSyncManager);
43
44 QDir m_cacheDir;
45 QNetworkAccessManager m_network;
46 QNetworkReply *m_routeUploadReply = nullptr;
47 QNetworkReply *m_routeListReply = nullptr;
48 QNetworkReply *m_routeDownloadReply = nullptr;
49 QNetworkReply *m_routeDeleteReply = nullptr;
50 QNetworkReply *m_authReply = nullptr;
51
52 QList<RouteItem> m_routeList;
53
54 QString m_routeUploadEndpoint;
55 QString m_routeListEndpoint;
56 QString m_routeDownloadEndpoint;
57 QString m_routeDeleteEndpoint;
58 QString m_routePreviewEndpoint;
59
60 CloudSyncManager *m_cloudSyncManager = nullptr;
61 QUrl m_apiUrl;
62};
63
64OwncloudSyncBackend::Private::Private(CloudSyncManager *cloudSyncManager)
65 : m_cacheDir(MarbleDirs::localPath() + QLatin1StringView("/cloudsync/cache/routes/"))
66 // Route API endpoints
67 , m_routeUploadEndpoint(QStringLiteral("routes/create"))
68 , m_routeListEndpoint(QStringLiteral("routes"))
69 , m_routeDownloadEndpoint(QStringLiteral("routes"))
70 , m_routeDeleteEndpoint(QStringLiteral("routes/delete"))
71 , m_routePreviewEndpoint(QStringLiteral("routes/preview"))
72 , m_cloudSyncManager(cloudSyncManager)
73{
74}
75
76OwncloudSyncBackend::OwncloudSyncBackend(CloudSyncManager *cloudSyncManager)
77 : d(new Private(cloudSyncManager))
78{
79 connect(d->m_cloudSyncManager, &CloudSyncManager::apiUrlChanged, this, &OwncloudSyncBackend::validateSettings);
80}
81
82OwncloudSyncBackend::~OwncloudSyncBackend()
83{
84 delete d;
85}
86
87void OwncloudSyncBackend::uploadRoute(const QString &timestamp)
88{
89 QString word = QStringLiteral("----MarbleCloudBoundary");
90 QString boundary = QStringLiteral("--%0").arg(word);
91 QNetworkRequest request(endpointUrl(d->m_routeUploadEndpoint));
92 request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data; boundary=%0").arg(word));
93
94 QByteArray data;
95 data.append(QString(boundary + "\r\n").toUtf8());
96
97 // Timestamp part
98 data.append("Content-Disposition: form-data; name=\"timestamp\"");
99 data.append("\r\n\r\n");
100 data.append(QString(timestamp + QStringLiteral("\r\n")).toUtf8());
101 data.append(QString(boundary + QStringLiteral("\r\n")).toUtf8());
102
103 // Name part
104 data.append("Content-Disposition: form-data; name=\"name\"");
105 data.append("\r\n\r\n");
106 data.append(routeName(timestamp).toUtf8());
107 data.append("\r\n");
108 data.append(QString(boundary + "\r\n").toUtf8());
109
110 QFile kmlFile(d->m_cacheDir.absolutePath() + QStringLiteral("/%0.kml").arg(timestamp));
111
112 if (!kmlFile.open(QFile::ReadOnly)) {
113 mDebug() << "Could not open " << timestamp << ".kml. Either it has not been saved"
114 << " to cache for upload or another application removed it from there.";
115 return;
116 }
117
118 GeoDataParser parser(GeoData_KML);
119 if (!parser.read(&kmlFile)) {
120 mDebug() << "[OwncloudSyncBackend] KML file" << kmlFile.fileName() << "is broken so I can't fill required properties";
121 return;
122 }
123
124 auto root = dynamic_cast<GeoDataDocument *>(parser.releaseDocument());
125 if (!root || root->size() < 2) {
126 mDebug() << "[OwncloudSyncBackend] Root document is broken";
127 return;
128 }
129
130 auto doc = geodata_cast<GeoDataDocument>(root->child(1));
131 if (!doc || doc->size() < 1) {
132 mDebug() << "[OwncloudSyncBackend] Tracking document is broken";
133 return;
134 }
135
136 auto placemark = geodata_cast<GeoDataPlacemark>(doc->child(0));
137 if (!placemark) {
138 mDebug() << "[OwncloudSyncBackend] Placemark is broken";
139 return;
140 }
141
142 // Duration part
143 double duration = QTime().secsTo(QTime::fromString(placemark->extendedData().value(QStringLiteral("duration")).value().toString(), Qt::ISODate)) / 60.0;
144 mDebug() << "[Owncloud] Duration on write is" << duration;
145 data.append("Content-Disposition: form-data; name=\"duration\"");
146 data.append("\r\n\r\n");
147 data.append(QString::number(duration).toUtf8());
148 data.append("\r\n");
149 data.append(QString(boundary + "\r\n").toUtf8());
150
151 // Distance part
152 double distance = placemark->extendedData().value(QStringLiteral("length")).value().toDouble();
153 mDebug() << "[Owncloud] Distance on write is" << distance;
154 data.append("Content-Disposition: form-data; name=\"distance\"");
155 data.append("\r\n\r\n");
156 data.append(QString::number(distance).toUtf8());
157 data.append("\r\n");
158 data.append(QString(boundary + "\r\n").toUtf8());
159
160 // KML part
161 data.append(QStringLiteral("Content-Disposition: form-data; name=\"kml\"; filename=\"%0.kml\"").arg(timestamp).toUtf8());
162 data.append("\r\n");
163 data.append("Content-Type: application/vnd.google-earth.kml+xml");
164 data.append("\r\n\r\n");
165
166 kmlFile.seek(0); // just to be sure
167 data.append(kmlFile.readAll());
168 data.append("\r\n");
169 data.append(QString(boundary + "\r\n").toUtf8());
170
171 kmlFile.close();
172
173 // Preview part
174 data.append(QStringLiteral("Content-Disposition: form-data; name=\"preview\"; filename=\"%0.jpg\"").arg(timestamp).toUtf8());
175 data.append("\r\n");
176 data.append("Content-Type: image/jpg");
177 data.append("\r\n\r\n");
178
179 QByteArray previewBytes;
180 QBuffer previewBuffer(&previewBytes);
181 QPixmap preview = createPreview(timestamp);
182 preview.save(&previewBuffer, "JPG");
183
184 data.append(previewBytes);
185 data.append("\r\n");
186 data.append(QString(boundary + "\r\n").toUtf8());
187
188 d->m_routeUploadReply = d->m_network.post(request, data);
189 connect(d->m_routeUploadReply, SIGNAL(uploadProgress(qint64, qint64)), this, SIGNAL(routeUploadProgress(qint64, qint64)));
190}
191
192void OwncloudSyncBackend::downloadRouteList()
193{
194 QNetworkRequest request(endpointUrl(d->m_routeListEndpoint));
195 d->m_routeListReply = d->m_network.get(request);
196 connect(d->m_routeListReply, SIGNAL(downloadProgress(qint64, qint64)), this, SIGNAL(routeListDownloadProgress(qint64, qint64)));
197 connect(d->m_routeListReply, SIGNAL(finished()), this, SLOT(prepareRouteList()));
198}
199
200void OwncloudSyncBackend::downloadRoute(const QString &timestamp)
201{
202 QNetworkRequest routeRequest(endpointUrl(d->m_routeDownloadEndpoint, timestamp));
203 d->m_routeDownloadReply = d->m_network.get(routeRequest);
204 connect(d->m_routeDownloadReply, SIGNAL(finished()), this, SLOT(saveDownloadedRoute()));
205 connect(d->m_routeDownloadReply, SIGNAL(downloadProgress(qint64, qint64)), this, SIGNAL(routeDownloadProgress(qint64, qint64)));
206}
207
208void OwncloudSyncBackend::deleteRoute(const QString &timestamp)
209{
210 QUrl url(endpointUrl(d->m_routeDeleteEndpoint, timestamp));
211 QNetworkRequest request(url);
212 d->m_routeDeleteReply = d->m_network.deleteResource(request);
213 connect(d->m_routeDeleteReply, SIGNAL(finished()), this, SIGNAL(routeDeleted()));
214}
215
216QPixmap OwncloudSyncBackend::createPreview(const QString &timestamp) const
217{
218 MarbleWidget mapWidget;
219 for (RenderPlugin *plugin : mapWidget.renderPlugins()) {
220 plugin->setEnabled(false);
221 }
222
223 mapWidget.setProjection(Mercator);
224 mapWidget.setMapThemeId(QStringLiteral("earth/openstreetmap/openstreetmap.dgml"));
225 mapWidget.resize(512, 512);
226
227 RoutingManager *manager = mapWidget.model()->routingManager();
228 manager->loadRoute(d->m_cacheDir.absolutePath() + QStringLiteral("/%0.kml").arg(timestamp));
229 GeoDataLatLonBox const bbox = manager->routingModel()->route().bounds();
230
231 if (!bbox.isEmpty()) {
232 mapWidget.centerOn(bbox);
233 }
234
235 QPixmap pixmap = mapWidget.grab();
236 QDir(d->m_cacheDir.absolutePath()).mkpath(QStringLiteral("preview"));
237 pixmap.save(d->m_cacheDir.absolutePath() + QLatin1StringView("/preview/") + timestamp + QLatin1StringView(".jpg"));
238
239 return pixmap;
240}
241
242QString OwncloudSyncBackend::routeName(const QString &timestamp) const
243{
244 QFile file(d->m_cacheDir.absolutePath() + QStringLiteral("/%0.kml").arg(timestamp));
245 file.open(QFile::ReadOnly);
246
247 GeoDataParser parser(GeoData_KML);
248 if (!parser.read(&file)) {
249 mDebug() << "Could not read " << timestamp << ".kml. Timestamp will be used as "
250 << "route name because of the problem";
251 return timestamp;
252 }
253 file.close();
254
255 QString routeName;
256 GeoDocument *geoDoc = parser.releaseDocument();
257 auto container = dynamic_cast<GeoDataDocument *>(geoDoc);
258 if (container && !container->isEmpty()) {
259 GeoDataFolder *folder = container->folderList().at(0);
260 for (GeoDataPlacemark *placemark : folder->placemarkList()) {
261 routeName.append(placemark->name());
262 routeName.append(QStringLiteral(" - "));
263 }
264 }
265
266 return routeName.left(routeName.length() - 3);
267}
268
269void OwncloudSyncBackend::validateSettings()
270{
271 if (!d->m_cloudSyncManager->owncloudServer().isEmpty() && !d->m_cloudSyncManager->owncloudUsername().isEmpty()
272 && !d->m_cloudSyncManager->owncloudPassword().isEmpty()) {
273 QNetworkRequest request(endpointUrl(d->m_routeListEndpoint));
274 d->m_authReply = d->m_network.get(request);
275 connect(d->m_authReply, &QNetworkReply::finished, this, &OwncloudSyncBackend::checkAuthReply);
276 connect(d->m_authReply, &QNetworkReply::errorOccurred, this, &OwncloudSyncBackend::checkAuthError);
277 } else {
278 // no server, make the error field blank
279 d->m_cloudSyncManager->setStatus({}, CloudSyncManager::Success);
280 }
281}
282
283void OwncloudSyncBackend::checkAuthError(QNetworkReply::NetworkError error)
284{
286 QString const status = tr("Server '%1' could not be reached").arg(d->m_cloudSyncManager->owncloudServer());
287 d->m_cloudSyncManager->setStatus(status, CloudSyncManager::Error);
288 }
289}
290
291void OwncloudSyncBackend::checkAuthReply()
292{
293 int statusCode = d->m_authReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
294
295 if (statusCode == 0) // request was cancelled
296 return;
297
298 QString result = QString::fromLatin1(d->m_authReply->readAll());
299
300 if (!result.startsWith(QLatin1Char('{'))) {
301 // not a JSON result
302 if (result.contains(QLatin1StringView("http://owncloud.org"))) {
303 // an owncloud login page was returned, marble app is not installed
304 d->m_cloudSyncManager->setStatus(tr("The Marble app is not installed on the ownCloud server"), CloudSyncManager::Error);
305 } else {
306 d->m_cloudSyncManager->setStatus(tr("The server is not an ownCloud server"), CloudSyncManager::Error);
307 }
308 } else if (result == QLatin1StringView(R"({"message":"Current user is not logged in"})") && statusCode == 401) {
309 // credentials were incorrect
310 d->m_cloudSyncManager->setStatus(tr("Username or password are incorrect"), CloudSyncManager::Error);
311 } else if (result.contains(QStringLiteral(R"("status":"success")")) && statusCode == 200) {
312 // credentials were correct
313 d->m_cloudSyncManager->setStatus(tr("Login successful"), CloudSyncManager::Success);
314 }
315}
316
317void OwncloudSyncBackend::cancelUpload()
318{
319 d->m_routeUploadReply->abort();
320}
321
322void OwncloudSyncBackend::prepareRouteList()
323{
324 QJsonDocument jsonDoc = QJsonDocument::fromJson(d->m_routeListReply->readAll());
325 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
326
327 d->m_routeList.clear();
328
329 if (dataValue.isArray()) {
330 QJsonArray dataArray = dataValue.toArray();
331 for (int index = 0; index < dataArray.size(); ++index) {
332 QJsonObject dataObject = dataArray[index].toObject();
333
334 RouteItem route;
335 route.setIdentifier(dataObject.value(QStringLiteral("timestamp")).toString());
336 route.setName(dataObject.value(QStringLiteral("name")).toString());
337 route.setDistance(dataObject.value(QStringLiteral("distance")).toString());
338 route.setDuration(dataObject.value(QStringLiteral("duration")).toString());
339 route.setPreviewUrl(endpointUrl(d->m_routePreviewEndpoint, route.identifier()));
340 route.setOnCloud(true);
341
342 d->m_routeList.append(route);
343 }
344 }
345
346 // FIXME Find why an empty item added to the end.
347 if (!d->m_routeList.isEmpty()) {
348 d->m_routeList.remove(d->m_routeList.count() - 1);
349 }
350
351 Q_EMIT routeListDownloaded(d->m_routeList);
352}
353
354void OwncloudSyncBackend::saveDownloadedRoute()
355{
356 QString timestamp = QFileInfo(d->m_routeDownloadReply->url().toString()).fileName();
357
358 bool pathCreated = d->m_cacheDir.mkpath(d->m_cacheDir.absolutePath());
359 if (!pathCreated) {
360 mDebug() << "Couldn't create the path " << d->m_cacheDir.absolutePath() << ". Check if your user has sufficient permissions for this operation.";
361 }
362
363 QString kmlFilePath = QStringLiteral("%0/%1.kml").arg(d->m_cacheDir.absolutePath(), timestamp);
364 QFile kmlFile(kmlFilePath);
365 bool fileOpened = kmlFile.open(QFile::ReadWrite);
366
367 if (!fileOpened) {
368 mDebug() << "Failed to open file" << kmlFilePath << " for writing."
369 << " Its directory either is missing or is not writable.";
370 return;
371 }
372
373 kmlFile.write(d->m_routeDownloadReply->readAll());
374 kmlFile.close();
375
376 QString previewPath = QStringLiteral("%0/preview/").arg(d->m_cacheDir.absolutePath());
377 bool previewPathCreated = d->m_cacheDir.mkpath(previewPath);
378 if (!previewPathCreated) {
379 mDebug() << "Couldn't create the path " << previewPath << ". Check if your user has sufficient permissions for this operation.";
380 }
381
382 QString previewFilePath = QStringLiteral("%0/preview/%1.jpg").arg(d->m_cacheDir.absolutePath(), timestamp);
383 QFile previewFile(previewFilePath);
384 bool previewFileOpened = previewFile.open(QFile::ReadWrite);
385
386 if (!previewFileOpened) {
387 mDebug() << "Failed to open file" << previewFilePath << "for writing."
388 << " Its directory either is missing or is not writable.";
389 return;
390 }
391
392 QPixmap preview = createPreview(timestamp);
393 preview.save(&previewFile, "JPG");
394 previewFile.close();
395
396 Q_EMIT routeDownloaded();
397}
398
399QUrl OwncloudSyncBackend::endpointUrl(const QString &endpoint) const
400{
401 const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint;
402 return QUrl(endpointUrl);
403}
404
405QUrl OwncloudSyncBackend::endpointUrl(const QString &endpoint, const QString &parameter) const
406{
407 const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint + QLatin1Char('/') + parameter;
408 return QUrl(endpointUrl);
409}
410
411void OwncloudSyncBackend::removeFromCache(const QDir &cacheDir, const QString &timestamp)
412{
413 bool fileRemoved = QFile(QStringLiteral("%0/%1.kml").arg(cacheDir.absolutePath(), timestamp)).remove();
414 bool previewRemoved = QFile(QStringLiteral("%0/preview/%1.jpg").arg(cacheDir.absolutePath(), timestamp)).remove();
415 if (!fileRemoved || !previewRemoved) {
416 mDebug() << "Failed to remove locally cached route " << timestamp
417 << ". It might "
418 "have been removed already, or its directory is missing / not writable.";
419 }
420
421 Q_EMIT removedFromCache(timestamp);
422}
423
424}
425
426#include "moc_OwncloudSyncBackend.cpp"
This file contains the headers for MarbleModel.
This file contains the headers for MarbleWidget.
Q_SCRIPTABLE CaptureState status()
char * toString(const EngineQuery &query)
Binds a QML item to a specific geodetic location in screen coordinates.
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
QByteArray & append(QByteArrayView data)
QString absolutePath() const const
bool mkpath(const QString &dirPath) const const
bool remove()
QString fileName() const const
void append(const QJsonValue &value)
qsizetype size() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QJsonValue value(QLatin1StringView key) const const
bool isArray() const const
QJsonArray toArray() const const
void errorOccurred(QNetworkReply::NetworkError code)
bool save(QIODevice *device, const char *format, int quality) const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
QString left(qsizetype n) const const
qsizetype length() const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QTime fromString(QStringView string, QStringView format)
int secsTo(QTime t) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 12:02:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.