diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index ea1024101b..ad7b3cb9ee 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -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; } diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index e3df0c24ba..830c0b298e 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -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 { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 65ab160b91..9b3303d5e8 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -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 diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index dc99fb1741..caccd2a711 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.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(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 diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index d37c659c51..e79fe849da 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -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 m_removable_drive_manager; - - std::unique_ptr m_imgui; - std::unique_ptr m_printhost_job_queue; - std::unique_ptr m_other_instance_message_handler; - std::unique_ptr m_app_updater; - std::unique_ptr m_single_instance_checker; - std::unique_ptr m_downloader; + std::unique_ptr m_removable_drive_manager; + std::unique_ptr m_imgui; + std::unique_ptr m_printhost_job_queue; + std::unique_ptr m_other_instance_message_handler; + std::unique_ptr m_app_updater; + std::unique_ptr m_single_instance_checker; + std::unique_ptr 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; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 56b66db03b..92e9287b0c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -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 notification_manager; std::unique_ptr user_account; + std::unique_ptr 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(q)) , user_account(std::make_unique(q, wxGetApp().app_config, wxGetApp().get_instance_hash_string())) + , preset_archive_database(std::make_unique(wxGetApp().app_config, q)) , m_worker{q, std::make_unique(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& 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(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d841d17378..7d21846724 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -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; diff --git a/src/slic3r/GUI/PresetArchiveDatabase.cpp b/src/slic3r/GUI/PresetArchiveDatabase.cpp new file mode 100644 index 0000000000..b8ea8c7ab2 --- /dev/null +++ b/src/slic3r/GUI/PresetArchiveDatabase.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +namespace pt = boost::property_tree; +namespace fs = boost::filesystem; +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(EVT_PRESET_ARCHIVE_DATABASE_SYNC_DONE, Event); + +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("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("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("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("index_url"); index_url) { + data.index_url = *index_url; + } + if (const auto description = ptree.get_optional("description"); description) { + data.description = *description; + } + if (const auto visibility = ptree.get_optional("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& 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(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(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 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(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 \ No newline at end of file diff --git a/src/slic3r/GUI/PresetArchiveDatabase.hpp b/src/slic3r/GUI/PresetArchiveDatabase.hpp new file mode 100644 index 0000000000..5b52c0e897 --- /dev/null +++ b/src/slic3r/GUI/PresetArchiveDatabase.hpp @@ -0,0 +1,120 @@ +#ifndef slic3r_PresetArchiveDatabase_hpp_ +#define slic3r_PresetArchiveDatabase_hpp_ + +#include "Event.hpp" + +#include +#include +#include + +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); + + +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> 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 m_local_archive_adresses; + std::string m_token; +}; + +}} // Slic3r::GUI + +#endif // PresetArchiveDatabase \ No newline at end of file diff --git a/src/slic3r/GUI/UserAccount.cpp b/src/slic3r/GUI/UserAccount.cpp index 8102c810f6..f8a5c6ac7c 100644 --- a/src/slic3r/GUI/UserAccount.cpp +++ b/src/slic3r/GUI/UserAccount.cpp @@ -20,7 +20,8 @@ namespace GUI { UserAccount::UserAccount(wxEvtHandler* evt_handler, AppConfig* app_config, const std::string& instance_hash) : m_communication(std::make_unique(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(); diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 2796ae525e..64fb93b3fb 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -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(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() diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index e465b6e17b..5e3ca273ad 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -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 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 m_action_queue; - std::queue m_priority_action_queue; + std::queue m_action_queue; + std::queue m_priority_action_queue; std::map> m_actions; wxEvtHandler* p_evt_handler; diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index eecc77de56..f14a8accf7 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -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// +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// +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// +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> 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& 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& 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& 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>& archives) +{ + for (const auto& pair : archives) { + add_additional_archive(pair.first, pair.second); + } +} } + diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index dd0d12ff3f..aa86be0bfb 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -5,6 +5,8 @@ #ifndef slic3r_PresetUpdate_hpp_ #define slic3r_PresetUpdate_hpp_ +#include "slic3r/GUI/PresetArchiveDatabase.hpp" + #include #include @@ -17,6 +19,8 @@ class AppConfig; class PresetBundle; class Semver; +typedef std::vector> 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>& archives); private: struct priv; std::unique_ptr p; + + std::vector> m_additional_archives; }; //wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);