Merge pull request #4892 from aws-lumberyard-dev/TrackDownloadProgress

Project Manager track progress of, and cancel downloads
monroegm-disable-blank-issue-2
AMZN-Phil 4 years ago committed by GitHub
commit 69771ab2e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,9 +8,11 @@
#include <DownloadController.h> #include <DownloadController.h>
#include <DownloadWorker.h> #include <DownloadWorker.h>
#include <PythonBindings.h>
#include <QMessageBox> #include <AzCore/std/algorithm.h>
#include <QMessageBox>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -46,6 +48,24 @@ namespace O3DE::ProjectManager
} }
} }
void DownloadController::CancelGemDownload(const QString& gemName)
{
auto findResult = AZStd::find(m_gemNames.begin(), m_gemNames.end(), gemName);
if (findResult != m_gemNames.end())
{
if (findResult == m_gemNames.begin())
{
// HandleResults will remove the gem upon cancelling
PythonBindingsInterface::Get()->CancelDownload();
}
else
{
m_gemNames.erase(findResult);
}
}
}
void DownloadController::UpdateUIProgress(int progress) void DownloadController::UpdateUIProgress(int progress)
{ {
m_lastProgress = progress; m_lastProgress = progress;
@ -75,10 +95,4 @@ namespace O3DE::ProjectManager
m_workerThread.wait(); m_workerThread.wait();
} }
} }
void DownloadController::HandleCancel()
{
m_workerThread.quit();
emit Done(false);
}
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -27,7 +27,8 @@ namespace O3DE::ProjectManager
explicit DownloadController(QWidget* parent = nullptr); explicit DownloadController(QWidget* parent = nullptr);
~DownloadController(); ~DownloadController();
void AddGemDownload(const QString& m_gemName); void AddGemDownload(const QString& gemName);
void CancelGemDownload(const QString& gemName);
bool IsDownloadQueueEmpty() bool IsDownloadQueueEmpty()
{ {
@ -54,7 +55,6 @@ namespace O3DE::ProjectManager
public slots: public slots:
void UpdateUIProgress(int progress); void UpdateUIProgress(int progress);
void HandleResults(const QString& result); void HandleResults(const QString& result);
void HandleCancel();
signals: signals:
void StartGemDownload(const QString& gemName); void StartGemDownload(const QString& gemName);

@ -155,6 +155,11 @@ namespace O3DE::ProjectManager
update(); update();
} }
void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName)
{
m_downloadController->CancelGemDownload(gemName);
}
void CartOverlayWidget::CreateDownloadSection() void CartOverlayWidget::CreateDownloadSection()
{ {
QWidget* widget = new QWidget(); QWidget* widget = new QWidget();
@ -235,7 +240,9 @@ namespace O3DE::ProjectManager
nameProgressLayout->addWidget(progress); nameProgressLayout->addWidget(progress);
QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
nameProgressLayout->addSpacerItem(spacer); nameProgressLayout->addSpacerItem(spacer);
QLabel* cancelText = new QLabel(tr("Cancel")); QLabel* cancelText = new QLabel(QString("<a href=\"%1\">Cancel</a>").arg(downloadQueue[downloadingGemNumber]));
cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
nameProgressLayout->addWidget(cancelText); nameProgressLayout->addWidget(cancelText);
downloadingItemLayout->addLayout(nameProgressLayout); downloadingItemLayout->addLayout(nameProgressLayout);
QProgressBar* downloadProgessBar = new QProgressBar(); QProgressBar* downloadProgessBar = new QProgressBar();

@ -41,6 +41,7 @@ namespace O3DE::ProjectManager
using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>; using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>;
void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices); void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices);
void CreateDownloadSection(); void CreateDownloadSection();
void OnCancelDownloadActivated(const QString& link);
QVBoxLayout* m_layout = nullptr; QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr; GemModel* m_gemModel = nullptr;

@ -223,6 +223,7 @@ namespace RedirectOutput
} }
} // namespace RedirectOutput } // namespace RedirectOutput
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath) PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
@ -1120,18 +1121,29 @@ namespace O3DE::ProjectManager
AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback)
{ {
// This process is currently limited to download a single gem at a time.
bool downloadSucceeded = false; bool downloadSucceeded = false;
m_requestCancelDownload = false;
auto result = ExecuteWithLockErrorHandling( auto result = ExecuteWithLockErrorHandling(
[&] [&]
{ {
auto downloadResult = m_download.attr("download_gem")( auto downloadResult = m_download.attr("download_gem")(
QString_To_Py_String(gemName), // gem name QString_To_Py_String(gemName), // gem name
pybind11::none(), // destination path pybind11::none(), // destination path
false// skip auto register false, // skip auto register
pybind11::cpp_function(
[this, gemProgressCallback](int progress)
{
gemProgressCallback(progress);
return m_requestCancelDownload;
}) // Callback for download progress and cancelling
); );
downloadSucceeded = (downloadResult.cast<int>() == 0); downloadSucceeded = (downloadResult.cast<int>() == 0);
}); });
if (!result.IsSuccess()) if (!result.IsSuccess())
{ {
return result; return result;
@ -1143,4 +1155,9 @@ namespace O3DE::ProjectManager
return AZ::Success(); return AZ::Success();
} }
void PythonBindings::CancelDownload()
{
m_requestCancelDownload = true;
}
} }

@ -63,6 +63,7 @@ namespace O3DE::ProjectManager
bool RemoveGemRepo(const QString& repoUri) override; bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override; AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override; AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override;
void CancelDownload() override;
private: private:
AZ_DISABLE_COPY_MOVE(PythonBindings); AZ_DISABLE_COPY_MOVE(PythonBindings);
@ -91,5 +92,7 @@ namespace O3DE::ProjectManager
pybind11::handle m_editProjectProperties; pybind11::handle m_editProjectProperties;
pybind11::handle m_download; pybind11::handle m_download;
pybind11::handle m_pathlib; pybind11::handle m_pathlib;
bool m_requestCancelDownload = false;
}; };
} }

@ -196,7 +196,18 @@ namespace O3DE::ProjectManager
*/ */
virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0; virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0;
/**
* Downloads and registers a Gem.
* @param gemName the name of the Gem to download
* @param gemProgressCallback a callback function that is called with an int percentage download value
* @return an outcome with a string error message on failure.
*/
virtual AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0; virtual AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0;
/**
* Cancels the current download.
*/
virtual void CancelDownload() = 0;
}; };
using PythonBindingsInterface = AZ::Interface<IPythonBindings>; using PythonBindingsInterface = AZ::Interface<IPythonBindings>;

@ -90,7 +90,8 @@ def get_downloadable(engine_name: str = None,
def download_o3de_object(object_name: str, default_folder_name: str, dest_path: str or pathlib.Path, def download_o3de_object(object_name: str, default_folder_name: str, dest_path: str or pathlib.Path,
object_type: str, downloadable_kwarg_key, skip_auto_register: bool) -> int: object_type: str, downloadable_kwarg_key, skip_auto_register: bool,
download_progress_callback = None) -> int:
download_path = manifest.get_o3de_cache_folder() / default_folder_name / object_name download_path = manifest.get_o3de_cache_folder() / default_folder_name / object_name
download_path.mkdir(parents=True, exist_ok=True) download_path.mkdir(parents=True, exist_ok=True)
@ -104,7 +105,7 @@ def download_o3de_object(object_name: str, default_folder_name: str, dest_path:
origin_uri = downloadable_object_data['originuri'] origin_uri = downloadable_object_data['originuri']
parsed_uri = urllib.parse.urlparse(origin_uri) parsed_uri = urllib.parse.urlparse(origin_uri)
download_zip_result = utils.download_zip_file(parsed_uri, download_zip_path) download_zip_result = utils.download_zip_file(parsed_uri, download_zip_path, download_progress_callback)
if download_zip_result != 0: if download_zip_result != 0:
return download_zip_result return download_zip_result
@ -147,33 +148,38 @@ def download_o3de_object(object_name: str, default_folder_name: str, dest_path:
def download_engine(engine_name: str, def download_engine(engine_name: str,
dest_path: str or pathlib.Path, dest_path: str or pathlib.Path,
skip_auto_register: bool) -> int: skip_auto_register: bool,
return download_o3de_object(engine_name, 'engines', dest_path, 'engine', 'engine_name', skip_auto_register) download_progress_callback = None) -> int:
return download_o3de_object(engine_name, 'engines', dest_path, 'engine', 'engine_name', skip_auto_register, download_progress_callback)
def download_project(project_name: str, def download_project(project_name: str,
dest_path: str or pathlib.Path, dest_path: str or pathlib.Path,
skip_auto_register: bool) -> int: skip_auto_register: bool,
return download_o3de_object(project_name, 'projects', dest_path, 'project', 'project_name', skip_auto_register) download_progress_callback = None) -> int:
return download_o3de_object(project_name, 'projects', dest_path, 'project', 'project_name', skip_auto_register, download_progress_callback)
def download_gem(gem_name: str, def download_gem(gem_name: str,
dest_path: str or pathlib.Path, dest_path: str or pathlib.Path,
skip_auto_register: bool) -> int: skip_auto_register: bool,
return download_o3de_object(gem_name, 'gems', dest_path, 'gem', 'gem_name', skip_auto_register) download_progress_callback = None) -> int:
return download_o3de_object(gem_name, 'gems', dest_path, 'gem', 'gem_name', skip_auto_register, download_progress_callback)
def download_template(template_name: str, def download_template(template_name: str,
dest_path: str or pathlib.Path, dest_path: str or pathlib.Path,
skip_auto_register: bool) -> int: skip_auto_register: bool,
return download_o3de_object(template_name, 'templates', dest_path, 'template', 'template_name', skip_auto_register) download_progress_callback = None) -> int:
return download_o3de_object(template_name, 'templates', dest_path, 'template', 'template_name', skip_auto_register, download_progress_callback)
def download_restricted(restricted_name: str, def download_restricted(restricted_name: str,
dest_path: str or pathlib.Path, dest_path: str or pathlib.Path,
skip_auto_register: bool) -> int: skip_auto_register: bool,
return download_o3de_object(restricted_name, 'restricted', dest_path, 'restricted', 'restricted_name', skip_auto_register) download_progress_callback = None) -> int:
return download_o3de_object(restricted_name, 'restricted', dest_path, 'restricted', 'restricted_name', skip_auto_register, download_progress_callback)
def _run_download(args: argparse) -> int: def _run_download(args: argparse) -> int:

@ -10,6 +10,7 @@ This file contains utility functions
""" """
import sys import sys
import uuid import uuid
import os
import pathlib import pathlib
import shutil import shutil
import urllib.request import urllib.request
@ -19,6 +20,29 @@ import zipfile
logger = logging.getLogger() logger = logging.getLogger()
logging.basicConfig() logging.basicConfig()
COPY_BUFSIZE = 64 * 1024
def copyfileobj(fsrc, fdst, callback, length=0):
# This is functionally the same as the python shutil copyfileobj but
# allows for a callback to return the download progress in blocks and allows
# to early out to cancel the copy.
if not length:
length = COPY_BUFSIZE
fsrc_read = fsrc.read
fdst_write = fdst.write
copied = 0
while True:
buf = fsrc_read(length)
if not buf:
break
fdst_write(buf)
copied += len(buf)
if callback(copied):
return 1
return 0
def validate_identifier(identifier: str) -> bool: def validate_identifier(identifier: str) -> bool:
""" """
Determine if the identifier supplied is valid. Determine if the identifier supplied is valid.
@ -93,18 +117,29 @@ def backup_folder(folder: str or pathlib.Path) -> None:
if backup_folder_name.is_dir(): if backup_folder_name.is_dir():
renamed = True renamed = True
def download_file(parsed_uri, download_path: pathlib.Path, download_progress_callback = None) -> int:
def download_file(parsed_uri, download_path: pathlib.Path) -> int:
""" """
:param parsed_uri: uniform resource identifier to zip file to download :param parsed_uri: uniform resource identifier to zip file to download
:param download_path: location path on disk to download file :param download_path: location path on disk to download file
:download_progress_callback: callback called with the download progress as a percentage, returns true to request to cancel the download
""" """
if download_path.is_file(): if download_path.is_file():
logger.warn(f'File already downloaded to {download_path}.') logger.warn(f'File already downloaded to {download_path}.')
elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']: elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']:
with urllib.request.urlopen(parsed_uri.geturl()) as s: with urllib.request.urlopen(parsed_uri.geturl()) as s:
download_file_size = 0
try:
download_file_size = s.headers['content-length']
except KeyError:
pass
def download_progress(blocks):
if download_progress_callback and download_file_size:
return download_progress_callback(int(blocks/int(download_file_size) * 100))
return False
with download_path.open('wb') as f: with download_path.open('wb') as f:
shutil.copyfileobj(s, f) download_cancelled = copyfileobj(s, f, download_progress)
if download_cancelled:
return 1
else: else:
origin_file = pathlib.Path(parsed_uri.geturl()).resolve() origin_file = pathlib.Path(parsed_uri.geturl()).resolve()
if not origin_file.is_file(): if not origin_file.is_file():
@ -114,12 +149,12 @@ def download_file(parsed_uri, download_path: pathlib.Path) -> int:
return 0 return 0
def download_zip_file(parsed_uri, download_zip_path: pathlib.Path) -> int: def download_zip_file(parsed_uri, download_zip_path: pathlib.Path, download_progress_callback = None) -> int:
""" """
:param parsed_uri: uniform resource identifier to zip file to download :param parsed_uri: uniform resource identifier to zip file to download
:param download_zip_path: path to output zip file :param download_zip_path: path to output zip file
""" """
download_file_result = download_file(parsed_uri, download_zip_path) download_file_result = download_file(parsed_uri, download_zip_path, download_progress_callback)
if download_file_result != 0: if download_file_result != 0:
return download_file_result return download_file_result

Loading…
Cancel
Save