Marble

NewstuffModel.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2012 Dennis Nienhüser <nienhueser@kde.org>
4//
5
6#include "NewstuffModel.h"
7
8#include "MarbleDebug.h"
9#include "MarbleDirs.h"
10#include "MarbleZipReader.h"
11
12#include <QUrl>
13#include <QVector>
14#include <QTemporaryFile>
15#include <QDir>
16#include <QFuture>
17#include <QPair>
18#include <QFutureWatcher>
19#include <QtConcurrentRun>
20#include <QProcessEnvironment>
21#include <QMutexLocker>
22#include <QIcon>
23#include <QNetworkAccessManager>
24#include <QNetworkReply>
25#include <QDomDocument>
26
27namespace Marble
28{
29
30class NewstuffItem
31{
32public:
33 QString m_category;
34 QString m_name;
35 QString m_author;
36 QString m_license;
37 QString m_summary;
38 QString m_version;
39 QString m_releaseDate;
40 QUrl m_previewUrl;
41 QIcon m_preview;
42 QUrl m_payloadUrl;
43 QDomNode m_registryNode;
44 qint64 m_payloadSize;
45 qint64 m_downloadedSize;
46
47 NewstuffItem();
48
49 QString installedVersion() const;
50 QString installedReleaseDate() const;
51 bool isUpgradable() const;
52 QStringList installedFiles() const;
53
54 static bool deeperThan( const QString &one, const QString &two );
55};
56
57class FetchPreviewJob;
58
59class NewstuffModelPrivate
60{
61public:
62 enum NodeAction {
63 Append,
64 Replace
65 };
66
67 enum UserAction {
68 Install,
69 Uninstall
70 };
71
72 typedef QPair<int, UserAction> Action;
73
74 NewstuffModel* m_parent;
75
77
78 QNetworkAccessManager m_networkAccessManager;
79
80 QString m_provider;
81
83
84 QNetworkReply* m_currentReply;
85
86 QTemporaryFile* m_currentFile;
87
88 QString m_targetDirectory;
89
90 QString m_registryFile;
91
92 NewstuffModel::IdTag m_idTag;
93
94 QDomDocument m_registryDocument;
95
96 QDomElement m_root;
97
98 Action m_currentAction;
99
100 QProcess* m_unpackProcess;
101
102 QMutex m_mutex;
103
104 QList<Action> m_actionQueue;
105
106 QHash<int, QByteArray> m_roleNames;
107
108 explicit NewstuffModelPrivate( NewstuffModel* parent );
109
110 QIcon preview( int index );
111 void setPreview( int index, const QIcon &previewIcon );
112
113 void handleProviderData( QNetworkReply* reply );
114
115 static bool canExecute( const QString &executable );
116
117 void installMap();
118
119 void updateModel();
120
121 void saveRegistry();
122
123 void uninstall( int index );
124
125 static void changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action );
126
127 void readInstalledFiles( QStringList* target, const QDomNode &node );
128
129 void processQueue();
130
131 static NewstuffItem importNode( const QDomNode &node );
132
133 bool isTransitioning( int index ) const;
134
135 void unzip();
136
137 void updateRegistry(const QStringList &files);
138
139 template<class T>
140 static void readValue( const QDomNode &node, const QString &key, T* target );
141};
142
143class FetchPreviewJob
144{
145public:
146 FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index );
147
148 void run( const QByteArray &data );
149
150private:
151 NewstuffModelPrivate *const m_modelPrivate;
152 const int m_index;
153};
154
155NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 )
156{
157 // nothing to do
158}
159
160QString NewstuffItem::installedVersion() const
161{
162 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "version" );
163 if ( nodes.size() == 1 ) {
164 return nodes.at( 0 ).toElement().text();
165 }
166
167 return QString();
168}
169
170QString NewstuffItem::installedReleaseDate() const
171{
172 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "releasedate" );
173 if ( nodes.size() == 1 ) {
174 return nodes.at( 0 ).toElement().text();
175 }
176
177 return QString();
178}
179
180bool NewstuffItem::isUpgradable() const
181{
182 bool installedOk, remoteOk;
183 double const installed = installedVersion().toDouble( &installedOk );
184 double const remote= m_version.toDouble( &remoteOk );
185 return installedOk && remoteOk && remote > installed;
186}
187
188QStringList NewstuffItem::installedFiles() const
189{
190 QStringList result;
191 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "installedfile" );
192 for ( int i=0; i<nodes.count(); ++i ) {
193 result << nodes.at( i ).toElement().text();
194 }
195 return result;
196}
197
198bool NewstuffItem::deeperThan(const QString &one, const QString &two)
199{
200 return one.length() > two.length();
201}
202
203FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index ) :
204 m_modelPrivate( modelPrivate ),
205 m_index( index )
206{
207}
208
209void FetchPreviewJob::run( const QByteArray &data )
210{
211 const QImage image = QImage::fromData( data );
212
213 if ( image.isNull() )
214 return;
215
216 const QPixmap pixmap = QPixmap::fromImage( image );
217 const QIcon previewIcon( pixmap );
218 m_modelPrivate->setPreview( m_index, previewIcon );
219}
220
221NewstuffModelPrivate::NewstuffModelPrivate( NewstuffModel* parent ) : m_parent( parent ),
222 m_networkAccessManager( nullptr ), m_currentReply( nullptr ), m_currentFile( nullptr ),
223 m_idTag( NewstuffModel::PayloadTag ), m_currentAction( -1, Install ), m_unpackProcess( nullptr )
224{
225 // nothing to do
226}
227
228QIcon NewstuffModelPrivate::preview( int index )
229{
230 if ( m_items.at( index ).m_preview.isNull() ) {
231 QPixmap dummyPixmap( 136, 136 );
232 dummyPixmap.fill( Qt::transparent );
233 setPreview( index, QIcon( dummyPixmap ) );
234 QNetworkReply *reply = m_networkAccessManager.get( QNetworkRequest( m_items.at( index ).m_previewUrl ) );
235 m_networkJobs.insert( reply, new FetchPreviewJob( this, index ) );
236 }
237
238 Q_ASSERT( !m_items.at( index ).m_preview.isNull() );
239
240 return m_items.at( index ).m_preview;
241}
242
243void NewstuffModelPrivate::setPreview( int index, const QIcon &previewIcon )
244{
245 NewstuffItem &item = m_items[index];
246 item.m_preview = previewIcon;
247 const QModelIndex affected = m_parent->index( index );
248 emit m_parent->dataChanged( affected, affected );
249}
250
251void NewstuffModelPrivate::handleProviderData(QNetworkReply *reply)
252{
254 const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
255 if ( !redirectionAttribute.isNull() ) {
256 for ( int i=0; i<m_items.size(); ++i ) {
257 NewstuffItem &item = m_items[i];
258 if ( item.m_payloadUrl == reply->url() ) {
259 item.m_payloadUrl = redirectionAttribute.toUrl();
260 }
261 }
262 m_networkAccessManager.head( QNetworkRequest( redirectionAttribute.toUrl() ) );
263 return;
264 }
265
267 if ( size.isValid() ) {
268 qint64 length = size.value<qint64>();
269 for ( int i=0; i<m_items.size(); ++i ) {
270 NewstuffItem &item = m_items[i];
271 if ( item.m_payloadUrl == reply->url() ) {
272 item.m_payloadSize = length;
273 QModelIndex const affected = m_parent->index( i );
274 emit m_parent->dataChanged( affected, affected );
275 }
276 }
277 }
278 return;
279 }
280
281 FetchPreviewJob *const job = m_networkJobs.take( reply );
282
283 // check if we are redirected
284 const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
285 if ( !redirectionAttribute.isNull() ) {
286 QNetworkReply *redirectReply = m_networkAccessManager.get( QNetworkRequest( QUrl( redirectionAttribute.toUrl() ) ) );
287 if ( job ) {
288 m_networkJobs.insert( redirectReply, job );
289 }
290 return;
291 }
292
293 if ( job ) {
294 job->run( reply->readAll() );
295 delete job;
296 return;
297 }
298
299 QDomDocument xml;
300 if ( !xml.setContent( reply->readAll() ) ) {
301 mDebug() << "Cannot parse newstuff xml data ";
302 return;
303 }
304
305 m_items.clear();
306
307 QDomElement root = xml.documentElement();
308 QDomNodeList items = root.elementsByTagName( "stuff" );
309 for (int i=0 ; i < items.length(); ++i ) {
310 m_items << importNode( items.item( i ) );
311 }
312
313 updateModel();
314}
315
316bool NewstuffModelPrivate::canExecute( const QString &executable )
317{
318 QString path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("PATH"), QStringLiteral("/usr/local/bin:/usr/bin:/bin"));
319 for( const QString &dir: path.split( QLatin1Char( ':' ) ) ) {
320 QFileInfo application( QDir( dir ), executable );
321 if ( application.exists() ) {
322 return true;
323 }
324 }
325
326 return false;
327}
328
329void NewstuffModelPrivate::installMap()
330{
331 if ( m_unpackProcess ) {
332 m_unpackProcess->close();
333 delete m_unpackProcess;
334 m_unpackProcess = nullptr;
335 } else if ( m_currentFile->fileName().endsWith( QLatin1String( "zip" ) ) ) {
336 unzip();
337 }
338 else if ( m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) && canExecute( "tar" ) ) {
339 m_unpackProcess = new QProcess;
340 QObject::connect( m_unpackProcess, SIGNAL(finished(int)),
341 m_parent, SLOT(contentsListed(int)) );
342 QStringList arguments = QStringList() << "-t" << "-z" << "-f" << m_currentFile->fileName();
343 m_unpackProcess->setWorkingDirectory( m_targetDirectory );
344 m_unpackProcess->start( "tar", arguments );
345 } else {
346 if ( !m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) ) {
347 mDebug() << "Can only handle tar.gz files";
348 } else {
349 mDebug() << "Cannot extract archive: tar executable not found in PATH.";
350 }
351 }
352}
353
354void NewstuffModelPrivate::unzip()
355{
356 MarbleZipReader zipReader(m_currentFile->fileName());
357 QStringList files;
358 for(const MarbleZipReader::FileInfo &fileInfo: zipReader.fileInfoList()) {
359 files << fileInfo.filePath;
360 }
361 updateRegistry(files);
362 zipReader.extractAll(m_targetDirectory);
363 m_parent->mapInstalled(0);
364}
365
366void NewstuffModelPrivate::updateModel()
367{
368 QDomNodeList items = m_root.elementsByTagName( "stuff" );
369 for (int i=0 ; i < items.length(); ++i ) {
370 QString const key = m_idTag == NewstuffModel::PayloadTag ? "payload" : "name";
371 QDomNodeList matches = items.item( i ).toElement().elementsByTagName( key );
372 if ( matches.size() == 1 ) {
373 QString const value = matches.at( 0 ).toElement().text();
374 bool found = false;
375 for ( int j=0; j<m_items.size() && !found; ++j ) {
376 NewstuffItem &item = m_items[j];
377 if ( m_idTag == NewstuffModel::PayloadTag && item.m_payloadUrl.toString() == value ) {
378 item.m_registryNode = items.item( i );
379 found = true;
380 }
381 if ( m_idTag == NewstuffModel::NameTag && item.m_name == value ) {
382 item.m_registryNode = items.item( i );
383 found = true;
384 }
385 }
386
387 if ( !found ) {
388 // Not found in newstuff or newstuff not there yet
389 NewstuffItem item = importNode( items.item( i ) );
390 if ( m_idTag == NewstuffModel::PayloadTag ) {
391 item.m_registryNode = items.item( i );
392 } else if ( m_idTag == NewstuffModel::NameTag ) {
393 item.m_registryNode = items.item( i );
394 }
395 m_items << item;
396 }
397 }
398 }
399
400 m_parent->beginResetModel();
401 m_parent->endResetModel();
402}
403
404void NewstuffModelPrivate::saveRegistry()
405{
406 QFile output( m_registryFile );
407 if ( !output.open( QFile::WriteOnly ) ) {
408 mDebug() << "Cannot open " << m_registryFile << " for writing";
409 } else {
410 QTextStream outStream( &output );
411 outStream << m_registryDocument.toString( 2 );
412 outStream.flush();
413 output.close();
414 }
415}
416
417void NewstuffModelPrivate::uninstall( int index )
418{
419 // Delete all files first, then directories (deeper ones first)
420
421 QStringList directories;
422 QStringList const files = m_items[index].installedFiles();
423 for( const QString &file: files ) {
424 if (file.endsWith(QLatin1Char('/'))) {
425 directories << file;
426 } else {
427 QFile::remove( file );
428 }
429 }
430
431 std::sort( directories.begin(), directories.end(), NewstuffItem::deeperThan );
432 for( const QString &dir: directories ) {
433 QDir::root().rmdir( dir );
434 }
435
436 m_items[index].m_registryNode.parentNode().removeChild( m_items[index].m_registryNode );
437 m_items[index].m_registryNode.clear();
438 saveRegistry();
439}
440
441void NewstuffModelPrivate::changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action )
442{
443 if ( action == Append ) {
444 QDomNode newNode = node.appendChild( domDocument.createElement( key ) );
445 newNode.appendChild( domDocument.createTextNode( value ) );
446 } else {
447 QDomNode oldNode = node.namedItem( key );
448 if ( !oldNode.isNull() ) {
449 oldNode.removeChild( oldNode.firstChild() );
450 oldNode.appendChild( domDocument.createTextNode( value ) );
451 }
452 }
453}
454
455template<class T>
456void NewstuffModelPrivate::readValue( const QDomNode &node, const QString &key, T* target )
457{
458 QDomNodeList matches = node.toElement().elementsByTagName( key );
459 if ( matches.size() == 1 ) {
460 *target = T(matches.at( 0 ).toElement().text());
461 } else {
462 for ( int i=0; i<matches.size(); ++i ) {
463 if ( matches.at( i ).attributes().contains(QStringLiteral("lang")) &&
464 matches.at( i ).attributes().namedItem(QStringLiteral("lang")).toAttr().value() == QLatin1String("en")) {
465 *target = T(matches.at( i ).toElement().text());
466 return;
467 }
468 }
469 }
470}
471
472NewstuffModel::NewstuffModel( QObject *parent ) :
473 QAbstractListModel( parent ), d( new NewstuffModelPrivate( this ) )
474{
475 setTargetDirectory(MarbleDirs::localPath() + QLatin1String("/maps"));
476 // no default registry file
477
478 connect( &d->m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
479 this, SLOT(handleProviderData(QNetworkReply*)) );
480
482 roles[Qt::DisplayRole] = "display";
483 roles[Name] = "name";
484 roles[Author] = "author";
485 roles[License] = "license";
486 roles[Summary] = "summary";
487 roles[Version] = "version";
488 roles[ReleaseDate] = "releasedate";
489 roles[Preview] = "preview";
490 roles[Payload] = "payload";
491 roles[InstalledVersion] = "installedversion";
492 roles[InstalledReleaseDate] = "installedreleasedate";
493 roles[InstalledFiles] = "installedfiles";
494 roles[IsInstalled] = "installed";
495 roles[IsUpgradable] = "upgradable";
496 roles[Category] = "category";
497 roles[IsTransitioning] = "transitioning";
498 roles[PayloadSize] = "size";
499 roles[DownloadedSize] = "downloaded";
500 d->m_roleNames = roles;
501}
502
503NewstuffModel::~NewstuffModel()
504{
505 delete d;
506}
507
508int NewstuffModel::rowCount ( const QModelIndex &parent ) const
509{
510 if ( !parent.isValid() ) {
511 return d->m_items.size();
512 }
513
514 return 0;
515}
516
517QVariant NewstuffModel::data ( const QModelIndex &index, int role ) const
518{
519 if ( index.isValid() && index.row() >= 0 && index.row() < d->m_items.size() ) {
520 switch ( role ) {
521 case Qt::DisplayRole: return d->m_items.at( index.row() ).m_name;
522 case Qt::DecorationRole: return d->preview( index.row() );
523 case Name: return d->m_items.at( index.row() ).m_name;
524 case Author: return d->m_items.at( index.row() ).m_author;
525 case License: return d->m_items.at( index.row() ).m_license;
526 case Summary: return d->m_items.at( index.row() ).m_summary;
527 case Version: return d->m_items.at( index.row() ).m_version;
528 case ReleaseDate: return d->m_items.at( index.row() ).m_releaseDate;
529 case Preview: return d->m_items.at( index.row() ).m_previewUrl;
530 case Payload: return d->m_items.at( index.row() ).m_payloadUrl;
531 case InstalledVersion: return d->m_items.at( index.row() ).installedVersion();
532 case InstalledReleaseDate: return d->m_items.at( index.row() ).installedReleaseDate();
533 case InstalledFiles: return d->m_items.at( index.row() ).installedFiles();
534 case IsInstalled: return !d->m_items.at( index.row() ).m_registryNode.isNull();
535 case IsUpgradable: return d->m_items.at( index.row() ).isUpgradable();
536 case Category: return d->m_items.at( index.row() ).m_category;
537 case IsTransitioning: return d->isTransitioning( index.row() );
538 case PayloadSize: {
539 qint64 const size = d->m_items.at( index.row() ).m_payloadSize;
540 QUrl const url = d->m_items.at( index.row() ).m_payloadUrl;
541 if ( size < -1 && !url.isEmpty() ) {
542 d->m_items[index.row()].m_payloadSize = -1; // prevent several head requests for the same item
543 d->m_networkAccessManager.head( QNetworkRequest( url ) );
544 }
545
546 return qMax<qint64>( -1, size );
547 }
548 case DownloadedSize: return d->m_items.at( index.row() ).m_downloadedSize;
549 }
550 }
551
552 return QVariant();
553}
554
555QHash<int, QByteArray> NewstuffModel::roleNames() const
556{
557 return d->m_roleNames;
558}
559
560
561int NewstuffModel::count() const
562{
563 return rowCount();
564}
565
566void NewstuffModel::setProvider( const QString &downloadUrl )
567{
568 if ( downloadUrl == d->m_provider ) {
569 return;
570 }
571
572 d->m_provider = downloadUrl;
573 emit providerChanged();
574 d->m_networkAccessManager.get( QNetworkRequest( QUrl( downloadUrl ) ) );
575}
576
577QString NewstuffModel::provider() const
578{
579 return d->m_provider;
580}
581
582void NewstuffModel::setTargetDirectory( const QString &targetDirectory )
583{
584 if ( targetDirectory != d->m_targetDirectory ) {
585 QFileInfo targetDir( targetDirectory );
586 if ( !targetDir.exists() ) {
587 if ( !QDir::root().mkpath( targetDir.absoluteFilePath() ) ) {
588 qDebug() << "Failed to create directory " << targetDirectory << ", newstuff installation might fail.";
589 }
590 }
591
592 d->m_targetDirectory = targetDirectory;
593 emit targetDirectoryChanged();
594 }
595}
596
597QString NewstuffModel::targetDirectory() const
598{
599 return d->m_targetDirectory;
600}
601
602void NewstuffModel::setRegistryFile( const QString &filename, IdTag idTag )
603{
604 QString registryFile = filename;
605 if (registryFile.startsWith(QLatin1Char('~')) && registryFile.length() > 1) {
606 registryFile = QDir::homePath() + registryFile.mid( 1 );
607 }
608
609 if ( d->m_registryFile != registryFile ) {
610 d->m_registryFile = registryFile;
611 d->m_idTag = idTag;
612 emit registryFileChanged();
613
614 QFileInfo inputFile( registryFile );
615 if ( !inputFile.exists() ) {
616 QDir::root().mkpath( inputFile.absolutePath() );
617 d->m_registryDocument = QDomDocument( "khotnewstuff3" );
618 QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction( "xml", "version=\"1.0\" encoding=\"utf-8\"" );
619 d->m_registryDocument.appendChild( header );
620 d->m_root = d->m_registryDocument.createElement( "hotnewstuffregistry" );
621 d->m_registryDocument.appendChild( d->m_root );
622 } else {
623 QFile input( registryFile );
624 if ( !input.open( QFile::ReadOnly ) ) {
625 mDebug() << "Cannot open newstuff registry " << registryFile;
626 return;
627 }
628
629 if ( !d->m_registryDocument.setContent( &input ) ) {
630 mDebug() << "Cannot parse newstuff registry " << registryFile;
631 return;
632 }
633 input.close();
634 d->m_root = d->m_registryDocument.documentElement();
635 }
636
637 d->updateModel();
638 }
639}
640
641QString NewstuffModel::registryFile() const
642{
643 return d->m_registryFile;
644}
645
646void NewstuffModel::install( int index )
647{
648 if ( index < 0 || index >= d->m_items.size() ) {
649 return;
650 }
651
652 NewstuffModelPrivate::Action action( index, NewstuffModelPrivate::Install );
653 { // <-- do not remove, mutex locker scope
654 QMutexLocker locker( &d->m_mutex );
655 if ( d->m_actionQueue.contains( action ) ) {
656 return;
657 }
658 d->m_actionQueue << action;
659 }
660
661 d->processQueue();
662}
663
664void NewstuffModel::uninstall( int idx )
665{
666 if ( idx < 0 || idx >= d->m_items.size() ) {
667 return;
668 }
669
670 if ( d->m_items[idx].m_registryNode.isNull() ) {
671 emit uninstallationFinished( idx );
672 }
673
674 NewstuffModelPrivate::Action action( idx, NewstuffModelPrivate::Uninstall );
675 { // <-- do not remove, mutex locker scope
676 QMutexLocker locker( &d->m_mutex );
677 if ( d->m_actionQueue.contains( action ) ) {
678 return;
679 }
680 d->m_actionQueue << action;
681 }
682
683 d->processQueue();
684}
685
686void NewstuffModel::cancel( int index )
687{
688 if ( !d->isTransitioning( index ) ) {
689 return;
690 }
691
692 { // <-- do not remove, mutex locker scope
693 QMutexLocker locker( &d->m_mutex );
694 if ( d->m_currentAction.first == index ) {
695 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
696 if ( d->m_currentReply ) {
697 d->m_currentReply->abort();
698 d->m_currentReply->deleteLater();
699 d->m_currentReply = nullptr;
700 }
701
702 if ( d->m_unpackProcess ) {
703 d->m_unpackProcess->terminate();
704 d->m_unpackProcess->deleteLater();
705 d->m_unpackProcess = nullptr;
706 }
707
708 if ( d->m_currentFile ) {
709 d->m_currentFile->deleteLater();
710 d->m_currentFile = nullptr;
711 }
712
713 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
714
715 emit installationFailed( d->m_currentAction.first, tr( "Installation aborted by user." ) );
716 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
717 } else {
718 // Shall we interrupt this?
719 }
720 } else {
721 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
722 NewstuffModelPrivate::Action install( index, NewstuffModelPrivate::Install );
723 d->m_actionQueue.removeAll( install );
724 emit installationFailed( index, tr( "Installation aborted by user." ) );
725 } else {
726 NewstuffModelPrivate::Action uninstall( index, NewstuffModelPrivate::Uninstall );
727 d->m_actionQueue.removeAll( uninstall );
728 emit uninstallationFinished( index ); // do we need failed here?
729 }
730 }
731 }
732
733 d->processQueue();
734}
735
736void NewstuffModel::updateProgress( qint64 bytesReceived, qint64 bytesTotal )
737{
738 qreal const progress = qBound<qreal>( 0.0, 0.9 * bytesReceived / qreal( bytesTotal ), 1.0 );
739 emit installationProgressed( d->m_currentAction.first, progress );
740 NewstuffItem &item = d->m_items[d->m_currentAction.first];
741 item.m_payloadSize = bytesTotal;
742 if ( qreal(bytesReceived-item.m_downloadedSize)/bytesTotal >= 0.01 || progress >= 0.9 ) {
743 // Only consider download progress of 1% and more as a data change
744 item.m_downloadedSize = bytesReceived;
745 QModelIndex const affected = index( d->m_currentAction.first );
746 emit dataChanged( affected, affected );
747 }
748}
749
750void NewstuffModel::retrieveData()
751{
752 if ( d->m_currentReply && d->m_currentReply->isReadable() ) {
753 // check if we are redirected
754 const QVariant redirectionAttribute = d->m_currentReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
755 if ( !redirectionAttribute.isNull() ) {
756 d->m_currentReply = d->m_networkAccessManager.get( QNetworkRequest( redirectionAttribute.toUrl() ) );
757 QObject::connect( d->m_currentReply, SIGNAL(readyRead()), this, SLOT(retrieveData()) );
758 QObject::connect( d->m_currentReply, SIGNAL(readChannelFinished()), this, SLOT(retrieveData()) );
759 QObject::connect( d->m_currentReply, SIGNAL(downloadProgress(qint64,qint64)),
760 this, SLOT(updateProgress(qint64,qint64)) );
761 } else {
762 d->m_currentFile->write( d->m_currentReply->readAll() );
763 if ( d->m_currentReply->isFinished() ) {
764 d->m_currentReply->deleteLater();
765 d->m_currentReply = nullptr;
766 d->m_currentFile->flush();
767 d->installMap();
768 }
769 }
770 }
771}
772
773void NewstuffModel::mapInstalled( int exitStatus )
774{
775 if ( d->m_unpackProcess ) {
776 d->m_unpackProcess->deleteLater();
777 d->m_unpackProcess = nullptr;
778 }
779
780 if ( d->m_currentFile ) {
781 d->m_currentFile->deleteLater();
782 d->m_currentFile = nullptr;
783 }
784
785 emit installationProgressed( d->m_currentAction.first, 1.0 );
786 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
787 if ( exitStatus == 0 ) {
788 emit installationFinished( d->m_currentAction.first );
789 } else {
790 mDebug() << "Process exit status " << exitStatus << " indicates an error.";
791 emit installationFailed( d->m_currentAction.first , QString( "Unable to unpack file. Process exited with status code %1." ).arg( exitStatus ) );
792 }
793 QModelIndex const affected = index( d->m_currentAction.first );
794
795 { // <-- do not remove, mutex locker scope
796 QMutexLocker locker( &d->m_mutex );
797 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
798 }
799 emit dataChanged( affected, affected );
800 d->processQueue();
801}
802
803void NewstuffModel::mapUninstalled()
804{
805 QModelIndex const affected = index( d->m_currentAction.first );
806 emit uninstallationFinished( d->m_currentAction.first );
807
808 { // <-- do not remove, mutex locker scope
809 QMutexLocker locker( &d->m_mutex );
810 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
811 }
812 emit dataChanged( affected, affected );
813 d->processQueue();
814}
815
816void NewstuffModel::contentsListed( int exitStatus )
817{
818 if ( exitStatus == 0 ) {
819 QStringList const files = QString(d->m_unpackProcess->readAllStandardOutput()).split(QLatin1Char('\n'), QString::SkipEmptyParts);
820 d->updateRegistry(files);
821
822 QObject::disconnect( d->m_unpackProcess, SIGNAL(finished(int)),
823 this, SLOT(contentsListed(int)) );
824 QObject::connect( d->m_unpackProcess, SIGNAL(finished(int)),
825 this, SLOT(mapInstalled(int)) );
826 QStringList arguments = QStringList() << "-x" << "-z" << "-f" << d->m_currentFile->fileName();
827 d->m_unpackProcess->start( "tar", arguments );
828 } else {
829 mDebug() << "Process exit status " << exitStatus << " indicates an error.";
830 emit installationFailed( d->m_currentAction.first , QString( "Unable to list file contents. Process exited with status code %1." ).arg( exitStatus ) );
831
832 { // <-- do not remove, mutex locker scope
833 QMutexLocker locker( &d->m_mutex );
834 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
835 }
836 d->processQueue();
837 }
838}
839
840void NewstuffModelPrivate::updateRegistry(const QStringList &files)
841{
842 emit m_parent->installationProgressed( m_currentAction.first, 0.92 );
843 if ( !m_registryFile.isEmpty() ) {
844 NewstuffItem &item = m_items[m_currentAction.first];
845 QDomNode node = item.m_registryNode;
846 NewstuffModelPrivate::NodeAction action = node.isNull() ? NewstuffModelPrivate::Append : NewstuffModelPrivate::Replace;
847 if ( node.isNull() ) {
848 node = m_root.appendChild( m_registryDocument.createElement( "stuff" ) );
849 }
850
851 node.toElement().setAttribute( "category", m_items[m_currentAction.first].m_category );
852 changeNode( node, m_registryDocument, "name", item.m_name, action );
853 changeNode( node, m_registryDocument, "providerid", m_provider, action );
854 changeNode( node, m_registryDocument, "author", item.m_author, action );
855 changeNode( node, m_registryDocument, "homepage", QString(), action );
856 changeNode( node, m_registryDocument, "licence", item.m_license, action );
857 changeNode( node, m_registryDocument, "version", item.m_version, action );
858 QString const itemId = m_idTag == NewstuffModel::PayloadTag ? item.m_payloadUrl.toString() : item.m_name;
859 changeNode( node, m_registryDocument, "id", itemId, action );
860 changeNode( node, m_registryDocument, "releasedate", item.m_releaseDate, action );
861 changeNode( node, m_registryDocument, "summary", item.m_summary, action );
862 changeNode( node, m_registryDocument, "changelog", QString(), action );
863 changeNode( node, m_registryDocument, "preview", item.m_previewUrl.toString(), action );
864 changeNode( node, m_registryDocument, "previewBig", item.m_previewUrl.toString(), action );
865 changeNode( node, m_registryDocument, "payload", item.m_payloadUrl.toString(), action );
866 changeNode( node, m_registryDocument, "status", "installed", action );
867 m_items[m_currentAction.first].m_registryNode = node;
868
869 bool hasChildren = true;
870 while ( hasChildren ) {
871 /** @todo FIXME: fileList does not contain all elements opposed to what docs say */
872 QDomNodeList fileList = node.toElement().elementsByTagName( "installedfile" );
873 hasChildren = !fileList.isEmpty();
874 for ( int i=0; i<fileList.count(); ++i ) {
875 node.removeChild( fileList.at( i ) );
876 }
877 }
878
879 for( const QString &file: files ) {
880 QDomNode fileNode = node.appendChild( m_registryDocument.createElement( "installedfile" ) );
881 fileNode.appendChild(m_registryDocument.createTextNode(m_targetDirectory + QLatin1Char('/') + file));
882 }
883
884 saveRegistry();
885 }
886}
887
888void NewstuffModelPrivate::processQueue()
889{
890 if ( m_actionQueue.empty() || m_currentAction.first >= 0 ) {
891 return;
892 }
893
894 { // <-- do not remove, mutex locker scope
895 QMutexLocker locker( &m_mutex );
896 m_currentAction = m_actionQueue.takeFirst();
897 }
898 if ( m_currentAction.second == Install ) {
899 if ( !m_currentFile ) {
900 QFileInfo const file = m_items.at( m_currentAction.first ).m_payloadUrl.path();
901 m_currentFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/marble-XXXXXX-") + file.fileName());
902 }
903
904 if ( m_currentFile->open() ) {
905 QUrl const payload = m_items.at( m_currentAction.first ).m_payloadUrl;
906 m_currentReply = m_networkAccessManager.get( QNetworkRequest( payload ) );
907 QObject::connect( m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()) );
908 QObject::connect( m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()) );
909 QObject::connect( m_currentReply, SIGNAL(downloadProgress(qint64,qint64)),
910 m_parent, SLOT(updateProgress(qint64,qint64)) );
911 /** @todo: handle download errors */
912 } else {
913 mDebug() << "Failed to write to " << m_currentFile->fileName();
914 }
915 } else {
916 // Run in a separate thread to keep the ui responsive
917 QFutureWatcher<void>* watcher = new QFutureWatcher<void>( m_parent );
918 QObject::connect( watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()) );
919 QObject::connect( watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()) );
920
921 QFuture<void> future = QtConcurrent::run( this, &NewstuffModelPrivate::uninstall, m_currentAction.first );
922 watcher->setFuture( future );
923 }
924}
925
926NewstuffItem NewstuffModelPrivate::importNode(const QDomNode &node)
927{
928 NewstuffItem item;
929 item.m_category = node.attributes().namedItem(QStringLiteral("category")).toAttr().value();
930 readValue<QString>( node, "name", &item.m_name );
931 readValue<QString>( node, "author", &item.m_author );
932 readValue<QString>( node, "licence", &item.m_license );
933 readValue<QString>( node, "summary", &item.m_summary );
934 readValue<QString>( node, "version", &item.m_version );
935 readValue<QString>( node, "releasedate", &item.m_releaseDate );
936 readValue<QUrl>( node, "preview", &item.m_previewUrl );
937 readValue<QUrl>( node, "payload", &item.m_payloadUrl );
938 return item;
939}
940
941bool NewstuffModelPrivate::isTransitioning( int index ) const
942{
943 if ( m_currentAction.first == index ) {
944 return true;
945 }
946
947 for( const Action &action: m_actionQueue ) {
948 if ( action.first == index ) {
949 return true;
950 }
951 }
952
953 return false;
954}
955
956}
957
958#include "moc_NewstuffModel.cpp"
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
Binds a QML item to a specific geodetic location in screen coordinates.
QString homePath()
bool mkpath(const QString &dirPath) const const
bool rmdir(const QString &dirName) const const
QDir root()
QString tempPath()
QString value() const const
QDomElement createElement(const QString &tagName)
QDomText createTextNode(const QString &value)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QDomNodeList elementsByTagName(const QString &tagname) const const
void setAttribute(const QString &name, const QString &value)
QString text() const const
bool contains(const QString &name) const const
QDomNode namedItem(const QString &name) const const
QDomNode appendChild(const QDomNode &newChild)
QDomNamedNodeMap attributes() const const
QDomNode firstChild() const const
bool isNull() const const
QDomNode namedItem(const QString &name) const const
QDomNode removeChild(const QDomNode &oldChild)
QDomAttr toAttr() const const
QDomElement toElement() const const
QDomNode at(int index) const const
int count() const const
bool isEmpty() const const
QDomNode item(int index) const const
int length() const const
int size() const const
bool remove()
QString fileName() const const
QString path() const const
void setFuture(const QFuture< T > &future)
qsizetype size() const const
QImage fromData(QByteArrayView data, const char *format)
bool isNull() const const
QByteArray readAll()
iterator begin()
iterator end()
bool isValid() const const
int row() const const
QVariant attribute(QNetworkRequest::Attribute code) const const
QVariant header(QNetworkRequest::KnownHeaders header) const const
QNetworkAccessManager::Operation operation() const const
QUrl url() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QProcessEnvironment systemEnvironment()
QString value(const QString &name, const QString &defaultValue) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
transparent
DisplayRole
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isEmpty() const const
T & get(QVariant &v)
bool isNull() const const
bool isValid() const const
QUrl toUrl() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:18:17 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.