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

KDE's Doxygen guidelines are available online.