Marble

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

KDE's Doxygen guidelines are available online.