From 4ac2626b65f8710925b42bd34149d6838d24bd23 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Mar 2024 23:48:35 -0500 Subject: [PATCH] Download manager implementation --- scwx-qt/scwx-qt.cmake | 8 +- .../scwx/qt/manager/download_manager.cpp | 201 ++++++++++++++++++ .../scwx/qt/manager/download_manager.hpp | 36 ++++ .../scwx/qt/request/download_request.cpp | 57 +++++ .../scwx/qt/request/download_request.hpp | 51 +++++ 5 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/manager/download_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/download_manager.hpp create mode 100644 scwx-qt/source/scwx/qt/request/download_request.cpp create mode 100644 scwx-qt/source/scwx/qt/request/download_request.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 3d734a84..0468e4f6 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -86,6 +86,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp + source/scwx/qt/manager/download_manager.hpp source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp @@ -98,6 +99,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp + source/scwx/qt/manager/download_manager.cpp source/scwx/qt/manager/font_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp @@ -154,8 +156,10 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp source/scwx/qt/model/radar_site_model.cpp source/scwx/qt/model/tree_item.cpp source/scwx/qt/model/tree_model.cpp) -set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp) -set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp) +set(HDR_REQUEST source/scwx/qt/request/download_request.hpp + source/scwx/qt/request/nexrad_file_request.hpp) +set(SRC_REQUEST source/scwx/qt/request/download_request.cpp + source/scwx/qt/request/nexrad_file_request.cpp) set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/map_settings.hpp diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp new file mode 100644 index 00000000..b5d4f30f --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -0,0 +1,201 @@ +#include +#include + +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::download_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class DownloadManager::Impl +{ +public: + explicit Impl(DownloadManager* self) : self_ {self} {} + + ~Impl() { threadPool_.join(); } + + boost::asio::thread_pool threadPool_ {1u}; + + DownloadManager* self_; +}; + +DownloadManager::DownloadManager() : p(std::make_unique(this)) {} +DownloadManager::~DownloadManager() = default; + +void DownloadManager::Download( + const std::shared_ptr& request) +{ + boost::asio::post( + p->threadPool_, + [=]() + { + // Prepare destination file + const std::filesystem::path& destinationPath = + request->destination_path(); + + if (!destinationPath.has_parent_path()) + { + logger_->error("Destination has no parent path: \"{}\""); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + const std::filesystem::path parentPath = destinationPath.parent_path(); + + // Create directory if it doesn't exist + if (!std::filesystem::exists(parentPath)) + { + if (!std::filesystem::create_directories(parentPath)) + { + logger_->error("Unable to create download directory: \"{}\"", + parentPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + } + + // Remove file if it exists + if (std::filesystem::exists(destinationPath)) + { + std::error_code error; + if (!std::filesystem::remove(destinationPath, error)) + { + logger_->error( + "Unable to remove existing destination file ({}): \"{}\"", + error.message(), + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + } + + // Open file for writing + std::ofstream ofs {destinationPath, + std::ios_base::out | std::ios_base::binary | + std::ios_base::trunc}; + if (!ofs.is_open() || !ofs.good()) + { + logger_->error( + "Unable to open destination file for writing: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Download file + cpr::Response response = cpr::Get( + cpr::Url {request->url()}, + cpr::ProgressCallback( + [=](cpr::cpr_off_t downloadTotal, + cpr::cpr_off_t downloadNow, + cpr::cpr_off_t /* uploadTotal */, + cpr::cpr_off_t /* uploadNow */, + std::intptr_t /* userdata */) + { + Q_EMIT request->ProgressUpdated(downloadNow, downloadTotal); + return !request->IsCanceled(); + }), + cpr::WriteCallback( + [=, &ofs](std::string data, std::intptr_t /* userdata */) + { + // Write file + ofs << data; + return !request->IsCanceled(); + })); + + bool ofsGood = ofs.good(); + ofs.close(); + + // Handle response + if (response.error.code == cpr::ErrorCode::OK && + !request->IsCanceled() && ofsGood) + { + logger_->info("Download complete: \"{}\"", request->url()); + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::OK); + } + else + { + request::DownloadRequest::CompleteReason reason = + request::DownloadRequest::CompleteReason::IOError; + + if (request->IsCanceled()) + { + logger_->info("Download request cancelled: \"{}\"", + request->url()); + + reason = request::DownloadRequest::CompleteReason::Canceled; + } + else if (response.error.code != cpr::ErrorCode::OK) + { + logger_->error("Error downloading file ({}): \"{}\"", + response.error.message, + request->url()); + + reason = request::DownloadRequest::CompleteReason::RemoteError; + } + else if (!ofsGood) + { + logger_->error("File I/O error: \"{}\"", + destinationPath.string()); + + reason = request::DownloadRequest::CompleteReason::IOError; + } + + std::error_code error; + if (!std::filesystem::remove(destinationPath, error)) + { + logger_->error("Unable to remove destination file: \"{}\", {}", + destinationPath.string(), + error.message()); + } + + Q_EMIT request->RequestComplete(reason); + } + }); +} + +std::shared_ptr DownloadManager::Instance() +{ + static std::weak_ptr downloadManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr downloadManager = + downloadManagerReference_.lock(); + + if (downloadManager == nullptr) + { + downloadManager = std::make_shared(); + downloadManagerReference_ = downloadManager; + } + + return downloadManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.hpp b/scwx-qt/source/scwx/qt/manager/download_manager.hpp new file mode 100644 index 00000000..de5b61f8 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/download_manager.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class DownloadManager : public QObject +{ + Q_OBJECT + +public: + explicit DownloadManager(); + ~DownloadManager(); + + void Download(const std::shared_ptr& request); + + static std::shared_ptr Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/request/download_request.cpp b/scwx-qt/source/scwx/qt/request/download_request.cpp new file mode 100644 index 00000000..56244d7f --- /dev/null +++ b/scwx-qt/source/scwx/qt/request/download_request.cpp @@ -0,0 +1,57 @@ +#include + +namespace scwx +{ +namespace qt +{ +namespace request +{ + +static const std::string logPrefix_ = "scwx::qt::request::download_request"; + +class DownloadRequest::Impl +{ +public: + explicit Impl(const std::string& url, + const std::filesystem::path& destinationPath) : + url_ {url}, destinationPath_ {destinationPath} + { + } + ~Impl() = default; + + const std::string url_; + const std::filesystem::path destinationPath_; + + bool canceled_ = false; +}; + +DownloadRequest::DownloadRequest(const std::string& url, + const std::filesystem::path& destinationPath) : + p(std::make_unique(url, destinationPath)) +{ +} +DownloadRequest::~DownloadRequest() = default; + +const std::string& DownloadRequest::url() const +{ + return p->url_; +} + +const std::filesystem::path& DownloadRequest::destination_path() const +{ + return p->destinationPath_; +} + +void DownloadRequest::Cancel() +{ + p->canceled_ = true; +} + +bool DownloadRequest::IsCanceled() const +{ + return p->canceled_; +} + +} // namespace request +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/request/download_request.hpp b/scwx-qt/source/scwx/qt/request/download_request.hpp new file mode 100644 index 00000000..4b461b8e --- /dev/null +++ b/scwx-qt/source/scwx/qt/request/download_request.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace request +{ + +class DownloadRequest : public QObject +{ + Q_OBJECT + +public: + enum class CompleteReason + { + OK, + Canceled, + IOError, + RemoteError + }; + + explicit DownloadRequest(const std::string& url, + const std::filesystem::path& destinationPath); + ~DownloadRequest(); + + const std::string& url() const; + const std::filesystem::path& destination_path() const; + + void Cancel(); + + bool IsCanceled() const; + +private: + class Impl; + std::unique_ptr p; + +signals: + void ProgressUpdated(std::ptrdiff_t downloadedBytes, + std::ptrdiff_t totalBytes); + void RequestComplete(CompleteReason reason); +}; + +} // namespace request +} // namespace qt +} // namespace scwx