Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsgeopackageprojectstorage.cpp
3 : : ---------------------
4 : : begin : March 2019
5 : : copyright : (C) 2019 by Alessandro Pasotti
6 : : email : elpaso at itopen dot it
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include "qgsgeopackageprojectstorage.h"
17 : : ///@cond PRIVATE
18 : :
19 : : #include <sqlite3.h>
20 : : #include <QUrlQuery>
21 : : #include <QUrl>
22 : : #include <QIODevice>
23 : : #include <QJsonDocument>
24 : : #include <QJsonObject>
25 : : #include <QFile>
26 : : #include <QRegularExpression>
27 : :
28 : : #include "qgsmessagelog.h"
29 : : #include "qgssqliteutils.h"
30 : : #include "qgsreadwritecontext.h"
31 : : #include "qgsapplication.h"
32 : : #include "qgsogrutils.h"
33 : : #include "qgsproject.h"
34 : :
35 : :
36 : 0 : static bool _parseMetadataDocument( const QJsonDocument &doc, QgsProjectStorage::Metadata &metadata )
37 : : {
38 : 0 : if ( !doc.isObject() )
39 : 0 : return false;
40 : :
41 : 0 : QJsonObject docObj = doc.object();
42 : 0 : metadata.lastModified = QDateTime();
43 : 0 : if ( docObj.contains( "last_modified_time" ) )
44 : : {
45 : 0 : QString lastModifiedTimeStr = docObj["last_modified_time"].toString();
46 : 0 : if ( !lastModifiedTimeStr.isEmpty() )
47 : : {
48 : 0 : QDateTime lastModifiedUtc = QDateTime::fromString( lastModifiedTimeStr, Qt::ISODate );
49 : 0 : lastModifiedUtc.setTimeSpec( Qt::UTC );
50 : 0 : metadata.lastModified = lastModifiedUtc.toLocalTime();
51 : 0 : }
52 : 0 : }
53 : 0 : return true;
54 : 0 : }
55 : :
56 : 0 : static bool _projectsTableExists( const QString &database )
57 : : {
58 : 0 : QString errCause;
59 : 0 : bool ok { false };
60 : 0 : sqlite3_database_unique_ptr db;
61 : 0 : sqlite3_statement_unique_ptr statement;
62 : :
63 : 0 : int status = db.open_v2( database, SQLITE_OPEN_READWRITE, nullptr );
64 : 0 : if ( status != SQLITE_OK )
65 : : {
66 : 0 : errCause = QObject::tr( "There was an error opening the database <b>%1</b>: %2" )
67 : 0 : .arg( database,
68 : 0 : QString::fromUtf8( sqlite3_errmsg( db.get() ) ) );
69 : 0 : }
70 : : else
71 : : {
72 : 0 : statement = db.prepare( QStringLiteral( "SELECT count(*) FROM sqlite_master WHERE type ='table' AND name = 'qgis_projects'" ), status );
73 : 0 : if ( status == SQLITE_OK )
74 : : {
75 : 0 : if ( sqlite3_step( statement.get() ) == SQLITE_ROW )
76 : : {
77 : 0 : ok = QString::fromUtf8( reinterpret_cast< const char * >( sqlite3_column_text( statement.get(), 0 ) ) ) == '1';
78 : 0 : }
79 : : else
80 : : {
81 : 0 : errCause = QObject::tr( "There was an error querying the database <b>%1</b>: %2" )
82 : 0 : .arg( database,
83 : 0 : QString::fromUtf8( sqlite3_errmsg( db.get() ) ) );
84 : :
85 : : }
86 : 0 : }
87 : : else
88 : : {
89 : 0 : errCause = QObject::tr( "There was an error querying the database <b>%1</b>: %2" )
90 : 0 : .arg( database,
91 : 0 : QString::fromUtf8( sqlite3_errmsg( db.get() ) ) );
92 : :
93 : : }
94 : 0 : if ( ! errCause.isEmpty() )
95 : 0 : QgsMessageLog::logMessage( errCause, QStringLiteral( "OGR" ), Qgis::MessageLevel::Info );
96 : : }
97 : 0 : return ok;
98 : 0 : }
99 : :
100 : 0 : QStringList QgsGeoPackageProjectStorage::listProjects( const QString &uri )
101 : : {
102 : 0 : QStringList lst;
103 : 0 : QString errCause;
104 : :
105 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
106 : 0 : if ( !projectUri.valid || ! _projectsTableExists( projectUri.database ) )
107 : 0 : return lst;
108 : :
109 : 0 : sqlite3_database_unique_ptr database;
110 : 0 : sqlite3_statement_unique_ptr statement;
111 : :
112 : 0 : int status = database.open_v2( projectUri.database, SQLITE_OPEN_READWRITE, nullptr );
113 : 0 : if ( status != SQLITE_OK )
114 : : {
115 : 0 : errCause = QObject::tr( "There was an error opening the database <b>%1</b>: %2" )
116 : 0 : .arg( projectUri.database,
117 : 0 : QString::fromUtf8( sqlite3_errmsg( database.get() ) ) );
118 : 0 : }
119 : : else
120 : : {
121 : 0 : statement = database.prepare( QStringLiteral( "SELECT name FROM qgis_projects" ), status );
122 : 0 : if ( status == SQLITE_OK )
123 : : {
124 : 0 : while ( sqlite3_step( statement.get() ) == SQLITE_ROW )
125 : : {
126 : 0 : lst << QString::fromUtf8( reinterpret_cast< const char * >( sqlite3_column_text( statement.get(), 0 ) ) );
127 : : }
128 : 0 : }
129 : : else
130 : : {
131 : 0 : errCause = QObject::tr( "There was an error querying the database <b>%1</b>: %2" )
132 : 0 : .arg( projectUri.database,
133 : 0 : QString::fromUtf8( sqlite3_errmsg( database.get() ) ) );
134 : : }
135 : : }
136 : 0 : if ( ! errCause.isEmpty() )
137 : 0 : QgsMessageLog::logMessage( errCause, QStringLiteral( "OGR" ), Qgis::MessageLevel::Info );
138 : 0 : return lst;
139 : 0 : }
140 : :
141 : 0 : bool QgsGeoPackageProjectStorage::readProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context )
142 : : {
143 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
144 : 0 : if ( !projectUri.valid )
145 : : {
146 : 0 : context.pushMessage( QObject::tr( "Invalid URI for GeoPackage OGR provider: " ) + uri, Qgis::Critical );
147 : 0 : return false;
148 : : }
149 : :
150 : 0 : QString errCause;
151 : 0 : QString xml;
152 : 0 : bool ok = false;
153 : 0 : sqlite3_database_unique_ptr database;
154 : 0 : sqlite3_statement_unique_ptr statement;
155 : :
156 : 0 : int status = database.open_v2( projectUri.database, SQLITE_OPEN_READWRITE, nullptr );
157 : 0 : if ( status != SQLITE_OK )
158 : : {
159 : 0 : context.pushMessage( QObject::tr( "Could not connect to the database: " ) + projectUri.database, Qgis::Critical );
160 : 0 : return false;
161 : : }
162 : : else
163 : : {
164 : 0 : statement = database.prepare( QStringLiteral( "SELECT content FROM qgis_projects WHERE name = %1" )
165 : 0 : .arg( QgsSqliteUtils::quotedValue( projectUri.projectName ) ), status );
166 : 0 : if ( status == SQLITE_OK )
167 : : {
168 : 0 : if ( sqlite3_step( statement.get() ) == SQLITE_ROW )
169 : : {
170 : 0 : xml = QString::fromUtf8( reinterpret_cast< const char * >( sqlite3_column_text( statement.get(), 0 ) ) );
171 : 0 : QString hexEncodedContent( xml );
172 : 0 : QByteArray binaryContent( QByteArray::fromHex( hexEncodedContent.toUtf8() ) );
173 : 0 : device->write( binaryContent );
174 : 0 : device->seek( 0 );
175 : 0 : ok = true;
176 : 0 : }
177 : : else
178 : : {
179 : 0 : errCause = QObject::tr( "There was an error querying the database <b>%1</b>: %2" )
180 : 0 : .arg( projectUri.database,
181 : 0 : QString::fromUtf8( sqlite3_errmsg( database.get() ) ) );
182 : :
183 : : }
184 : 0 : }
185 : : else
186 : : {
187 : 0 : errCause = QObject::tr( "There was an error querying the database <b>%1</b>: %2" )
188 : 0 : .arg( projectUri.database,
189 : 0 : QString::fromUtf8( sqlite3_errmsg( database.get() ) ) );
190 : : }
191 : : }
192 : : // TODO: do not log if table does not exists
193 : 0 : if ( ! errCause.isEmpty() )
194 : 0 : QgsMessageLog::logMessage( errCause, QStringLiteral( "OGR" ), Qgis::MessageLevel::Info );
195 : :
196 : 0 : return ok;
197 : :
198 : 0 : }
199 : :
200 : 0 : bool QgsGeoPackageProjectStorage::writeProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context )
201 : : {
202 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
203 : :
204 : 0 : QString errCause;
205 : :
206 : 0 : if ( !QFile::exists( projectUri.database ) )
207 : : {
208 : 0 : OGRSFDriverH hGpkgDriver = OGRGetDriverByName( "GPKG" );
209 : 0 : if ( !hGpkgDriver )
210 : : {
211 : 0 : errCause = QObject::tr( "GeoPackage driver not found." );
212 : 0 : }
213 : : else
214 : : {
215 : 0 : gdal::ogr_datasource_unique_ptr hDS( OGR_Dr_CreateDataSource( hGpkgDriver, projectUri.database.toUtf8().constData(), nullptr ) );
216 : 0 : if ( !hDS )
217 : 0 : errCause = QObject::tr( "Creation of database failed (OGR error: %1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) );
218 : 0 : }
219 : 0 : }
220 : :
221 : 0 : if ( errCause.isEmpty() && !_projectsTableExists( projectUri.database ) )
222 : : {
223 : 0 : errCause = _executeSql( projectUri.database, QStringLiteral( "CREATE TABLE qgis_projects(name TEXT PRIMARY KEY, metadata BLOB, content BLOB)" ) );
224 : 0 : }
225 : :
226 : 0 : if ( !errCause.isEmpty() )
227 : : {
228 : 0 : errCause = QObject::tr( "Unable to save project. It's not possible to create the destination table on the database. <b>%1</b>: %2" )
229 : 0 : .arg( projectUri.database,
230 : : errCause );
231 : :
232 : 0 : context.pushMessage( errCause, Qgis::Critical );
233 : 0 : return false;
234 : : }
235 : :
236 : : // read from device and write to the table
237 : 0 : QByteArray content = device->readAll();
238 : 0 : QString metadataExpr = QStringLiteral( "{\"last_modified_time\": \"%1\", \"last_modified_user\": \"%2\" }" ).arg(
239 : 0 : QDateTime::currentDateTime().toString( Qt::DateFormat::ISODate ),
240 : 0 : QgsApplication::instance()->userLoginName()
241 : : );
242 : 0 : QString sql;
243 : 0 : if ( listProjects( uri ).contains( projectUri.projectName ) )
244 : : {
245 : 0 : sql = QStringLiteral( "UPDATE qgis_projects SET metadata = %2, content = %3 WHERE name = %1" );
246 : 0 : }
247 : : else
248 : : {
249 : 0 : sql = QStringLiteral( "INSERT INTO qgis_projects VALUES (%1, %2, %3)" );
250 : : }
251 : 0 : sql = sql.arg( QgsSqliteUtils::quotedIdentifier( projectUri.projectName ),
252 : 0 : QgsSqliteUtils::quotedValue( metadataExpr ),
253 : 0 : QgsSqliteUtils::quotedValue( QString::fromLatin1( content.toHex() ) )
254 : : );
255 : :
256 : 0 : errCause = _executeSql( projectUri.database, sql );
257 : 0 : if ( !errCause.isEmpty() )
258 : : {
259 : 0 : errCause = QObject::tr( "Unable to insert or update project (project=%1) in the destination table on the database: %2" )
260 : 0 : .arg( uri,
261 : : errCause );
262 : :
263 : 0 : context.pushMessage( errCause, Qgis::Critical );
264 : 0 : return false;
265 : : }
266 : 0 : return true;
267 : 0 : }
268 : :
269 : 0 : QString QgsGeoPackageProjectStorage::encodeUri( const QgsGeoPackageProjectUri &gpkgUri )
270 : : {
271 : 0 : QUrl u;
272 : 0 : QUrlQuery urlQuery;
273 : 0 : u.setScheme( QStringLiteral( "geopackage" ) );
274 : :
275 : : // Check for windows network shares: github issue #31310
276 : 0 : QString database { gpkgUri.database };
277 : 0 : if ( database.startsWith( QLatin1String( "//" ) ) )
278 : : {
279 : 0 : u.setPath( database.replace( '/', '\\' ) );
280 : 0 : }
281 : : else
282 : : {
283 : 0 : u.setPath( database );
284 : : }
285 : :
286 : 0 : if ( !gpkgUri.projectName.isEmpty() )
287 : 0 : urlQuery.addQueryItem( QStringLiteral( "projectName" ), gpkgUri.projectName );
288 : 0 : u.setQuery( urlQuery );
289 : 0 : return QString::fromUtf8( u.toEncoded() );
290 : 0 : }
291 : :
292 : :
293 : 0 : QgsGeoPackageProjectUri QgsGeoPackageProjectStorage::decodeUri( const QString &uri )
294 : : {
295 : 0 : QUrl url = QUrl::fromEncoded( uri.toUtf8() );
296 : 0 : QUrlQuery urlQuery( url.query() );
297 : 0 : const QString urlAsString( url.toString( ) );
298 : :
299 : 0 : QgsGeoPackageProjectUri gpkgUri;
300 : :
301 : : // Check for windows paths: github issue #33057
302 : 0 : const QRegularExpression winLocalPath { R"(^[A-Za-z]:)" };
303 : : // Check for windows network shares: github issue #31310
304 : 0 : const QString path { ( winLocalPath.match( urlAsString ).hasMatch() ||
305 : 0 : urlAsString.startsWith( QLatin1String( "//" ) ) ) ?
306 : 0 : urlAsString :
307 : 0 : url.path() };
308 : :
309 : 0 : gpkgUri.valid = QFile::exists( path );
310 : 0 : gpkgUri.database = path;
311 : 0 : gpkgUri.projectName = urlQuery.queryItemValue( "projectName" );
312 : :
313 : 0 : return gpkgUri;
314 : 0 : }
315 : :
316 : 0 : QString QgsGeoPackageProjectStorage::filePath( const QString &uri )
317 : : {
318 : 0 : const QgsGeoPackageProjectUri gpkgUri { decodeUri( uri ) };
319 : 0 : return gpkgUri.valid ? gpkgUri.database : QString();
320 : 0 : }
321 : :
322 : :
323 : 0 : QString QgsGeoPackageProjectStorage::_executeSql( const QString &uri, const QString &sql )
324 : : {
325 : :
326 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
327 : 0 : if ( !projectUri.valid )
328 : : {
329 : 0 : return QObject::tr( "Invalid URI for GeoPackage OGR provider: %1" ).arg( uri );
330 : : }
331 : :
332 : 0 : sqlite3_database_unique_ptr db;
333 : 0 : sqlite3_statement_unique_ptr statement;
334 : :
335 : 0 : int status = db.open_v2( projectUri.database, SQLITE_OPEN_READWRITE, nullptr );
336 : 0 : if ( status != SQLITE_OK )
337 : : {
338 : 0 : return QObject::tr( "Could not connect to the database: %1" ).arg( projectUri.database );
339 : : }
340 : :
341 : 0 : QString errCause;
342 : 0 : char *errmsg = nullptr;
343 : 0 : ( void )sqlite3_exec(
344 : 0 : db.get(), /* An open database */
345 : 0 : sql.toUtf8(), /* SQL to be evaluated */
346 : : nullptr, /* Callback function */
347 : : nullptr, /* 1st argument to callback */
348 : : &errmsg /* Error msg written here */
349 : : );
350 : 0 : if ( status != SQLITE_OK || errmsg )
351 : : {
352 : 0 : errCause = QString::fromUtf8( errmsg );
353 : 0 : }
354 : 0 : return errCause;
355 : 0 : }
356 : :
357 : 0 : bool QgsGeoPackageProjectStorage::removeProject( const QString &uri )
358 : : {
359 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
360 : 0 : QString errCause = _executeSql( projectUri.database, QStringLiteral( "DELETE FROM qgis_projects WHERE name = %1" ).arg( QgsSqliteUtils::quotedValue( projectUri.projectName ) ) );
361 : 0 : if ( ! errCause.isEmpty() )
362 : : {
363 : 0 : errCause = QObject::tr( "Could not remove project %1: %2" ).arg( uri, errCause );
364 : 0 : QgsMessageLog::logMessage( errCause, QStringLiteral( "OGR" ), Qgis::MessageLevel::Warning );
365 : 0 : }
366 : 0 : else if ( QgsProject::instance()->fileName() == uri )
367 : : {
368 : 0 : QgsMessageLog::logMessage( QStringLiteral( "Current project was removed from storage, marking it dirty." ), QStringLiteral( "OGR" ), Qgis::MessageLevel::Warning );
369 : 0 : QgsProject::instance()->setDirty( true );
370 : 0 : }
371 : 0 : return errCause.isEmpty();
372 : 0 : }
373 : :
374 : 0 : bool QgsGeoPackageProjectStorage::renameProject( const QString &uri, const QString &uriNew )
375 : : {
376 : 0 : QgsGeoPackageProjectUri projectNewUri = decodeUri( uriNew );
377 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
378 : 0 : QString errCause = _executeSql( projectUri.database, QStringLiteral( "UPDATE qgis_projects SET name = %1 WHERE name = %1" )
379 : 0 : .arg( QgsSqliteUtils::quotedValue( projectUri.projectName ) )
380 : 0 : .arg( QgsSqliteUtils::quotedValue( projectNewUri.projectName ) ) );
381 : 0 : if ( ! errCause.isEmpty() )
382 : : {
383 : 0 : errCause = QObject::tr( "Could not rename project %1: %2" ).arg( uri, errCause );
384 : 0 : QgsMessageLog::logMessage( errCause, QStringLiteral( "OGR" ), Qgis::MessageLevel::Warning );
385 : 0 : }
386 : 0 : return errCause.isEmpty();
387 : 0 : }
388 : :
389 : 0 : bool QgsGeoPackageProjectStorage::readProjectStorageMetadata( const QString &uri, QgsProjectStorage::Metadata &metadata )
390 : : {
391 : 0 : QgsGeoPackageProjectUri projectUri = decodeUri( uri );
392 : 0 : if ( !projectUri.valid )
393 : 0 : return false;
394 : :
395 : 0 : bool ok = false;
396 : :
397 : 0 : sqlite3_database_unique_ptr database;
398 : 0 : sqlite3_statement_unique_ptr statement;
399 : :
400 : 0 : int status = database.open_v2( projectUri.database, SQLITE_OPEN_READWRITE, nullptr );
401 : 0 : if ( status != SQLITE_OK )
402 : : {
403 : 0 : return false;
404 : : }
405 : : else
406 : : {
407 : 0 : statement = database.prepare( QStringLiteral( "SELECT metadata FROM qgis_projects WHERE name = %1" )
408 : 0 : .arg( QgsSqliteUtils::quotedValue( projectUri.projectName ) ), status );
409 : :
410 : 0 : if ( status == SQLITE_OK )
411 : : {
412 : 0 : if ( sqlite3_step( statement.get() ) == SQLITE_ROW )
413 : : {
414 : 0 : QString metadataStr = QString::fromUtf8( reinterpret_cast< const char * >( sqlite3_column_text( statement.get(), 0 ) ) );
415 : 0 : metadata.name = projectUri.projectName;
416 : 0 : QJsonDocument doc( QJsonDocument::fromJson( metadataStr.toUtf8() ) );
417 : 0 : ok = _parseMetadataDocument( doc, metadata );
418 : 0 : }
419 : 0 : }
420 : : }
421 : :
422 : 0 : return ok;
423 : 0 : }
424 : :
425 : : ///@endcond
|