diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index 5234cded6b..64cc8e18e8 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -140,6 +140,18 @@ Downloader::Downloader() Bind(EVT_DWNLDR_FILE_CANCELED, &Downloader::on_canceled, this); } +namespace { +bool is_any_subdomain(const std::string& url, const std::vector& subdomains) +{ + for (const std::string& sub : subdomains) + { + if(FileGet::is_subdomain(url, sub)) + return true; + } + return false; +} + +} void Downloader::start_download(const std::string& full_url) { assert(m_initialized); @@ -156,14 +168,14 @@ void Downloader::start_download(const std::string& full_url) size_t id = get_next_id(); - if (!boost::starts_with(escaped_url, "https://") || !FileGet::is_subdomain(escaped_url, "printables.com")) { - std::string msg = format(_L("Download won't start. Download URL doesn't point to https://printables.com : %1%"), escaped_url); + if (!boost::starts_with(escaped_url, "https://") || !is_any_subdomain(escaped_url, {"printables.com", "thingiverse.com"})) { + std::string msg = format(_L("Download won't start. Download URL doesn't point to allowed subdomains : %1%"), escaped_url); BOOST_LOG_TRIVIAL(error) << msg; NotificationManager* ntf_mngr = wxGetApp().notification_manager(); ntf_mngr->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, msg); return; } - + m_downloads.emplace_back(std::make_unique(id, std::move(escaped_url), this, m_dest_folder, true)); NotificationManager* ntf_mngr = wxGetApp().notification_manager(); ntf_mngr->push_download_URL_progress_notification(id, m_downloads.back()->get_filename(), std::bind(&Downloader::user_action_callback, this, std::placeholders::_1, std::placeholders::_2)); @@ -202,7 +214,7 @@ void Downloader::on_progress(wxCommandEvent& event) float percent = (float)std::stoi(into_u8(event.GetString())) / 100.f; //BOOST_LOG_TRIVIAL(error) << "progress " << id << ": " << percent; NotificationManager* ntf_mngr = wxGetApp().notification_manager(); - BOOST_LOG_TRIVIAL(trace) << "Download "<< id << ": " << percent; + //BOOST_LOG_TRIVIAL(trace) << "Download "<< id << ": " << percent; ntf_mngr->set_download_URL_progress(id, percent); } void Downloader::on_error(wxCommandEvent& event) @@ -250,7 +262,9 @@ bool Downloader::user_action_callback(DownloaderUserAction action, int id) void Downloader::on_name_change(wxCommandEvent& event) { - + size_t id = event.GetInt(); + NotificationManager* ntf_mngr = wxGetApp().notification_manager(); + ntf_mngr->set_download_URL_filename(id, into_u8(event.GetString())); } void Downloader::on_paused(wxCommandEvent& event) diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp index 7de1996e90..c311736e8e 100644 --- a/src/slic3r/GUI/DownloaderFileGet.cpp +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "format.hpp" #include "GUI.hpp" @@ -68,6 +69,26 @@ unsigned get_current_pid() return ::getpid(); #endif } + +std::string extract_filename_from_header(const std::string& headers) { + // Split the headers into lines + std::istringstream header_stream(headers); + std::string line; + + while (std::getline(header_stream, line)) { + if (line.find("content-disposition") != std::string::npos) { + // Apply regex to extract filename from the content-disposition line + std::regex filename_regex("filename\\s*=\\s*\"([^\"]+)\"", std::regex::icase); + std::smatch match; + + if (std::regex_search(line, match, filename_regex) && match.size() > 1) { + return match.str(1); + } + } + } + + return {}; +} } // int = DOWNLOAD ID; string = file path @@ -111,6 +132,8 @@ FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEv , m_dest_folder(dest_folder) , m_load_after(load_after) { + // Prevent ':' in filename + m_filename.erase(std::remove(m_filename.begin(), m_filename.end(), ':'), m_filename.end()); } void FileGet::priv::get_perform() @@ -129,7 +152,7 @@ void FileGet::priv::get_perform() std::string extension = dest_path.extension().string(); std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size()); std::string final_filename = just_filename; - // Find unsed filename + // Find unused filename try { size_t version = 0; while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download"))) @@ -154,7 +177,6 @@ void FileGet::priv::get_perform() } m_filename = final_filename + extension; - m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download"); wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE); @@ -247,13 +269,48 @@ void FileGet::priv::get_perform() m_written = written_previously + written_this_session; } wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS); - int percent_total = (written_previously + progress.dlnow) * 100 / m_absolute_size; + int percent_total = m_absolute_size == 0 ? 0 : (written_previously + progress.dlnow) * 100 / m_absolute_size; evt->SetString(std::to_string(percent_total)); evt->SetInt(m_id); m_evt_handler->QueueEvent(evt); } }) + .on_headers([&](const std::string& headers) { + // we are looking for content-disposition header in response, to use it as correct filename + std::string new_filename = extract_filename_from_header(headers); + if (new_filename.empty()) { + return; + } + // Find unused filename + boost::filesystem::path temp_dest_path = m_dest_folder / new_filename; + std::string extension = temp_dest_path.extension().string(); + std::string just_filename = new_filename.substr(0, new_filename.size() - extension.size()); + std::string final_filename = just_filename; + try { + size_t version = 0; + while (boost::filesystem::exists(m_dest_folder / (final_filename + extension))) + { + ++version; + if (version > 999) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Failed to find suitable filename. Last name: %1%." , (m_dest_folder / (final_filename + extension)).string()); + return; + } + final_filename = GUI::format("%1%(%2%)", just_filename, std::to_string(version)); + } + } catch (const boost::filesystem::filesystem_error& e) + { + BOOST_LOG_TRIVIAL(error) << "Failed to resolved filename from headers."; + return; + } + m_filename = final_filename + extension; + dest_path = m_dest_folder / m_filename; + + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE); + evt->SetString(from_u8(m_filename)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + }) .on_error([&](std::string body, std::string error, unsigned http_status) { if (file != NULL) fclose(file); @@ -268,19 +325,31 @@ void FileGet::priv::get_perform() .on_complete([&](std::string body, unsigned /* http_status */) { try { + // If server is not sending Content-Length header, the progress function does not write all data to file. + // We need to write it now. + if (written_this_session < body.size()) { + std::string part_for_write = body.substr(written_this_session); + fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); + } fclose(file); boost::filesystem::rename(m_tmp_path, dest_path); } - catch (const std::exception& /*e*/) + catch (const std::exception& e) { //TODO: report? //error_message = GUI::format("Failed to write and move %1% to %2%", tmp_path, dest_path); wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR); - evt->SetString("Failed to write and move."); + evt->SetString(GUI::format("Failed to write and move %1% to %2%", m_tmp_path, dest_path)); evt->SetInt(m_id); m_evt_handler->QueueEvent(evt); return; } + wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS); + int percent_total = 100; + evt->SetString(std::to_string(percent_total)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + DownloadEventData event_data = {m_id, dest_path.wstring(), m_load_after}; wxQueueEvent(m_evt_handler, new Event(EVT_DWNLDR_FILE_COMPLETE, event_data)); }) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 4839e754b4..08650a9bbf 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1119,6 +1119,11 @@ void NotificationManager::URLDownloadWithPrintablesLinkNotification::render_text //------URLDownloadNotification---------------- +void NotificationManager::URLDownloadNotification::set_filename(const std::string& filename_line) +{ + m_text1 = filename_line; + init(); +} void NotificationManager::URLDownloadNotification::render_close_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) { @@ -2624,6 +2629,19 @@ void NotificationManager::set_download_URL_error(size_t id, const std::string& t } } } +void NotificationManager::set_download_URL_filename(size_t id, const std::string& filename) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::URLDownload) { + URLDownloadNotification* ntf = dynamic_cast(notification.get()); + if (ntf->get_download_id() != id) + continue; + ntf->set_filename(_u8L("Download") + ": " + filename); + wxGetApp().plater()->get_current_canvas3D()->schedule_extra_frame(0); + return; + } + } +} void NotificationManager::init_slicing_progress_notification(std::function cancel_callback) { diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 96e49f162b..0452a97427 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -258,6 +258,7 @@ public: void set_download_URL_paused(size_t id); void set_download_URL_canceled(size_t id); void set_download_URL_error(size_t id, const std::string& text); + void set_download_URL_filename(size_t id, const std::string& filename); // slicing progress void init_slicing_progress_notification(std::function cancel_callback); void set_slicing_progress_began(); @@ -561,6 +562,7 @@ private: void set_paused(bool paused) { m_download_paused = paused; } void set_error_message(const std::string& message) { m_error_message = message; } bool compare_text(const std::string& text) const override { return false; }; + void set_filename(const std::string& filename_line); protected: void render_close_button(const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) override; diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index de2785bc78..c00d93682c 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -137,12 +137,14 @@ struct Http::priv Http::ProgressFn progressfn; Http::IPResolveFn ipresolvefn; Http::RetryFn retryfn; + Http::HeadersFn headersfn; priv(const std::string &url); ~priv(); static bool ca_file_supported(::CURL *curl); static size_t writecb(void *data, size_t size, size_t nmemb, void *userp); + static size_t headercb(void *data, size_t size, size_t nmemb, void *userp); static int xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); static int xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow); static size_t form_file_read_cb(char *buffer, size_t size, size_t nitems, void *userp); @@ -230,6 +232,14 @@ size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp) return realsize; } +size_t Http::priv::headercb(void *data, size_t size, size_t nmemb, void *userp) +{ + std::string header(reinterpret_cast(data), size * nmemb); + std::string *header_data = static_cast(userp); + header_data->append(header); + return size * nmemb; +} + int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { auto self = static_cast(userp); @@ -377,6 +387,10 @@ void Http::priv::http_perform(const HttpRetryOpt& retry_opts) ::curl_easy_setopt(curl, CURLOPT_VERBOSE, get_logging_level() >= 5); + std::string header_data; + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headercb); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &header_data); + if (headerlist != nullptr) { ::curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist); } @@ -444,6 +458,9 @@ void Http::priv::http_perform(const HttpRetryOpt& retry_opts) if (http_status >= 400) { if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); } } else { + if (headersfn && !header_data.empty()) { + headersfn(header_data); + } if (completefn) { completefn(std::move(buffer), http_status); } if (ipresolvefn) { char* ct; @@ -657,6 +674,12 @@ Http& Http::on_retry(RetryFn fn) return *this; } +Http& Http::on_headers(HeadersFn fn) +{ + if (p) { p->headersfn = std::move(fn); } + return *this; +} + Http& Http::cookie_file(const std::string& file_path) { if (p) { diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index f5120bbeb7..bbd2572898 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -67,6 +67,8 @@ public: // typedef std::function RetryFn; + typedef std::function HeadersFn; + Http(Http &&other); // Note: strings are expected to be UTF-8-encoded @@ -147,6 +149,8 @@ public: Http& on_retry(RetryFn fn); + Http& on_headers(HeadersFn fn); + Http& cookie_file(const std::string& file_path); Http& cookie_jar(const std::string& file_path); Http& set_referer(const std::string& referer);