Add the ability for Python to pass download progress to Project Manager and to cancel downloads.

Signed-off-by: AMZN-Phil <pconroy@amazon.com>
monroegm-disable-blank-issue-2
AMZN-Phil 4 years ago
parent 50068bcae7
commit e3cfcd4cc7

@ -8,10 +8,15 @@
#include <DownloadController.h>
#include <DownloadWorker.h>
#include <PythonBindings.h>
#include <AzCore/std/algorithm.h>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
DownloadController::DownloadController(QWidget* parent)
@ -46,6 +51,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)
{
m_lastProgress = progress;
@ -75,10 +98,4 @@ namespace O3DE::ProjectManager
m_workerThread.wait();
}
}
void DownloadController::HandleCancel()
{
m_workerThread.quit();
emit Done(false);
}
} // namespace O3DE::ProjectManager

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

@ -154,6 +154,11 @@ namespace O3DE::ProjectManager
update();
}
void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName)
{
m_downloadController->CancelGemDownload(gemName);
}
void CartOverlayWidget::CreateDownloadSection()
{
QWidget* widget = new QWidget();
@ -188,6 +193,8 @@ namespace O3DE::ProjectManager
downloadingItemLayout->setAlignment(Qt::AlignTop);
downloadingItemWidget->setLayout(downloadingItemLayout);
m_downloadController->AddGemDownload("TestGem");
auto update = [=](int downloadProgress)
{
if (m_downloadController->IsDownloadQueueEmpty())
@ -234,7 +241,9 @@ namespace O3DE::ProjectManager
nameProgressLayout->addWidget(progress);
QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
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);
downloadingItemLayout->addLayout(nameProgressLayout);
QProgressBar* downloadProgessBar = new QProgressBar();

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

@ -223,6 +223,50 @@ namespace RedirectOutput
}
} // namespace RedirectOutput
namespace O3DEProjectManagerPy
{
using DownloadProgressFunc = AZStd::function<void(int)>;
DownloadProgressFunc currentProgressCallback;
bool requestCancelDownload = false;
static PyObject* CLICancelDownload(PyObject* /*self*/, PyObject* /*args*/)
{
if (requestCancelDownload)
{
return Py_True;
}
else
{
return Py_False;
}
}
static PyObject* CLIDownloadProgress(PyObject* /*self*/, PyObject* args)
{
int progress_percentage = 0;
if (!PyArg_ParseTuple(args, "i", &progress_percentage))
return NULL;
if (currentProgressCallback)
{
currentProgressCallback(progress_percentage);
}
Py_RETURN_NONE;
}
static PyMethodDef O3DEPMPyMethods[] = { { "download_progress", CLIDownloadProgress, METH_VARARGS, "Used to call back to the UI to inform of download progress." },
{ "request_cancel_download", CLICancelDownload, METH_NOARGS, "Returns that the UI is requesting that the current download be cancelled." },
{ NULL, NULL, 0, NULL } };
static PyModuleDef O3DEPMPyModule = { PyModuleDef_HEAD_INIT, "o3de_projectmanager", NULL, -1, O3DEPMPyMethods, NULL, NULL, NULL, NULL };
static PyObject* PyInit_O3DEPMPy(void)
{
return PyModule_Create(&O3DEPMPyModule);
}
} // namespace O3DEProjectManagerPy
namespace O3DE::ProjectManager
{
PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath)
@ -270,6 +314,7 @@ namespace O3DE::ProjectManager
AZ_TracePrintf("python", "Py_GetProgramFullPath=%ls \n", Py_GetProgramFullPath());
PyImport_AppendInittab("azlmbr_redirect", RedirectOutput::PyInit_RedirectOutput);
PyImport_AppendInittab("o3de_projectmanager", &O3DEProjectManagerPy::PyInit_O3DEPMPy);
try
{
@ -1080,6 +1125,8 @@ namespace O3DE::ProjectManager
AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback)
{
bool downloadSucceeded = false;
O3DEProjectManagerPy::currentProgressCallback = gemProgressCallback;
O3DEProjectManagerPy::requestCancelDownload = false;
auto result = ExecuteWithLockErrorHandling(
[&]
{
@ -1091,6 +1138,8 @@ namespace O3DE::ProjectManager
downloadSucceeded = (downloadResult.cast<int>() == 0);
});
O3DEProjectManagerPy::currentProgressCallback = nullptr;
if (!result.IsSuccess())
{
return result;
@ -1102,4 +1151,9 @@ namespace O3DE::ProjectManager
return AZ::Success();
}
void PythonBindings::CancelDownload()
{
O3DEProjectManagerPy::requestCancelDownload = true;
}
}

@ -62,6 +62,7 @@ namespace O3DE::ProjectManager
bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override;
void CancelDownload() override;
private:
AZ_DISABLE_COPY_MOVE(PythonBindings);

@ -188,7 +188,18 @@ namespace O3DE::ProjectManager
*/
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;
/**
* Cancels the current download.
*/
virtual void CancelDownload() = 0;
};
using PythonBindingsInterface = AZ::Interface<IPythonBindings>;

@ -10,15 +10,44 @@ This file contains utility functions
"""
import sys
import uuid
import os
import pathlib
import shutil
import urllib.request
import logging
import zipfile
try:
import o3de_projectmanager
except ImportError:
pass
logger = logging.getLogger()
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:
if o3de_projectmanager and o3de_projectmanager.request_cancel_download():
return 1
buf = fsrc_read(length)
if not buf:
break
fdst_write(buf)
copied += len(buf)
callback(copied)
return 0
def validate_identifier(identifier: str) -> bool:
"""
Determine if the identifier supplied is valid.
@ -93,7 +122,6 @@ def backup_folder(folder: str or pathlib.Path) -> None:
if backup_folder_name.is_dir():
renamed = True
def download_file(parsed_uri, download_path: pathlib.Path) -> int:
"""
:param parsed_uri: uniform resource identifier to zip file to download
@ -103,8 +131,18 @@ def download_file(parsed_uri, download_path: pathlib.Path) -> int:
logger.warn(f'File already downloaded to {download_path}.')
elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']:
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 o3de_projectmanager and download_file_size:
o3de_projectmanager.download_progress(int(blocks/int(download_file_size) * 100))
with download_path.open('wb') as f:
shutil.copyfileobj(s, f)
download_cancelled = copyfileobj(s, f, download_progress)
if download_cancelled:
return 1
else:
origin_file = pathlib.Path(parsed_uri.geturl()).resolve()
if not origin_file.is_file():

Loading…
Cancel
Save