6#include "NewstuffModel.h"
8#include "MarbleDebug.h"
10#include "MarbleZipReader.h"
13#include <QDomDocument>
15#include <QFutureWatcher>
18#include <QMutexLocker>
19#include <QNetworkAccessManager>
20#include <QNetworkReply>
22#include <QProcessEnvironment>
23#include <QTemporaryFile>
25#include <QtConcurrentRun>
45 qint64 m_downloadedSize;
49 QString installedVersion()
const;
50 QString installedReleaseDate()
const;
51 bool isUpgradable()
const;
59class NewstuffModelPrivate
72 using Action = QPair<int, UserAction>;
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);
146 FetchPreviewJob(NewstuffModelPrivate *modelPrivate,
int index);
151 NewstuffModelPrivate *
const m_modelPrivate;
155NewstuffItem::NewstuffItem()
157 , m_downloadedSize(0)
162QString NewstuffItem::installedVersion()
const
164 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"version");
165 if (nodes.
size() == 1) {
172QString NewstuffItem::installedReleaseDate()
const
174 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"releasedate");
175 if (nodes.
size() == 1) {
182bool NewstuffItem::isUpgradable()
const
184 bool installedOk, remoteOk;
185 double const installed = installedVersion().toDouble(&installedOk);
186 double const remote = m_version.toDouble(&remoteOk);
187 return installedOk && remoteOk && remote > installed;
193 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"installedfile");
194 for (
int i = 0; i < nodes.
count(); ++i) {
200bool NewstuffItem::deeperThan(
const QString &one,
const QString &two)
205FetchPreviewJob::FetchPreviewJob(NewstuffModelPrivate *modelPrivate,
int index)
206 : m_modelPrivate(modelPrivate)
211void FetchPreviewJob::run(
const QByteArray &data)
219 const QIcon previewIcon(pixmap);
220 m_modelPrivate->setPreview(m_index, previewIcon);
223NewstuffModelPrivate::NewstuffModelPrivate(NewstuffModel *parent)
225 , m_networkAccessManager(nullptr)
226 , m_currentReply(nullptr)
227 , m_currentFile(nullptr)
228 , m_idTag(NewstuffModel::PayloadTag)
229 , m_currentAction(-1, Install)
230 , m_unpackProcess(nullptr)
235QIcon NewstuffModelPrivate::preview(
int index)
237 if (m_items.at(index).m_preview.isNull()) {
240 setPreview(index,
QIcon(dummyPixmap));
242 m_networkJobs.insert(reply,
new FetchPreviewJob(
this, index));
245 Q_ASSERT(!m_items.at(index).m_preview.isNull());
247 return m_items.at(index).m_preview;
250void NewstuffModelPrivate::setPreview(
int index,
const QIcon &previewIcon)
252 NewstuffItem &item = m_items[index];
253 item.m_preview = previewIcon;
254 const QModelIndex affected = m_parent->index(index);
255 Q_EMIT m_parent->dataChanged(affected, affected);
258void NewstuffModelPrivate::handleProviderData(
QNetworkReply *reply)
262 if (!redirectionAttribute.
isNull()) {
263 for (
int i = 0; i < m_items.size(); ++i) {
264 NewstuffItem &item = m_items[i];
265 if (item.m_payloadUrl == reply->
url()) {
266 item.m_payloadUrl = redirectionAttribute.
toUrl();
275 auto length = size.
value<qint64>();
276 for (
int i = 0; i < m_items.size(); ++i) {
277 NewstuffItem &item = m_items[i];
278 if (item.m_payloadUrl == reply->
url()) {
279 item.m_payloadSize = length;
281 Q_EMIT m_parent->dataChanged(affected, affected);
288 FetchPreviewJob *
const job = m_networkJobs.take(reply);
292 if (!redirectionAttribute.
isNull()) {
295 m_networkJobs.insert(redirectReply, job);
308 mDebug() <<
"Cannot parse newstuff xml data ";
316 for (
int i = 0; i < items.
length(); ++i) {
317 m_items << importNode(items.
item(i));
323bool NewstuffModelPrivate::canExecute(
const QString &executable)
328 if (application.exists()) {
336void NewstuffModelPrivate::installMap()
338 if (m_unpackProcess) {
339 m_unpackProcess->close();
340 delete m_unpackProcess;
341 m_unpackProcess =
nullptr;
344 }
else if (m_currentFile->fileName().endsWith(
QLatin1StringView(
"tar.gz")) && canExecute(
"tar")) {
346 QObject::connect(m_unpackProcess, SIGNAL(finished(
int)), m_parent, SLOT(contentsListed(
int)));
349 <<
"-f" << m_currentFile->fileName();
350 m_unpackProcess->setWorkingDirectory(m_targetDirectory);
351 m_unpackProcess->start(
"tar", arguments);
354 mDebug() <<
"Can only handle tar.gz files";
356 mDebug() <<
"Cannot extract archive: tar executable not found in PATH.";
361void NewstuffModelPrivate::unzip()
363 MarbleZipReader zipReader(m_currentFile->fileName());
365 for (
const MarbleZipReader::FileInfo &fileInfo : zipReader.fileInfoList()) {
366 files << fileInfo.filePath;
368 updateRegistry(files);
369 zipReader.extractAll(m_targetDirectory);
370 m_parent->mapInstalled(0);
373void NewstuffModelPrivate::updateModel()
376 for (
int i = 0; i < items.
length(); ++i) {
377 QString const key = m_idTag == NewstuffModel::PayloadTag ?
"payload" :
"name";
379 if (matches.
size() == 1) {
382 for (
int j = 0; j < m_items.size() && !found; ++j) {
383 NewstuffItem &item = m_items[j];
384 if (m_idTag == NewstuffModel::PayloadTag && item.m_payloadUrl.toString() == value) {
385 item.m_registryNode = items.
item(i);
388 if (m_idTag == NewstuffModel::NameTag && item.m_name == value) {
389 item.m_registryNode = items.
item(i);
396 NewstuffItem item = importNode(items.
item(i));
397 if (m_idTag == NewstuffModel::PayloadTag) {
398 item.m_registryNode = items.
item(i);
399 }
else if (m_idTag == NewstuffModel::NameTag) {
400 item.m_registryNode = items.
item(i);
407 m_parent->beginResetModel();
408 m_parent->endResetModel();
411void NewstuffModelPrivate::saveRegistry()
413 QFile output(m_registryFile);
415 mDebug() <<
"Cannot open " << m_registryFile <<
" for writing";
418 outStream << m_registryDocument.toString(2);
424void NewstuffModelPrivate::uninstall(
int index)
429 QStringList const files = m_items[index].installedFiles();
430 for (
const QString &file : files) {
438 std::sort(directories.
begin(), directories.
end(), NewstuffItem::deeperThan);
439 for (
const QString &dir : directories) {
443 m_items[index].m_registryNode.parentNode().removeChild(m_items[index].m_registryNode);
444 m_items[index].m_registryNode.clear();
450 if (action == Append) {
463void NewstuffModelPrivate::readValue(
const QDomNode &node,
const QString &key, T *target)
466 if (matches.
size() == 1) {
469 for (
int i = 0; i < matches.
size(); ++i) {
479NewstuffModel::NewstuffModel(
QObject *parent)
481 , d(new NewstuffModelPrivate(this))
490 roles[Name] =
"name";
493 roles[Summary] =
"summary";
494 roles[Version] =
"version";
495 roles[ReleaseDate] =
"releasedate";
496 roles[Preview] =
"preview";
498 roles[InstalledVersion] =
"installedversion";
499 roles[InstalledReleaseDate] =
"installedreleasedate";
500 roles[InstalledFiles] =
"installedfiles";
501 roles[IsInstalled] =
"installed";
502 roles[IsUpgradable] =
"upgradable";
504 roles[IsTransitioning] =
"transitioning";
505 roles[PayloadSize] =
"size";
506 roles[DownloadedSize] =
"downloaded";
507 d->m_roleNames = roles;
510NewstuffModel::~NewstuffModel()
515int NewstuffModel::rowCount(
const QModelIndex &parent)
const
518 return d->m_items.size();
526 if (index.
isValid() && index.
row() >= 0 && index.
row() < d->m_items.size()) {
529 return d->m_items.at(index.
row()).m_name;
531 return d->preview(index.
row());
533 return d->m_items.at(index.
row()).m_name;
535 return d->m_items.at(index.
row()).m_author;
537 return d->m_items.at(index.
row()).m_license;
539 return d->m_items.at(index.
row()).m_summary;
541 return d->m_items.at(index.
row()).m_version;
543 return d->m_items.at(index.
row()).m_releaseDate;
545 return d->m_items.at(index.
row()).m_previewUrl;
547 return d->m_items.at(index.
row()).m_payloadUrl;
548 case InstalledVersion:
549 return d->m_items.at(index.
row()).installedVersion();
550 case InstalledReleaseDate:
551 return d->m_items.at(index.
row()).installedReleaseDate();
553 return d->m_items.at(index.
row()).installedFiles();
555 return !d->m_items.at(index.
row()).m_registryNode.isNull();
557 return d->m_items.at(index.
row()).isUpgradable();
559 return d->m_items.at(index.
row()).m_category;
560 case IsTransitioning:
561 return d->isTransitioning(index.
row());
563 qint64
const size = d->m_items.at(index.
row()).m_payloadSize;
564 QUrl const url = d->m_items.at(index.
row()).m_payloadUrl;
565 if (size < -1 && !url.
isEmpty()) {
566 d->m_items[index.
row()].m_payloadSize = -1;
570 return qMax<qint64>(-1, size);
573 return d->m_items.at(index.
row()).m_downloadedSize;
582 return d->m_roleNames;
585int NewstuffModel::count()
const
590void NewstuffModel::setProvider(
const QString &downloadUrl)
592 if (downloadUrl == d->m_provider) {
596 d->m_provider = downloadUrl;
597 Q_EMIT providerChanged();
601QString NewstuffModel::provider()
const
603 return d->m_provider;
606void NewstuffModel::setTargetDirectory(
const QString &targetDirectory)
608 if (targetDirectory != d->m_targetDirectory) {
610 if (!targetDir.exists()) {
612 qDebug() <<
"Failed to create directory " << targetDirectory <<
", newstuff installation might fail.";
616 d->m_targetDirectory = targetDirectory;
617 Q_EMIT targetDirectoryChanged();
621QString NewstuffModel::targetDirectory()
const
623 return d->m_targetDirectory;
626void NewstuffModel::setRegistryFile(
const QString &filename, IdTag idTag)
628 QString registryFile = filename;
633 if (d->m_registryFile != registryFile) {
634 d->m_registryFile = registryFile;
636 Q_EMIT registryFileChanged();
639 if (!inputFile.exists()) {
642 QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction(
"xml", R
"(version="1.0" encoding="utf-8")");
644 d->m_root = d->m_registryDocument.createElement("hotnewstuffregistry");
647 QFile input(registryFile);
649 mDebug() <<
"Cannot open newstuff registry " << registryFile;
653 if (!d->m_registryDocument.setContent(&input)) {
654 mDebug() <<
"Cannot parse newstuff registry " << registryFile;
658 d->m_root = d->m_registryDocument.documentElement();
665QString NewstuffModel::registryFile()
const
667 return d->m_registryFile;
670void NewstuffModel::install(
int index)
672 if (index < 0 || index >= d->m_items.size()) {
676 NewstuffModelPrivate::Action action(index, NewstuffModelPrivate::Install);
679 if (d->m_actionQueue.contains(action)) {
682 d->m_actionQueue << action;
688void NewstuffModel::uninstall(
int idx)
690 if (idx < 0 || idx >= d->m_items.size()) {
694 if (d->m_items[idx].m_registryNode.isNull()) {
695 Q_EMIT uninstallationFinished(idx);
698 NewstuffModelPrivate::Action action(idx, NewstuffModelPrivate::Uninstall);
701 if (d->m_actionQueue.contains(action)) {
704 d->m_actionQueue << action;
710void NewstuffModel::cancel(
int index)
712 if (!d->isTransitioning(index)) {
718 if (d->m_currentAction.first == index) {
719 if (d->m_currentAction.second == NewstuffModelPrivate::Install) {
720 if (d->m_currentReply) {
721 d->m_currentReply->abort();
722 d->m_currentReply->deleteLater();
723 d->m_currentReply =
nullptr;
726 if (d->m_unpackProcess) {
727 d->m_unpackProcess->terminate();
728 d->m_unpackProcess->deleteLater();
729 d->m_unpackProcess =
nullptr;
732 if (d->m_currentFile) {
733 d->m_currentFile->deleteLater();
734 d->m_currentFile =
nullptr;
737 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
739 Q_EMIT installationFailed(d->m_currentAction.first, tr(
"Installation aborted by user."));
740 d->m_currentAction = NewstuffModelPrivate::Action(-1, NewstuffModelPrivate::Install);
745 if (d->m_currentAction.second == NewstuffModelPrivate::Install) {
746 NewstuffModelPrivate::Action install(index, NewstuffModelPrivate::Install);
747 d->m_actionQueue.removeAll(install);
748 Q_EMIT installationFailed(index, tr(
"Installation aborted by user."));
750 NewstuffModelPrivate::Action uninstall(index, NewstuffModelPrivate::Uninstall);
751 d->m_actionQueue.removeAll(uninstall);
752 Q_EMIT uninstallationFinished(index);
760void NewstuffModel::updateProgress(qint64 bytesReceived, qint64 bytesTotal)
762 qreal
const progress = qBound<qreal>(0.0, 0.9 * bytesReceived / qreal(bytesTotal), 1.0);
763 Q_EMIT installationProgressed(d->m_currentAction.first, progress);
764 NewstuffItem &item = d->m_items[d->m_currentAction.first];
765 item.m_payloadSize = bytesTotal;
766 if (qreal(bytesReceived - item.m_downloadedSize) / bytesTotal >= 0.01 || progress >= 0.9) {
768 item.m_downloadedSize = bytesReceived;
769 QModelIndex const affected = index(d->m_currentAction.first);
770 Q_EMIT dataChanged(affected, affected);
774void NewstuffModel::retrieveData()
776 if (d->m_currentReply && d->m_currentReply->isReadable()) {
779 if (!redirectionAttribute.
isNull()) {
780 d->m_currentReply = d->m_networkAccessManager.get(
QNetworkRequest(redirectionAttribute.
toUrl()));
781 QObject::connect(d->m_currentReply, SIGNAL(readyRead()),
this, SLOT(retrieveData()));
782 QObject::connect(d->m_currentReply, SIGNAL(readChannelFinished()),
this, SLOT(retrieveData()));
783 QObject::connect(d->m_currentReply, SIGNAL(downloadProgress(qint64, qint64)),
this, SLOT(updateProgress(qint64, qint64)));
785 d->m_currentFile->write(d->m_currentReply->readAll());
786 if (d->m_currentReply->isFinished()) {
787 d->m_currentReply->deleteLater();
788 d->m_currentReply =
nullptr;
789 d->m_currentFile->flush();
796void NewstuffModel::mapInstalled(
int exitStatus)
798 if (d->m_unpackProcess) {
799 d->m_unpackProcess->deleteLater();
800 d->m_unpackProcess =
nullptr;
803 if (d->m_currentFile) {
804 d->m_currentFile->deleteLater();
805 d->m_currentFile =
nullptr;
808 Q_EMIT installationProgressed(d->m_currentAction.first, 1.0);
809 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
810 if (exitStatus == 0) {
811 Q_EMIT installationFinished(d->m_currentAction.first);
813 mDebug() <<
"Process exit status " << exitStatus <<
" indicates an error.";
814 Q_EMIT installationFailed(d->m_currentAction.first, QStringLiteral(
"Unable to unpack file. Process exited with status code %1.").arg(exitStatus));
816 QModelIndex const affected = index(d->m_currentAction.first);
820 d->m_currentAction = NewstuffModelPrivate::Action(-1, NewstuffModelPrivate::Install);
822 Q_EMIT dataChanged(affected, affected);
826void NewstuffModel::mapUninstalled()
828 QModelIndex const affected = index(d->m_currentAction.first);
829 Q_EMIT uninstallationFinished(d->m_currentAction.first);
833 d->m_currentAction = NewstuffModelPrivate::Action(-1, NewstuffModelPrivate::Install);
835 Q_EMIT dataChanged(affected, affected);
839void NewstuffModel::contentsListed(
int exitStatus)
841 if (exitStatus == 0) {
843 d->updateRegistry(files);
845 QObject::disconnect(d->m_unpackProcess, SIGNAL(finished(
int)),
this, SLOT(contentsListed(
int)));
846 QObject::connect(d->m_unpackProcess, SIGNAL(finished(
int)),
this, SLOT(mapInstalled(
int)));
849 <<
"-f" << d->m_currentFile->fileName();
850 d->m_unpackProcess->start(
"tar", arguments);
852 mDebug() <<
"Process exit status " << exitStatus <<
" indicates an error.";
853 Q_EMIT installationFailed(d->m_currentAction.first,
854 QStringLiteral(
"Unable to list file contents. Process exited with status code %1.").arg(exitStatus));
858 d->m_currentAction = NewstuffModelPrivate::Action(-1, NewstuffModelPrivate::Install);
864void NewstuffModelPrivate::updateRegistry(
const QStringList &files)
866 Q_EMIT m_parent->installationProgressed(m_currentAction.first, 0.92);
867 if (!m_registryFile.isEmpty()) {
868 NewstuffItem &item = m_items[m_currentAction.first];
869 QDomNode node = item.m_registryNode;
870 NewstuffModelPrivate::NodeAction action = node.
isNull() ? NewstuffModelPrivate::Append : NewstuffModelPrivate::Replace;
872 node = m_root.
appendChild(m_registryDocument.createElement(
"stuff"));
876 changeNode(node, m_registryDocument,
"name", item.m_name, action);
877 changeNode(node, m_registryDocument,
"providerid", m_provider, action);
878 changeNode(node, m_registryDocument,
"author", item.m_author, action);
879 changeNode(node, m_registryDocument,
"homepage",
QString(), action);
880 changeNode(node, m_registryDocument,
"licence", item.m_license, action);
881 changeNode(node, m_registryDocument,
"version", item.m_version, action);
882 QString const itemId = m_idTag == NewstuffModel::PayloadTag ? item.m_payloadUrl.toString() : item.m_name;
883 changeNode(node, m_registryDocument,
"id", itemId, action);
884 changeNode(node, m_registryDocument,
"releasedate", item.m_releaseDate, action);
885 changeNode(node, m_registryDocument,
"summary", item.m_summary, action);
886 changeNode(node, m_registryDocument,
"changelog",
QString(), action);
887 changeNode(node, m_registryDocument,
"preview", item.m_previewUrl.toString(), action);
888 changeNode(node, m_registryDocument,
"previewBig", item.m_previewUrl.toString(), action);
889 changeNode(node, m_registryDocument,
"payload", item.m_payloadUrl.toString(), action);
890 changeNode(node, m_registryDocument,
"status",
"installed", action);
891 m_items[m_currentAction.first].m_registryNode = node;
893 bool hasChildren =
true;
894 while (hasChildren) {
897 hasChildren = !fileList.
isEmpty();
898 for (
int i = 0; i < fileList.
count(); ++i) {
903 for (
const QString &file : files) {
912void NewstuffModelPrivate::processQueue()
914 if (m_actionQueue.empty() || m_currentAction.first >= 0) {
920 m_currentAction = m_actionQueue.takeFirst();
922 if (m_currentAction.second == Install) {
923 if (!m_currentFile) {
924 QFileInfo const file =
QFileInfo(m_items.at(m_currentAction.first).m_payloadUrl.path());
928 if (m_currentFile->open()) {
929 QUrl const payload = m_items.at(m_currentAction.first).m_payloadUrl;
931 QObject::connect(m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()));
932 QObject::connect(m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()));
933 QObject::connect(m_currentReply, SIGNAL(downloadProgress(qint64, qint64)), m_parent, SLOT(updateProgress(qint64, qint64)));
936 mDebug() <<
"Failed to write to " << m_currentFile->fileName();
941 QObject::connect(watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()));
942 QObject::connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()));
945 watcher->setFuture(future);
949NewstuffItem NewstuffModelPrivate::importNode(
const QDomNode &node)
953 readValue<QString>(node,
"name", &item.m_name);
954 readValue<QString>(node,
"author", &item.m_author);
955 readValue<QString>(node,
"licence", &item.m_license);
956 readValue<QString>(node,
"summary", &item.m_summary);
957 readValue<QString>(node,
"version", &item.m_version);
958 readValue<QString>(node,
"releasedate", &item.m_releaseDate);
959 readValue<QUrl>(node,
"preview", &item.m_previewUrl);
960 readValue<QUrl>(node,
"payload", &item.m_payloadUrl);
964bool NewstuffModelPrivate::isTransitioning(
int index)
const
966 if (m_currentAction.first == index) {
970 for (
const Action &action : m_actionQueue) {
971 if (action.first == index) {
981#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.
bool mkpath(const QString &dirPath) const const
bool rmdir(const QString &dirName) const const
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
bool isEmpty() const const
QDomNode item(int index) const const
QString fileName() const const
QImage fromData(QByteArrayView data, const char *format)
bool isNull() const const
bool isValid() const const
QVariant attribute(QNetworkRequest::Attribute code) const const
QNetworkAccessManager::Operation operation() const const
RedirectionTargetAttribute
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
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isEmpty() const const
bool isNull() const const
bool isValid() const const