File indexing completed on 2024-04-28 15:15:59
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2012 Dennis Nienhüser <nienhueser@kde.org> 0004 // 0005 0006 #include "NewstuffModel.h" 0007 0008 #include "MarbleDebug.h" 0009 #include "MarbleDirs.h" 0010 #include "MarbleZipReader.h" 0011 0012 #include <QUrl> 0013 #include <QVector> 0014 #include <QTemporaryFile> 0015 #include <QDir> 0016 #include <QFuture> 0017 #include <QPair> 0018 #include <QFutureWatcher> 0019 #include <QtConcurrentRun> 0020 #include <QProcessEnvironment> 0021 #include <QMutexLocker> 0022 #include <QIcon> 0023 #include <QNetworkAccessManager> 0024 #include <QNetworkReply> 0025 #include <QDomDocument> 0026 0027 namespace Marble 0028 { 0029 0030 class NewstuffItem 0031 { 0032 public: 0033 QString m_category; 0034 QString m_name; 0035 QString m_author; 0036 QString m_license; 0037 QString m_summary; 0038 QString m_version; 0039 QString m_releaseDate; 0040 QUrl m_previewUrl; 0041 QIcon m_preview; 0042 QUrl m_payloadUrl; 0043 QDomNode m_registryNode; 0044 qint64 m_payloadSize; 0045 qint64 m_downloadedSize; 0046 0047 NewstuffItem(); 0048 0049 QString installedVersion() const; 0050 QString installedReleaseDate() const; 0051 bool isUpgradable() const; 0052 QStringList installedFiles() const; 0053 0054 static bool deeperThan( const QString &one, const QString &two ); 0055 }; 0056 0057 class FetchPreviewJob; 0058 0059 class NewstuffModelPrivate 0060 { 0061 public: 0062 enum NodeAction { 0063 Append, 0064 Replace 0065 }; 0066 0067 enum UserAction { 0068 Install, 0069 Uninstall 0070 }; 0071 0072 typedef QPair<int, UserAction> Action; 0073 0074 NewstuffModel* m_parent; 0075 0076 QVector<NewstuffItem> m_items; 0077 0078 QNetworkAccessManager m_networkAccessManager; 0079 0080 QString m_provider; 0081 0082 QMap<QNetworkReply *, FetchPreviewJob *> m_networkJobs; 0083 0084 QNetworkReply* m_currentReply; 0085 0086 QTemporaryFile* m_currentFile; 0087 0088 QString m_targetDirectory; 0089 0090 QString m_registryFile; 0091 0092 NewstuffModel::IdTag m_idTag; 0093 0094 QDomDocument m_registryDocument; 0095 0096 QDomElement m_root; 0097 0098 Action m_currentAction; 0099 0100 QProcess* m_unpackProcess; 0101 0102 QMutex m_mutex; 0103 0104 QList<Action> m_actionQueue; 0105 0106 QHash<int, QByteArray> m_roleNames; 0107 0108 explicit NewstuffModelPrivate( NewstuffModel* parent ); 0109 0110 QIcon preview( int index ); 0111 void setPreview( int index, const QIcon &previewIcon ); 0112 0113 void handleProviderData( QNetworkReply* reply ); 0114 0115 static bool canExecute( const QString &executable ); 0116 0117 void installMap(); 0118 0119 void updateModel(); 0120 0121 void saveRegistry(); 0122 0123 void uninstall( int index ); 0124 0125 static void changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action ); 0126 0127 void readInstalledFiles( QStringList* target, const QDomNode &node ); 0128 0129 void processQueue(); 0130 0131 static NewstuffItem importNode( const QDomNode &node ); 0132 0133 bool isTransitioning( int index ) const; 0134 0135 void unzip(); 0136 0137 void updateRegistry(const QStringList &files); 0138 0139 template<class T> 0140 static void readValue( const QDomNode &node, const QString &key, T* target ); 0141 }; 0142 0143 class FetchPreviewJob 0144 { 0145 public: 0146 FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index ); 0147 0148 void run( const QByteArray &data ); 0149 0150 private: 0151 NewstuffModelPrivate *const m_modelPrivate; 0152 const int m_index; 0153 }; 0154 0155 NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 ) 0156 { 0157 // nothing to do 0158 } 0159 0160 QString NewstuffItem::installedVersion() const 0161 { 0162 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "version" ); 0163 if ( nodes.size() == 1 ) { 0164 return nodes.at( 0 ).toElement().text(); 0165 } 0166 0167 return QString(); 0168 } 0169 0170 QString NewstuffItem::installedReleaseDate() const 0171 { 0172 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "releasedate" ); 0173 if ( nodes.size() == 1 ) { 0174 return nodes.at( 0 ).toElement().text(); 0175 } 0176 0177 return QString(); 0178 } 0179 0180 bool NewstuffItem::isUpgradable() const 0181 { 0182 bool installedOk, remoteOk; 0183 double const installed = installedVersion().toDouble( &installedOk ); 0184 double const remote= m_version.toDouble( &remoteOk ); 0185 return installedOk && remoteOk && remote > installed; 0186 } 0187 0188 QStringList NewstuffItem::installedFiles() const 0189 { 0190 QStringList result; 0191 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "installedfile" ); 0192 for ( int i=0; i<nodes.count(); ++i ) { 0193 result << nodes.at( i ).toElement().text(); 0194 } 0195 return result; 0196 } 0197 0198 bool NewstuffItem::deeperThan(const QString &one, const QString &two) 0199 { 0200 return one.length() > two.length(); 0201 } 0202 0203 FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index ) : 0204 m_modelPrivate( modelPrivate ), 0205 m_index( index ) 0206 { 0207 } 0208 0209 void FetchPreviewJob::run( const QByteArray &data ) 0210 { 0211 const QImage image = QImage::fromData( data ); 0212 0213 if ( image.isNull() ) 0214 return; 0215 0216 const QPixmap pixmap = QPixmap::fromImage( image ); 0217 const QIcon previewIcon( pixmap ); 0218 m_modelPrivate->setPreview( m_index, previewIcon ); 0219 } 0220 0221 NewstuffModelPrivate::NewstuffModelPrivate( NewstuffModel* parent ) : m_parent( parent ), 0222 m_networkAccessManager( nullptr ), m_currentReply( nullptr ), m_currentFile( nullptr ), 0223 m_idTag( NewstuffModel::PayloadTag ), m_currentAction( -1, Install ), m_unpackProcess( nullptr ) 0224 { 0225 // nothing to do 0226 } 0227 0228 QIcon NewstuffModelPrivate::preview( int index ) 0229 { 0230 if ( m_items.at( index ).m_preview.isNull() ) { 0231 QPixmap dummyPixmap( 136, 136 ); 0232 dummyPixmap.fill( Qt::transparent ); 0233 setPreview( index, QIcon( dummyPixmap ) ); 0234 QNetworkReply *reply = m_networkAccessManager.get( QNetworkRequest( m_items.at( index ).m_previewUrl ) ); 0235 m_networkJobs.insert( reply, new FetchPreviewJob( this, index ) ); 0236 } 0237 0238 Q_ASSERT( !m_items.at( index ).m_preview.isNull() ); 0239 0240 return m_items.at( index ).m_preview; 0241 } 0242 0243 void NewstuffModelPrivate::setPreview( int index, const QIcon &previewIcon ) 0244 { 0245 NewstuffItem &item = m_items[index]; 0246 item.m_preview = previewIcon; 0247 const QModelIndex affected = m_parent->index( index ); 0248 emit m_parent->dataChanged( affected, affected ); 0249 } 0250 0251 void NewstuffModelPrivate::handleProviderData(QNetworkReply *reply) 0252 { 0253 if ( reply->operation() == QNetworkAccessManager::HeadOperation ) { 0254 const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ); 0255 if ( !redirectionAttribute.isNull() ) { 0256 for ( int i=0; i<m_items.size(); ++i ) { 0257 NewstuffItem &item = m_items[i]; 0258 if ( item.m_payloadUrl == reply->url() ) { 0259 item.m_payloadUrl = redirectionAttribute.toUrl(); 0260 } 0261 } 0262 m_networkAccessManager.head( QNetworkRequest( redirectionAttribute.toUrl() ) ); 0263 return; 0264 } 0265 0266 QVariant const size = reply->header( QNetworkRequest::ContentLengthHeader ); 0267 if ( size.isValid() ) { 0268 qint64 length = size.value<qint64>(); 0269 for ( int i=0; i<m_items.size(); ++i ) { 0270 NewstuffItem &item = m_items[i]; 0271 if ( item.m_payloadUrl == reply->url() ) { 0272 item.m_payloadSize = length; 0273 QModelIndex const affected = m_parent->index( i ); 0274 emit m_parent->dataChanged( affected, affected ); 0275 } 0276 } 0277 } 0278 return; 0279 } 0280 0281 FetchPreviewJob *const job = m_networkJobs.take( reply ); 0282 0283 // check if we are redirected 0284 const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ); 0285 if ( !redirectionAttribute.isNull() ) { 0286 QNetworkReply *redirectReply = m_networkAccessManager.get( QNetworkRequest( QUrl( redirectionAttribute.toUrl() ) ) ); 0287 if ( job ) { 0288 m_networkJobs.insert( redirectReply, job ); 0289 } 0290 return; 0291 } 0292 0293 if ( job ) { 0294 job->run( reply->readAll() ); 0295 delete job; 0296 return; 0297 } 0298 0299 QDomDocument xml; 0300 if ( !xml.setContent( reply->readAll() ) ) { 0301 mDebug() << "Cannot parse newstuff xml data "; 0302 return; 0303 } 0304 0305 m_items.clear(); 0306 0307 QDomElement root = xml.documentElement(); 0308 QDomNodeList items = root.elementsByTagName( "stuff" ); 0309 for (int i=0 ; i < items.length(); ++i ) { 0310 m_items << importNode( items.item( i ) ); 0311 } 0312 0313 updateModel(); 0314 } 0315 0316 bool NewstuffModelPrivate::canExecute( const QString &executable ) 0317 { 0318 QString path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("PATH"), QStringLiteral("/usr/local/bin:/usr/bin:/bin")); 0319 for( const QString &dir: path.split( QLatin1Char( ':' ) ) ) { 0320 QFileInfo application( QDir( dir ), executable ); 0321 if ( application.exists() ) { 0322 return true; 0323 } 0324 } 0325 0326 return false; 0327 } 0328 0329 void NewstuffModelPrivate::installMap() 0330 { 0331 if ( m_unpackProcess ) { 0332 m_unpackProcess->close(); 0333 delete m_unpackProcess; 0334 m_unpackProcess = nullptr; 0335 } else if ( m_currentFile->fileName().endsWith( QLatin1String( "zip" ) ) ) { 0336 unzip(); 0337 } 0338 else if ( m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) && canExecute( "tar" ) ) { 0339 m_unpackProcess = new QProcess; 0340 QObject::connect( m_unpackProcess, SIGNAL(finished(int)), 0341 m_parent, SLOT(contentsListed(int)) ); 0342 QStringList arguments = QStringList() << "-t" << "-z" << "-f" << m_currentFile->fileName(); 0343 m_unpackProcess->setWorkingDirectory( m_targetDirectory ); 0344 m_unpackProcess->start( "tar", arguments ); 0345 } else { 0346 if ( !m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) ) { 0347 mDebug() << "Can only handle tar.gz files"; 0348 } else { 0349 mDebug() << "Cannot extract archive: tar executable not found in PATH."; 0350 } 0351 } 0352 } 0353 0354 void NewstuffModelPrivate::unzip() 0355 { 0356 MarbleZipReader zipReader(m_currentFile->fileName()); 0357 QStringList files; 0358 for(const MarbleZipReader::FileInfo &fileInfo: zipReader.fileInfoList()) { 0359 files << fileInfo.filePath; 0360 } 0361 updateRegistry(files); 0362 zipReader.extractAll(m_targetDirectory); 0363 m_parent->mapInstalled(0); 0364 } 0365 0366 void NewstuffModelPrivate::updateModel() 0367 { 0368 QDomNodeList items = m_root.elementsByTagName( "stuff" ); 0369 for (int i=0 ; i < items.length(); ++i ) { 0370 QString const key = m_idTag == NewstuffModel::PayloadTag ? "payload" : "name"; 0371 QDomNodeList matches = items.item( i ).toElement().elementsByTagName( key ); 0372 if ( matches.size() == 1 ) { 0373 QString const value = matches.at( 0 ).toElement().text(); 0374 bool found = false; 0375 for ( int j=0; j<m_items.size() && !found; ++j ) { 0376 NewstuffItem &item = m_items[j]; 0377 if ( m_idTag == NewstuffModel::PayloadTag && item.m_payloadUrl.toString() == value ) { 0378 item.m_registryNode = items.item( i ); 0379 found = true; 0380 } 0381 if ( m_idTag == NewstuffModel::NameTag && item.m_name == value ) { 0382 item.m_registryNode = items.item( i ); 0383 found = true; 0384 } 0385 } 0386 0387 if ( !found ) { 0388 // Not found in newstuff or newstuff not there yet 0389 NewstuffItem item = importNode( items.item( i ) ); 0390 if ( m_idTag == NewstuffModel::PayloadTag ) { 0391 item.m_registryNode = items.item( i ); 0392 } else if ( m_idTag == NewstuffModel::NameTag ) { 0393 item.m_registryNode = items.item( i ); 0394 } 0395 m_items << item; 0396 } 0397 } 0398 } 0399 0400 m_parent->beginResetModel(); 0401 m_parent->endResetModel(); 0402 } 0403 0404 void NewstuffModelPrivate::saveRegistry() 0405 { 0406 QFile output( m_registryFile ); 0407 if ( !output.open( QFile::WriteOnly ) ) { 0408 mDebug() << "Cannot open " << m_registryFile << " for writing"; 0409 } else { 0410 QTextStream outStream( &output ); 0411 outStream << m_registryDocument.toString( 2 ); 0412 outStream.flush(); 0413 output.close(); 0414 } 0415 } 0416 0417 void NewstuffModelPrivate::uninstall( int index ) 0418 { 0419 // Delete all files first, then directories (deeper ones first) 0420 0421 QStringList directories; 0422 QStringList const files = m_items[index].installedFiles(); 0423 for( const QString &file: files ) { 0424 if (file.endsWith(QLatin1Char('/'))) { 0425 directories << file; 0426 } else { 0427 QFile::remove( file ); 0428 } 0429 } 0430 0431 std::sort( directories.begin(), directories.end(), NewstuffItem::deeperThan ); 0432 for( const QString &dir: directories ) { 0433 QDir::root().rmdir( dir ); 0434 } 0435 0436 m_items[index].m_registryNode.parentNode().removeChild( m_items[index].m_registryNode ); 0437 m_items[index].m_registryNode.clear(); 0438 saveRegistry(); 0439 } 0440 0441 void NewstuffModelPrivate::changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action ) 0442 { 0443 if ( action == Append ) { 0444 QDomNode newNode = node.appendChild( domDocument.createElement( key ) ); 0445 newNode.appendChild( domDocument.createTextNode( value ) ); 0446 } else { 0447 QDomNode oldNode = node.namedItem( key ); 0448 if ( !oldNode.isNull() ) { 0449 oldNode.removeChild( oldNode.firstChild() ); 0450 oldNode.appendChild( domDocument.createTextNode( value ) ); 0451 } 0452 } 0453 } 0454 0455 template<class T> 0456 void NewstuffModelPrivate::readValue( const QDomNode &node, const QString &key, T* target ) 0457 { 0458 QDomNodeList matches = node.toElement().elementsByTagName( key ); 0459 if ( matches.size() == 1 ) { 0460 *target = T(matches.at( 0 ).toElement().text()); 0461 } else { 0462 for ( int i=0; i<matches.size(); ++i ) { 0463 if ( matches.at( i ).attributes().contains(QStringLiteral("lang")) && 0464 matches.at( i ).attributes().namedItem(QStringLiteral("lang")).toAttr().value() == QLatin1String("en")) { 0465 *target = T(matches.at( i ).toElement().text()); 0466 return; 0467 } 0468 } 0469 } 0470 } 0471 0472 NewstuffModel::NewstuffModel( QObject *parent ) : 0473 QAbstractListModel( parent ), d( new NewstuffModelPrivate( this ) ) 0474 { 0475 setTargetDirectory(MarbleDirs::localPath() + QLatin1String("/maps")); 0476 // no default registry file 0477 0478 connect( &d->m_networkAccessManager, SIGNAL(finished(QNetworkReply*)), 0479 this, SLOT(handleProviderData(QNetworkReply*)) ); 0480 0481 QHash<int,QByteArray> roles; 0482 roles[Qt::DisplayRole] = "display"; 0483 roles[Name] = "name"; 0484 roles[Author] = "author"; 0485 roles[License] = "license"; 0486 roles[Summary] = "summary"; 0487 roles[Version] = "version"; 0488 roles[ReleaseDate] = "releasedate"; 0489 roles[Preview] = "preview"; 0490 roles[Payload] = "payload"; 0491 roles[InstalledVersion] = "installedversion"; 0492 roles[InstalledReleaseDate] = "installedreleasedate"; 0493 roles[InstalledFiles] = "installedfiles"; 0494 roles[IsInstalled] = "installed"; 0495 roles[IsUpgradable] = "upgradable"; 0496 roles[Category] = "category"; 0497 roles[IsTransitioning] = "transitioning"; 0498 roles[PayloadSize] = "size"; 0499 roles[DownloadedSize] = "downloaded"; 0500 d->m_roleNames = roles; 0501 } 0502 0503 NewstuffModel::~NewstuffModel() 0504 { 0505 delete d; 0506 } 0507 0508 int NewstuffModel::rowCount ( const QModelIndex &parent ) const 0509 { 0510 if ( !parent.isValid() ) { 0511 return d->m_items.size(); 0512 } 0513 0514 return 0; 0515 } 0516 0517 QVariant NewstuffModel::data ( const QModelIndex &index, int role ) const 0518 { 0519 if ( index.isValid() && index.row() >= 0 && index.row() < d->m_items.size() ) { 0520 switch ( role ) { 0521 case Qt::DisplayRole: return d->m_items.at( index.row() ).m_name; 0522 case Qt::DecorationRole: return d->preview( index.row() ); 0523 case Name: return d->m_items.at( index.row() ).m_name; 0524 case Author: return d->m_items.at( index.row() ).m_author; 0525 case License: return d->m_items.at( index.row() ).m_license; 0526 case Summary: return d->m_items.at( index.row() ).m_summary; 0527 case Version: return d->m_items.at( index.row() ).m_version; 0528 case ReleaseDate: return d->m_items.at( index.row() ).m_releaseDate; 0529 case Preview: return d->m_items.at( index.row() ).m_previewUrl; 0530 case Payload: return d->m_items.at( index.row() ).m_payloadUrl; 0531 case InstalledVersion: return d->m_items.at( index.row() ).installedVersion(); 0532 case InstalledReleaseDate: return d->m_items.at( index.row() ).installedReleaseDate(); 0533 case InstalledFiles: return d->m_items.at( index.row() ).installedFiles(); 0534 case IsInstalled: return !d->m_items.at( index.row() ).m_registryNode.isNull(); 0535 case IsUpgradable: return d->m_items.at( index.row() ).isUpgradable(); 0536 case Category: return d->m_items.at( index.row() ).m_category; 0537 case IsTransitioning: return d->isTransitioning( index.row() ); 0538 case PayloadSize: { 0539 qint64 const size = d->m_items.at( index.row() ).m_payloadSize; 0540 QUrl const url = d->m_items.at( index.row() ).m_payloadUrl; 0541 if ( size < -1 && !url.isEmpty() ) { 0542 d->m_items[index.row()].m_payloadSize = -1; // prevent several head requests for the same item 0543 d->m_networkAccessManager.head( QNetworkRequest( url ) ); 0544 } 0545 0546 return qMax<qint64>( -1, size ); 0547 } 0548 case DownloadedSize: return d->m_items.at( index.row() ).m_downloadedSize; 0549 } 0550 } 0551 0552 return QVariant(); 0553 } 0554 0555 QHash<int, QByteArray> NewstuffModel::roleNames() const 0556 { 0557 return d->m_roleNames; 0558 } 0559 0560 0561 int NewstuffModel::count() const 0562 { 0563 return rowCount(); 0564 } 0565 0566 void NewstuffModel::setProvider( const QString &downloadUrl ) 0567 { 0568 if ( downloadUrl == d->m_provider ) { 0569 return; 0570 } 0571 0572 d->m_provider = downloadUrl; 0573 emit providerChanged(); 0574 d->m_networkAccessManager.get( QNetworkRequest( QUrl( downloadUrl ) ) ); 0575 } 0576 0577 QString NewstuffModel::provider() const 0578 { 0579 return d->m_provider; 0580 } 0581 0582 void NewstuffModel::setTargetDirectory( const QString &targetDirectory ) 0583 { 0584 if ( targetDirectory != d->m_targetDirectory ) { 0585 QFileInfo targetDir( targetDirectory ); 0586 if ( !targetDir.exists() ) { 0587 if ( !QDir::root().mkpath( targetDir.absoluteFilePath() ) ) { 0588 qDebug() << "Failed to create directory " << targetDirectory << ", newstuff installation might fail."; 0589 } 0590 } 0591 0592 d->m_targetDirectory = targetDirectory; 0593 emit targetDirectoryChanged(); 0594 } 0595 } 0596 0597 QString NewstuffModel::targetDirectory() const 0598 { 0599 return d->m_targetDirectory; 0600 } 0601 0602 void NewstuffModel::setRegistryFile( const QString &filename, IdTag idTag ) 0603 { 0604 QString registryFile = filename; 0605 if (registryFile.startsWith(QLatin1Char('~')) && registryFile.length() > 1) { 0606 registryFile = QDir::homePath() + registryFile.mid( 1 ); 0607 } 0608 0609 if ( d->m_registryFile != registryFile ) { 0610 d->m_registryFile = registryFile; 0611 d->m_idTag = idTag; 0612 emit registryFileChanged(); 0613 0614 QFileInfo inputFile( registryFile ); 0615 if ( !inputFile.exists() ) { 0616 QDir::root().mkpath( inputFile.absolutePath() ); 0617 d->m_registryDocument = QDomDocument( "khotnewstuff3" ); 0618 QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction( "xml", "version=\"1.0\" encoding=\"utf-8\"" ); 0619 d->m_registryDocument.appendChild( header ); 0620 d->m_root = d->m_registryDocument.createElement( "hotnewstuffregistry" ); 0621 d->m_registryDocument.appendChild( d->m_root ); 0622 } else { 0623 QFile input( registryFile ); 0624 if ( !input.open( QFile::ReadOnly ) ) { 0625 mDebug() << "Cannot open newstuff registry " << registryFile; 0626 return; 0627 } 0628 0629 if ( !d->m_registryDocument.setContent( &input ) ) { 0630 mDebug() << "Cannot parse newstuff registry " << registryFile; 0631 return; 0632 } 0633 input.close(); 0634 d->m_root = d->m_registryDocument.documentElement(); 0635 } 0636 0637 d->updateModel(); 0638 } 0639 } 0640 0641 QString NewstuffModel::registryFile() const 0642 { 0643 return d->m_registryFile; 0644 } 0645 0646 void NewstuffModel::install( int index ) 0647 { 0648 if ( index < 0 || index >= d->m_items.size() ) { 0649 return; 0650 } 0651 0652 NewstuffModelPrivate::Action action( index, NewstuffModelPrivate::Install ); 0653 { // <-- do not remove, mutex locker scope 0654 QMutexLocker locker( &d->m_mutex ); 0655 if ( d->m_actionQueue.contains( action ) ) { 0656 return; 0657 } 0658 d->m_actionQueue << action; 0659 } 0660 0661 d->processQueue(); 0662 } 0663 0664 void NewstuffModel::uninstall( int idx ) 0665 { 0666 if ( idx < 0 || idx >= d->m_items.size() ) { 0667 return; 0668 } 0669 0670 if ( d->m_items[idx].m_registryNode.isNull() ) { 0671 emit uninstallationFinished( idx ); 0672 } 0673 0674 NewstuffModelPrivate::Action action( idx, NewstuffModelPrivate::Uninstall ); 0675 { // <-- do not remove, mutex locker scope 0676 QMutexLocker locker( &d->m_mutex ); 0677 if ( d->m_actionQueue.contains( action ) ) { 0678 return; 0679 } 0680 d->m_actionQueue << action; 0681 } 0682 0683 d->processQueue(); 0684 } 0685 0686 void NewstuffModel::cancel( int index ) 0687 { 0688 if ( !d->isTransitioning( index ) ) { 0689 return; 0690 } 0691 0692 { // <-- do not remove, mutex locker scope 0693 QMutexLocker locker( &d->m_mutex ); 0694 if ( d->m_currentAction.first == index ) { 0695 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) { 0696 if ( d->m_currentReply ) { 0697 d->m_currentReply->abort(); 0698 d->m_currentReply->deleteLater(); 0699 d->m_currentReply = nullptr; 0700 } 0701 0702 if ( d->m_unpackProcess ) { 0703 d->m_unpackProcess->terminate(); 0704 d->m_unpackProcess->deleteLater(); 0705 d->m_unpackProcess = nullptr; 0706 } 0707 0708 if ( d->m_currentFile ) { 0709 d->m_currentFile->deleteLater(); 0710 d->m_currentFile = nullptr; 0711 } 0712 0713 d->m_items[d->m_currentAction.first].m_downloadedSize = 0; 0714 0715 emit installationFailed( d->m_currentAction.first, tr( "Installation aborted by user." ) ); 0716 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install ); 0717 } else { 0718 // Shall we interrupt this? 0719 } 0720 } else { 0721 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) { 0722 NewstuffModelPrivate::Action install( index, NewstuffModelPrivate::Install ); 0723 d->m_actionQueue.removeAll( install ); 0724 emit installationFailed( index, tr( "Installation aborted by user." ) ); 0725 } else { 0726 NewstuffModelPrivate::Action uninstall( index, NewstuffModelPrivate::Uninstall ); 0727 d->m_actionQueue.removeAll( uninstall ); 0728 emit uninstallationFinished( index ); // do we need failed here? 0729 } 0730 } 0731 } 0732 0733 d->processQueue(); 0734 } 0735 0736 void NewstuffModel::updateProgress( qint64 bytesReceived, qint64 bytesTotal ) 0737 { 0738 qreal const progress = qBound<qreal>( 0.0, 0.9 * bytesReceived / qreal( bytesTotal ), 1.0 ); 0739 emit installationProgressed( d->m_currentAction.first, progress ); 0740 NewstuffItem &item = d->m_items[d->m_currentAction.first]; 0741 item.m_payloadSize = bytesTotal; 0742 if ( qreal(bytesReceived-item.m_downloadedSize)/bytesTotal >= 0.01 || progress >= 0.9 ) { 0743 // Only consider download progress of 1% and more as a data change 0744 item.m_downloadedSize = bytesReceived; 0745 QModelIndex const affected = index( d->m_currentAction.first ); 0746 emit dataChanged( affected, affected ); 0747 } 0748 } 0749 0750 void NewstuffModel::retrieveData() 0751 { 0752 if ( d->m_currentReply && d->m_currentReply->isReadable() ) { 0753 // check if we are redirected 0754 const QVariant redirectionAttribute = d->m_currentReply->attribute( QNetworkRequest::RedirectionTargetAttribute ); 0755 if ( !redirectionAttribute.isNull() ) { 0756 d->m_currentReply = d->m_networkAccessManager.get( QNetworkRequest( redirectionAttribute.toUrl() ) ); 0757 QObject::connect( d->m_currentReply, SIGNAL(readyRead()), this, SLOT(retrieveData()) ); 0758 QObject::connect( d->m_currentReply, SIGNAL(readChannelFinished()), this, SLOT(retrieveData()) ); 0759 QObject::connect( d->m_currentReply, SIGNAL(downloadProgress(qint64,qint64)), 0760 this, SLOT(updateProgress(qint64,qint64)) ); 0761 } else { 0762 d->m_currentFile->write( d->m_currentReply->readAll() ); 0763 if ( d->m_currentReply->isFinished() ) { 0764 d->m_currentReply->deleteLater(); 0765 d->m_currentReply = nullptr; 0766 d->m_currentFile->flush(); 0767 d->installMap(); 0768 } 0769 } 0770 } 0771 } 0772 0773 void NewstuffModel::mapInstalled( int exitStatus ) 0774 { 0775 if ( d->m_unpackProcess ) { 0776 d->m_unpackProcess->deleteLater(); 0777 d->m_unpackProcess = nullptr; 0778 } 0779 0780 if ( d->m_currentFile ) { 0781 d->m_currentFile->deleteLater(); 0782 d->m_currentFile = nullptr; 0783 } 0784 0785 emit installationProgressed( d->m_currentAction.first, 1.0 ); 0786 d->m_items[d->m_currentAction.first].m_downloadedSize = 0; 0787 if ( exitStatus == 0 ) { 0788 emit installationFinished( d->m_currentAction.first ); 0789 } else { 0790 mDebug() << "Process exit status " << exitStatus << " indicates an error."; 0791 emit installationFailed( d->m_currentAction.first , QString( "Unable to unpack file. Process exited with status code %1." ).arg( exitStatus ) ); 0792 } 0793 QModelIndex const affected = index( d->m_currentAction.first ); 0794 0795 { // <-- do not remove, mutex locker scope 0796 QMutexLocker locker( &d->m_mutex ); 0797 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install ); 0798 } 0799 emit dataChanged( affected, affected ); 0800 d->processQueue(); 0801 } 0802 0803 void NewstuffModel::mapUninstalled() 0804 { 0805 QModelIndex const affected = index( d->m_currentAction.first ); 0806 emit uninstallationFinished( d->m_currentAction.first ); 0807 0808 { // <-- do not remove, mutex locker scope 0809 QMutexLocker locker( &d->m_mutex ); 0810 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install ); 0811 } 0812 emit dataChanged( affected, affected ); 0813 d->processQueue(); 0814 } 0815 0816 void NewstuffModel::contentsListed( int exitStatus ) 0817 { 0818 if ( exitStatus == 0 ) { 0819 QStringList const files = QString(d->m_unpackProcess->readAllStandardOutput()).split(QLatin1Char('\n'), QString::SkipEmptyParts); 0820 d->updateRegistry(files); 0821 0822 QObject::disconnect( d->m_unpackProcess, SIGNAL(finished(int)), 0823 this, SLOT(contentsListed(int)) ); 0824 QObject::connect( d->m_unpackProcess, SIGNAL(finished(int)), 0825 this, SLOT(mapInstalled(int)) ); 0826 QStringList arguments = QStringList() << "-x" << "-z" << "-f" << d->m_currentFile->fileName(); 0827 d->m_unpackProcess->start( "tar", arguments ); 0828 } else { 0829 mDebug() << "Process exit status " << exitStatus << " indicates an error."; 0830 emit installationFailed( d->m_currentAction.first , QString( "Unable to list file contents. Process exited with status code %1." ).arg( exitStatus ) ); 0831 0832 { // <-- do not remove, mutex locker scope 0833 QMutexLocker locker( &d->m_mutex ); 0834 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install ); 0835 } 0836 d->processQueue(); 0837 } 0838 } 0839 0840 void NewstuffModelPrivate::updateRegistry(const QStringList &files) 0841 { 0842 emit m_parent->installationProgressed( m_currentAction.first, 0.92 ); 0843 if ( !m_registryFile.isEmpty() ) { 0844 NewstuffItem &item = m_items[m_currentAction.first]; 0845 QDomNode node = item.m_registryNode; 0846 NewstuffModelPrivate::NodeAction action = node.isNull() ? NewstuffModelPrivate::Append : NewstuffModelPrivate::Replace; 0847 if ( node.isNull() ) { 0848 node = m_root.appendChild( m_registryDocument.createElement( "stuff" ) ); 0849 } 0850 0851 node.toElement().setAttribute( "category", m_items[m_currentAction.first].m_category ); 0852 changeNode( node, m_registryDocument, "name", item.m_name, action ); 0853 changeNode( node, m_registryDocument, "providerid", m_provider, action ); 0854 changeNode( node, m_registryDocument, "author", item.m_author, action ); 0855 changeNode( node, m_registryDocument, "homepage", QString(), action ); 0856 changeNode( node, m_registryDocument, "licence", item.m_license, action ); 0857 changeNode( node, m_registryDocument, "version", item.m_version, action ); 0858 QString const itemId = m_idTag == NewstuffModel::PayloadTag ? item.m_payloadUrl.toString() : item.m_name; 0859 changeNode( node, m_registryDocument, "id", itemId, action ); 0860 changeNode( node, m_registryDocument, "releasedate", item.m_releaseDate, action ); 0861 changeNode( node, m_registryDocument, "summary", item.m_summary, action ); 0862 changeNode( node, m_registryDocument, "changelog", QString(), action ); 0863 changeNode( node, m_registryDocument, "preview", item.m_previewUrl.toString(), action ); 0864 changeNode( node, m_registryDocument, "previewBig", item.m_previewUrl.toString(), action ); 0865 changeNode( node, m_registryDocument, "payload", item.m_payloadUrl.toString(), action ); 0866 changeNode( node, m_registryDocument, "status", "installed", action ); 0867 m_items[m_currentAction.first].m_registryNode = node; 0868 0869 bool hasChildren = true; 0870 while ( hasChildren ) { 0871 /** @todo FIXME: fileList does not contain all elements opposed to what docs say */ 0872 QDomNodeList fileList = node.toElement().elementsByTagName( "installedfile" ); 0873 hasChildren = !fileList.isEmpty(); 0874 for ( int i=0; i<fileList.count(); ++i ) { 0875 node.removeChild( fileList.at( i ) ); 0876 } 0877 } 0878 0879 for( const QString &file: files ) { 0880 QDomNode fileNode = node.appendChild( m_registryDocument.createElement( "installedfile" ) ); 0881 fileNode.appendChild(m_registryDocument.createTextNode(m_targetDirectory + QLatin1Char('/') + file)); 0882 } 0883 0884 saveRegistry(); 0885 } 0886 } 0887 0888 void NewstuffModelPrivate::processQueue() 0889 { 0890 if ( m_actionQueue.empty() || m_currentAction.first >= 0 ) { 0891 return; 0892 } 0893 0894 { // <-- do not remove, mutex locker scope 0895 QMutexLocker locker( &m_mutex ); 0896 m_currentAction = m_actionQueue.takeFirst(); 0897 } 0898 if ( m_currentAction.second == Install ) { 0899 if ( !m_currentFile ) { 0900 QFileInfo const file = m_items.at( m_currentAction.first ).m_payloadUrl.path(); 0901 m_currentFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/marble-XXXXXX-") + file.fileName()); 0902 } 0903 0904 if ( m_currentFile->open() ) { 0905 QUrl const payload = m_items.at( m_currentAction.first ).m_payloadUrl; 0906 m_currentReply = m_networkAccessManager.get( QNetworkRequest( payload ) ); 0907 QObject::connect( m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()) ); 0908 QObject::connect( m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()) ); 0909 QObject::connect( m_currentReply, SIGNAL(downloadProgress(qint64,qint64)), 0910 m_parent, SLOT(updateProgress(qint64,qint64)) ); 0911 /** @todo: handle download errors */ 0912 } else { 0913 mDebug() << "Failed to write to " << m_currentFile->fileName(); 0914 } 0915 } else { 0916 // Run in a separate thread to keep the ui responsive 0917 QFutureWatcher<void>* watcher = new QFutureWatcher<void>( m_parent ); 0918 QObject::connect( watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()) ); 0919 QObject::connect( watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()) ); 0920 0921 QFuture<void> future = QtConcurrent::run( this, &NewstuffModelPrivate::uninstall, m_currentAction.first ); 0922 watcher->setFuture( future ); 0923 } 0924 } 0925 0926 NewstuffItem NewstuffModelPrivate::importNode(const QDomNode &node) 0927 { 0928 NewstuffItem item; 0929 item.m_category = node.attributes().namedItem(QStringLiteral("category")).toAttr().value(); 0930 readValue<QString>( node, "name", &item.m_name ); 0931 readValue<QString>( node, "author", &item.m_author ); 0932 readValue<QString>( node, "licence", &item.m_license ); 0933 readValue<QString>( node, "summary", &item.m_summary ); 0934 readValue<QString>( node, "version", &item.m_version ); 0935 readValue<QString>( node, "releasedate", &item.m_releaseDate ); 0936 readValue<QUrl>( node, "preview", &item.m_previewUrl ); 0937 readValue<QUrl>( node, "payload", &item.m_payloadUrl ); 0938 return item; 0939 } 0940 0941 bool NewstuffModelPrivate::isTransitioning( int index ) const 0942 { 0943 if ( m_currentAction.first == index ) { 0944 return true; 0945 } 0946 0947 for( const Action &action: m_actionQueue ) { 0948 if ( action.first == index ) { 0949 return true; 0950 } 0951 } 0952 0953 return false; 0954 } 0955 0956 } 0957 0958 #include "moc_NewstuffModel.cpp"