6 #include "NewstuffModel.h"
8 #include "MarbleDebug.h"
9 #include "MarbleDirs.h"
10 #include "MarbleZipReader.h"
14 #include <QTemporaryFile>
18 #include <QFutureWatcher>
19 #include <QtConcurrentRun>
20 #include <QProcessEnvironment>
21 #include <QMutexLocker>
23 #include <QNetworkAccessManager>
24 #include <QNetworkReply>
25 #include <QDomDocument>
45 qint64 m_downloadedSize;
49 QString installedVersion()
const;
50 QString installedReleaseDate()
const;
51 bool isUpgradable()
const;
57 class FetchPreviewJob;
59 class NewstuffModelPrivate
74 NewstuffModel* m_parent;
92 NewstuffModel::IdTag m_idTag;
108 explicit NewstuffModelPrivate( NewstuffModel* parent );
110 QIcon preview(
int index );
111 void setPreview(
int index,
const QIcon &previewIcon );
115 static bool canExecute(
const QString &executable );
123 void uninstall(
int index );
131 static NewstuffItem importNode(
const QDomNode &node );
133 bool isTransitioning(
int index )
const;
140 static void readValue(
const QDomNode &node,
const QString &key, T* target );
143 class FetchPreviewJob
146 FetchPreviewJob( NewstuffModelPrivate *modelPrivate,
int index );
151 NewstuffModelPrivate *
const m_modelPrivate;
155 NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 )
160 QString NewstuffItem::installedVersion()
const
162 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"version" );
163 if ( nodes.
size() == 1 ) {
170 QString NewstuffItem::installedReleaseDate()
const
172 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"releasedate" );
173 if ( nodes.
size() == 1 ) {
180 bool NewstuffItem::isUpgradable()
const
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;
191 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"installedfile" );
192 for (
int i=0; i<nodes.
count(); ++i ) {
198 bool NewstuffItem::deeperThan(
const QString &one,
const QString &two)
203 FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate,
int index ) :
204 m_modelPrivate( modelPrivate ),
209 void FetchPreviewJob::run(
const QByteArray &data )
217 const QIcon previewIcon( pixmap );
218 m_modelPrivate->setPreview( m_index, previewIcon );
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 )
228 QIcon NewstuffModelPrivate::preview(
int index )
230 if ( m_items.at( index ).m_preview.isNull() ) {
231 QPixmap dummyPixmap( 136, 136 );
233 setPreview( index,
QIcon( dummyPixmap ) );
235 m_networkJobs.insert( reply,
new FetchPreviewJob(
this, index ) );
238 Q_ASSERT( !m_items.at( index ).m_preview.isNull() );
240 return m_items.at( index ).m_preview;
243 void NewstuffModelPrivate::setPreview(
int index,
const QIcon &previewIcon )
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 );
251 void NewstuffModelPrivate::handleProviderData(
QNetworkReply *reply)
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();
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;
274 emit m_parent->dataChanged( affected, affected );
281 FetchPreviewJob *
const job = m_networkJobs.take( reply );
285 if ( !redirectionAttribute.
isNull() ) {
288 m_networkJobs.insert( redirectReply, job );
301 mDebug() <<
"Cannot parse newstuff xml data ";
309 for (
int i=0 ; i < items.
length(); ++i ) {
310 m_items << importNode( items.
item( i ) );
316 bool NewstuffModelPrivate::canExecute(
const QString &executable )
321 if ( application.exists() ) {
329 void NewstuffModelPrivate::installMap()
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" ) ) ) {
338 else if ( m_currentFile->fileName().endsWith(
QLatin1String(
"tar.gz" ) ) && canExecute(
"tar" ) ) {
341 m_parent, SLOT(contentsListed(
int)) );
343 m_unpackProcess->setWorkingDirectory( m_targetDirectory );
344 m_unpackProcess->start(
"tar", arguments );
346 if ( !m_currentFile->fileName().endsWith(
QLatin1String(
"tar.gz" ) ) ) {
347 mDebug() <<
"Can only handle tar.gz files";
349 mDebug() <<
"Cannot extract archive: tar executable not found in PATH.";
354 void NewstuffModelPrivate::unzip()
356 MarbleZipReader zipReader(m_currentFile->fileName());
358 for(
const MarbleZipReader::FileInfo &fileInfo: zipReader.fileInfoList()) {
359 files << fileInfo.filePath;
361 updateRegistry(files);
362 zipReader.extractAll(m_targetDirectory);
363 m_parent->mapInstalled(0);
366 void NewstuffModelPrivate::updateModel()
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";
372 if ( matches.
size() == 1 ) {
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 );
381 if ( m_idTag == NewstuffModel::NameTag && item.m_name == value ) {
382 item.m_registryNode = items.
item( i );
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 );
400 m_parent->beginResetModel();
401 m_parent->endResetModel();
404 void NewstuffModelPrivate::saveRegistry()
406 QFile output( m_registryFile );
408 mDebug() <<
"Cannot open " << m_registryFile <<
" for writing";
411 outStream << m_registryDocument.toString( 2 );
417 void NewstuffModelPrivate::uninstall(
int index )
422 QStringList const files = m_items[index].installedFiles();
423 for(
const QString &file: files ) {
431 std::sort( directories.
begin(), directories.
end(), NewstuffItem::deeperThan );
432 for(
const QString &dir: directories ) {
436 m_items[index].m_registryNode.parentNode().removeChild( m_items[index].m_registryNode );
437 m_items[index].m_registryNode.clear();
443 if ( action == Append ) {
448 if ( !oldNode.
isNull() ) {
456 void NewstuffModelPrivate::readValue(
const QDomNode &node,
const QString &key, T* target )
459 if ( matches.
size() == 1 ) {
462 for (
int i=0; i<matches.
size(); ++i ) {
472 NewstuffModel::NewstuffModel(
QObject *parent ) :
475 setTargetDirectory(MarbleDirs::localPath() +
QLatin1String(
"/maps"));
478 connect( &d->m_networkAccessManager, SIGNAL(finished(
QNetworkReply*)),
483 roles[Name] =
"name";
486 roles[Summary] =
"summary";
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";
497 roles[IsTransitioning] =
"transitioning";
498 roles[PayloadSize] =
"size";
499 roles[DownloadedSize] =
"downloaded";
500 d->m_roleNames = roles;
503 NewstuffModel::~NewstuffModel()
508 int NewstuffModel::rowCount (
const QModelIndex &parent )
const
511 return d->m_items.
size();
519 if ( index.
isValid() && index.
row() >= 0 && index.
row() < d->m_items.size() ) {
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() );
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;
546 return qMax<qint64>( -1, size );
548 case DownloadedSize:
return d->m_items.at( index.
row() ).m_downloadedSize;
557 return d->m_roleNames;
561 int NewstuffModel::count()
const
566 void NewstuffModel::setProvider(
const QString &downloadUrl )
568 if ( downloadUrl == d->m_provider ) {
572 d->m_provider = downloadUrl;
573 emit providerChanged();
577 QString NewstuffModel::provider()
const
579 return d->m_provider;
582 void NewstuffModel::setTargetDirectory(
const QString &targetDirectory )
584 if ( targetDirectory != d->m_targetDirectory ) {
586 if ( !targetDir.exists() ) {
588 qDebug() <<
"Failed to create directory " << targetDirectory <<
", newstuff installation might fail.";
592 d->m_targetDirectory = targetDirectory;
593 emit targetDirectoryChanged();
597 QString NewstuffModel::targetDirectory()
const
599 return d->m_targetDirectory;
602 void NewstuffModel::setRegistryFile(
const QString &filename, IdTag idTag )
604 QString registryFile = filename;
609 if ( d->m_registryFile != registryFile ) {
610 d->m_registryFile = registryFile;
612 emit registryFileChanged();
615 if ( !inputFile.exists() ) {
617 d->m_registryDocument =
QDomDocument(
"khotnewstuff3" );
618 QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction(
"xml",
"version=\"1.0\" encoding=\"utf-8\"" );
620 d->m_root = d->m_registryDocument.createElement(
"hotnewstuffregistry" );
623 QFile input( registryFile );
625 mDebug() <<
"Cannot open newstuff registry " << registryFile;
629 if ( !d->m_registryDocument.setContent( &input ) ) {
630 mDebug() <<
"Cannot parse newstuff registry " << registryFile;
634 d->m_root = d->m_registryDocument.documentElement();
641 QString NewstuffModel::registryFile()
const
643 return d->m_registryFile;
646 void NewstuffModel::install(
int index )
648 if ( index < 0 || index >= d->m_items.size() ) {
652 NewstuffModelPrivate::Action action( index, NewstuffModelPrivate::Install );
655 if ( d->m_actionQueue.contains( action ) ) {
658 d->m_actionQueue << action;
664 void NewstuffModel::uninstall(
int idx )
666 if ( idx < 0 || idx >= d->m_items.size() ) {
670 if ( d->m_items[idx].m_registryNode.isNull() ) {
671 emit uninstallationFinished( idx );
674 NewstuffModelPrivate::Action action( idx, NewstuffModelPrivate::Uninstall );
677 if ( d->m_actionQueue.contains( action ) ) {
680 d->m_actionQueue << action;
686 void NewstuffModel::cancel(
int index )
688 if ( !d->isTransitioning( index ) ) {
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;
702 if ( d->m_unpackProcess ) {
703 d->m_unpackProcess->terminate();
704 d->m_unpackProcess->deleteLater();
705 d->m_unpackProcess =
nullptr;
708 if ( d->m_currentFile ) {
709 d->m_currentFile->deleteLater();
710 d->m_currentFile =
nullptr;
713 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
715 emit installationFailed( d->m_currentAction.first, tr(
"Installation aborted by user." ) );
716 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
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." ) );
726 NewstuffModelPrivate::Action uninstall( index, NewstuffModelPrivate::Uninstall );
727 d->m_actionQueue.removeAll( uninstall );
728 emit uninstallationFinished( index );
736 void NewstuffModel::updateProgress( qint64 bytesReceived, qint64 bytesTotal )
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 ) {
744 item.m_downloadedSize = bytesReceived;
745 QModelIndex const affected = index( d->m_currentAction.first );
746 emit dataChanged( affected, affected );
750 void NewstuffModel::retrieveData()
752 if ( d->m_currentReply && d->m_currentReply->isReadable() ) {
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)) );
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();
773 void NewstuffModel::mapInstalled(
int exitStatus )
775 if ( d->m_unpackProcess ) {
776 d->m_unpackProcess->deleteLater();
777 d->m_unpackProcess =
nullptr;
780 if ( d->m_currentFile ) {
781 d->m_currentFile->deleteLater();
782 d->m_currentFile =
nullptr;
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 );
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 ) );
793 QModelIndex const affected = index( d->m_currentAction.first );
797 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
799 emit dataChanged( affected, affected );
803 void NewstuffModel::mapUninstalled()
805 QModelIndex const affected = index( d->m_currentAction.first );
806 emit uninstallationFinished( d->m_currentAction.first );
810 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
812 emit dataChanged( affected, affected );
816 void NewstuffModel::contentsListed(
int exitStatus )
818 if ( exitStatus == 0 ) {
820 d->updateRegistry(files);
823 this, SLOT(contentsListed(
int)) );
825 this, SLOT(mapInstalled(
int)) );
827 d->m_unpackProcess->start(
"tar", arguments );
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 ) );
834 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
840 void NewstuffModelPrivate::updateRegistry(
const QStringList &files)
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;
848 node = m_root.
appendChild( m_registryDocument.createElement(
"stuff" ) );
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;
869 bool hasChildren =
true;
870 while ( hasChildren ) {
873 hasChildren = !fileList.
isEmpty();
874 for (
int i=0; i<fileList.
count(); ++i ) {
879 for(
const QString &file: files ) {
880 QDomNode fileNode = node.
appendChild( m_registryDocument.createElement(
"installedfile" ) );
888 void NewstuffModelPrivate::processQueue()
890 if ( m_actionQueue.empty() || m_currentAction.first >= 0 ) {
896 m_currentAction = m_actionQueue.takeFirst();
898 if ( m_currentAction.second == Install ) {
899 if ( !m_currentFile ) {
900 QFileInfo const file = m_items.at( m_currentAction.first ).m_payloadUrl.
path();
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()) );
910 m_parent, SLOT(updateProgress(qint64,qint64)) );
913 mDebug() <<
"Failed to write to " << m_currentFile->fileName();
918 QObject::connect( watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()) );
919 QObject::connect( watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()) );
926 NewstuffItem NewstuffModelPrivate::importNode(
const QDomNode &node)
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 );
941 bool NewstuffModelPrivate::isTransitioning(
int index )
const
943 if ( m_currentAction.first == index ) {
947 for(
const Action &action: m_actionQueue ) {
948 if ( action.first == index ) {
958 #include "moc_NewstuffModel.cpp"