Marble

NewstuffModel.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2012 Dennis Nienhüser <[email protected]>
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 
27 namespace Marble
28 {
29 
30 class NewstuffItem
31 {
32 public:
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 
57 class FetchPreviewJob;
58 
59 class NewstuffModelPrivate
60 {
61 public:
62  enum NodeAction {
63  Append,
64  Replace
65  };
66 
67  enum UserAction {
68  Install,
69  Uninstall
70  };
71 
73 
74  NewstuffModel* m_parent;
75 
76  QVector<NewstuffItem> m_items;
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 
143 class FetchPreviewJob
144 {
145 public:
146  FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index );
147 
148  void run( const QByteArray &data );
149 
150 private:
151  NewstuffModelPrivate *const m_modelPrivate;
152  const int m_index;
153 };
154 
155 NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 )
156 {
157  // nothing to do
158 }
159 
160 QString 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 
170 QString 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 
180 bool 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 
188 QStringList 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 
198 bool NewstuffItem::deeperThan(const QString &one, const QString &two)
199 {
200  return one.length() > two.length();
201 }
202 
203 FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index ) :
204  m_modelPrivate( modelPrivate ),
205  m_index( index )
206 {
207 }
208 
209 void 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 
221 NewstuffModelPrivate::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 
228 QIcon 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 
243 void 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 
251 void 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 
316 bool 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 
329 void 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 
354 void 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 
366 void 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 
404 void 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 
417 void 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 
441 void 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 
455 template<class T>
456 void 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 
472 NewstuffModel::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 
481  QHash<int,QByteArray> roles;
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 
503 NewstuffModel::~NewstuffModel()
504 {
505  delete d;
506 }
507 
508 int NewstuffModel::rowCount ( const QModelIndex &parent ) const
509 {
510  if ( !parent.isValid() ) {
511  return d->m_items.size();
512  }
513 
514  return 0;
515 }
516 
517 QVariant 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 
555 QHash<int, QByteArray> NewstuffModel::roleNames() const
556 {
557  return d->m_roleNames;
558 }
559 
560 
561 int NewstuffModel::count() const
562 {
563  return rowCount();
564 }
565 
566 void 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 
577 QString NewstuffModel::provider() const
578 {
579  return d->m_provider;
580 }
581 
582 void 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 
597 QString NewstuffModel::targetDirectory() const
598 {
599  return d->m_targetDirectory;
600 }
601 
602 void 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 
641 QString NewstuffModel::registryFile() const
642 {
643  return d->m_registryFile;
644 }
645 
646 void 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 
664 void 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 
686 void 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 
736 void 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 
750 void 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 
773 void 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 
803 void 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 
816 void 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 
840 void 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 
888 void 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 
926 NewstuffItem 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 
941 bool 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"
bool isNull() const const
QString path() const const
int count() const const
QString text() const const
bool isValid() const const
QFuture< T > run(Function function,...)
QDomNode firstChild() const const
DisplayRole
QPixmap fromImage(const QImage &image, Qt::ImageConversionFlags flags)
QDomElement toElement() const const
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
QUrl toUrl() const const
bool remove()
QDomNode removeChild(const QDomNode &oldChild)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
T value() const const
QDomNodeList elementsByTagName(const QString &tagname) const const
bool isNull() const const
QDomText createTextNode(const QString &value)
QDir root()
QProcessEnvironment systemEnvironment()
QDomNamedNodeMap attributes() const const
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
License
QString homePath()
QNetworkAccessManager::Operation operation() const const
QDomElement createElement(const QString &tagName)
QDomNode at(int index) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
int size() const const
QString tempPath()
QDomAttr toAttr() const const
void setAttribute(const QString &name, const QString &value)
QUrl url() const const
bool contains(const QString &name) const const
int size() const const
QDomNode item(int index) const const
bool isEmpty() const const
bool rmdir(const QString &dirName) const const
int length() const const
bool isNull() const const
bool mkpath(const QString &dirPath) const const
bool isEmpty() const const
Author
Binds a QML item to a specific geodetic location in screen coordinates.
KIOWIDGETS_EXPORT bool run(const QUrl &_url, bool _is_local)
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
QDomElement documentElement() const const
bool isValid() const const
QString fileName() const const
QString value() const const
QVariant header(QNetworkRequest::KnownHeaders header) const const
int row() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QVariant attribute(QNetworkRequest::Attribute code) const const
void setFuture(const QFuture< T > &future)
QDomNode namedItem(const QString &name) const const
QDomNode appendChild(const QDomNode &newChild)
QString path(const QString &relativePath)
QList::iterator begin()
QByteArray readAll()
QList::iterator end()
QString mid(int position, int n) const const
QImage fromData(const uchar *data, int size, const char *format)
int length() const const
transparent
QString value(const QString &name, const QString &defaultValue) const const
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
QDomNode namedItem(const QString &name) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Oct 2 2023 03:52:09 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.