From 6228644ef381cfaa953d2b0513a466819cca0e2d Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 00:50:28 +0200 Subject: [PATCH 1/9] Download files via a command line parameter --- src/commandline.cpp | 13 +++++++++++++ src/commandline.h | 5 +++++ src/downloadmanager.cpp | 22 +++++++++++++++++----- src/moapplication.cpp | 3 +++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/commandline.cpp b/src/commandline.cpp index f64ff7cb5..501673c7c 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -187,6 +187,8 @@ std::optional CommandLine::process(const std::wstring& line) // not a shortcut, try a link if (isNxmLink(qs)) { m_nxmLink = qs; + } else if (qs.startsWith("https://", Qt::CaseInsensitive) || qs.startsWith("http://", Qt::CaseInsensitive)) { + m_downloadLink = qs; } else { // assume an executable name/binary m_executable = qs; @@ -221,6 +223,8 @@ bool CommandLine::forwardToPrimary(MOMultiProcess& multiProcess) multiProcess.sendMessage(m_shortcut.toString()); } else if (m_nxmLink) { multiProcess.sendMessage(*m_nxmLink); + } else if (m_downloadLink) { + multiProcess.sendMessage(*m_downloadLink); } else if (m_command && m_command->canForwardToPrimary()) { multiProcess.sendMessage(QString::fromWCharArray(GetCommandLineW())); } else { @@ -297,6 +301,9 @@ std::optional CommandLine::runPostOrganizer(OrganizerCore& core) } else if (m_nxmLink) { log::debug("starting download from command line: {}", *m_nxmLink); core.downloadRequestedNXM(*m_nxmLink); + } else if (m_downloadLink) { + log::debug("starting direct download from command line: {}", *m_downloadLink); + core.downloadManager()->startDownloadURLs(QStringList() << *m_downloadLink); } else if (m_executable) { const QString exeName = *m_executable; log::debug("starting {} from command line", exeName); @@ -332,6 +339,7 @@ void CommandLine::clear() m_vm.clear(); m_shortcut = {}; m_nxmLink = {}; + m_downloadLink = {}; } void CommandLine::createOptions() @@ -468,6 +476,11 @@ std::optional CommandLine::nxmLink() const return m_nxmLink; } +std::optional CommandLine::downloadLink() const +{ + return m_downloadLink; +} + std::optional CommandLine::executable() const { return m_executable; diff --git a/src/commandline.h b/src/commandline.h index a0b70fb2e..2ee3d0671 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -342,6 +342,10 @@ class CommandLine // std::optional nxmLink() const; + // returns the direct download link, if any + // + std::optional downloadLink() const; + // returns the executable/binary, if any // std::optional executable() const; @@ -358,6 +362,7 @@ class CommandLine po::variables_map m_vm; MOShortcut m_shortcut; std::optional m_nxmLink; + std::optional m_downloadLink; std::optional m_executable; QStringList m_untouched; Command* m_command; diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 5bf91c012..3e66be60f 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -431,6 +431,8 @@ bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, QString fileName = QFileInfo(URLs.first()).fileName(); if (fileName.isEmpty()) { fileName = "unknown"; + } else { + fileName = QUrl::fromPercentEncoding(fileName.toUtf8()); } QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); @@ -440,7 +442,10 @@ bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, h2Conf.setStreamReceiveWindowSize(16777215); QNetworkRequest request(preferredUrl); request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); request.setHttp2Configuration(h2Conf); + return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); } @@ -580,7 +585,10 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl newDownload->m_State != STATE_READY && newDownload->m_State != STATE_FETCHINGMODINFO && reply->isFinished()) { - downloadFinished(indexByInfo(newDownload)); + int index = indexByInfo(newDownload); + if (index >= 0) { + downloadFinished(index); + } return; } } else @@ -916,7 +924,7 @@ void DownloadManager::resumeDownloadInt(int index) // Check for finished download; if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr - && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { + && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { setState(info, STATE_DOWNLOADING); downloadFinished(index); return; @@ -1477,7 +1485,7 @@ QString DownloadManager::getDownloadFileName(const QString &baseName, bool renam QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) { if (reply->hasRawHeader("Content-Disposition")) { - std::regex exp("filename=\"(.*)\""); + std::regex exp("filename=\"(.+)\""); std::cmatch result; if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { @@ -2076,10 +2084,14 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, void DownloadManager::downloadFinished(int index) { DownloadInfo *info; - if (index) + if (index > 0) info = m_ActiveDownloads[index]; - else + else { info = findDownload(this->sender(), &index); + if (info == nullptr && index == 0) { + info = m_ActiveDownloads[index]; + } + } if (info != nullptr) { QNetworkReply *reply = info->m_Reply; diff --git a/src/moapplication.cpp b/src/moapplication.cpp index ef33663da..edb9a0f1f 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -405,6 +405,9 @@ void MOApplication::externalMessage(const QString& message) } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false); m_core->downloadRequestedNXM(message); + } else if (message.startsWith("https://", Qt::CaseInsensitive) || message.startsWith("http://", Qt::CaseInsensitive)) { + MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false); + m_core->downloadManager()->startDownloadURLs(QStringList() << message); } else { cl::CommandLine cl; From f0494dc605f8f3f3747d8f31cb1c5e76f48f4a4b Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 00:53:27 +0200 Subject: [PATCH 2/9] Use a GET parameter to predetermine Content-Disposition header in S3-compatible temporary URLs --- src/downloadmanager.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 3e66be60f..ff15a1f02 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -435,6 +435,19 @@ bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, fileName = QUrl::fromPercentEncoding(fileName.toUtf8()); } + // Temporary URLs for S3-compatible storage are signed for a single method, removing the ability to make HEAD requests to such URLs. + // We can use the response-content-disposition GET parameter, setting the Content-Disposition header, to predetermine intended file name without a subrequest. + if (fileName.contains("response-content-disposition=")) { + std::regex exp("filename=\"(.+)\""); + std::cmatch result; + if (std::regex_search(fileName.toStdString().c_str(), result, exp)) { + fileName = MOBase::sanitizeFileName(QString::fromUtf8(result.str(1).c_str())); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + } + } + QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); log::debug("selected download url: {}", preferredUrl.toString()); QHttp2Configuration h2Conf; From dfa9800ef5c99cb137a6d54b35bf5b7e310413ab Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 12:33:50 +0200 Subject: [PATCH 3/9] Reverted resumeDownloadInt --- src/downloadmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index ff15a1f02..38f0aa6da 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -937,7 +937,7 @@ void DownloadManager::resumeDownloadInt(int index) // Check for finished download; if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr - && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { + && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { setState(info, STATE_DOWNLOADING); downloadFinished(index); return; From 28216060c81f86acfb45dce765f8556d1b53d2e9 Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 13:52:56 +0200 Subject: [PATCH 4/9] Revert "Reverted resumeDownloadInt" This reverts commit dfa9800ef5c99cb137a6d54b35bf5b7e310413ab. --- src/downloadmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 38f0aa6da..ff15a1f02 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -937,7 +937,7 @@ void DownloadManager::resumeDownloadInt(int index) // Check for finished download; if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr - && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { + && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { setState(info, STATE_DOWNLOADING); downloadFinished(index); return; From c1f41f2c616dfc89483e2df8fff8dc70d0020743 Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 14:39:16 +0200 Subject: [PATCH 5/9] Moved downloading to a dedicated command --- src/commandline.cpp | 61 ++++++++++++++++++++++++++++++++++--------- src/commandline.h | 20 ++++++++++---- src/moapplication.cpp | 3 --- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/commandline.cpp b/src/commandline.cpp index 501673c7c..734de0584 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -3,6 +3,7 @@ #include "organizercore.h" #include "instancemanager.h" #include "multiprocess.h" +#include "messagedialog.h" #include "loglist.h" #include "shared/util.h" #include "shared/appconfig.h" @@ -60,6 +61,7 @@ CommandLine::CommandLine() add< RunCommand, ReloadPluginCommand, + DownloadFileCommand, RefreshCommand, CrashDumpCommand, LaunchCommand>(); @@ -187,8 +189,6 @@ std::optional CommandLine::process(const std::wstring& line) // not a shortcut, try a link if (isNxmLink(qs)) { m_nxmLink = qs; - } else if (qs.startsWith("https://", Qt::CaseInsensitive) || qs.startsWith("http://", Qt::CaseInsensitive)) { - m_downloadLink = qs; } else { // assume an executable name/binary m_executable = qs; @@ -223,8 +223,6 @@ bool CommandLine::forwardToPrimary(MOMultiProcess& multiProcess) multiProcess.sendMessage(m_shortcut.toString()); } else if (m_nxmLink) { multiProcess.sendMessage(*m_nxmLink); - } else if (m_downloadLink) { - multiProcess.sendMessage(*m_downloadLink); } else if (m_command && m_command->canForwardToPrimary()) { multiProcess.sendMessage(QString::fromWCharArray(GetCommandLineW())); } else { @@ -301,9 +299,6 @@ std::optional CommandLine::runPostOrganizer(OrganizerCore& core) } else if (m_nxmLink) { log::debug("starting download from command line: {}", *m_nxmLink); core.downloadRequestedNXM(*m_nxmLink); - } else if (m_downloadLink) { - log::debug("starting direct download from command line: {}", *m_downloadLink); - core.downloadManager()->startDownloadURLs(QStringList() << *m_downloadLink); } else if (m_executable) { const QString exeName = *m_executable; log::debug("starting {} from command line", exeName); @@ -339,7 +334,6 @@ void CommandLine::clear() m_vm.clear(); m_shortcut = {}; m_nxmLink = {}; - m_downloadLink = {}; } void CommandLine::createOptions() @@ -476,11 +470,6 @@ std::optional CommandLine::nxmLink() const return m_nxmLink; } -std::optional CommandLine::downloadLink() const -{ - return m_downloadLink; -} - std::optional CommandLine::executable() const { return m_executable; @@ -921,6 +910,52 @@ std::optional ReloadPluginCommand::runPostOrganizer(OrganizerCore& core) } +Command::Meta DownloadFileCommand::meta() const +{ + return { + "download", + "downloads a file", + "URL", + "" + }; +} + +po::options_description DownloadFileCommand::getInternalOptions() const +{ + po::options_description d; + + d.add_options() + ("URL", po::value()->required(), "file URL"); + + return d; +} + +po::positional_options_description DownloadFileCommand::getPositional() const +{ + po::positional_options_description d; + + d.add("URL", 1); + + return d; +} + +bool DownloadFileCommand::canForwardToPrimary() const +{ + return true; +} + +std::optional DownloadFileCommand::runPostOrganizer(OrganizerCore& core) +{ + const QString url = QString::fromStdString(vm()["URL"].as()); + + log::debug("starting direct download from command line: {}", url.toStdString()); + MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), false); + core.downloadManager()->startDownloadURLs(QStringList() << url); + + return {}; +} + + Command::Meta RefreshCommand::meta() const { return { diff --git a/src/commandline.h b/src/commandline.h index 2ee3d0671..c2990b4af 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -218,6 +218,21 @@ class ReloadPluginCommand : public Command }; +// downloads a file +// +class DownloadFileCommand : public Command +{ +protected: + Meta meta() const override; + + po::options_description getInternalOptions() const override; + po::positional_options_description getPositional() const override; + + bool canForwardToPrimary() const override; + std::optional runPostOrganizer(OrganizerCore& core) override; +}; + + // refreshes mo // class RefreshCommand : public Command @@ -342,10 +357,6 @@ class CommandLine // std::optional nxmLink() const; - // returns the direct download link, if any - // - std::optional downloadLink() const; - // returns the executable/binary, if any // std::optional executable() const; @@ -362,7 +373,6 @@ class CommandLine po::variables_map m_vm; MOShortcut m_shortcut; std::optional m_nxmLink; - std::optional m_downloadLink; std::optional m_executable; QStringList m_untouched; Command* m_command; diff --git a/src/moapplication.cpp b/src/moapplication.cpp index edb9a0f1f..ef33663da 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -405,9 +405,6 @@ void MOApplication::externalMessage(const QString& message) } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false); m_core->downloadRequestedNXM(message); - } else if (message.startsWith("https://", Qt::CaseInsensitive) || message.startsWith("http://", Qt::CaseInsensitive)) { - MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false); - m_core->downloadManager()->startDownloadURLs(QStringList() << message); } else { cl::CommandLine cl; From 72727c7b542498c60726c56cf180d1bc0e09c974 Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 14:49:02 +0200 Subject: [PATCH 6/9] Enforce HTTPS download links --- src/commandline.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commandline.cpp b/src/commandline.cpp index 734de0584..f3f749ce6 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -948,6 +948,12 @@ std::optional DownloadFileCommand::runPostOrganizer(OrganizerCore& core) { const QString url = QString::fromStdString(vm()["URL"].as()); + if (!url.startsWith("https://")) { + reportError( + QObject::tr("Download URL must start with https://")); + return {}; + } + log::debug("starting direct download from command line: {}", url.toStdString()); MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), false); core.downloadManager()->startDownloadURLs(QStringList() << url); From cf45dfd9c5217f9c651358a945bd7b0e09acdba5 Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 16:37:26 +0200 Subject: [PATCH 7/9] Return numeric error codes --- src/commandline.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commandline.cpp b/src/commandline.cpp index cae7dacc2..45870c37c 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -881,14 +881,14 @@ std::optional DownloadFileCommand::runPostOrganizer(OrganizerCore& core) if (!url.startsWith("https://")) { reportError( QObject::tr("Download URL must start with https://")); - return {}; + return 1; } log::debug("starting direct download from command line: {}", url.toStdString()); MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), false); core.downloadManager()->startDownloadURLs(QStringList() << url); - return {}; + return 0; } From 8908df40d09de27050c55433db600ad273524ffa Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 16:40:53 +0200 Subject: [PATCH 8/9] Return numeric error codes --- src/commandline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commandline.cpp b/src/commandline.cpp index 45870c37c..7af8ad635 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -888,7 +888,7 @@ std::optional DownloadFileCommand::runPostOrganizer(OrganizerCore& core) MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), false); core.downloadManager()->startDownloadURLs(QStringList() << url); - return 0; + return {}; } From 1c6ad481b7ba257d05f7e0bdadc86a7d9926735b Mon Sep 17 00:00:00 2001 From: Eddoursul Date: Fri, 15 Sep 2023 20:38:38 +0200 Subject: [PATCH 9/9] clang-format pass --- src/commandline.cpp | 29 ++++++++--------------------- src/commandline.h | 2 -- src/downloadmanager.cpp | 14 ++++++++------ 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/commandline.cpp b/src/commandline.cpp index 7af8ad635..1d44e0435 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -1,8 +1,8 @@ #include "commandline.h" #include "env.h" #include "instancemanager.h" -#include "messagedialog.h" #include "loglist.h" +#include "messagedialog.h" #include "multiprocess.h" #include "organizercore.h" #include "shared/appconfig.h" @@ -50,13 +50,8 @@ CommandLine::CommandLine() : m_command(nullptr) { createOptions(); - add< - RunCommand, - ReloadPluginCommand, - DownloadFileCommand, - RefreshCommand, - CrashDumpCommand, - LaunchCommand>(); + add(); } std::optional CommandLine::process(const std::wstring& line) @@ -839,23 +834,16 @@ std::optional ReloadPluginCommand::runPostOrganizer(OrganizerCore& core) return {}; } - Command::Meta DownloadFileCommand::meta() const { - return { - "download", - "downloads a file", - "URL", - "" - }; + return {"download", "downloads a file", "URL", ""}; } po::options_description DownloadFileCommand::getInternalOptions() const { po::options_description d; - d.add_options() - ("URL", po::value()->required(), "file URL"); + d.add_options()("URL", po::value()->required(), "file URL"); return d; } @@ -879,19 +867,18 @@ std::optional DownloadFileCommand::runPostOrganizer(OrganizerCore& core) const QString url = QString::fromStdString(vm()["URL"].as()); if (!url.startsWith("https://")) { - reportError( - QObject::tr("Download URL must start with https://")); + reportError(QObject::tr("Download URL must start with https://")); return 1; } log::debug("starting direct download from command line: {}", url.toStdString()); - MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), false); + MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(), + false); core.downloadManager()->startDownloadURLs(QStringList() << url); return {}; } - Command::Meta RefreshCommand::meta() const { return {"refresh", "refreshes MO (same as F5)", "", ""}; diff --git a/src/commandline.h b/src/commandline.h index 2d038199a..3d7821054 100644 --- a/src/commandline.h +++ b/src/commandline.h @@ -205,7 +205,6 @@ class ReloadPluginCommand : public Command std::optional runPostOrganizer(OrganizerCore& core) override; }; - // downloads a file // class DownloadFileCommand : public Command @@ -220,7 +219,6 @@ class DownloadFileCommand : public Command std::optional runPostOrganizer(OrganizerCore& core) override; }; - // refreshes mo // class RefreshCommand : public Command diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index ae8685b4b..5ccbdb4da 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -439,8 +439,10 @@ bool DownloadManager::addDownload(const QStringList& URLs, QString gameName, int fileName = QUrl::fromPercentEncoding(fileName.toUtf8()); } - // Temporary URLs for S3-compatible storage are signed for a single method, removing the ability to make HEAD requests to such URLs. - // We can use the response-content-disposition GET parameter, setting the Content-Disposition header, to predetermine intended file name without a subrequest. + // Temporary URLs for S3-compatible storage are signed for a single method, removing + // the ability to make HEAD requests to such URLs. We can use the + // response-content-disposition GET parameter, setting the Content-Disposition header, + // to predetermine intended file name without a subrequest. if (fileName.contains("response-content-disposition=")) { std::regex exp("filename=\"(.+)\""); std::cmatch result; @@ -462,7 +464,8 @@ bool DownloadManager::addDownload(const QStringList& URLs, QString gameName, int request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, + QNetworkRequest::AlwaysNetwork); request.setHttp2Configuration(h2Conf); return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); @@ -965,8 +968,7 @@ void DownloadManager::resumeDownloadInt(int index) // Check for finished download; if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr && - info->m_Reply->isFinished() && - info->m_State != STATE_ERROR) { + info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { setState(info, STATE_DOWNLOADING); downloadFinished(index); return; @@ -2161,7 +2163,7 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, void DownloadManager::downloadFinished(int index) { - DownloadInfo *info; + DownloadInfo* info; if (index > 0) info = m_ActiveDownloads[index]; else {