mirror of
https://github.com/ciphervance/supercell-wx.git
synced 2025-11-01 08:00:05 +00:00
Merge pull request #172 from dpaulat/feature/installer
Add Windows Installer and Updater
This commit is contained in:
commit
ef1101ac4b
30 changed files with 1304 additions and 23 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -195,6 +195,20 @@ jobs:
|
||||||
${{ github.workspace }}/build/bin/*.debug
|
${{ github.workspace }}/build/bin/*.debug
|
||||||
${{ github.workspace }}/build/lib/*.debug
|
${{ github.workspace }}/build/lib/*.debug
|
||||||
|
|
||||||
|
- name: Build Installer (Windows)
|
||||||
|
if: matrix.os == 'windows-2022'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
cd build
|
||||||
|
cpack
|
||||||
|
|
||||||
|
- name: Upload Installer (Windows)
|
||||||
|
if: matrix.os == 'windows-2022'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: supercell-wx-installer-${{ matrix.artifact_suffix }}
|
||||||
|
path: ${{ github.workspace }}/build/supercell-wx-*.msi*
|
||||||
|
|
||||||
- name: Build AppImage (Linux)
|
- name: Build AppImage (Linux)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
set(PROJECT_NAME supercell-wx)
|
set(PROJECT_NAME supercell-wx)
|
||||||
project(${PROJECT_NAME} C CXX)
|
project(${PROJECT_NAME}
|
||||||
|
VERSION 0.4.3
|
||||||
|
DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer."
|
||||||
|
HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx"
|
||||||
|
LANGUAGES C CXX)
|
||||||
|
|
||||||
set(CMAKE_POLICY_DEFAULT_CMP0054 NEW)
|
set(CMAKE_POLICY_DEFAULT_CMP0054 NEW)
|
||||||
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2021 Dan Paulat
|
Copyright (c) 2021-2024 Dan Paulat
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
BIN
scwx-qt/res/images/scwx-banner.png
Normal file
BIN
scwx-qt/res/images/scwx-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
scwx-qt/res/images/scwx-dialog.png
Normal file
BIN
scwx-qt/res/images/scwx-dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -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/placefile_triangles.cpp
|
||||||
source/scwx/qt/gl/draw/rectangle.cpp)
|
source/scwx/qt/gl/draw/rectangle.cpp)
|
||||||
set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
|
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/font_manager.hpp
|
||||||
source/scwx/qt/manager/media_manager.hpp
|
source/scwx/qt/manager/media_manager.hpp
|
||||||
source/scwx/qt/manager/placefile_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/timeline_manager.hpp
|
||||||
source/scwx/qt/manager/update_manager.hpp)
|
source/scwx/qt/manager/update_manager.hpp)
|
||||||
set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
|
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/font_manager.cpp
|
||||||
source/scwx/qt/manager/media_manager.cpp
|
source/scwx/qt/manager/media_manager.cpp
|
||||||
source/scwx/qt/manager/placefile_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/radar_site_model.cpp
|
||||||
source/scwx/qt/model/tree_item.cpp
|
source/scwx/qt/model/tree_item.cpp
|
||||||
source/scwx/qt/model/tree_model.cpp)
|
source/scwx/qt/model/tree_model.cpp)
|
||||||
set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp)
|
set(HDR_REQUEST source/scwx/qt/request/download_request.hpp
|
||||||
set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp)
|
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
|
set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
|
||||||
source/scwx/qt/settings/general_settings.hpp
|
source/scwx/qt/settings/general_settings.hpp
|
||||||
source/scwx/qt/settings/map_settings.hpp
|
source/scwx/qt/settings/map_settings.hpp
|
||||||
|
|
@ -217,6 +221,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
|
||||||
source/scwx/qt/ui/animation_dock_widget.hpp
|
source/scwx/qt/ui/animation_dock_widget.hpp
|
||||||
source/scwx/qt/ui/collapsible_group.hpp
|
source/scwx/qt/ui/collapsible_group.hpp
|
||||||
source/scwx/qt/ui/county_dialog.hpp
|
source/scwx/qt/ui/county_dialog.hpp
|
||||||
|
source/scwx/qt/ui/download_dialog.hpp
|
||||||
source/scwx/qt/ui/flow_layout.hpp
|
source/scwx/qt/ui/flow_layout.hpp
|
||||||
source/scwx/qt/ui/imgui_debug_dialog.hpp
|
source/scwx/qt/ui/imgui_debug_dialog.hpp
|
||||||
source/scwx/qt/ui/imgui_debug_widget.hpp
|
source/scwx/qt/ui/imgui_debug_widget.hpp
|
||||||
|
|
@ -228,6 +233,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
|
||||||
source/scwx/qt/ui/open_url_dialog.hpp
|
source/scwx/qt/ui/open_url_dialog.hpp
|
||||||
source/scwx/qt/ui/placefile_dialog.hpp
|
source/scwx/qt/ui/placefile_dialog.hpp
|
||||||
source/scwx/qt/ui/placefile_settings_widget.hpp
|
source/scwx/qt/ui/placefile_settings_widget.hpp
|
||||||
|
source/scwx/qt/ui/progress_dialog.hpp
|
||||||
source/scwx/qt/ui/radar_site_dialog.hpp
|
source/scwx/qt/ui/radar_site_dialog.hpp
|
||||||
source/scwx/qt/ui/settings_dialog.hpp
|
source/scwx/qt/ui/settings_dialog.hpp
|
||||||
source/scwx/qt/ui/update_dialog.hpp)
|
source/scwx/qt/ui/update_dialog.hpp)
|
||||||
|
|
@ -237,6 +243,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
|
||||||
source/scwx/qt/ui/animation_dock_widget.cpp
|
source/scwx/qt/ui/animation_dock_widget.cpp
|
||||||
source/scwx/qt/ui/collapsible_group.cpp
|
source/scwx/qt/ui/collapsible_group.cpp
|
||||||
source/scwx/qt/ui/county_dialog.cpp
|
source/scwx/qt/ui/county_dialog.cpp
|
||||||
|
source/scwx/qt/ui/download_dialog.cpp
|
||||||
source/scwx/qt/ui/flow_layout.cpp
|
source/scwx/qt/ui/flow_layout.cpp
|
||||||
source/scwx/qt/ui/imgui_debug_dialog.cpp
|
source/scwx/qt/ui/imgui_debug_dialog.cpp
|
||||||
source/scwx/qt/ui/imgui_debug_widget.cpp
|
source/scwx/qt/ui/imgui_debug_widget.cpp
|
||||||
|
|
@ -248,6 +255,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
|
||||||
source/scwx/qt/ui/open_url_dialog.cpp
|
source/scwx/qt/ui/open_url_dialog.cpp
|
||||||
source/scwx/qt/ui/placefile_dialog.cpp
|
source/scwx/qt/ui/placefile_dialog.cpp
|
||||||
source/scwx/qt/ui/placefile_settings_widget.cpp
|
source/scwx/qt/ui/placefile_settings_widget.cpp
|
||||||
|
source/scwx/qt/ui/progress_dialog.cpp
|
||||||
source/scwx/qt/ui/radar_site_dialog.cpp
|
source/scwx/qt/ui/radar_site_dialog.cpp
|
||||||
source/scwx/qt/ui/settings_dialog.cpp
|
source/scwx/qt/ui/settings_dialog.cpp
|
||||||
source/scwx/qt/ui/update_dialog.cpp)
|
source/scwx/qt/ui/update_dialog.cpp)
|
||||||
|
|
@ -262,6 +270,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui
|
||||||
source/scwx/qt/ui/open_url_dialog.ui
|
source/scwx/qt/ui/open_url_dialog.ui
|
||||||
source/scwx/qt/ui/placefile_dialog.ui
|
source/scwx/qt/ui/placefile_dialog.ui
|
||||||
source/scwx/qt/ui/placefile_settings_widget.ui
|
source/scwx/qt/ui/placefile_settings_widget.ui
|
||||||
|
source/scwx/qt/ui/progress_dialog.ui
|
||||||
source/scwx/qt/ui/radar_site_dialog.ui
|
source/scwx/qt/ui/radar_site_dialog.ui
|
||||||
source/scwx/qt/ui/settings_dialog.ui
|
source/scwx/qt/ui/settings_dialog.ui
|
||||||
source/scwx/qt/ui/update_dialog.ui)
|
source/scwx/qt/ui/update_dialog.ui)
|
||||||
|
|
@ -608,3 +617,25 @@ install(SCRIPT ${deploy_script_qmaplibre_core}
|
||||||
|
|
||||||
install(SCRIPT ${deploy_script_scwx}
|
install(SCRIPT ${deploy_script_scwx}
|
||||||
COMPONENT supercell-wx)
|
COMPONENT supercell-wx)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
set(CPACK_PACKAGE_NAME "Supercell Wx")
|
||||||
|
set(CPACK_PACKAGE_VENDOR "Dan Paulat")
|
||||||
|
set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-windows-x64")
|
||||||
|
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Supercell Wx")
|
||||||
|
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/icons/scwx-256.ico")
|
||||||
|
set(CPACK_PACKAGE_CHECKSUM SHA256)
|
||||||
|
set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt")
|
||||||
|
set(CPACK_GENERATOR WIX)
|
||||||
|
set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx")
|
||||||
|
set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C)
|
||||||
|
set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-banner.png")
|
||||||
|
set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-dialog.png")
|
||||||
|
set(CPACK_WIX_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/wix.template.in")
|
||||||
|
set(CPACK_WIX_EXTENSIONS WixUIExtension WiXUtilExtension)
|
||||||
|
|
||||||
|
set(CPACK_INSTALL_CMAKE_PROJECTS
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/")
|
||||||
|
|
||||||
|
include(CPack)
|
||||||
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -627,9 +627,13 @@ void MainWindowImpl::AsyncSetup()
|
||||||
// Check for updates
|
// Check for updates
|
||||||
if (generalSettings.update_notifications_enabled().GetValue())
|
if (generalSettings.update_notifications_enabled().GetValue())
|
||||||
{
|
{
|
||||||
boost::asio::post(
|
boost::asio::post(threadPool_,
|
||||||
threadPool_,
|
[this]()
|
||||||
[this]() { updateManager_->CheckForUpdates(main::kVersionString_); });
|
{
|
||||||
|
manager::UpdateManager::RemoveTemporaryReleases();
|
||||||
|
updateManager_->CheckForUpdates(
|
||||||
|
main::kVersionString_);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
280
scwx-qt/source/scwx/qt/manager/download_manager.cpp
Normal file
280
scwx-qt/source/scwx/qt/manager/download_manager.cpp
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#include <scwx/qt/manager/download_manager.hpp>
|
||||||
|
#include <scwx/util/digest.hpp>
|
||||||
|
#include <scwx/util/logger.hpp>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
#include <boost/asio/thread_pool.hpp>
|
||||||
|
#include <cpr/cpr.h>
|
||||||
|
|
||||||
|
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<Impl>(this)) {}
|
||||||
|
DownloadManager::~DownloadManager() = default;
|
||||||
|
|
||||||
|
void DownloadManager::Download(
|
||||||
|
const std::shared_ptr<request::DownloadRequest>& 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point lastUpdated {};
|
||||||
|
cpr::cpr_off_t lastDownloadNow {};
|
||||||
|
cpr::cpr_off_t lastDownloadTotal {};
|
||||||
|
|
||||||
|
// 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 */)
|
||||||
|
{
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point now =
|
||||||
|
std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
// Only emit an update every 100ms
|
||||||
|
if ((now > lastUpdated + 100ms ||
|
||||||
|
downloadNow == downloadTotal) &&
|
||||||
|
(downloadNow != lastDownloadNow ||
|
||||||
|
downloadTotal != lastDownloadTotal))
|
||||||
|
{
|
||||||
|
logger_->trace("Downloaded: {} / {}",
|
||||||
|
downloadNow,
|
||||||
|
downloadTotal);
|
||||||
|
|
||||||
|
Q_EMIT request->ProgressUpdated(downloadNow,
|
||||||
|
downloadTotal);
|
||||||
|
|
||||||
|
lastUpdated = now;
|
||||||
|
lastDownloadNow = downloadNow;
|
||||||
|
lastDownloadTotal = downloadTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !request->IsCanceled();
|
||||||
|
}),
|
||||||
|
cpr::WriteCallback(
|
||||||
|
[&](std::string data, std::intptr_t /* userdata */)
|
||||||
|
{
|
||||||
|
// Write file
|
||||||
|
ofs << data;
|
||||||
|
return !request->IsCanceled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
bool ofsGood = ofs.good();
|
||||||
|
ofs.close();
|
||||||
|
|
||||||
|
// Handle error response
|
||||||
|
if (response.error.code != cpr::ErrorCode::OK ||
|
||||||
|
request->IsCanceled() || !ofsGood)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle response
|
||||||
|
const auto contentMd5 = response.header.find("content-md5");
|
||||||
|
if (contentMd5 != response.header.cend() &&
|
||||||
|
!contentMd5->second.empty())
|
||||||
|
{
|
||||||
|
// Open file for reading
|
||||||
|
std::ifstream is {destinationPath,
|
||||||
|
std::ios_base::in | std::ios_base::binary};
|
||||||
|
if (!is.is_open() || !is.good())
|
||||||
|
{
|
||||||
|
logger_->error("Unable to open destination file for reading: {}",
|
||||||
|
destinationPath.string());
|
||||||
|
|
||||||
|
Q_EMIT request->RequestComplete(
|
||||||
|
request::DownloadRequest::CompleteReason::IOError);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute MD5
|
||||||
|
std::vector<std::uint8_t> digest {};
|
||||||
|
if (!util::ComputeDigest(EVP_md5(), is, digest))
|
||||||
|
{
|
||||||
|
logger_->error("Failed to compute MD5: {}",
|
||||||
|
destinationPath.string());
|
||||||
|
|
||||||
|
Q_EMIT request->RequestComplete(
|
||||||
|
request::DownloadRequest::CompleteReason::IOError);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare calculated MD5 with digest in response header
|
||||||
|
QByteArray expectedDigestArray =
|
||||||
|
QByteArray::fromBase64(contentMd5->second.c_str());
|
||||||
|
std::vector<std::uint8_t> expectedDigest(
|
||||||
|
expectedDigestArray.cbegin(), expectedDigestArray.cend());
|
||||||
|
|
||||||
|
if (digest != expectedDigest)
|
||||||
|
{
|
||||||
|
QByteArray calculatedDigest(
|
||||||
|
reinterpret_cast<char*>(digest.data()), digest.size());
|
||||||
|
|
||||||
|
logger_->error("Digest mismatch: {} != {}",
|
||||||
|
calculatedDigest.toBase64().toStdString(),
|
||||||
|
contentMd5->second);
|
||||||
|
|
||||||
|
Q_EMIT request->RequestComplete(
|
||||||
|
request::DownloadRequest::CompleteReason::DigestError);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger_->info("Download complete: {}", request->url());
|
||||||
|
Q_EMIT request->RequestComplete(
|
||||||
|
request::DownloadRequest::CompleteReason::OK);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<DownloadManager> DownloadManager::Instance()
|
||||||
|
{
|
||||||
|
static std::weak_ptr<DownloadManager> downloadManagerReference_ {};
|
||||||
|
static std::mutex instanceMutex_ {};
|
||||||
|
|
||||||
|
std::unique_lock lock(instanceMutex_);
|
||||||
|
|
||||||
|
std::shared_ptr<DownloadManager> downloadManager =
|
||||||
|
downloadManagerReference_.lock();
|
||||||
|
|
||||||
|
if (downloadManager == nullptr)
|
||||||
|
{
|
||||||
|
downloadManager = std::make_shared<DownloadManager>();
|
||||||
|
downloadManagerReference_ = downloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace manager
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
36
scwx-qt/source/scwx/qt/manager/download_manager.hpp
Normal file
36
scwx-qt/source/scwx/qt/manager/download_manager.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <scwx/qt/request/download_request.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace manager
|
||||||
|
{
|
||||||
|
|
||||||
|
class DownloadManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DownloadManager();
|
||||||
|
~DownloadManager();
|
||||||
|
|
||||||
|
void Download(const std::shared_ptr<request::DownloadRequest>& request);
|
||||||
|
|
||||||
|
static std::shared_ptr<DownloadManager> Instance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> p;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace manager
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <boost/json.hpp>
|
#include <boost/json.hpp>
|
||||||
#include <cpr/cpr.h>
|
#include <cpr/cpr.h>
|
||||||
#include <re2/re2.h>
|
#include <re2/re2.h>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
namespace scwx
|
namespace scwx
|
||||||
{
|
{
|
||||||
|
|
@ -230,6 +231,34 @@ UpdateManager::Impl::FindLatestRelease()
|
||||||
return {latestRelease, latestReleaseVersion};
|
return {latestRelease, latestReleaseVersion};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UpdateManager::RemoveTemporaryReleases()
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const std::string destination {
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::TempLocation)
|
||||||
|
.toStdString()};
|
||||||
|
const std::filesystem::path destinationPath {destination};
|
||||||
|
std::filesystem::directory_iterator it {destinationPath};
|
||||||
|
|
||||||
|
for (auto& file : it)
|
||||||
|
{
|
||||||
|
if (file.is_regular_file() && file.path().string().ends_with(".msi") &&
|
||||||
|
file.path().stem().string().starts_with("supercell-wx-"))
|
||||||
|
{
|
||||||
|
logger_->info("Removing temporary installer: {}",
|
||||||
|
file.path().string());
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
if (!std::filesystem::remove(file.path(), error))
|
||||||
|
{
|
||||||
|
logger_->warn("Error removing temporary installer: {}",
|
||||||
|
error.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<UpdateManager> UpdateManager::Instance()
|
std::shared_ptr<UpdateManager> UpdateManager::Instance()
|
||||||
{
|
{
|
||||||
static std::weak_ptr<UpdateManager> updateManagerReference_ {};
|
static std::weak_ptr<UpdateManager> updateManagerReference_ {};
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ public:
|
||||||
|
|
||||||
bool CheckForUpdates(const std::string& currentVersion = {});
|
bool CheckForUpdates(const std::string& currentVersion = {});
|
||||||
|
|
||||||
|
static void RemoveTemporaryReleases();
|
||||||
|
|
||||||
static std::shared_ptr<UpdateManager> Instance();
|
static std::shared_ptr<UpdateManager> Instance();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
|
||||||
57
scwx-qt/source/scwx/qt/request/download_request.cpp
Normal file
57
scwx-qt/source/scwx/qt/request/download_request.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#include <scwx/qt/request/download_request.hpp>
|
||||||
|
|
||||||
|
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<Impl>(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
|
||||||
52
scwx-qt/source/scwx/qt/request/download_request.hpp
Normal file
52
scwx-qt/source/scwx/qt/request/download_request.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace request
|
||||||
|
{
|
||||||
|
|
||||||
|
class DownloadRequest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class CompleteReason
|
||||||
|
{
|
||||||
|
OK,
|
||||||
|
Canceled,
|
||||||
|
IOError,
|
||||||
|
RemoteError,
|
||||||
|
DigestError
|
||||||
|
};
|
||||||
|
|
||||||
|
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<Impl> p;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void ProgressUpdated(std::ptrdiff_t downloadedBytes,
|
||||||
|
std::ptrdiff_t totalBytes);
|
||||||
|
void RequestComplete(CompleteReason reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace request
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
|
|
@ -11,10 +11,25 @@ namespace types
|
||||||
namespace gh
|
namespace gh
|
||||||
{
|
{
|
||||||
|
|
||||||
|
ReleaseAsset tag_invoke(boost::json::value_to_tag<ReleaseAsset>,
|
||||||
|
const boost::json::value& jv)
|
||||||
|
{
|
||||||
|
auto& jo = jv.as_object();
|
||||||
|
|
||||||
|
ReleaseAsset asset {};
|
||||||
|
|
||||||
|
// Required parameters
|
||||||
|
asset.name_ = jo.at("name").as_string();
|
||||||
|
asset.contentType_ = jo.at("content_type").as_string();
|
||||||
|
asset.browserDownloadUrl_ = jo.at("browser_download_url").as_string();
|
||||||
|
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
Release tag_invoke(boost::json::value_to_tag<Release>,
|
Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||||
const boost::json::value& jv)
|
const boost::json::value& jv)
|
||||||
{
|
{
|
||||||
auto jo = jv.as_object();
|
auto& jo = jv.as_object();
|
||||||
|
|
||||||
Release release {};
|
Release release {};
|
||||||
|
|
||||||
|
|
@ -24,6 +39,9 @@ Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||||
release.draft_ = jo.at("draft").as_bool();
|
release.draft_ = jo.at("draft").as_bool();
|
||||||
release.prerelease_ = jo.at("prerelease").as_bool();
|
release.prerelease_ = jo.at("prerelease").as_bool();
|
||||||
|
|
||||||
|
release.assets_ =
|
||||||
|
boost::json::value_to<std::vector<ReleaseAsset>>(jo.at("assets"));
|
||||||
|
|
||||||
// Optional parameters
|
// Optional parameters
|
||||||
if (jo.contains("body"))
|
if (jo.contains("body"))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,18 @@ namespace types
|
||||||
namespace gh
|
namespace gh
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief GitHub Release Asset object
|
||||||
|
*
|
||||||
|
* <https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28>
|
||||||
|
*/
|
||||||
|
struct ReleaseAsset
|
||||||
|
{
|
||||||
|
std::string name_ {};
|
||||||
|
std::string contentType_ {};
|
||||||
|
std::string browserDownloadUrl_ {};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief GitHub Release object
|
* @brief GitHub Release object
|
||||||
*
|
*
|
||||||
|
|
@ -25,8 +37,12 @@ struct Release
|
||||||
std::string body_ {};
|
std::string body_ {};
|
||||||
bool draft_ {};
|
bool draft_ {};
|
||||||
bool prerelease_ {};
|
bool prerelease_ {};
|
||||||
|
|
||||||
|
std::vector<ReleaseAsset> assets_ {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ReleaseAsset tag_invoke(boost::json::value_to_tag<ReleaseAsset>,
|
||||||
|
const boost::json::value& jv);
|
||||||
Release tag_invoke(boost::json::value_to_tag<Release>,
|
Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||||
const boost::json::value& jv);
|
const boost::json::value& jv);
|
||||||
|
|
||||||
|
|
|
||||||
105
scwx-qt/source/scwx/qt/ui/download_dialog.cpp
Normal file
105
scwx-qt/source/scwx/qt/ui/download_dialog.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#include <scwx/qt/ui/download_dialog.hpp>
|
||||||
|
#include <scwx/util/strings.hpp>
|
||||||
|
|
||||||
|
#include <boost/timer/timer.hpp>
|
||||||
|
#include <fmt/chrono.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
|
||||||
|
class DownloadDialog::Impl
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Impl() {};
|
||||||
|
~Impl() = default;
|
||||||
|
|
||||||
|
boost::timer::cpu_timer timer_ {};
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadDialog::DownloadDialog(QWidget* parent) :
|
||||||
|
ProgressDialog(parent), p {std::make_unique<Impl>()}
|
||||||
|
{
|
||||||
|
auto buttonBox = button_box();
|
||||||
|
buttonBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok |
|
||||||
|
QDialogButtonBox::StandardButton::Cancel);
|
||||||
|
buttonBox->button(QDialogButtonBox::StandardButton::Ok)
|
||||||
|
->setText("Install Now");
|
||||||
|
|
||||||
|
setWindowTitle(tr("Download File"));
|
||||||
|
SetRange(0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadDialog::~DownloadDialog() {}
|
||||||
|
|
||||||
|
void DownloadDialog::set_filename(const std::string& filename)
|
||||||
|
{
|
||||||
|
QString label = tr("Downloading %1...").arg(filename.c_str());
|
||||||
|
SetTopLabelText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadDialog::StartDownload()
|
||||||
|
{
|
||||||
|
// Hide the OK button until the download is finished
|
||||||
|
button_box()
|
||||||
|
->button(QDialogButtonBox::StandardButton::Ok)
|
||||||
|
->setVisible(false);
|
||||||
|
|
||||||
|
SetValue(0);
|
||||||
|
SetBottomLabelText(tr("Waiting for download to begin..."));
|
||||||
|
p->timer_.start();
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadDialog::UpdateProgress(std::ptrdiff_t downloadedBytes,
|
||||||
|
std::ptrdiff_t totalBytes)
|
||||||
|
{
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
const std::chrono::nanoseconds elapsed {p->timer_.elapsed().wall};
|
||||||
|
|
||||||
|
const double percentComplete =
|
||||||
|
(totalBytes > 0.0) ? static_cast<double>(downloadedBytes) / totalBytes :
|
||||||
|
0.0;
|
||||||
|
const int progressValue = static_cast<int>(percentComplete * 100.0);
|
||||||
|
|
||||||
|
SetValue(progressValue);
|
||||||
|
|
||||||
|
const std::chrono::seconds timeRemaining =
|
||||||
|
(percentComplete > 0.0) ?
|
||||||
|
std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
elapsed / percentComplete - elapsed) :
|
||||||
|
0s;
|
||||||
|
const std::chrono::hours hoursRemaining =
|
||||||
|
std::chrono::duration_cast<std::chrono::hours>(timeRemaining);
|
||||||
|
|
||||||
|
const std::string progressText =
|
||||||
|
fmt::format("{} of {} downloaded ({}:{:%M:%S} remaining)",
|
||||||
|
util::BytesToString(downloadedBytes),
|
||||||
|
util::BytesToString(totalBytes),
|
||||||
|
hoursRemaining.count(),
|
||||||
|
timeRemaining);
|
||||||
|
|
||||||
|
SetBottomLabelText(QString::fromStdString(progressText));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadDialog::FinishDownload()
|
||||||
|
{
|
||||||
|
button_box()->button(QDialogButtonBox::StandardButton::Ok)->setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadDialog::CancelDownload()
|
||||||
|
{
|
||||||
|
SetValue(0);
|
||||||
|
SetBottomLabelText(tr("Error occurred while downloading"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
38
scwx-qt/source/scwx/qt/ui/download_dialog.hpp
Normal file
38
scwx-qt/source/scwx/qt/ui/download_dialog.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <scwx/qt/ui/progress_dialog.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class DownloadDialog : public ProgressDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(DownloadDialog)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DownloadDialog(QWidget* parent = nullptr);
|
||||||
|
~DownloadDialog();
|
||||||
|
|
||||||
|
void set_filename(const std::string& filename);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void StartDownload();
|
||||||
|
void UpdateProgress(std::ptrdiff_t downloadedBytes,
|
||||||
|
std::ptrdiff_t totalBytes);
|
||||||
|
void FinishDownload();
|
||||||
|
void CancelDownload();
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> p;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
66
scwx-qt/source/scwx/qt/ui/progress_dialog.cpp
Normal file
66
scwx-qt/source/scwx/qt/ui/progress_dialog.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include "progress_dialog.hpp"
|
||||||
|
#include "ui_progress_dialog.h"
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
|
||||||
|
class ProgressDialog::Impl
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Impl() = default;
|
||||||
|
~Impl() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
ProgressDialog::ProgressDialog(QWidget* parent) :
|
||||||
|
QDialog(parent), p {std::make_unique<Impl>()}, ui(new Ui::ProgressDialog)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressDialog::~ProgressDialog()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDialogButtonBox* ProgressDialog::button_box() const
|
||||||
|
{
|
||||||
|
return ui->buttonBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetTopLabelText(const QString& text)
|
||||||
|
{
|
||||||
|
ui->topLabel->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetBottomLabelText(const QString& text)
|
||||||
|
{
|
||||||
|
ui->bottomLabel->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetMinimum(int minimum)
|
||||||
|
{
|
||||||
|
ui->progressBar->setMinimum(minimum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetMaximum(int maximum)
|
||||||
|
{
|
||||||
|
ui->progressBar->setMaximum(maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetRange(int minimum, int maximum)
|
||||||
|
{
|
||||||
|
ui->progressBar->setRange(minimum, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressDialog::SetValue(int value)
|
||||||
|
{
|
||||||
|
ui->progressBar->setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
46
scwx-qt/source/scwx/qt/ui/progress_dialog.hpp
Normal file
46
scwx-qt/source/scwx/qt/ui/progress_dialog.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
class QDialogButtonBox;
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class ProgressDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace qt
|
||||||
|
{
|
||||||
|
namespace ui
|
||||||
|
{
|
||||||
|
class ProgressDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(ProgressDialog)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ProgressDialog(QWidget* parent = nullptr);
|
||||||
|
~ProgressDialog();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QDialogButtonBox* button_box() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void SetTopLabelText(const QString& text);
|
||||||
|
void SetBottomLabelText(const QString& text);
|
||||||
|
void SetMinimum(int minimum);
|
||||||
|
void SetMaximum(int maximum);
|
||||||
|
void SetRange(int minimum, int maximum);
|
||||||
|
void SetValue(int value);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> p;
|
||||||
|
Ui::ProgressDialog* ui;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace qt
|
||||||
|
} // namespace scwx
|
||||||
85
scwx-qt/source/scwx/qt/ui/progress_dialog.ui
Normal file
85
scwx-qt/source/scwx/qt/ui/progress_dialog.ui
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ProgressDialog</class>
|
||||||
|
<widget class="QDialog" name="ProgressDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>394</width>
|
||||||
|
<height>116</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="topLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Downloading supercell-wx-v0.4.4-windows-x64.msi...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QProgressBar" name="progressBar">
|
||||||
|
<property name="value">
|
||||||
|
<number>24</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="bottomLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>25.3 MB of 69.1 MB downloaded (00:00:04 remaining)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>ProgressDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>ProgressDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
#include "update_dialog.hpp"
|
#include "update_dialog.hpp"
|
||||||
#include "ui_update_dialog.h"
|
#include "ui_update_dialog.h"
|
||||||
#include <scwx/qt/main/versions.hpp>
|
#include <scwx/qt/main/versions.hpp>
|
||||||
|
#include <scwx/qt/manager/download_manager.hpp>
|
||||||
#include <scwx/qt/manager/font_manager.hpp>
|
#include <scwx/qt/manager/font_manager.hpp>
|
||||||
|
#include <scwx/qt/ui/download_dialog.hpp>
|
||||||
|
#include <scwx/util/logger.hpp>
|
||||||
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFontDatabase>
|
#include <QFontDatabase>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
namespace scwx
|
namespace scwx
|
||||||
{
|
{
|
||||||
|
|
@ -13,19 +18,29 @@ namespace qt
|
||||||
namespace ui
|
namespace ui
|
||||||
{
|
{
|
||||||
|
|
||||||
class UpdateDialogImpl
|
static const std::string logPrefix_ = "scwx::qt::ui::update_dialog";
|
||||||
|
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
||||||
|
|
||||||
|
class UpdateDialog::Impl
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit UpdateDialogImpl() = default;
|
explicit Impl(UpdateDialog* self) : self_ {self} {};
|
||||||
~UpdateDialogImpl() = default;
|
~Impl() = default;
|
||||||
|
|
||||||
|
void HandleAsset(const types::gh::ReleaseAsset& asset);
|
||||||
|
|
||||||
|
UpdateDialog* self_;
|
||||||
|
|
||||||
|
std::shared_ptr<manager::DownloadManager> downloadManager_ {
|
||||||
|
manager::DownloadManager::Instance()};
|
||||||
|
|
||||||
std::string downloadUrl_ {};
|
std::string downloadUrl_ {};
|
||||||
|
std::string installUrl_ {};
|
||||||
|
std::string installFilename_ {};
|
||||||
};
|
};
|
||||||
|
|
||||||
UpdateDialog::UpdateDialog(QWidget* parent) :
|
UpdateDialog::UpdateDialog(QWidget* parent) :
|
||||||
QDialog(parent),
|
QDialog(parent), p {std::make_unique<Impl>(this)}, ui(new Ui::UpdateDialog)
|
||||||
p {std::make_unique<UpdateDialogImpl>()},
|
|
||||||
ui(new Ui::UpdateDialog)
|
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
|
@ -37,6 +52,8 @@ UpdateDialog::UpdateDialog(QWidget* parent) :
|
||||||
ui->bannerLabel->setFont(titleFont);
|
ui->bannerLabel->setFont(titleFont);
|
||||||
|
|
||||||
ui->releaseNotesText->setOpenExternalLinks(true);
|
ui->releaseNotesText->setOpenExternalLinks(true);
|
||||||
|
|
||||||
|
ui->installUpdateButton->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDialog::~UpdateDialog()
|
UpdateDialog::~UpdateDialog()
|
||||||
|
|
@ -56,6 +73,27 @@ void UpdateDialog::UpdateReleaseInfo(const std::string& latestVersion,
|
||||||
QString::fromStdString(latestRelease.body_));
|
QString::fromStdString(latestRelease.body_));
|
||||||
|
|
||||||
p->downloadUrl_ = latestRelease.htmlUrl_;
|
p->downloadUrl_ = latestRelease.htmlUrl_;
|
||||||
|
|
||||||
|
ui->installUpdateButton->setVisible(false);
|
||||||
|
|
||||||
|
for (auto& asset : latestRelease.assets_)
|
||||||
|
{
|
||||||
|
p->HandleAsset(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (asset.name_.ends_with(".msi"))
|
||||||
|
{
|
||||||
|
self_->ui->installUpdateButton->setVisible(true);
|
||||||
|
installUrl_ = asset.browserDownloadUrl_;
|
||||||
|
installFilename_ = asset.name_;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Q_UNUSED(asset)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateDialog::on_downloadButton_clicked()
|
void UpdateDialog::on_downloadButton_clicked()
|
||||||
|
|
@ -66,6 +104,86 @@ void UpdateDialog::on_downloadButton_clicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UpdateDialog::on_installUpdateButton_clicked()
|
||||||
|
{
|
||||||
|
if (!p->installUrl_.empty())
|
||||||
|
{
|
||||||
|
ui->installUpdateButton->setEnabled(false);
|
||||||
|
|
||||||
|
std::string destinationPath {
|
||||||
|
QStandardPaths::writableLocation(QStandardPaths::TempLocation)
|
||||||
|
.toStdString()};
|
||||||
|
|
||||||
|
std::shared_ptr<request::DownloadRequest> request =
|
||||||
|
std::make_shared<request::DownloadRequest>(
|
||||||
|
p->installUrl_,
|
||||||
|
std::filesystem::path(destinationPath) / p->installFilename_);
|
||||||
|
|
||||||
|
DownloadDialog* downloadDialog = new DownloadDialog(this);
|
||||||
|
downloadDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
// Connect request signals
|
||||||
|
connect(request.get(),
|
||||||
|
&request::DownloadRequest::ProgressUpdated,
|
||||||
|
downloadDialog,
|
||||||
|
&DownloadDialog::UpdateProgress);
|
||||||
|
connect(request.get(),
|
||||||
|
&request::DownloadRequest::RequestComplete,
|
||||||
|
downloadDialog,
|
||||||
|
[=](request::DownloadRequest::CompleteReason reason)
|
||||||
|
{
|
||||||
|
switch (reason)
|
||||||
|
{
|
||||||
|
case request::DownloadRequest::CompleteReason::OK:
|
||||||
|
downloadDialog->FinishDownload();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
downloadDialog->CancelDownload();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect dialog signals
|
||||||
|
connect(
|
||||||
|
downloadDialog,
|
||||||
|
&QDialog::accepted,
|
||||||
|
this,
|
||||||
|
[=, this]()
|
||||||
|
{
|
||||||
|
std::filesystem::path installerPackage =
|
||||||
|
request->destination_path();
|
||||||
|
installerPackage.make_preferred();
|
||||||
|
|
||||||
|
logger_->info("Launching application installer: {}",
|
||||||
|
installerPackage.string());
|
||||||
|
|
||||||
|
if (!QProcess::startDetached(
|
||||||
|
"msiexec.exe",
|
||||||
|
{"/i", QString::fromStdString(installerPackage.string())}))
|
||||||
|
{
|
||||||
|
logger_->error("Failed to launch installer");
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->installUpdateButton->setEnabled(true);
|
||||||
|
});
|
||||||
|
connect(downloadDialog,
|
||||||
|
&QDialog::rejected,
|
||||||
|
this,
|
||||||
|
[=, this]()
|
||||||
|
{
|
||||||
|
request->Cancel();
|
||||||
|
|
||||||
|
ui->installUpdateButton->setEnabled(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadDialog->set_filename(p->installFilename_);
|
||||||
|
downloadDialog->StartDownload();
|
||||||
|
|
||||||
|
p->downloadManager_->Download(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
} // namespace qt
|
} // namespace qt
|
||||||
} // namespace scwx
|
} // namespace scwx
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@ namespace qt
|
||||||
namespace ui
|
namespace ui
|
||||||
{
|
{
|
||||||
|
|
||||||
class UpdateDialogImpl;
|
|
||||||
|
|
||||||
class UpdateDialog : public QDialog
|
class UpdateDialog : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(UpdateDialog)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit UpdateDialog(QWidget* parent = nullptr);
|
explicit UpdateDialog(QWidget* parent = nullptr);
|
||||||
|
|
@ -31,10 +30,11 @@ public:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void on_downloadButton_clicked();
|
void on_downloadButton_clicked();
|
||||||
|
void on_installUpdateButton_clicked();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend UpdateDialogImpl;
|
class Impl;
|
||||||
std::unique_ptr<UpdateDialogImpl> p;
|
std::unique_ptr<Impl> p;
|
||||||
Ui::UpdateDialog* ui;
|
Ui::UpdateDialog* ui;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,13 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="installUpdateButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Install Update</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|
|
||||||
68
scwx-qt/wix.template.in
Normal file
68
scwx-qt/wix.template.in
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?include "cpack_variables.wxi"?>
|
||||||
|
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||||
|
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
|
||||||
|
@CPACK_WIX_CUSTOM_XMLNS_EXPANDED@
|
||||||
|
RequiredVersion="3.6.3303.0">
|
||||||
|
|
||||||
|
<Product Id="$(var.CPACK_WIX_PRODUCT_GUID)"
|
||||||
|
Name="$(var.CPACK_PACKAGE_NAME)"
|
||||||
|
Language="1033"
|
||||||
|
Version="$(var.CPACK_PACKAGE_VERSION)"
|
||||||
|
Manufacturer="$(var.CPACK_PACKAGE_VENDOR)"
|
||||||
|
UpgradeCode="$(var.CPACK_WIX_UPGRADE_GUID)">
|
||||||
|
|
||||||
|
<Package InstallerVersion="301" Compressed="yes" InstallScope="perMachine"/>
|
||||||
|
|
||||||
|
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes"/>
|
||||||
|
|
||||||
|
<MajorUpgrade
|
||||||
|
Schedule="afterInstallInitialize"
|
||||||
|
AllowSameVersionUpgrades="yes"
|
||||||
|
DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
|
||||||
|
|
||||||
|
<WixVariable Id="WixUILicenseRtf" Value="$(var.CPACK_WIX_LICENSE_RTF)"/>
|
||||||
|
<Property Id="WIXUI_INSTALLDIR" Value="INSTALL_ROOT"/>
|
||||||
|
|
||||||
|
<?ifdef CPACK_WIX_PRODUCT_ICON?>
|
||||||
|
<Property Id="ARPPRODUCTICON">ProductIcon.ico</Property>
|
||||||
|
<Icon Id="ProductIcon.ico" SourceFile="$(var.CPACK_WIX_PRODUCT_ICON)"/>
|
||||||
|
<?endif?>
|
||||||
|
|
||||||
|
<?ifdef CPACK_WIX_UI_BANNER?>
|
||||||
|
<WixVariable Id="WixUIBannerBmp" Value="$(var.CPACK_WIX_UI_BANNER)"/>
|
||||||
|
<?endif?>
|
||||||
|
|
||||||
|
<?ifdef CPACK_WIX_UI_DIALOG?>
|
||||||
|
<WixVariable Id="WixUIDialogBmp" Value="$(var.CPACK_WIX_UI_DIALOG)"/>
|
||||||
|
<?endif?>
|
||||||
|
|
||||||
|
<FeatureRef Id="ProductFeature"/>
|
||||||
|
|
||||||
|
<UIRef Id="$(var.CPACK_WIX_UI_REF)" />
|
||||||
|
<UIRef Id="WixUI_ErrorProgressText" />
|
||||||
|
|
||||||
|
<UI>
|
||||||
|
<Publish Dialog="ExitDialog"
|
||||||
|
Control="Finish"
|
||||||
|
Event="DoAction"
|
||||||
|
Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
|
||||||
|
</UI>
|
||||||
|
|
||||||
|
<util:CloseApplication
|
||||||
|
Id="CloseSupercellWx"
|
||||||
|
Target="supercell-wx.exe"
|
||||||
|
RebootPrompt="no"
|
||||||
|
PromptToContinue="yes"
|
||||||
|
Description="Supercell Wx should be closed before continuing the install."/>
|
||||||
|
|
||||||
|
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch Supercell Wx" />
|
||||||
|
<Property Id="WixShellExecTarget" Value="[#CM_FP_bin.supercell_wx.exe]" />
|
||||||
|
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
|
||||||
|
|
||||||
|
<?include "properties.wxi"?>
|
||||||
|
<?include "product_fragment.wxi"?>
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
||||||
|
|
@ -7,6 +7,35 @@ namespace scwx
|
||||||
namespace util
|
namespace util
|
||||||
{
|
{
|
||||||
|
|
||||||
|
class BytesToStringTest :
|
||||||
|
public testing::TestWithParam<std::pair<std::ptrdiff_t, std::string>>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_P(BytesToStringTest, BytesToString)
|
||||||
|
{
|
||||||
|
auto& [bytes, expected] = GetParam();
|
||||||
|
|
||||||
|
std::string s = BytesToString(bytes);
|
||||||
|
|
||||||
|
EXPECT_EQ(s, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(StringsTest,
|
||||||
|
BytesToStringTest,
|
||||||
|
testing::Values(std::make_pair(123, "123 bytes"),
|
||||||
|
std::make_pair(1000, "0.98 KB"),
|
||||||
|
std::make_pair(1018, "0.99 KB"),
|
||||||
|
std::make_pair(1024, "1.0 KB"),
|
||||||
|
std::make_pair(1127, "1.1 KB"),
|
||||||
|
std::make_pair(1260, "1.23 KB"),
|
||||||
|
std::make_pair(24012, "23.4 KB"),
|
||||||
|
std::make_pair(353974, "346 KB"),
|
||||||
|
std::make_pair(1024000, "0.98 MB"),
|
||||||
|
std::make_pair(1048576000, "0.98 GB"),
|
||||||
|
std::make_pair(1073741824000,
|
||||||
|
"0.98 TB")));
|
||||||
|
|
||||||
TEST(StringsTest, ParseTokensColor)
|
TEST(StringsTest, ParseTokensColor)
|
||||||
{
|
{
|
||||||
static const std::string line {"Color: red green blue alpha discarded"};
|
static const std::string line {"Color: red green blue alpha discarded"};
|
||||||
|
|
|
||||||
18
wxdata/include/scwx/util/digest.hpp
Normal file
18
wxdata/include/scwx/util/digest.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <istream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace util
|
||||||
|
{
|
||||||
|
|
||||||
|
bool ComputeDigest(const EVP_MD* mdtype,
|
||||||
|
std::istream& is,
|
||||||
|
std::vector<std::uint8_t>& digest);
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace scwx
|
||||||
|
|
@ -9,6 +9,16 @@ namespace scwx
|
||||||
namespace util
|
namespace util
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Print the number of bytes using a dynamic suffix and limited number of
|
||||||
|
* decimal points.
|
||||||
|
*
|
||||||
|
* @param [in] bytes Number of bytes
|
||||||
|
*
|
||||||
|
* @return Human readable size string
|
||||||
|
*/
|
||||||
|
std::string BytesToString(std::ptrdiff_t bytes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Parse a list of tokens from a string
|
* @brief Parse a list of tokens from a string
|
||||||
*
|
*
|
||||||
|
|
|
||||||
82
wxdata/source/scwx/util/digest.cpp
Normal file
82
wxdata/source/scwx/util/digest.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#include <scwx/util/digest.hpp>
|
||||||
|
#include <scwx/util/logger.hpp>
|
||||||
|
|
||||||
|
namespace scwx
|
||||||
|
{
|
||||||
|
namespace util
|
||||||
|
{
|
||||||
|
|
||||||
|
static const std::string logPrefix_ = "scwx::util::digest";
|
||||||
|
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
||||||
|
|
||||||
|
bool ComputeDigest(const EVP_MD* mdtype,
|
||||||
|
std::istream& is,
|
||||||
|
std::vector<std::uint8_t>& digest)
|
||||||
|
{
|
||||||
|
int mdsize;
|
||||||
|
EVP_MD_CTX* mdctx = nullptr;
|
||||||
|
|
||||||
|
digest.clear();
|
||||||
|
|
||||||
|
if ((mdsize = EVP_MD_get_size(mdtype)) < 1)
|
||||||
|
{
|
||||||
|
logger_->error("Invalid digest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mdctx = EVP_MD_CTX_new()) == nullptr)
|
||||||
|
{
|
||||||
|
logger_->error("Error allocating a digest context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EVP_DigestInit_ex(mdctx, mdtype, nullptr))
|
||||||
|
{
|
||||||
|
logger_->error("Message digest initialization failed");
|
||||||
|
EVP_MD_CTX_free(mdctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
is.seekg(0, std::ios_base::end);
|
||||||
|
const std::size_t streamSize = is.tellg();
|
||||||
|
is.seekg(0, std::ios_base::beg);
|
||||||
|
|
||||||
|
std::size_t bytesRead = 0;
|
||||||
|
std::size_t chunkSize = 4096;
|
||||||
|
std::string fileData;
|
||||||
|
fileData.resize(chunkSize);
|
||||||
|
|
||||||
|
while (bytesRead < streamSize)
|
||||||
|
{
|
||||||
|
const std::size_t bytesRemaining = streamSize - bytesRead;
|
||||||
|
const std::size_t readSize = std::min(chunkSize, bytesRemaining);
|
||||||
|
|
||||||
|
is.read(fileData.data(), readSize);
|
||||||
|
|
||||||
|
if (!is.good() || !EVP_DigestUpdate(mdctx, fileData.data(), readSize))
|
||||||
|
{
|
||||||
|
logger_->error("Message digest update failed");
|
||||||
|
EVP_MD_CTX_free(mdctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead += readSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
digest.resize(mdsize);
|
||||||
|
|
||||||
|
if (!EVP_DigestFinal_ex(mdctx, digest.data(), nullptr))
|
||||||
|
{
|
||||||
|
logger_->error("Message digest finalization failed");
|
||||||
|
EVP_MD_CTX_free(mdctx);
|
||||||
|
digest.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EVP_MD_CTX_free(mdctx);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace scwx
|
||||||
|
|
@ -4,12 +4,76 @@
|
||||||
|
|
||||||
#include <boost/algorithm/string/trim.hpp>
|
#include <boost/algorithm/string/trim.hpp>
|
||||||
#include <boost/lexical_cast.hpp>
|
#include <boost/lexical_cast.hpp>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
namespace scwx
|
namespace scwx
|
||||||
{
|
{
|
||||||
namespace util
|
namespace util
|
||||||
{
|
{
|
||||||
|
|
||||||
|
std::string BytesToString(std::ptrdiff_t bytes)
|
||||||
|
{
|
||||||
|
auto FormatNumber = [](double number) -> std::string
|
||||||
|
{
|
||||||
|
int precision;
|
||||||
|
|
||||||
|
// Determine precision
|
||||||
|
if (number >= 100.0)
|
||||||
|
{
|
||||||
|
precision = 0;
|
||||||
|
}
|
||||||
|
else if (number >= 10.0)
|
||||||
|
{
|
||||||
|
precision = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
precision = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the number
|
||||||
|
std::string formattedNum = fmt::format("{:.{}f}", number, precision);
|
||||||
|
|
||||||
|
// Remove trailing zeroes
|
||||||
|
std::size_t found = formattedNum.find_last_not_of('0');
|
||||||
|
if (found != std::string::npos && formattedNum[found] == '.')
|
||||||
|
{
|
||||||
|
// Keep one trailing zero if it's a decimal point
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
formattedNum.erase(found + 1, std::string::npos);
|
||||||
|
|
||||||
|
return formattedNum;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Print with appropriate suffix
|
||||||
|
if (bytes < 1000)
|
||||||
|
{
|
||||||
|
return fmt::format("{} bytes", bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
double kilobytes = bytes / 1024.0;
|
||||||
|
if (kilobytes < 1000.0)
|
||||||
|
{
|
||||||
|
return fmt::format("{} KB", FormatNumber(kilobytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
double megabytes = kilobytes / 1024.0;
|
||||||
|
if (megabytes < 1000.0)
|
||||||
|
{
|
||||||
|
return fmt::format("{} MB", FormatNumber(megabytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
double gigabytes = megabytes / 1024.0;
|
||||||
|
if (gigabytes < 1000.0)
|
||||||
|
{
|
||||||
|
return fmt::format("{} GB", FormatNumber(gigabytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
double terabytes = gigabytes / 1024.0;
|
||||||
|
return fmt::format("{} TB", FormatNumber(terabytes));
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> ParseTokens(const std::string& s,
|
std::vector<std::string> ParseTokens(const std::string& s,
|
||||||
std::vector<std::string> delimiters,
|
std::vector<std::string> delimiters,
|
||||||
std::size_t pos)
|
std::size_t pos)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp
|
||||||
source/scwx/provider/nexrad_data_provider.cpp
|
source/scwx/provider/nexrad_data_provider.cpp
|
||||||
source/scwx/provider/nexrad_data_provider_factory.cpp
|
source/scwx/provider/nexrad_data_provider_factory.cpp
|
||||||
source/scwx/provider/warnings_provider.cpp)
|
source/scwx/provider/warnings_provider.cpp)
|
||||||
set(HDR_UTIL include/scwx/util/enum.hpp
|
set(HDR_UTIL include/scwx/util/digest.hpp
|
||||||
|
include/scwx/util/enum.hpp
|
||||||
include/scwx/util/environment.hpp
|
include/scwx/util/environment.hpp
|
||||||
include/scwx/util/float.hpp
|
include/scwx/util/float.hpp
|
||||||
include/scwx/util/hash.hpp
|
include/scwx/util/hash.hpp
|
||||||
|
|
@ -80,7 +81,8 @@ set(HDR_UTIL include/scwx/util/enum.hpp
|
||||||
include/scwx/util/threads.hpp
|
include/scwx/util/threads.hpp
|
||||||
include/scwx/util/time.hpp
|
include/scwx/util/time.hpp
|
||||||
include/scwx/util/vectorbuf.hpp)
|
include/scwx/util/vectorbuf.hpp)
|
||||||
set(SRC_UTIL source/scwx/util/environment.cpp
|
set(SRC_UTIL source/scwx/util/digest.cpp
|
||||||
|
source/scwx/util/environment.cpp
|
||||||
source/scwx/util/float.cpp
|
source/scwx/util/float.cpp
|
||||||
source/scwx/util/hash.cpp
|
source/scwx/util/hash.cpp
|
||||||
source/scwx/util/logger.cpp
|
source/scwx/util/logger.cpp
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue