swift
downloadcomponent.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2017 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
4 #include "downloadcomponent.h"
5 
6 #include <QDesktopServices>
7 #include <QFileDialog>
8 #include <QMessageBox>
9 #include <QPointer>
10 #include <QProcess>
11 #include <QStandardPaths>
12 #include <QTimer>
13 
14 #include "ui_downloadcomponent.h"
15 
16 #include "config/buildconfig.h"
17 #include "gui/guiapplication.h"
19 #include "misc/directoryutils.h"
20 #include "misc/fileutils.h"
21 #include "misc/logmessage.h"
23 
24 using namespace swift::config;
25 using namespace swift::misc;
26 using namespace swift::misc::db;
27 using namespace swift::misc::network;
28 using namespace swift::misc::simulation;
29 
30 namespace swift::gui::components
31 {
32  CDownloadComponent::CDownloadComponent(QWidget *parent)
34  {
35  ui->setupUi(this);
36  this->setOverlaySizeFactors(0.8, 0.9);
37  this->setForceSmall(true);
38 
39  ui->le_DownloadDir->setText(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
40  ui->prb_Current->setMinimum(0);
41  ui->prb_Current->setMaximum(1); // min/max 0,0 means busy indicator
42  ui->prb_Current->setValue(0);
43  ui->prb_Total->setMinimum(0);
44  ui->prb_Total->setMaximum(1);
45  ui->prb_Total->setValue(0);
46 
47  connect(ui->tb_DialogDownloadDir, &QToolButton::pressed, this, &CDownloadComponent::selectDownloadDirectory);
48  connect(ui->tb_ResetDownloadDir, &QToolButton::pressed, this, &CDownloadComponent::resetDownloadDir);
49  connect(ui->tb_CancelDownload, &QToolButton::pressed, this, &CDownloadComponent::cancelOngoingDownloads);
50  connect(ui->pb_Download, &QPushButton::pressed, [=] { this->triggerDownloadingOfFiles(); });
51  connect(ui->pb_OpenDownloadDir, &QPushButton::pressed, this, &CDownloadComponent::openDownloadDir);
52  connect(ui->pb_Launch, &QPushButton::pressed, this, &CDownloadComponent::startDownloadedExecutable);
53  }
54 
56 
58  {
59  return this->setDownloadFiles(CRemoteFileList { remoteFile });
60  }
61 
63  {
64  if (!m_waitingForDownload.isEmpty()) { return false; }
65  m_remoteFiles = remoteFiles;
66  this->clear();
67  return true;
68  }
69 
70  bool CDownloadComponent::setDownloadDirectory(const QString &path)
71  {
72  const QDir d(path);
73  if (!d.exists()) return false;
74  ui->le_DownloadDir->setText(d.absolutePath());
75  return true;
76  }
77 
78  void CDownloadComponent::selectDownloadDirectory()
79  {
80  QString downloadDir = ui->le_DownloadDir->text().trimmed();
81  downloadDir = QFileDialog::getExistingDirectory(parentWidget(), tr("Choose your download directory"),
82  downloadDir, m_fileDialogOptions);
83 
84  if (downloadDir.isEmpty()) { return; } // canceled
85  if (!QDir(downloadDir).exists())
86  {
87  const CStatusMessage msg =
88  CStatusMessage(this, CLogCategories::validation()).warning(u"'%1' is not a valid download directory")
89  << downloadDir;
90  this->showOverlayMessage(msg, CDownloadComponent::OverlayMsgTimeout);
91  return;
92  }
93  ui->le_DownloadDir->setText(downloadDir);
94  }
95 
97  {
98  ui->pb_Download->setEnabled(false);
99  ui->pb_Launch->setEnabled(false);
100  if (m_remoteFiles.isEmpty()) { return false; }
101  if (!m_waitingForDownload.isEmpty()) { return false; }
102  if (delayMs > 0)
103  {
104  const QPointer<CDownloadComponent> myself(this);
105  QTimer::singleShot(delayMs, this, [=] {
106  if (!myself || !sGui || sGui->isShuttingDown()) { return; }
107  this->triggerDownloadingOfFiles();
108  });
109  return true;
110  }
111  m_waitingForDownload = m_remoteFiles;
112  this->showFileInfo();
113  return this->triggerDownloadingOfNextFile();
114  }
115 
116  bool CDownloadComponent::isDownloading() const { return m_reply || m_fileInProgress.hasName(); }
117 
119  {
120  if (this->isDownloading()) { return false; }
121  if (!m_waitingForDownload.isEmpty()) { return false; }
122  return true;
123  }
124 
125  CDownloadComponent::Mode CDownloadComponent::getMode() const
126  {
127  Mode mode = ui->cb_Shutdown->isChecked() ? ShutdownSwift : JustDownload;
128  if (ui->cb_StartAfterDownload) { mode |= StartAfterDownload; }
129  return mode;
130  }
131 
133  {
134  ui->cb_Shutdown->setChecked(mode.testFlag(ShutdownSwift));
135  ui->cb_StartAfterDownload->setChecked(mode.testFlag(StartAfterDownload));
136  }
137 
139  {
140  if (m_reply)
141  {
142  m_reply->abort();
143  m_reply = nullptr;
144  }
145 
146  m_waitingForDownload.clear();
147  m_fileInProgress = CRemoteFile();
148 
149  ui->prb_Current->setValue(0);
150  ui->prb_Total->setValue(0);
151 
152  ui->le_Completed->clear();
153  ui->le_CompletedNumber->clear();
154  ui->le_CompletedUrl->clear();
155  ui->le_Started->clear();
156  ui->le_StartedNumber->clear();
157  ui->le_StartedUrl->clear();
158  this->showFileInfo();
159  ui->pb_Download->setEnabled(true);
160  }
161 
162  bool CDownloadComponent::triggerDownloadingOfNextFile()
163  {
164  if (m_waitingForDownload.isEmpty()) { return false; }
165  const CRemoteFile rf = m_waitingForDownload.front();
166  m_waitingForDownload.pop_front();
167  return this->triggerDownloadingOfFile(rf);
168  }
169 
170  bool CDownloadComponent::triggerDownloadingOfFile(const CRemoteFile &remoteFile)
171  {
172  if (!sGui || !sGui->hasWebDataServices() || sGui->isShuttingDown()) { return false; }
173  if (!this->existsDownloadDir())
174  {
175  const CStatusMessage msg =
176  CStatusMessage(this, CLogCategories::validation()).error(u"Invalid download directory");
177  this->showOverlayMessage(msg, CDownloadComponent::OverlayMsgTimeout);
178  return false;
179  }
180 
181  const CUrl download = remoteFile.getSmartUrl();
182  if (download.isEmpty())
183  {
184  const CStatusMessage msg =
185  CStatusMessage(this, CLogCategories::validation()).error(u"No download URL for file name '%1'")
186  << remoteFile.getBaseNameAndSize();
187  this->showOverlayMessage(msg, CDownloadComponent::OverlayMsgTimeout);
188  return false;
189  }
190 
191  this->showStartedFileMessage(remoteFile);
192  m_fileInProgress = remoteFile;
193  const QString saveAsFile = CFileUtils::appendFilePaths(ui->le_DownloadDir->text(), remoteFile.getBaseName());
194  const QFileInfo fiSaveAs(saveAsFile);
195  if (fiSaveAs.exists())
196  {
197  const QString msg = QStringLiteral("File '%1' already exists locally.\n\nDo you want to reload the file?")
198  .arg(fiSaveAs.absoluteFilePath());
199  QMessageBox::StandardButton reply =
200  QMessageBox::question(this, "File exists", msg, QMessageBox::Yes | QMessageBox::No);
201  if (reply != QMessageBox::Yes)
202  {
203  const QPointer<CDownloadComponent> myself(this);
204  QTimer::singleShot(10, this, [=] {
205  if (!myself || !sGui || sGui->isShuttingDown()) { return; }
206  this->downloadedFile(CStatusMessage(this).info(u"File was already downloaded"));
207  });
208  return true;
209  }
210  }
211 
212  QNetworkReply *reply =
213  sGui->downloadFromNetwork(download, saveAsFile, { this, &CDownloadComponent::downloadedFile });
214  bool success = false;
215  if (reply)
216  {
217  // this->showLoading(10 * 1000);
218  CLogMessage(this).info(u"Triggered downloading of file from '%1'") << download.getHost();
219  connect(reply, &QNetworkReply::downloadProgress, this, &CDownloadComponent::downloadProgress,
220  Qt::QueuedConnection);
221  m_reply = reply;
222  success = true;
223  }
224  else
225  {
226  const CStatusMessage msg =
227  CStatusMessage(this, CLogCategories::validation()).error(u"Starting download for '%1' failed")
228  << download.getFullUrl();
229  this->showOverlayMessage(msg, CDownloadComponent::OverlayMsgTimeout);
230  }
231  return success;
232  }
233 
234  void CDownloadComponent::downloadedFile(const CStatusMessage &status)
235  {
236  // reset in progress
237  const CRemoteFile justDownloaded(m_fileInProgress);
238  m_fileInProgress = CRemoteFile();
239  m_reply = nullptr;
240  this->showCompletedFileMessage(justDownloaded);
241  this->hideLoading();
242 
243  if (sGui && sGui->isShuttingDown()) { return; }
244  if (status.isWarningOrAbove())
245  {
246  this->showOverlayMessage(status, CDownloadComponent::OverlayMsgTimeout);
247  this->clear();
248  return;
249  }
250 
251  const bool t = this->triggerDownloadingOfNextFile();
252  if (!t) { this->lastFileDownloaded(); }
253  }
254 
255  void CDownloadComponent::lastFileDownloaded()
256  {
257  const QPointer<CDownloadComponent> myself(this);
258  QTimer::singleShot(0, this, [=] {
259  if (!myself || !sGui || sGui->isShuttingDown()) { return; }
260  myself->ui->pb_Download->setEnabled(true);
261  myself->ui->pb_Launch->setEnabled(true);
262  emit allDownloadsCompleted();
263  });
264  this->startDownloadedExecutable();
265  }
266 
267  void CDownloadComponent::startDownloadedExecutable()
268  {
269  if (!ui->cb_StartAfterDownload->isChecked()) { return; }
270  if (!this->haveAllDownloadsCompleted()) { return; }
271  const CRemoteFileList executables = m_remoteFiles.findExecutableFiles();
272  if (executables.isEmpty()) { return; }
273 
274  // try to start
275  const QDir dir(ui->le_DownloadDir->text());
276  if (!dir.exists()) { return; }
277 
278  QString msg;
279  if (CBuildConfig::isRunningOnMacOSPlatform())
280  {
281  msg = "To install close swift, "
282  "mount the disk image '%1' and run the installer inside "
283  "to proceed with the update.";
284  }
285  else { msg = ui->cb_Shutdown->isChecked() ? QString("Start '%1' and close swift?") : QString("Start '%1'?"); }
286 
287  for (const CRemoteFile &rf : executables)
288  {
289  const QString executable = CFileUtils::appendFilePaths(dir.absolutePath(), rf.getBaseName());
290  QFile executableFile(executable);
291  if (!executableFile.exists()) { continue; }
292 
293  QMessageBox::StandardButton reply =
294  QMessageBox::question(this, "Start?", msg.arg(rf.getName()), QMessageBox::Yes | QMessageBox::No);
295  if (reply != QMessageBox::Yes) { return; }
296 
297  const CPlatform p = CArtifact::artifactNameToPlatform(rf.getName());
298  if (!CPlatform::canRunOnCurrentPlatform(p))
299  {
300  // cannot run on this OS, just show the directory where the download resides
301  // do not close
302  ui->pb_OpenDownloadDir->click();
303  return;
304  }
305 
306  if (CBuildConfig::isRunningOnLinuxPlatform() && !executableFile.permissions().testFlag(QFile::ExeOwner))
307  {
308  executableFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
309  QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther);
310  }
311 
312  const bool shutdown = ui->cb_Shutdown->isChecked();
313  const bool started = QProcess::startDetached(executable, {}, dir.absolutePath());
314  if (started && shutdown && sGui)
315  {
316  QTimer::singleShot(250, sGui, [] {
317  if (!sGui) { return; }
319  });
320  break;
321  }
322  } // files
323  }
324 
325  bool CDownloadComponent::existsDownloadDir() const
326  {
327  if (ui->le_DownloadDir->text().isEmpty()) { return false; }
328  const QDir dir(ui->le_DownloadDir->text());
329  return dir.exists() && dir.isReadable();
330  }
331 
332  void CDownloadComponent::openDownloadDir()
333  {
334  if (!this->existsDownloadDir()) { return; }
335  QDesktopServices::openUrl(QUrl::fromLocalFile(ui->le_DownloadDir->text()));
336  }
337 
338  void CDownloadComponent::resetDownloadDir()
339  {
340  ui->le_DownloadDir->setText(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
341  }
342 
343  void CDownloadComponent::showStartedFileMessage(const CRemoteFile &rf)
344  {
345  const int current = m_remoteFiles.size() - m_waitingForDownload.size();
346  ui->le_Started->setText(rf.getBaseName());
347  ui->le_StartedNumber->setText(QStringLiteral("%1/%2").arg(current).arg(m_remoteFiles.size()));
348  ui->le_StartedUrl->setText(rf.getUrl().getFullUrl());
349  ui->prb_Total->setMaximum(m_remoteFiles.size());
350  ui->prb_Total->setValue(current - 1);
351  }
352 
353  void CDownloadComponent::showCompletedFileMessage(const CRemoteFile &rf)
354  {
355  const int current = m_remoteFiles.size() - m_waitingForDownload.size();
356  ui->le_Completed->setText(rf.getBaseName());
357  ui->le_CompletedNumber->setText(QStringLiteral("%1/%2").arg(current).arg(m_remoteFiles.size()));
358  ui->le_CompletedUrl->setText(rf.getUrl().getFullUrl());
359  ui->prb_Total->setMaximum(m_remoteFiles.size());
360  ui->prb_Total->setValue(current);
361  }
362 
364 
365  void CDownloadComponent::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
366  {
367  ui->prb_Current->setMaximum(static_cast<int>(bytesTotal));
368  ui->prb_Current->setValue(static_cast<int>(bytesReceived));
369  }
370 
371  void CDownloadComponent::showFileInfo()
372  {
373  ui->le_Info->setText(QStringLiteral("Files: %1 size: %2")
374  .arg(m_remoteFiles.size())
375  .arg(m_remoteFiles.getTotalFileSizeHumanReadable()));
376  }
377 } // namespace swift::gui::components
QNetworkReply * downloadFromNetwork(const swift::misc::network::CUrl &url, const QString &saveAsFileName, const swift::misc::CSlot< void(const swift::misc::CStatusMessage &)> &callback, int maxRedirects=DefaultMaxRedirects)
Download file from network and store it as passed.
bool hasWebDataServices() const
Web data services available?
bool isShuttingDown() const
Is application shutting down?
static void exit(int retcode=0)
Exit application, perform graceful shutdown and exit.
Enable widget class for load indicator.
void hideLoading()
Hide load indicator.
bool showOverlayMessage(const swift::misc::CStatusMessage &message, std::chrono::milliseconds timeout=std::chrono::milliseconds(0))
Show single message.
void setForceSmall(bool force)
Force small (smaller layout)
void setOverlaySizeFactors(double widthFactor, double heightFactor, double middleFactor=2)
Set the size factors.
Using this class provides a QFrame with the overlay functionality already integrated.
@ ShutdownSwift
for installers, stop swift before running
bool triggerDownloadingOfFiles(int delayMs=-1)
Trigger downloading of the file.
bool setDownloadFile(const swift::misc::network::CRemoteFile &remoteFile)
Set file to be downloaded.
void allDownloadsCompleted()
All downloads have been completed.
bool haveAllDownloadsCompleted() const
Have all downloads completed?
bool setDownloadDirectory(const QString &path)
Set donwload directory.
bool isDownloading() const
Downloads in progress.
bool setDownloadFiles(const swift::misc::network::CRemoteFileList &remoteFiles)
Set files to be downloaded.
void cancelOngoingDownloads()
Cancel ongoing downloads.
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & error(const char16_t(&format)[N])
Set the severity to error, providing a format string.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
Platform (i.e.
Definition: platform.h:24
size_type size() const
Returns number of elements in the sequence.
Definition: sequence.h:273
reference front()
Access the first element.
Definition: sequence.h:225
void clear()
Removes all elements in the sequence.
Definition: sequence.h:288
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
void pop_front()
Removes an element at the front of the sequence.
Definition: sequence.h:374
Streamable status message, e.g.
bool isWarningOrAbove() const
Warning or above.
QString getBaseName() const
Name with directory stripped.
Definition: remotefile.h:55
CUrl getSmartUrl() const
Automatically concatenates the name if missing.
Definition: remotefile.cpp:39
const CUrl & getUrl() const
Get URL.
Definition: remotefile.h:76
bool hasName() const
Has name?
Definition: remotefile.h:58
QString getBaseNameAndSize() const
Name + human readable size.
Definition: remotefile.cpp:24
const QString & getName() const
Name.
Definition: remotefile.h:52
Value object encapsulating a list of remote files.
QString getTotalFileSizeHumanReadable() const
Size formatted.
CRemoteFileList findExecutableFiles() const
Find all executable files (decided by appendix)
Value object encapsulating information of a location, kind of simplified CValueObject compliant versi...
Definition: url.h:27
bool isEmpty() const
Empty.
Definition: url.cpp:54
const QString & getHost() const
Get host.
Definition: url.h:55
QString getFullUrl(bool withQuery=true) const
Qualified name.
Definition: url.cpp:84
SWIFT_GUI_EXPORT swift::gui::CGuiApplication * sGui
Single instance of GUI application object.
High level reusable GUI components.
Definition: aboutdialog.cpp:13
Free functions in swift::misc.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30