Merge branch 'dk_thingiverse'

This commit is contained in:
Lukas Matena 2025-04-07 17:46:42 +02:00
commit 575824443a
6 changed files with 140 additions and 10 deletions

View File

@ -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<std::string>& 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<Download>(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)

View File

@ -12,6 +12,7 @@
#include <boost/algorithm/string.hpp>
#include <boost/nowide/cstdio.hpp>
#include <iostream>
#include <regex>
#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<DownloadEventData>(EVT_DWNLDR_FILE_COMPLETE, event_data));
})

View File

@ -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<PopNotification>& notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::URLDownload) {
URLDownloadNotification* ntf = dynamic_cast<URLDownloadNotification*>(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<bool()> cancel_callback)
{

View File

@ -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<bool()> 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;

View File

@ -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<char*>(data), size * nmemb);
std::string *header_data = static_cast<std::string*>(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<priv*>(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) {

View File

@ -67,6 +67,8 @@ public:
//<bool - false if canceled(int - attempt number, unsigned - ms to next attempt, 0 if last)>
typedef std::function<bool(int, unsigned)> RetryFn;
typedef std::function<void(const std::string&)> 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);