mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 07:11:10 +00:00
* Sanitize Server-Supplied Download File Names The URL downloader used the file name from the Content-Disposition header as given, without the cleaning and unused-name search applied to the URL-derived name. Reduce the header name to a sanitized base name with the new sanitize_file_basename helper, which splits on both path separators and rejects names made only of dots and spaces. Run the result through the same unused-name search as the URL-derived name, now shared in find_unused_filename, and fall back to the URL-derived name when nothing usable remains. * Sanitize Download Names Before Choosing an Unused One The unused-name search probed the name as given and sanitized the result afterwards, so a name whose special characters are replaced could be mapped onto a file that already exists. Move the search into libslic3r as find_unused_filename, sanitize first and probe the name that is actually written. The download marker path is shared through download_marker_path. Restore the last tried name in the error reported when no free name is found, and cover the search with unit tests. * Keep Downloads on an Unused Name Until They Complete When the server supplied the name, the download marker stayed under the URL-derived name, so the adopted name was not reserved against other downloads. The final rename also replaced any file that took the name while the download ran. Move the marker to the adopted name before any data is written, and check the name again right before the final rename, picking the next free name if it is taken by then. * Sanitize the File Name of Model Import Links The model import took the file name from the link as given and only avoided an existing file with a substring match on the folder listing. Reduce the name to a sanitized base name, falling back to untitled.3mf, choose the name with the shared unused-name search, and check it again before the final rename. * Handle Filesystem Errors When Finishing a Model Import Download Choosing the final name and moving the downloaded project into place could throw from inside the download callback. Any such error now removes the temporary file and reports the existing import failure message.
441 lines
13 KiB
C++
441 lines
13 KiB
C++
#include "DownloaderFileGet.hpp"
|
|
|
|
#include <thread>
|
|
#include <curl/curl.h>
|
|
#include <boost/nowide/fstream.hpp>
|
|
#include <boost/nowide/convert.hpp>
|
|
#include <boost/format.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <iostream>
|
|
|
|
#include "format.hpp"
|
|
#include "GUI.hpp"
|
|
#include "I18N.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
|
|
namespace Slic3r {
|
|
namespace GUI {
|
|
|
|
const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024;
|
|
const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024;
|
|
|
|
std::string FileGet::escape_url(const std::string& unescaped)
|
|
{
|
|
std::string ret_val;
|
|
CURL* curl = curl_easy_init();
|
|
if (curl) {
|
|
int decodelen;
|
|
char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen);
|
|
if (decoded) {
|
|
ret_val = std::string(decoded);
|
|
curl_free(decoded);
|
|
}
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
return ret_val;
|
|
}
|
|
bool FileGet::is_subdomain(const std::string& url, const std::string& domain)
|
|
{
|
|
// domain should be f.e. printables.com (.com including)
|
|
char* host;
|
|
std::string host_string;
|
|
CURLUcode rc;
|
|
CURLU* curl = curl_url();
|
|
if (!curl) {
|
|
BOOST_LOG_TRIVIAL(error) << "Failed to init Curl library in function is_domain.";
|
|
return false;
|
|
}
|
|
rc = curl_url_set(curl, CURLUPART_URL, url.c_str(), 0);
|
|
if (rc != CURLUE_OK) {
|
|
curl_url_cleanup(curl);
|
|
return false;
|
|
}
|
|
rc = curl_url_get(curl, CURLUPART_HOST, &host, 0);
|
|
if (rc != CURLUE_OK || !host) {
|
|
curl_url_cleanup(curl);
|
|
return false;
|
|
}
|
|
host_string = std::string(host);
|
|
curl_free(host);
|
|
// now host should be subdomain.domain or just domain
|
|
if (domain == host_string) {
|
|
curl_url_cleanup(curl);
|
|
return true;
|
|
}
|
|
if(boost::ends_with(host_string, "." + domain)) {
|
|
curl_url_cleanup(curl);
|
|
return true;
|
|
}
|
|
curl_url_cleanup(curl);
|
|
return false;
|
|
}
|
|
|
|
// int = DOWNLOAD ID; string = file path
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent);
|
|
// int = DOWNLOAD ID; string = error msg
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_ERROR, wxCommandEvent);
|
|
// int = DOWNLOAD ID; string = progress percent
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_PROGRESS, wxCommandEvent);
|
|
// int = DOWNLOAD ID; string = name
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_NAME_CHANGE, wxCommandEvent);
|
|
// int = DOWNLOAD ID;
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_PAUSED, wxCommandEvent);
|
|
// int = DOWNLOAD ID;
|
|
wxDEFINE_EVENT(EVT_DWNLDR_FILE_CANCELED, wxCommandEvent);
|
|
|
|
struct FileGet::priv
|
|
{
|
|
const int m_id;
|
|
std::string m_url;
|
|
std::string m_filename;
|
|
std::thread m_io_thread;
|
|
wxEvtHandler* m_evt_handler;
|
|
boost::filesystem::path m_dest_folder;
|
|
boost::filesystem::path m_tmp_path; // path when ongoing download
|
|
std::atomic_bool m_cancel { false };
|
|
std::atomic_bool m_pause { false };
|
|
std::atomic_bool m_stopped { false }; // either canceled or paused - download is not running
|
|
size_t m_written { 0 };
|
|
size_t m_absolute_size { 0 };
|
|
priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder);
|
|
|
|
void get_perform();
|
|
};
|
|
|
|
FileGet::priv::priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder)
|
|
: m_id(ID)
|
|
, m_url(std::move(url))
|
|
, m_filename(filename)
|
|
, m_evt_handler(evt_handler)
|
|
, m_dest_folder(dest_folder)
|
|
{
|
|
}
|
|
std::string extract_remote_filename(const std::string& str) {
|
|
std::regex r("filename=\"([^\"]*)\"");
|
|
std::smatch match;
|
|
if (std::regex_search(str.begin(), str.end(), match, r)) {
|
|
return match.str(1);
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
void FileGet::priv::get_perform()
|
|
{
|
|
assert(m_evt_handler);
|
|
assert(!m_url.empty());
|
|
assert(!m_filename.empty());
|
|
assert(boost::filesystem::is_directory(m_dest_folder));
|
|
|
|
m_stopped = false;
|
|
|
|
// open dest file
|
|
std::string extension;
|
|
if (m_written == 0)
|
|
{
|
|
std::string final_filename;
|
|
bool found = false;
|
|
try {
|
|
found = find_unused_filename(m_dest_folder, m_filename, m_tmp_path, final_filename);
|
|
} catch (const boost::filesystem::filesystem_error& e)
|
|
{
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
|
evt->SetString(e.what());
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
}
|
|
if (!found) {
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
|
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / final_filename).string()));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
}
|
|
|
|
m_filename = final_filename;
|
|
extension = boost::filesystem::path(m_filename).extension().string();
|
|
|
|
m_tmp_path = download_marker_path(m_dest_folder, m_filename);
|
|
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
|
evt->SetString(boost::nowide::widen(m_filename));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
}
|
|
|
|
boost::filesystem::path dest_path;
|
|
if(!extension.empty())
|
|
dest_path = m_dest_folder / m_filename;
|
|
|
|
wxString temp_path_wstring(m_tmp_path.wstring());
|
|
|
|
//std::cout << "dest_path: " << dest_path.string() << std::endl;
|
|
//std::cout << "m_tmp_path: " << m_tmp_path.string() << std::endl;
|
|
|
|
BOOST_LOG_TRIVIAL(info) << GUI::format("Starting download from %1% to %2%. Temp path is %3%",m_url, dest_path, m_tmp_path);
|
|
|
|
FILE* file;
|
|
// open file for writting
|
|
if (m_written == 0)
|
|
file = fopen(temp_path_wstring.c_str(), "wb");
|
|
else
|
|
file = fopen(temp_path_wstring.c_str(), "ab");
|
|
|
|
//assert(file != NULL);
|
|
if (file == NULL) {
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
|
// TRN %1% = file path
|
|
evt->SetString(GUI::format_wxstr(_L("Can't create file at %1%"), temp_path_wstring));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
}
|
|
|
|
std:: string range_string = std::to_string(m_written) + "-";
|
|
|
|
size_t written_previously = m_written;
|
|
size_t written_this_session = 0;
|
|
Http::get(m_url)
|
|
.size_limit(DOWNLOAD_SIZE_LIMIT) //more?
|
|
.set_range(range_string)
|
|
.on_header_callback([&](std::string header) {
|
|
if(dest_path.empty()) {
|
|
std::string filename = extract_remote_filename(header);
|
|
if (!filename.empty()) {
|
|
// The name comes from the server: keep it inside the destination folder and never
|
|
// replace an existing file. Keep the current name if nothing usable remains.
|
|
filename = sanitize_file_basename(filename);
|
|
std::string unused;
|
|
try {
|
|
if (filename.empty() || !find_unused_filename(m_dest_folder, filename, m_tmp_path, unused))
|
|
unused.clear();
|
|
} catch (const boost::filesystem::filesystem_error&) {
|
|
unused.clear();
|
|
}
|
|
const boost::filesystem::path tmp_path = unused.empty() ? m_tmp_path : download_marker_path(m_dest_folder, unused);
|
|
if (tmp_path != m_tmp_path) {
|
|
// Move the marker to the adopted name so that other downloads see the name as taken.
|
|
// Only before anything is written, so that no downloaded data has to be carried over.
|
|
FILE* tmp_file = m_written == 0 ? fopen(wxString(tmp_path.wstring()).c_str(), "wb") : nullptr;
|
|
if (tmp_file != nullptr) {
|
|
fclose(file);
|
|
boost::system::error_code ec;
|
|
boost::filesystem::remove(m_tmp_path, ec);
|
|
file = tmp_file;
|
|
m_tmp_path = tmp_path;
|
|
} else
|
|
unused.clear();
|
|
}
|
|
if (!unused.empty())
|
|
m_filename = unused;
|
|
dest_path = m_dest_folder / m_filename;
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
|
evt->SetString(boost::nowide::widen(m_filename));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
}
|
|
}
|
|
})
|
|
.on_progress([&](Http::Progress progress, bool& cancel) {
|
|
// to prevent multiple calls into following ifs (m_cancel / m_pause)
|
|
if (m_stopped){
|
|
cancel = true;
|
|
return;
|
|
}
|
|
if (m_cancel) {
|
|
m_stopped = true;
|
|
fclose(file);
|
|
// remove canceled file
|
|
std::remove(m_tmp_path.string().c_str());
|
|
m_written = 0;
|
|
cancel = true;
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED);
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
// TODO: send canceled event?
|
|
}
|
|
if (m_pause) {
|
|
m_stopped = true;
|
|
fclose(file);
|
|
cancel = true;
|
|
if (m_written == 0)
|
|
std::remove(m_tmp_path.string().c_str());
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PAUSED);
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
}
|
|
|
|
if (m_absolute_size < progress.dltotal) {
|
|
m_absolute_size = progress.dltotal;
|
|
}
|
|
|
|
if (progress.dlnow != 0) {
|
|
if (progress.dlnow - written_this_session > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) {
|
|
try
|
|
{
|
|
std::string part_for_write = progress.buffer.substr(written_this_session, progress.dlnow);
|
|
fwrite(part_for_write.c_str(), 1, part_for_write.size(), file);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
// fclose(file); do it?
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
|
evt->SetString(e.what());
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
cancel = true;
|
|
return;
|
|
}
|
|
written_this_session = progress.dlnow;
|
|
m_written = written_previously + written_this_session;
|
|
}
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS);
|
|
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_error([&](std::string body, std::string error, unsigned http_status) {
|
|
if (file != NULL)
|
|
fclose(file);
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
|
|
if (!error.empty())
|
|
evt->SetString(GUI::from_u8(error));
|
|
else
|
|
evt->SetString(GUI::from_u8(body));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
})
|
|
.on_complete([&](std::string body, unsigned /* http_status */) {
|
|
|
|
// TODO: perform a body size check
|
|
//
|
|
//size_t body_size = body.size();
|
|
//if (body_size != expected_size) {
|
|
// return;
|
|
//}
|
|
try
|
|
{
|
|
// Orca: thingiverse need this
|
|
if (m_written < body.size())
|
|
{
|
|
// this code should never be entered. As there should be on_progress call after last bit downloaded.
|
|
std::string part_for_write = body.substr(m_written);
|
|
fwrite(part_for_write.c_str(), 1, part_for_write.size(), file);
|
|
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_PROGRESS);
|
|
evt->SetString(std::to_string(100));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
}
|
|
fclose(file);
|
|
// Another file may have taken the name while downloading.
|
|
if (!dest_path.empty() && boost::filesystem::exists(dest_path)) {
|
|
std::string unused;
|
|
if (!find_unused_filename(m_dest_folder, m_filename, m_tmp_path, unused))
|
|
throw std::runtime_error("No unused file name.");
|
|
m_filename = unused;
|
|
dest_path = m_dest_folder / m_filename;
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
|
|
evt->SetString(boost::nowide::widen(m_filename));
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
}
|
|
boost::filesystem::rename(m_tmp_path, dest_path);
|
|
}
|
|
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->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
return;
|
|
}
|
|
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_COMPLETE);
|
|
evt->SetString(dest_path.wstring());
|
|
evt->SetInt(m_id);
|
|
m_evt_handler->QueueEvent(evt);
|
|
})
|
|
.perform_sync();
|
|
|
|
}
|
|
|
|
FileGet::FileGet(int ID, std::string url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder)
|
|
: p(new priv(ID, std::move(url), filename, evt_handler, dest_folder))
|
|
{}
|
|
|
|
FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {}
|
|
|
|
FileGet::~FileGet()
|
|
{
|
|
if (p && p->m_io_thread.joinable()) {
|
|
p->m_cancel = true;
|
|
p->m_io_thread.join();
|
|
}
|
|
}
|
|
|
|
void FileGet::get()
|
|
{
|
|
assert(p);
|
|
if (p->m_io_thread.joinable()) {
|
|
// This will stop transfers being done by the thread, if any.
|
|
// Cancelling takes some time, but should complete soon enough.
|
|
p->m_cancel = true;
|
|
p->m_io_thread.join();
|
|
}
|
|
p->m_cancel = false;
|
|
p->m_pause = false;
|
|
p->m_io_thread = std::thread([this]() {
|
|
p->get_perform();
|
|
});
|
|
}
|
|
|
|
void FileGet::cancel()
|
|
{
|
|
if(p && p->m_stopped) {
|
|
if (p->m_io_thread.joinable()) {
|
|
p->m_cancel = true;
|
|
p->m_io_thread.join();
|
|
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_CANCELED);
|
|
evt->SetInt(p->m_id);
|
|
p->m_evt_handler->QueueEvent(evt);
|
|
}
|
|
}
|
|
|
|
if (p)
|
|
p->m_cancel = true;
|
|
|
|
}
|
|
|
|
void FileGet::pause()
|
|
{
|
|
if (p) {
|
|
p->m_pause = true;
|
|
}
|
|
}
|
|
void FileGet::resume()
|
|
{
|
|
assert(p);
|
|
if (p->m_io_thread.joinable()) {
|
|
// This will stop transfers being done by the thread, if any.
|
|
// Cancelling takes some time, but should complete soon enough.
|
|
p->m_cancel = true;
|
|
p->m_io_thread.join();
|
|
}
|
|
p->m_cancel = false;
|
|
p->m_pause = false;
|
|
p->m_io_thread = std::thread([this]() {
|
|
p->get_perform();
|
|
});
|
|
}
|
|
}
|
|
}
|