mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-07-31 06:42:05 +08:00
Preset Archive Database
testing version of multiple archives with indicies WIP Archive db Getting archives from web mockup WIP paste archives to updater Preset Updater sync_config with Online Archive Repository UserAccount -> PresetArchiveDatabase -> PresetUpdater dataflow LocalArchiveRepository get_file by copying file Local Archive Repository fix of thread save multiple call of preset_updater sync Testing JSON string with repos WIP offline archive repo unzip before use Local repository unzipping to temp. get_file() refactored. todo: checking and writing repository ids correctly. Refactoring of loading data for archive repository Comments WIP download repo manifest Preset Archive Database sync in thread delete testing code escape subpath before download post rebase fix resources getting fix vendor profile id -> name fix
This commit is contained in:
parent
f889bf470c
commit
ece5207783
@ -156,6 +156,11 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem
|
||||
res.templates_profile = templates_profile->second.data() == "1";
|
||||
}
|
||||
|
||||
const auto repo_id = vendor_section.find("repo_id");
|
||||
if (repo_id != vendor_section.not_found()) {
|
||||
res.repo_id = repo_id->second.data();
|
||||
}
|
||||
|
||||
if (! load_all) {
|
||||
return res;
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ public:
|
||||
Semver config_version;
|
||||
std::string config_update_url;
|
||||
std::string changelog_url;
|
||||
std::string repo_id;
|
||||
bool templates_profile { false };
|
||||
|
||||
struct PrinterVariant {
|
||||
|
@ -315,6 +315,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/DownloaderFileGet.hpp
|
||||
GUI/LoginDialog.cpp
|
||||
GUI/LoginDialog.hpp
|
||||
GUI/PresetArchiveDatabase.cpp
|
||||
GUI/PresetArchiveDatabase.hpp
|
||||
Utils/AppUpdater.cpp
|
||||
Utils/AppUpdater.hpp
|
||||
Utils/Http.cpp
|
||||
|
@ -102,6 +102,7 @@
|
||||
#include "UserAccount.hpp"
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "LoginDialog.hpp"
|
||||
#include "PresetArchiveDatabase.hpp"
|
||||
|
||||
#include "BitmapCache.hpp"
|
||||
//#include "Notebook.hpp"
|
||||
@ -833,8 +834,8 @@ void GUI_App::post_init()
|
||||
#endif
|
||||
CallAfter([this] {
|
||||
// preset_updater->sync downloads profile updates on background so it must begin after config wizard finished.
|
||||
// its call was moved to start_preset_updater method
|
||||
bool cw_showed = this->config_wizard_startup();
|
||||
this->preset_updater->sync(preset_bundle, this);
|
||||
if (! cw_showed) {
|
||||
// The CallAfter is needed as well, without it, GL extensions did not show.
|
||||
// Also, we only want to show this when the wizard does not, so the new user
|
||||
@ -3127,19 +3128,29 @@ bool GUI_App::may_switch_to_SLA_preset(const wxString& caption)
|
||||
bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page)
|
||||
{
|
||||
wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null");
|
||||
|
||||
// Cancel sync before starting wizard to prevent two downloads at same time.
|
||||
preset_updater->cancel_sync();
|
||||
// Show login dialog before wizard.
|
||||
#if 0
|
||||
if (!plater()->get_user_account()->is_logged()) {
|
||||
bool user_was_logged = plater()->get_user_account()->is_logged();
|
||||
if (!user_was_logged) {
|
||||
m_login_dialog = std::make_unique<LoginDialog>(mainframe, plater()->get_user_account());
|
||||
m_login_dialog->ShowModal();
|
||||
mainframe->RemoveChild(m_login_dialog.get());
|
||||
m_login_dialog->Destroy();
|
||||
// Destructor does not call Destroy
|
||||
// Destructor does not call Destroy.
|
||||
m_login_dialog.reset();
|
||||
}
|
||||
// Update archive db if login status changed, otherwise we expect to have archive db on date.
|
||||
if (user_was_logged != plater()->get_user_account()->is_logged()) {
|
||||
plater()->get_preset_archive_database()->sync_blocking();
|
||||
}
|
||||
#endif // 0
|
||||
if (reason == ConfigWizard::RR_USER) {
|
||||
// Cancel sync before starting wizard to prevent two downloads at same time
|
||||
preset_updater->cancel_sync();
|
||||
// Do blocking sync on every start of wizard, so user is always offered recent profiles.
|
||||
preset_updater->sync_blocking(preset_bundle, this, plater()->get_preset_archive_database()->get_archives());
|
||||
// Offer update installation (of already installed profiles) only when run by user.
|
||||
if (reason == ConfigWizard::RR_USER) {
|
||||
preset_updater->update_index_db();
|
||||
if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED)
|
||||
return false;
|
||||
@ -3843,5 +3854,14 @@ void GUI_App::show_printer_webview_tab()
|
||||
mainframe->show_printer_webview_tab(preset_bundle->physical_printers.get_selected_printer_config());
|
||||
}
|
||||
|
||||
void GUI_App::start_preset_updater(bool forced)
|
||||
{
|
||||
if (m_started_preset_updater && !forced) {
|
||||
return;
|
||||
}
|
||||
this->preset_updater->cancel_sync();
|
||||
this->preset_updater->sync(preset_bundle, this, plater()->get_preset_archive_database()->get_archives());
|
||||
m_started_preset_updater = true;
|
||||
}
|
||||
} // GUI
|
||||
} //Slic3r
|
||||
|
@ -60,7 +60,7 @@ class Downloader;
|
||||
struct GUI_InitParams;
|
||||
class GalleryDialog;
|
||||
class LoginDialog;
|
||||
|
||||
class PresetArchiveDatabase;
|
||||
|
||||
enum FileType
|
||||
{
|
||||
@ -170,14 +170,14 @@ private:
|
||||
|
||||
OpenGLManager m_opengl_mgr;
|
||||
|
||||
std::unique_ptr<RemovableDriveManager> m_removable_drive_manager;
|
||||
|
||||
std::unique_ptr<ImGuiWrapper> m_imgui;
|
||||
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
|
||||
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
|
||||
std::unique_ptr <AppUpdater> m_app_updater;
|
||||
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
|
||||
std::unique_ptr <Downloader> m_downloader;
|
||||
std::unique_ptr<RemovableDriveManager> m_removable_drive_manager;
|
||||
std::unique_ptr<ImGuiWrapper> m_imgui;
|
||||
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
|
||||
std::unique_ptr<OtherInstanceMessageHandler> m_other_instance_message_handler;
|
||||
std::unique_ptr<AppUpdater> m_app_updater;
|
||||
std::unique_ptr<wxSingleInstanceChecker> m_single_instance_checker;
|
||||
std::unique_ptr<Downloader> m_downloader;
|
||||
|
||||
std::string m_instance_hash_string;
|
||||
size_t m_instance_hash_int;
|
||||
|
||||
@ -427,6 +427,8 @@ public:
|
||||
void request_open_project(std::string project_id) {}
|
||||
void request_remove_project(std::string project_id) {}
|
||||
|
||||
void start_preset_updater(bool forced);
|
||||
|
||||
private:
|
||||
bool on_init_inner();
|
||||
void init_app_config();
|
||||
@ -450,6 +452,7 @@ private:
|
||||
|
||||
bool m_wifi_config_dialog_shown { false };
|
||||
bool m_wifi_config_dialog_was_declined { false };
|
||||
bool m_started_preset_updater { false };
|
||||
// change to vector of items when adding more items that require update
|
||||
//wxMenuItem* m_login_config_menu_item { nullptr };
|
||||
std::map< ConfigMenuIDs, wxMenuItem*> m_config_menu_updatable_items;
|
||||
|
@ -124,6 +124,7 @@
|
||||
#include "UserAccount.hpp"
|
||||
#include "DesktopIntegrationDialog.hpp"
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "PresetArchiveDatabase.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "Gizmos/GLGizmosManager.hpp"
|
||||
@ -268,6 +269,7 @@ struct Plater::priv
|
||||
Preview *preview;
|
||||
std::unique_ptr<NotificationManager> notification_manager;
|
||||
std::unique_ptr<UserAccount> user_account;
|
||||
std::unique_ptr<PresetArchiveDatabase> preset_archive_database;
|
||||
|
||||
ProjectDirtyStateManager dirty_state;
|
||||
|
||||
@ -615,6 +617,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
, sidebar(new Sidebar(q))
|
||||
, notification_manager(std::make_unique<NotificationManager>(q))
|
||||
, user_account(std::make_unique<UserAccount>(q, wxGetApp().app_config, wxGetApp().get_instance_hash_string()))
|
||||
, preset_archive_database(std::make_unique<PresetArchiveDatabase>(wxGetApp().app_config, q))
|
||||
, m_worker{q, std::make_unique<NotificationProgressIndicator>(notification_manager.get()), "ui_worker"}
|
||||
, m_sla_import_dlg{new SLAImportDialog{q}}
|
||||
, delayed_scene_refresh(false)
|
||||
@ -891,11 +894,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
this->q->Bind(EVT_UA_ID_USER_SUCCESS, [this](UserAccountSuccessEvent& evt) {
|
||||
std::string username;
|
||||
if (user_account->on_user_id_success(evt.data, username)) {
|
||||
// login notification
|
||||
std::string text = format(_u8L("Logged to Prusa Account as %1%."), username);
|
||||
// login notification
|
||||
this->notification_manager->close_notification_of_type(NotificationType::UserAccountID);
|
||||
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text);
|
||||
// show connect tab
|
||||
this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text);
|
||||
this->main_frame->add_connect_webview_tab();
|
||||
// Update User name in TopBar
|
||||
this->main_frame->refresh_account_menu();
|
||||
@ -903,6 +906,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
wxGetApp().update_login_dialog();
|
||||
#endif // 0
|
||||
this->show_action_buttons(this->ready_to_slice);
|
||||
preset_archive_database->set_token("ABCD");
|
||||
preset_archive_database->sync();
|
||||
} else {
|
||||
// data were corrupt and username was not retrieved
|
||||
// procced as if EVT_UA_RESET was recieved
|
||||
@ -928,6 +933,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
this->main_frame->refresh_account_menu(true);
|
||||
// Update sidebar printer status
|
||||
sidebar->update_printer_presets_combobox();
|
||||
preset_archive_database->set_token({});
|
||||
preset_archive_database->sync();
|
||||
});
|
||||
this->q->Bind(EVT_UA_FAIL, [this](UserAccountFailEvent& evt) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed communication with Prusa Account: " << evt.data;
|
||||
@ -986,6 +993,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
});
|
||||
}
|
||||
|
||||
this->q->Bind(EVT_PRESET_ARCHIVE_DATABASE_SYNC_DONE, [this](Event<ArchiveRepositorySyncData>& evt) {
|
||||
preset_archive_database->set_archives(evt.data.json);
|
||||
wxGetApp().start_preset_updater(evt.data.force_updater);
|
||||
});
|
||||
|
||||
wxGetApp().other_instance_message_handler()->init(this->q);
|
||||
|
||||
// collapse sidebar according to saved value
|
||||
@ -6805,6 +6817,16 @@ const NotificationManager * Plater::get_notification_manager() const
|
||||
return p->notification_manager.get();
|
||||
}
|
||||
|
||||
PresetArchiveDatabase* Plater::get_preset_archive_database()
|
||||
{
|
||||
return p->preset_archive_database.get();
|
||||
}
|
||||
|
||||
const PresetArchiveDatabase* Plater::get_preset_archive_database() const
|
||||
{
|
||||
return p->preset_archive_database.get();
|
||||
}
|
||||
|
||||
UserAccount* Plater::get_user_account()
|
||||
{
|
||||
return p->user_account.get();
|
||||
|
@ -63,6 +63,7 @@ class NotificationManager;
|
||||
struct Camera;
|
||||
class GLToolbar;
|
||||
class UserAccount;
|
||||
class PresetArchiveDatabase;
|
||||
|
||||
class Plater: public wxPanel
|
||||
{
|
||||
@ -355,6 +356,9 @@ public:
|
||||
NotificationManager* get_notification_manager();
|
||||
const NotificationManager* get_notification_manager() const;
|
||||
|
||||
PresetArchiveDatabase* get_preset_archive_database();
|
||||
const PresetArchiveDatabase* get_preset_archive_database() const;
|
||||
|
||||
UserAccount* get_user_account();
|
||||
const UserAccount* get_user_account() const;
|
||||
|
||||
|
403
src/slic3r/GUI/PresetArchiveDatabase.cpp
Normal file
403
src/slic3r/GUI/PresetArchiveDatabase.cpp
Normal file
@ -0,0 +1,403 @@
|
||||
#include "PresetArchiveDatabase.hpp"
|
||||
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <cctype>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
namespace fs = boost::filesystem;
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
wxDEFINE_EVENT(EVT_PRESET_ARCHIVE_DATABASE_SYNC_DONE, Event<ArchiveRepositorySyncData>);
|
||||
|
||||
static const char* TMP_EXTENSION = ".download";
|
||||
|
||||
namespace {
|
||||
bool unzip_repository(const fs::path& source_path, const fs::path& target_path)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
mz_zip_zero_struct(&archive);
|
||||
if (!open_zip_reader(&archive, source_path.string())) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Couldn't open zipped Archive Repository. " << source_path;
|
||||
return false;
|
||||
}
|
||||
size_t num_files = mz_zip_reader_get_num_files(&archive);
|
||||
|
||||
for (size_t i = 0; i < num_files; ++i) {
|
||||
mz_zip_archive_file_stat file_stat;
|
||||
if (!mz_zip_reader_file_stat(&archive, i, &file_stat)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to get file stat for file #" << i << " in the zip archive. Ending Unzipping.";
|
||||
mz_zip_reader_end(&archive);
|
||||
return false;
|
||||
}
|
||||
fs::path extracted_path = target_path / file_stat.m_filename;
|
||||
if (file_stat.m_is_directory) {
|
||||
// Create directory if it doesn't exist
|
||||
fs::create_directories(extracted_path);
|
||||
continue;
|
||||
}
|
||||
// Create parent directory if it doesn't exist
|
||||
fs::create_directories(extracted_path.parent_path());
|
||||
// Extract file
|
||||
if (!mz_zip_reader_extract_to_file(&archive, i, extracted_path.string().c_str(), 0)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to extract file #" << i << " from the zip archive. Ending Unzipping.";
|
||||
mz_zip_reader_end(&archive);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
mz_zip_reader_end(&archive);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool extract_repository_header(const pt::ptree& ptree, ArchiveRepository::RepositoryManifest& data)
|
||||
{
|
||||
// mandatory atributes
|
||||
if (const auto name = ptree.get_optional<std::string>("name"); name){
|
||||
data.name = *name;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to find \"name\" parameter in repository header. Repository is invalid.";
|
||||
return false;
|
||||
}
|
||||
if (const auto id = ptree.get_optional<std::string>("id"); id) {
|
||||
data.id = *id;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to find \"id\" parameter in repository header. Repository is invalid.";
|
||||
return false;
|
||||
}
|
||||
if (const auto url = ptree.get_optional<std::string>("url"); url) {
|
||||
data.url = *url;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to find \"url\" parameter in repository header. Repository is invalid.";
|
||||
return false;
|
||||
}
|
||||
// optional atributes
|
||||
if (const auto index_url = ptree.get_optional<std::string>("index_url"); index_url) {
|
||||
data.index_url = *index_url;
|
||||
}
|
||||
if (const auto description = ptree.get_optional<std::string>("description"); description) {
|
||||
data.description = *description;
|
||||
}
|
||||
if (const auto visibility = ptree.get_optional<std::string>("visibility"); visibility) {
|
||||
data.visibility = *visibility;
|
||||
data.m_secret = data.visibility.empty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void delete_path_recursive(const fs::path& path)
|
||||
{
|
||||
try {
|
||||
if (fs::exists(path)) {
|
||||
for (fs::directory_iterator it(path); it != fs::directory_iterator(); ++it) {
|
||||
const fs::path subpath = it->path();
|
||||
if (fs::is_directory(subpath)) {
|
||||
delete_path_recursive(subpath);
|
||||
} else {
|
||||
fs::remove(subpath);
|
||||
}
|
||||
}
|
||||
fs::remove(path);
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to delete files at: " << path;
|
||||
}
|
||||
}
|
||||
|
||||
bool extract_local_archive_repository(const fs::path& zip_path, fs::path& unq_tmp_path, ArchiveRepository::RepositoryManifest& header_data)
|
||||
{
|
||||
// Path where data will be unzipped.
|
||||
header_data.local_path = unq_tmp_path / zip_path.stem();
|
||||
// Delete previous data before unzip.
|
||||
// We have unique path in temp set for whole run of slicer and in it folder for each repo.
|
||||
delete_path_recursive(header_data.local_path);
|
||||
fs::create_directories(header_data.local_path);
|
||||
// Unzip repository zip to unique path in temp directory.
|
||||
if (!unzip_repository(zip_path, header_data.local_path)) {
|
||||
return false;
|
||||
}
|
||||
// Read the header file.
|
||||
fs::path header_path = header_data.local_path / "header.json";
|
||||
try
|
||||
{
|
||||
pt::ptree ptree;
|
||||
pt::read_json(header_path.string(), ptree);
|
||||
if (!extract_repository_header(ptree, header_data)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to load repository: " << zip_path;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to read repository header JSON " << header_path << ". reason: " << e.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void deserialize_string(const std::string& opt, std::vector<std::string>& result)
|
||||
{
|
||||
std::string val;
|
||||
for (size_t i = 0; i < opt.length(); i++) {
|
||||
if (std::isspace(opt[i])) {
|
||||
continue;
|
||||
}
|
||||
if (opt[i] != ';') {
|
||||
val += opt[i];
|
||||
}
|
||||
else {
|
||||
result.emplace_back(std::move(val));
|
||||
}
|
||||
}
|
||||
if (!val.empty()) {
|
||||
result.emplace_back(std::move(val));
|
||||
}
|
||||
}
|
||||
|
||||
std::string escape_string(const std::string& unescaped)
|
||||
{
|
||||
std::string ret_val;
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl) {
|
||||
char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size());
|
||||
if (decoded) {
|
||||
ret_val = std::string(decoded);
|
||||
curl_free(decoded);
|
||||
}
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
return ret_val;
|
||||
}
|
||||
std::string escape_path_by_element(const std::string& path_string)
|
||||
{
|
||||
const boost::filesystem::path path(path_string);
|
||||
std::string ret_val = escape_string(path.filename().string());
|
||||
boost::filesystem::path parent(path.parent_path());
|
||||
while (!parent.empty() && parent.string() != "/") // "/" check is for case "/file.gcode" was inserted. Then boost takes "/" as parent_path.
|
||||
{
|
||||
ret_val = escape_string(parent.filename().string()) + "/" + ret_val;
|
||||
parent = parent.parent_path();
|
||||
}
|
||||
return ret_val;
|
||||
}
|
||||
}
|
||||
|
||||
bool OnlineArchiveRepository::get_file_inner(const std::string& url, const fs::path& target_path) const
|
||||
{
|
||||
|
||||
bool res = false;
|
||||
fs::path tmp_path = target_path;
|
||||
tmp_path += format(".%1%%2%", get_current_pid(), TMP_EXTENSION);
|
||||
BOOST_LOG_TRIVIAL(info) << format("Get: `%1%`\n\t-> `%2%`\n\tvia tmp path `%3%`",
|
||||
url,
|
||||
target_path.string(),
|
||||
tmp_path.string());
|
||||
|
||||
Http::get(url)
|
||||
.on_progress([](Http::Progress, bool& cancel) {
|
||||
//if (cancel) { cancel = true; }
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned http_status) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%",
|
||||
url,
|
||||
http_status,
|
||||
body);
|
||||
})
|
||||
.on_complete([&](std::string body, unsigned /* http_status */) {
|
||||
if (body.empty()) {
|
||||
return;
|
||||
}
|
||||
fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file.write(body.c_str(), body.size());
|
||||
file.close();
|
||||
fs::rename(tmp_path, target_path);
|
||||
res = true;
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool OnlineArchiveRepository::get_archive(const fs::path& target_path) const
|
||||
{
|
||||
return get_file_inner(m_data.index_url.empty() ? m_data.url + "vendor_indices.zip" : m_data.index_url, target_path);
|
||||
}
|
||||
|
||||
bool OnlineArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id) const
|
||||
{
|
||||
if (repository_id != m_data.id) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Error getting file " << source_subpath << ". The repository_id was not matching.";
|
||||
return false;
|
||||
}
|
||||
const std::string escaped_source_subpath = escape_path_by_element(source_subpath);
|
||||
return get_file_inner(m_data.url + escaped_source_subpath, target_path);
|
||||
}
|
||||
|
||||
bool OnlineArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path) const
|
||||
{
|
||||
const std::string escaped_source_subpath = escape_path_by_element(source_subpath);
|
||||
return get_file_inner(m_data.url + escaped_source_subpath, target_path);
|
||||
}
|
||||
|
||||
bool LocalArchiveRepository::get_file_inner(const fs::path& source_path, const fs::path& target_path) const
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << format("Copying %1% to %2%", source_path, target_path);
|
||||
std::string error_message;
|
||||
CopyFileResult cfr = Slic3r::copy_file(source_path.string(), target_path.string(), error_message, false);
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Copying of " << source_path << " to " << target_path << " has failed (" << cfr << "): " << error_message;
|
||||
// remove target file, even if it was there before
|
||||
if (fs::exists(target_path)) {
|
||||
boost::system::error_code ec;
|
||||
fs::remove(target_path, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Failed to delete file: %1%", ec.message());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Permissions should be copied from the source file by copy_file(). We are not sure about the source
|
||||
// permissions, let's rewrite them with 644.
|
||||
static constexpr const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read;
|
||||
fs::permissions(target_path, perms);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalArchiveRepository::get_file(const std::string& source_subpath, const fs::path& target_path, const std::string& repository_id) const
|
||||
{
|
||||
if (repository_id != m_data.id) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Error getting file " << source_subpath << ". The repository_id was not matching.";
|
||||
return false;
|
||||
}
|
||||
return get_file_inner(m_data.local_path / source_subpath, target_path);
|
||||
}
|
||||
bool LocalArchiveRepository::get_ini_no_id(const std::string& source_subpath, const fs::path& target_path) const
|
||||
{
|
||||
return get_file_inner(m_data.local_path / source_subpath, target_path);
|
||||
}
|
||||
bool LocalArchiveRepository::get_archive(const fs::path& target_path) const
|
||||
{
|
||||
fs::path source_path = fs::path(m_data.local_path) / "vendor_indices.zip";
|
||||
return get_file_inner(std::move(source_path), target_path);
|
||||
}
|
||||
|
||||
PresetArchiveDatabase::PresetArchiveDatabase(AppConfig* app_config, wxEvtHandler* evt_handler)
|
||||
: p_evt_handler(evt_handler)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
m_unq_tmp_path = fs::temp_directory_path() / fs::unique_path();
|
||||
fs::create_directories(m_unq_tmp_path, ec);
|
||||
assert(!ec);
|
||||
|
||||
set_local_archives(app_config);
|
||||
}
|
||||
|
||||
void PresetArchiveDatabase::set_archives(const std::string& json_body)
|
||||
{
|
||||
m_archives.clear();
|
||||
// Online repo headers are in json_body.
|
||||
try
|
||||
{
|
||||
std::stringstream ss(json_body);
|
||||
pt::ptree ptree;
|
||||
pt::read_json(ss, ptree);
|
||||
for (const auto& subtree : ptree) {
|
||||
ArchiveRepository::RepositoryManifest header;
|
||||
if (extract_repository_header(subtree.second, header)) {
|
||||
m_archives.emplace_back(std::make_unique<OnlineArchiveRepository>(std::move(header)));
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to read one of repository headers.";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to read archives JSON. " << e.what();
|
||||
}
|
||||
// Local archives are stored as paths to zip.
|
||||
// PresetArchiveDatabase has its folder in temp, local archives are extracted there
|
||||
for (const std::string& archive_opt : m_local_archive_adresses) {
|
||||
ArchiveRepository::RepositoryManifest header_data;
|
||||
if (extract_local_archive_repository(archive_opt, m_unq_tmp_path, header_data)) {
|
||||
m_archives.emplace_back(std::make_unique<LocalArchiveRepository>(std::move(header_data)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PresetArchiveDatabase::set_local_archives(AppConfig* app_config)
|
||||
{
|
||||
m_local_archive_adresses.clear();
|
||||
std::string opt = app_config->get("local_archives");
|
||||
std::vector<std::string> options;
|
||||
deserialize_string(opt, m_local_archive_adresses);
|
||||
}
|
||||
|
||||
// test_json is only for testing
|
||||
namespace {
|
||||
std::string test_json(bool secret)
|
||||
{
|
||||
std::string test = "["
|
||||
"{\"name\": \"Prusa Research\", \"id\": \"PrusaResearch\", \"url\": \"https://github.com/kocikdav/PrusaSlicer-settings/raw/master/archive_repos/PrusaResearch\", \"description\": \"Prusa Research\", \"visibility\":\"\"}, "
|
||||
"{\"name\": \"Other Vendors\", \"id\": \"OtherVendors\", \"url\": \"https://github.com/kocikdav/PrusaSlicer-settings/raw/master/archive_repos/OtherVendors\",\"description\": \"Other Vendors\", \"visibility\":\"\"} ";
|
||||
//if (secret) {
|
||||
// test += ", {\"name\": \"Davit\", \"id\": \"Davit\", \"archive_url\": \"https://github.com/kocikdav/PrusaSlicer-settings/raw/master/other_source\", \"secret\": true}";
|
||||
//}
|
||||
test += "]";
|
||||
return std::move(test);
|
||||
}
|
||||
|
||||
bool sync_inner(std::string& manifest)
|
||||
{
|
||||
bool ret = false;
|
||||
std::string url = "http://10.24.3.3:8001/v1/repos";
|
||||
Http::get(std::move(url))
|
||||
.on_error([&](std::string body, std::string error, unsigned http_status) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to get online archive repository manifests: "<< body << " ; " << error << " ; " << http_status;
|
||||
ret = false;
|
||||
})
|
||||
.on_complete([&](std::string body, unsigned /* http_status */) {
|
||||
manifest = body;
|
||||
ret = true;
|
||||
})
|
||||
.perform_sync();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PresetArchiveDatabase::sync()
|
||||
{
|
||||
|
||||
std::thread thread([this]() {
|
||||
std::string manifest;
|
||||
if (!sync_inner(manifest))
|
||||
return;
|
||||
// Force update when logged in (token not empty).
|
||||
wxQueueEvent(this->p_evt_handler, new Event<ArchiveRepositorySyncData>(EVT_PRESET_ARCHIVE_DATABASE_SYNC_DONE, {std::move(manifest), !m_token.empty()}));
|
||||
});
|
||||
thread.join();
|
||||
}
|
||||
|
||||
void PresetArchiveDatabase::sync_blocking()
|
||||
{
|
||||
std::string manifest;
|
||||
if (!sync_inner(manifest))
|
||||
return;
|
||||
set_archives(std::move(manifest));
|
||||
}
|
||||
|
||||
}} // Slic3r::GUI
|
120
src/slic3r/GUI/PresetArchiveDatabase.hpp
Normal file
120
src/slic3r/GUI/PresetArchiveDatabase.hpp
Normal file
@ -0,0 +1,120 @@
|
||||
#ifndef slic3r_PresetArchiveDatabase_hpp_
|
||||
#define slic3r_PresetArchiveDatabase_hpp_
|
||||
|
||||
#include "Event.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
class boost::filesystem::path;
|
||||
|
||||
namespace Slic3r {
|
||||
class AppConfig;
|
||||
namespace GUI {
|
||||
|
||||
struct ArchiveRepositorySyncData
|
||||
{
|
||||
std::string json;
|
||||
bool force_updater;
|
||||
};
|
||||
|
||||
wxDECLARE_EVENT(EVT_PRESET_ARCHIVE_DATABASE_SYNC_DONE, Event<ArchiveRepositorySyncData>);
|
||||
|
||||
|
||||
struct ArchiveRepositoryGetFileArgs {
|
||||
boost::filesystem::path target_path;
|
||||
|
||||
std::string repository_id;
|
||||
};
|
||||
|
||||
class ArchiveRepository
|
||||
{
|
||||
public:
|
||||
struct RepositoryManifest {
|
||||
// mandatory
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string url;
|
||||
// optional
|
||||
std::string index_url;
|
||||
std::string description;
|
||||
std::string visibility;
|
||||
// not read from manifest json
|
||||
boost::filesystem::path local_path;
|
||||
bool m_secret { false };
|
||||
};
|
||||
// Use std::move when calling constructor.
|
||||
ArchiveRepository(RepositoryManifest&& data) : m_data(std::move(data)) {}
|
||||
~ArchiveRepository() {}
|
||||
// Gets vendor_indices.zip to target_path
|
||||
virtual bool get_archive(const boost::filesystem::path& target_path) const = 0;
|
||||
// Gets file if repository_id arg matches m_id.
|
||||
// Should be used to get the most recent ini file and every missing resource.
|
||||
virtual bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const = 0;
|
||||
// Gets file without id check - for not yet encountered vendors only!
|
||||
virtual bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const = 0;
|
||||
protected:
|
||||
RepositoryManifest m_data;
|
||||
};
|
||||
|
||||
typedef std::vector<std::unique_ptr<const ArchiveRepository>> ArchiveRepositoryVector;
|
||||
|
||||
class OnlineArchiveRepository : public ArchiveRepository
|
||||
{
|
||||
public:
|
||||
OnlineArchiveRepository(RepositoryManifest&& data) : ArchiveRepository(std::move(data))
|
||||
{
|
||||
if (m_data.url.back() != '/') {
|
||||
m_data.url += "/";
|
||||
}
|
||||
}
|
||||
// Gets vendor_indices.zip to target_path.
|
||||
bool get_archive(const boost::filesystem::path& target_path) const override;
|
||||
// Gets file if repository_id arg matches m_id.
|
||||
// Should be used to get the most recent ini file and every missing resource.
|
||||
bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const override;
|
||||
// Gets file without checking id.
|
||||
// Should be used only if no previous ini file exists.
|
||||
bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const override;
|
||||
private:
|
||||
bool get_file_inner(const std::string& url, const boost::filesystem::path& target_path) const;
|
||||
};
|
||||
class LocalArchiveRepository : public ArchiveRepository
|
||||
{
|
||||
public:
|
||||
LocalArchiveRepository(RepositoryManifest&& data) : ArchiveRepository(std::move(data)) {}
|
||||
// Gets vendor_indices.zip to target_path.
|
||||
bool get_archive(const boost::filesystem::path& target_path) const override;
|
||||
// Gets file if repository_id arg matches m_id.
|
||||
// Should be used to get the most recent ini file and every missing resource.
|
||||
bool get_file(const std::string& source_subpath, const boost::filesystem::path& target_path, const std::string& repository_id) const override;
|
||||
// Gets file without checking id.
|
||||
// Should be used only if no previous ini file exists.
|
||||
bool get_ini_no_id(const std::string& source_subpath, const boost::filesystem::path& target_path) const override;
|
||||
private:
|
||||
bool get_file_inner(const boost::filesystem::path& source_path, const boost::filesystem::path& target_path) const;
|
||||
};
|
||||
class PresetArchiveDatabase
|
||||
{
|
||||
public:
|
||||
PresetArchiveDatabase(AppConfig* app_config, wxEvtHandler* evt_handler);
|
||||
~PresetArchiveDatabase() {}
|
||||
|
||||
const ArchiveRepositoryVector& get_archives() const { return m_archives; }
|
||||
void sync();
|
||||
void sync_blocking();
|
||||
void set_token(const std::string token) { m_token = token; }
|
||||
void set_local_archives(AppConfig* app_config);
|
||||
void set_archives(const std::string& json_body);
|
||||
private:
|
||||
wxEvtHandler* p_evt_handler;
|
||||
boost::filesystem::path m_unq_tmp_path;
|
||||
ArchiveRepositoryVector m_archives;
|
||||
std::vector<std::string> m_local_archive_adresses;
|
||||
std::string m_token;
|
||||
};
|
||||
|
||||
}} // Slic3r::GUI
|
||||
|
||||
#endif // PresetArchiveDatabase
|
@ -20,7 +20,8 @@ namespace GUI {
|
||||
UserAccount::UserAccount(wxEvtHandler* evt_handler, AppConfig* app_config, const std::string& instance_hash)
|
||||
: m_communication(std::make_unique<UserAccountCommunication>(evt_handler, app_config))
|
||||
, m_instance_hash(instance_hash)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
UserAccount::~UserAccount()
|
||||
{}
|
||||
@ -135,7 +136,7 @@ bool UserAccount::on_user_id_success(const std::string data, std::string& out_us
|
||||
std::string public_username = m_account_user_data["public_username"];
|
||||
set_username(public_username);
|
||||
out_username = public_username;
|
||||
// equeue GET with avatar url
|
||||
// enqueue GET with avatar url
|
||||
if (m_account_user_data.find("avatar") != m_account_user_data.end()) {
|
||||
const boost::filesystem::path server_file(m_account_user_data["avatar"]);
|
||||
m_avatar_extension = server_file.extension().string();
|
||||
|
@ -45,6 +45,9 @@ namespace fs = boost::filesystem;
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
wxDEFINE_EVENT(EVT_UA_NO_TOKENS, UserAccountFailEvent);
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
std::string get_code_from_message(const std::string& url_message)
|
||||
@ -151,8 +154,12 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap
|
||||
m_session = std::make_unique<UserAccountSession>(evt_handler, access_token, refresh_token, shared_session_key, m_app_config->get_bool("connect_polling"));
|
||||
init_session_thread();
|
||||
// perform login at the start, but only with tokens
|
||||
if (has_token)
|
||||
if (has_token) {
|
||||
do_login();
|
||||
} else {
|
||||
// send evt so preset archive database knows it can sync
|
||||
wxQueueEvent(evt_handler, new UserAccountFailEvent(EVT_UA_NO_TOKENS, {}));
|
||||
}
|
||||
}
|
||||
|
||||
UserAccountCommunication::~UserAccountCommunication()
|
||||
|
@ -27,6 +27,7 @@ wxDECLARE_EVENT(EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, UserAccountSuccessEven
|
||||
wxDECLARE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); // Soft fail - clears only after some number of fails
|
||||
wxDECLARE_EVENT(EVT_UA_RESET, UserAccountFailEvent); // Hard fail - clears all
|
||||
wxDECLARE_EVENT(EVT_UA_PRUSACONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); // Failed to get data for printer to select, soft fail, action does not repeat
|
||||
wxDECLARE_EVENT(EVT_UA_NO_TOKENS, UserAccountFailEvent); // when login wont be performed on startup
|
||||
|
||||
|
||||
typedef std::function<void(const std::string& body)> UserActionSuccessFn;
|
||||
@ -90,7 +91,7 @@ public:
|
||||
|
||||
struct ActionQueueData
|
||||
{
|
||||
UserAccountActionID action_id;
|
||||
UserAccountActionID action_id;
|
||||
UserActionSuccessFn success_callback;
|
||||
UserActionFailFn fail_callback;
|
||||
std::string input;
|
||||
@ -173,8 +174,8 @@ private:
|
||||
std::string m_refresh_token;
|
||||
std::string m_shared_session_key;
|
||||
|
||||
std::queue<ActionQueueData> m_action_queue;
|
||||
std::queue<ActionQueueData> m_priority_action_queue;
|
||||
std::queue<ActionQueueData> m_action_queue;
|
||||
std::queue<ActionQueueData> m_priority_action_queue;
|
||||
std::map<UserAccountActionID, std::unique_ptr<UserAction>> m_actions;
|
||||
|
||||
wxEvtHandler* p_evt_handler;
|
||||
|
@ -184,18 +184,20 @@ struct PresetUpdater::priv
|
||||
priv();
|
||||
|
||||
void set_download_prefs(const AppConfig *app_config);
|
||||
bool get_file(const std::string &url, const fs::path &target_path) const;
|
||||
//bool get_file(const std::string &url, const fs::path &target_path) const;
|
||||
void prune_tmps() const;
|
||||
void sync_config(const VendorMap vendors, const std::string& index_archive_url);
|
||||
void sync_config(const VendorMap& vendors, const GUI::ArchiveRepository& archive);
|
||||
|
||||
void check_install_indices() const;
|
||||
Updates get_config_updates(const Semver& old_slic3r_version) const;
|
||||
bool perform_updates(Updates &&updates, bool snapshot = true) const;
|
||||
void set_waiting_updates(Updates u);
|
||||
// checks existence and downloads resource to cache
|
||||
void get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const;
|
||||
void get_missing_resource(const GUI::ArchiveRepository& archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const;
|
||||
// checks existence and downloads resource to vendor or copy from cache to vendor
|
||||
void get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const;
|
||||
void get_or_copy_missing_resource(const GUI::ArchiveRepository& archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const;
|
||||
// checks existence and copies resource to vendor from cache to vendor
|
||||
void copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const;
|
||||
void update_index_db();
|
||||
};
|
||||
|
||||
@ -227,39 +229,39 @@ void PresetUpdater::priv::set_download_prefs(const AppConfig *app_config)
|
||||
}
|
||||
|
||||
// Downloads a file (http get operation). Cancels if the Updater is being destroyed.
|
||||
bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const
|
||||
{
|
||||
bool res = false;
|
||||
fs::path tmp_path = target_path;
|
||||
tmp_path += format(".%1%%2%", get_current_pid(), TMP_EXTENSION);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << format("Get: `%1%`\n\t-> `%2%`\n\tvia tmp path `%3%`",
|
||||
url,
|
||||
target_path.string(),
|
||||
tmp_path.string());
|
||||
|
||||
Http::get(url)
|
||||
.on_progress([](Http::Progress, bool &cancel) {
|
||||
if (cancel) { cancel = true; }
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned http_status) {
|
||||
(void)body;
|
||||
BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%",
|
||||
url,
|
||||
http_status,
|
||||
error);
|
||||
})
|
||||
.on_complete([&](std::string body, unsigned /* http_status */) {
|
||||
fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file.write(body.c_str(), body.size());
|
||||
file.close();
|
||||
fs::rename(tmp_path, target_path);
|
||||
res = true;
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
return res;
|
||||
}
|
||||
//bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const
|
||||
//{
|
||||
// bool res = false;
|
||||
// fs::path tmp_path = target_path;
|
||||
// tmp_path += format(".%1%%2%", get_current_pid(), TMP_EXTENSION);
|
||||
//
|
||||
// BOOST_LOG_TRIVIAL(info) << format("Get: `%1%`\n\t-> `%2%`\n\tvia tmp path `%3%`",
|
||||
// url,
|
||||
// target_path.string(),
|
||||
// tmp_path.string());
|
||||
//
|
||||
// Http::get(url)
|
||||
// .on_progress([](Http::Progress, bool &cancel) {
|
||||
// if (cancel) { cancel = true; }
|
||||
// })
|
||||
// .on_error([&](std::string body, std::string error, unsigned http_status) {
|
||||
// (void)body;
|
||||
// BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%",
|
||||
// url,
|
||||
// http_status,
|
||||
// error);
|
||||
// })
|
||||
// .on_complete([&](std::string body, unsigned /* http_status */) {
|
||||
// fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
// file.write(body.c_str(), body.size());
|
||||
// file.close();
|
||||
// fs::rename(tmp_path, target_path);
|
||||
// res = true;
|
||||
// })
|
||||
// .perform_sync();
|
||||
//
|
||||
// return res;
|
||||
//}
|
||||
|
||||
// Remove leftover paritally downloaded files, if any.
|
||||
void PresetUpdater::priv::prune_tmps() const
|
||||
@ -271,18 +273,15 @@ void PresetUpdater::priv::prune_tmps() const
|
||||
}
|
||||
}
|
||||
|
||||
void PresetUpdater::priv::get_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const
|
||||
// gets resource to cache/<vendor_name>/
|
||||
void PresetUpdater::priv::get_missing_resource(const GUI::ArchiveRepository& archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const
|
||||
{
|
||||
if (filename.empty() || vendor.empty())
|
||||
return;
|
||||
assert(!filename.empty() && !vendor.empty());
|
||||
//if (filename.empty() || vendor.empty()) {
|
||||
// BOOST_LOG_TRIVIAL(error) << "PresetUpdater::get_missing_resource - wrong input. vendor: "<< vendor << " filename: " << filename;
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
|
||||
!boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
|
||||
{
|
||||
throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url));
|
||||
}
|
||||
|
||||
std::string escaped_filename = escape_string_url(filename);
|
||||
const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_cache(cache_path / (vendor + "/" + filename));
|
||||
@ -299,24 +298,60 @@ void PresetUpdater::priv::get_missing_resource(const std::string& vendor, const
|
||||
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in cache folder. No need to download.";
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Resources check could not find " << vendor << " / " << filename << " bed texture. Downloading.";
|
||||
|
||||
const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url
|
||||
|
||||
if (!fs::exists(file_in_cache.parent_path()))
|
||||
fs::create_directory(file_in_cache.parent_path());
|
||||
|
||||
get_file(resource_url, file_in_cache);
|
||||
//std::string escaped_filename = escape_string_url(filename);
|
||||
const std::string resource_subpath = GUI::format("%1%/%2%",vendor, filename);
|
||||
archive.get_file(resource_subpath, file_in_cache, repository_id_from_ini);
|
||||
return;
|
||||
}
|
||||
// gets resource to vendor/<vendor_name>/
|
||||
void PresetUpdater::priv::get_or_copy_missing_resource(const GUI::ArchiveRepository& archive, const std::string& vendor, const std::string& filename, const std::string& repository_id_from_ini) const
|
||||
{
|
||||
assert(!filename.empty() && !vendor.empty() /*&& !repository_id_from_ini.empty()*/);
|
||||
//if (filename.empty() || vendor.empty()) {
|
||||
// BOOST_LOG_TRIVIAL(error) << "PresetUpdater::get_or_copy_missing_resource - wrong input. vendor: " << vendor << " filename: " << filename;
|
||||
// return;
|
||||
//}
|
||||
|
||||
void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const
|
||||
const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_cache(cache_path / (vendor + "/" + filename));
|
||||
// Already in vendor. No need to do anything.
|
||||
if (fs::exists(file_in_vendor)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in vendor folder. No need to download.";
|
||||
return;
|
||||
}
|
||||
// In resources dir since installation. No need to do anything.
|
||||
if (fs::exists(file_in_rsrc)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download.";
|
||||
return;
|
||||
}
|
||||
// Create vendor_name dir in vendor.
|
||||
if (!fs::exists(file_in_vendor.parent_path())) {
|
||||
fs::create_directory(file_in_vendor.parent_path());
|
||||
}
|
||||
// No file to copy. Download it to straight to the vendor dir.
|
||||
if (!fs::exists(file_in_cache)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename;
|
||||
|
||||
//std::string escaped_filename = escape_string_url(filename);
|
||||
const std::string resource_subpath = GUI::format("%1%/%2%", vendor, filename);
|
||||
archive.get_file(resource_subpath, file_in_vendor, repository_id_from_ini);
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "Copiing: " << file_in_cache << " to " << file_in_vendor;
|
||||
copy_file_fix(file_in_cache, file_in_vendor);
|
||||
}
|
||||
// gets resource to vendor/<vendor_name>/
|
||||
void PresetUpdater::priv::copy_missing_resource(const std::string& vendor, const std::string& filename, const std::string& url) const
|
||||
{
|
||||
if (filename.empty() || vendor.empty())
|
||||
return;
|
||||
|
||||
std::string escaped_filename = escape_string_url(filename);
|
||||
const fs::path file_in_vendor(vendor_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_rsrc(rsrc_path / (vendor + "/" + filename));
|
||||
const fs::path file_in_cache(cache_path / (vendor + "/" + filename));
|
||||
@ -329,20 +364,8 @@ void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor
|
||||
BOOST_LOG_TRIVIAL(info) << "Resource " << vendor << " / " << filename << " found in resources folder. No need to download.";
|
||||
return;
|
||||
}
|
||||
if (!fs::exists(file_in_cache)) { // No file to copy. Download it to straight to the vendor dir.
|
||||
if (!boost::starts_with(url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
|
||||
!boost::starts_with(url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
|
||||
{
|
||||
throw Slic3r::CriticalException(GUI::format("URL outside prusa3d.com network: %1%", url));
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Downloading resources missing in cache directory: " << vendor << " / " << filename;
|
||||
|
||||
const auto resource_url = format("%1%%2%%3%", url, url.back() == '/' ? "" : "/", escaped_filename); // vendor should already be in url
|
||||
|
||||
if (!fs::exists(file_in_vendor.parent_path()))
|
||||
fs::create_directory(file_in_vendor.parent_path());
|
||||
|
||||
get_file(resource_url, file_in_vendor);
|
||||
if (!fs::exists(file_in_cache)) { // No file to copy. Bad!
|
||||
BOOST_LOG_TRIVIAL(error) << "Resource " << vendor << " / " << filename << " not found!";
|
||||
return;
|
||||
}
|
||||
|
||||
@ -355,29 +378,15 @@ void PresetUpdater::priv::get_or_copy_missing_resource(const std::string& vendor
|
||||
|
||||
// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
|
||||
// Both are saved in cache.
|
||||
void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string& index_archive_url)
|
||||
void PresetUpdater::priv::sync_config(const VendorMap& vendors, const GUI::ArchiveRepository& archive_repository)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache";
|
||||
|
||||
if (!enabled_config_update) { return; }
|
||||
|
||||
// Download profiles archive zip
|
||||
// dk: Do we want to return here on error? Or skip archive dwnld and unzip and work with previous run state cache / vendor? I think return.
|
||||
// Any error here also doesnt show any info in UI. Do we want maybe notification?
|
||||
fs::path archive_path(cache_path / "vendor_indices.zip");
|
||||
if (index_archive_url.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Downloading profile archive failed - url has no value.";
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "Downloading vedor profiles archive zip from " << index_archive_url;
|
||||
//check if idx_url is leading to our site
|
||||
if (!boost::starts_with(index_archive_url, "http://files.prusa3d.com/wp-content/uploads/repository/") &&
|
||||
!boost::starts_with(index_archive_url, "https://files.prusa3d.com/wp-content/uploads/repository/"))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Unsafe url path for vedor profiles archive zip. Download is rejected.";
|
||||
return;
|
||||
}
|
||||
if (!get_file(index_archive_url, archive_path)) {
|
||||
if (!archive_repository.get_archive(archive_path)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Download of vedor profiles archive zip failed.";
|
||||
return;
|
||||
}
|
||||
@ -387,10 +396,11 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
|
||||
enum class VendorStatus
|
||||
{
|
||||
IN_ARCHIVE,
|
||||
IN_CACHE,
|
||||
NEW_VERSION,
|
||||
INSTALLED
|
||||
IN_ARCHIVE, // index was unzipped from archive to /cache/vendors/
|
||||
IN_CACHE, // vendor does exists in index_db, probably bc its ini is in resources and idx was copied to /cache/. It is not istalled, so new version of ini would be in /cache/vendors/
|
||||
INSTALLED, // vendor is installed, ini is in /vendors/ folder, no new version is available.
|
||||
NEW_VERSION, // vendor is installed, in /cache/ is new ini version waiting for installation.
|
||||
|
||||
};
|
||||
|
||||
std::vector<std::pair<std::string, VendorStatus>> vendors_with_status;
|
||||
@ -455,19 +465,18 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
}
|
||||
auto archive_it = std::find_if(vendors_with_status.begin(), vendors_with_status.end(),
|
||||
[&index](const std::pair<std::string, VendorStatus>& element) { return element.first == index.vendor(); });
|
||||
//assert(archive_it != vendors_with_status.end()); // this would mean there is a index for vendor that is missing in recently downloaded archive
|
||||
|
||||
const auto vendor_it = vendors.find(index.vendor());
|
||||
if (vendor_it == vendors.end()) {
|
||||
// Not installed vendor yet we need to check missing thumbnails (of new printers)
|
||||
BOOST_LOG_TRIVIAL(debug) << "No such vendor: " << index.vendor();
|
||||
if (archive_it != vendors_with_status.end())
|
||||
archive_it->second = VendorStatus::IN_CACHE;
|
||||
if (archive_it == vendors_with_status.end()) {
|
||||
// index for vendor that is missing in recently downloaded archive
|
||||
continue;
|
||||
}
|
||||
|
||||
if (archive_it != vendors_with_status.end())
|
||||
archive_it->second = VendorStatus::INSTALLED;
|
||||
const auto vendor_it = vendors.find(index.vendor());
|
||||
if (vendor_it == vendors.end()) {
|
||||
// Not installed vendor yet - later new version and missing resources will be checked.
|
||||
BOOST_LOG_TRIVIAL(debug) << "No such vendor: " << index.vendor();
|
||||
archive_it->second = VendorStatus::IN_CACHE;
|
||||
continue;
|
||||
}
|
||||
archive_it->second = VendorStatus::INSTALLED;
|
||||
|
||||
const VendorProfile &vendor = vendor_it->second;
|
||||
const std::string idx_path = (cache_path / (vendor.id + ".idx")).string();
|
||||
@ -518,15 +527,14 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
if (vendor.config_version >= recommended) { continue; }
|
||||
|
||||
// vendors that are checked here, doesnt need to be checked again later
|
||||
if (archive_it != vendors_with_status.end())
|
||||
archive_it->second = VendorStatus::NEW_VERSION;
|
||||
archive_it->second = VendorStatus::NEW_VERSION;
|
||||
|
||||
// Download recomended ini to cache
|
||||
const auto path_in_cache = cache_path / (vendor.id + ".ini");
|
||||
BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name;
|
||||
const auto bundle_url = format("%1%/%2%.ini", vendor.config_update_url, recommended.to_string());
|
||||
const auto bundle_path = cache_path / (vendor.id + ".ini");
|
||||
if (!get_file(bundle_url, bundle_path))
|
||||
const std::string source_subpath = GUI::format("%1%/%2%.ini", vendor.id, recommended.to_string());
|
||||
const fs::path bundle_path = cache_path / (vendor.id + ".ini");
|
||||
if (!archive_repository.get_file(source_subpath, bundle_path, vendor.repo_id))
|
||||
continue;
|
||||
if (cancel)
|
||||
return;
|
||||
@ -545,7 +553,7 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
if (! res.empty()) {
|
||||
try
|
||||
{
|
||||
get_missing_resource(vp.id, res, vendor.config_update_url);
|
||||
get_missing_resource(archive_repository, vp.id, res, vendor.repo_id);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@ -558,8 +566,8 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
}
|
||||
}
|
||||
}
|
||||
// Download missing thumbnails for not-installed vendors.
|
||||
//for (const std::string& vendor : vendors_only_in_archive)
|
||||
// Now status of each vendor is already decided.
|
||||
// Download missing for non-installed vendors.
|
||||
for (const std::pair<std::string, VendorStatus >& vendor : vendors_with_status) {
|
||||
if (vendor.second == VendorStatus::IN_ARCHIVE) {
|
||||
// index in archive and not in cache and not installed vendor
|
||||
@ -583,25 +591,30 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
}
|
||||
const auto recommended = recommended_it->config_version;
|
||||
if (!fs::exists(ini_path_in_archive)){
|
||||
// Download recommneded to vendor - we do not have any existing ini file so we have to use hardcoded url.
|
||||
const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url();
|
||||
const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string());
|
||||
if (!get_file(bundle_url, ini_path_in_archive))
|
||||
// Download recommneded to vendor - we do not have any existing ini file so we have to use archive url.
|
||||
const std::string source_subpath = GUI::format("%1%/%2%.ini", vendor.first, recommended.to_string());
|
||||
if (!archive_repository.get_ini_no_id(source_subpath, ini_path_in_archive))
|
||||
continue;
|
||||
} else {
|
||||
// check existing ini version
|
||||
// then download recommneded to vendor if needed
|
||||
// then download recommended to vendor if needed
|
||||
VendorProfile vp;
|
||||
try {
|
||||
vp = VendorProfile::from_ini(ini_path_in_archive, true);
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_archive, e.what());
|
||||
// Delete the file
|
||||
boost::system::error_code ec;
|
||||
fs::remove(ini_path_in_archive, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Failed to delete file: %1%", ec.message());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (vp.config_version != recommended) {
|
||||
const std::string fixed_url = GUI::wxGetApp().app_config->profile_folder_url();
|
||||
const auto bundle_url = format("%1%/%2%/%3%.ini", fixed_url, vendor.first, recommended.to_string());
|
||||
if (!get_file(bundle_url, ini_path_in_archive))
|
||||
// Take url from existing ini. This way we prevent downloading files from multiple sources.
|
||||
const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended.to_string());
|
||||
if (!archive_repository.get_file(source_subpath, ini_path_in_archive, vp.repo_id))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -618,7 +631,7 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
if (!model.thumbnail.empty()) {
|
||||
try
|
||||
{
|
||||
get_missing_resource(vp.id, model.thumbnail, vp.config_update_url);
|
||||
get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@ -690,11 +703,12 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Corrupted profile file for vendor %1% at %2%, message: %3%", vendor.first, ini_path_in_rsrc, e.what());
|
||||
// This means resources are corrupted.
|
||||
continue;
|
||||
}
|
||||
const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string());
|
||||
if (!get_file(bundle_url, ini_path_in_archive)) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_rsrc);
|
||||
const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended_archive.to_string());
|
||||
if (!archive_repository.get_file(source_subpath, ini_path_in_archive, vp.repo_id)) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Failed to get new vendor .ini file when checking missing resources: %1%", ini_path_in_archive.string());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@ -709,8 +723,8 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
continue;
|
||||
}
|
||||
if (vp.config_version != recommended_archive) {
|
||||
const auto bundle_url = format("%1%/%2%.ini", vp.config_update_url, recommended_archive.to_string());
|
||||
if (!get_file(bundle_url, ini_path_in_archive)) {
|
||||
const std::string source_subpath = GUI::format("%1%/%2%.ini", vp.id, recommended_archive.to_string());
|
||||
if (!archive_repository.get_file(source_subpath, ini_path_in_archive, vp.repo_id)) {
|
||||
BOOST_LOG_TRIVIAL(error) << format("Failed to open vendor .ini file when checking missing resources: %1%", ini_path_in_archive);
|
||||
continue;
|
||||
}
|
||||
@ -734,7 +748,7 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
if (!model.thumbnail.empty()) {
|
||||
try
|
||||
{
|
||||
get_missing_resource(vp.id, model.thumbnail, vp.config_update_url);
|
||||
get_missing_resource(archive_repository, vp.id, model.thumbnail, vp.repo_id);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@ -764,7 +778,7 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors, const std::string
|
||||
if (!model.thumbnail.empty()) {
|
||||
try
|
||||
{
|
||||
get_or_copy_missing_resource(vp.id, res, vp.config_update_url);
|
||||
get_or_copy_missing_resource(archive_repository, vp.id, res, vp.repo_id);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@ -1089,7 +1103,7 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
|
||||
continue;
|
||||
try
|
||||
{
|
||||
get_or_copy_missing_resource(vp.id, resource, vp.config_update_url);
|
||||
copy_missing_resource(vp.id, resource, vp.config_update_url);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@ -1128,20 +1142,16 @@ PresetUpdater::~PresetUpdater()
|
||||
}
|
||||
}
|
||||
|
||||
void PresetUpdater::sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler)
|
||||
void PresetUpdater::sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler, const ArchiveRepositoryVector& repositories)
|
||||
{
|
||||
p->set_download_prefs(GUI::wxGetApp().app_config);
|
||||
if (!p->enabled_version_check && !p->enabled_config_update) { return; }
|
||||
|
||||
// Copy the whole vendors data for use in the background thread
|
||||
// Unfortunatelly as of C++11, it needs to be copied again
|
||||
// into the closure (but perhaps the compiler can elide this).
|
||||
VendorMap vendors = preset_bundle->vendors;
|
||||
std::string index_archive_url = GUI::wxGetApp().app_config->index_archive_url();
|
||||
|
||||
p->thread = std::thread([this, vendors, index_archive_url, evt_handler]() {
|
||||
if (!p->enabled_config_update) { return; }
|
||||
|
||||
p->thread = std::thread([this, &vendors = preset_bundle->vendors, &repositories, evt_handler]() {
|
||||
this->p->prune_tmps();
|
||||
this->p->sync_config(std::move(vendors), index_archive_url);
|
||||
for(const auto& archive : repositories) {
|
||||
this->p->sync_config(vendors, *archive);
|
||||
}
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CONFIG_UPDATER_SYNC_DONE);
|
||||
evt_handler->QueueEvent(evt);
|
||||
});
|
||||
@ -1155,6 +1165,20 @@ void PresetUpdater::cancel_sync()
|
||||
p->cancel = true;
|
||||
p->thread.join();
|
||||
}
|
||||
p->cancel = false;
|
||||
}
|
||||
|
||||
void PresetUpdater::sync_blocking(const PresetBundle* preset_bundle, wxEvtHandler* evt_handler, const ArchiveRepositoryVector& repositories)
|
||||
{
|
||||
p->set_download_prefs(GUI::wxGetApp().app_config);
|
||||
if (!p->enabled_config_update) { return; }
|
||||
|
||||
this->p->prune_tmps();
|
||||
for (const auto& archive : repositories) {
|
||||
this->p->sync_config(preset_bundle->vendors, *archive);
|
||||
}
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CONFIG_UPDATER_SYNC_DONE);
|
||||
evt_handler->QueueEvent(evt);
|
||||
}
|
||||
|
||||
void PresetUpdater::slic3r_update_notify()
|
||||
@ -1488,5 +1512,19 @@ void PresetUpdater::update_index_db()
|
||||
{
|
||||
p->update_index_db();
|
||||
}
|
||||
void PresetUpdater::add_additional_archive(const std::string& archive_url, const std::string& download_url)
|
||||
{
|
||||
if (std::find_if(m_additional_archives.begin(), m_additional_archives.end(), [archive_url](const std::pair<std::string, std::string>& it) { return it.first == archive_url; }) == m_additional_archives.end()) {
|
||||
m_additional_archives.emplace_back(archive_url, download_url);
|
||||
}
|
||||
}
|
||||
|
||||
void PresetUpdater::add_additional_archives(const std::vector<std::pair<std::string, std::string>>& archives)
|
||||
{
|
||||
for (const auto& pair : archives) {
|
||||
add_additional_archive(pair.first, pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
#ifndef slic3r_PresetUpdate_hpp_
|
||||
#define slic3r_PresetUpdate_hpp_
|
||||
|
||||
#include "slic3r/GUI/PresetArchiveDatabase.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
@ -17,6 +19,8 @@ class AppConfig;
|
||||
class PresetBundle;
|
||||
class Semver;
|
||||
|
||||
typedef std::vector<std::unique_ptr<const GUI::ArchiveRepository>> ArchiveRepositoryVector;
|
||||
|
||||
static constexpr const int SLIC3R_VERSION_BODY_MAX = 256;
|
||||
|
||||
class PresetUpdater
|
||||
@ -30,9 +34,11 @@ public:
|
||||
~PresetUpdater();
|
||||
|
||||
// If either version check or config updating is enabled, get the appropriate data in the background and cache it.
|
||||
void sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler);
|
||||
void sync(const PresetBundle *preset_bundle, wxEvtHandler* evt_handler, const ArchiveRepositoryVector& repositories);
|
||||
void cancel_sync();
|
||||
|
||||
void sync_blocking(const PresetBundle* preset_bundle, wxEvtHandler* evt_handler, const ArchiveRepositoryVector& repositories);
|
||||
|
||||
// If version check is enabled, check if chaced online slic3r version is newer, notify if so.
|
||||
void slic3r_update_notify();
|
||||
|
||||
@ -67,9 +73,13 @@ public:
|
||||
|
||||
bool version_check_enabled() const;
|
||||
|
||||
void add_additional_archive(const std::string& archive_url, const std::string& download_url);
|
||||
void add_additional_archives(const std::vector<std::pair<std::string, std::string>>& archives);
|
||||
private:
|
||||
struct priv;
|
||||
std::unique_ptr<priv> p;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> m_additional_archives;
|
||||
};
|
||||
|
||||
//wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);
|
||||
|
Loading…
x
Reference in New Issue
Block a user