diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 14c1db6cdc..85bd0f1bac 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -115,8 +115,8 @@ BackgroundSlicingProcess::BackgroundSlicingProcess() m_temp_output_path = temp_path.string(); } -BackgroundSlicingProcess::~BackgroundSlicingProcess() -{ +BackgroundSlicingProcess::~BackgroundSlicingProcess() +{ this->stop(); this->join_background_thread(); boost::nowide::remove(m_temp_output_path.c_str()); @@ -161,7 +161,7 @@ void BackgroundSlicingProcess::process_fff() m_print->process(); wxCommandEvent evt(m_event_slicing_completed_id); // Post the Slicing Finished message for the G-code viewer to update. - // Passing the timestamp + // Passing the timestamp evt.SetInt((int)(m_fff_print->step_state_with_timestamp(PrintStep::psSlicingFinished).timestamp)); wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt.Clone()); m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, [this](const ThumbnailsParams& params) { return this->render_thumbnails(params); }); @@ -261,7 +261,7 @@ void BackgroundSlicingProcess::thread_proc() if (m_print->cancel_status() != Print::CANCELED_INTERNAL) { // Only post the canceled event, if canceled by user. // Don't post the canceled event, if canceled from Print::apply(). - SlicingProcessCompletedEvent evt(m_event_finished_id, 0, + SlicingProcessCompletedEvent evt(m_event_finished_id, 0, (m_state == STATE_CANCELED) ? SlicingProcessCompletedEvent::Cancelled : exception ? SlicingProcessCompletedEvent::Error : SlicingProcessCompletedEvent::Finished, exception); wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt.Clone()); @@ -540,7 +540,7 @@ void BackgroundSlicingProcess::stop_internal() m_print->set_cancel_callback([](){}); } -// Execute task from background thread on the UI thread. Returns true if processed, false if cancelled. +// Execute task from background thread on the UI thread. Returns true if processed, false if cancelled. bool BackgroundSlicingProcess::execute_ui_task(std::function task) { bool running = false; @@ -613,7 +613,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn assert(m_print != nullptr); assert(config.opt_enum("printer_technology") == m_print->technology()); Print::ApplyStatus invalidated = m_print->apply(model, config); - if (physical_printer_config && + if (physical_printer_config && (m_print->physical_printer_config().diff(*physical_printer_config).size() != 0 || m_print->physical_printer_config().keys() != physical_printer_config->keys()) ) { m_print->physical_printer_config().clear(); m_print->physical_printer_config().apply(*physical_printer_config); @@ -646,7 +646,7 @@ void BackgroundSlicingProcess::set_task(const PrintBase::TaskParams ¶ms) // Set the output path of the G-code. void BackgroundSlicingProcess::schedule_export(const std::string &path, bool export_path_on_removable_media) -{ +{ assert(m_export_path.empty()); if (! m_export_path.empty()) return; @@ -691,17 +691,17 @@ void BackgroundSlicingProcess::reset_export() } bool BackgroundSlicingProcess::set_step_started(BackgroundSlicingProcessStep step) -{ +{ return m_step_state.set_started(step, m_print->state_mutex(), [this](){ this->throw_if_canceled(); }); } void BackgroundSlicingProcess::set_step_done(BackgroundSlicingProcessStep step) -{ +{ m_step_state.set_done(step, m_print->state_mutex(), [this](){ this->throw_if_canceled(); }); } bool BackgroundSlicingProcess::is_step_done(BackgroundSlicingProcessStep step) const -{ +{ return m_step_state.is_done(step, m_print->state_mutex()); } @@ -712,7 +712,7 @@ bool BackgroundSlicingProcess::invalidate_step(BackgroundSlicingProcessStep step } bool BackgroundSlicingProcess::invalidate_all_steps() -{ +{ return m_step_state.invalidate_all([this](){ this->stop_internal(); }); } @@ -796,7 +796,7 @@ void BackgroundSlicingProcess::prepare_upload() throw Slic3r::RuntimeError(_utf8(L("Copying of the temporary G-code to the output G-code failed"))); m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); // Make a copy of the source path, as run_post_process_scripts() is allowed to change it when making a copy of the source file - // (not here, but when the final target is a file). + // (not here, but when the final target is a file). std::string source_path_str = source_path.string(); std::string output_name_str = m_upload_job.upload_data.upload_path.string(); if (run_post_process_scripts(source_path_str, false, m_upload_job.printhost->get_name(), output_name_str, m_fff_print->full_print_config())) diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 762d92cf9a..ccfe31a172 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -638,7 +638,6 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->show_field("host_type"); m_optgroup->enable_field("print_host"); - m_optgroup->enable_field("print_host_webui"); m_optgroup->enable_field("printhost_cafile"); m_optgroup->enable_field("printhost_ssl_ignore_revoke"); if (m_printhost_cafile_browse_btn) @@ -648,21 +647,12 @@ void PhysicalPrinterDialog::update(bool printer_change) if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp) { const auto current_host = temp->GetValue(); - if (current_host == "https://simplyprint.io") { + if (current_host == "https://simplyprint.io" || current_host == "https://simplyprint.io/panel") { temp->SetValue(wxString()); } } } - if (Field* printhost_webui_field = m_optgroup->get_field("print_host_webui"); printhost_webui_field) { - if (wxTextCtrl* temp = dynamic_cast(printhost_webui_field)->text_ctrl(); temp) { - const auto current_host = temp->GetValue(); - if (current_host == "https://simplyprint.io/panel") { - temp->SetValue(wxString()); - } - } - } - if (opt && opt->value == htPrusaLink) - { + if (opt->value == htPrusaLink) { // PrusaConnect does NOT allow http digest m_optgroup->show_field("printhost_authorization_type"); AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value; m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index c94a609e1d..a1f9a8cf2d 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -97,7 +97,7 @@ class Sidebar : public wxPanel ObjectLayers* obj_layers(); wxScrolledWindow* scrolled_panel(); wxPanel* presets_panel(); - + ConfigOptionsGroup* og_freq_chng_params(PrinterTechnology tech); wxButton* get_wiping_dialog_button(); void update_objects_list_extruder_column(size_t extruders_count); @@ -267,7 +267,7 @@ class Plater: public wxPanel bool undo_redo_string_getter(const bool is_undo, int idx, const char** out_text); void undo_redo_topmost_string_getter(const bool is_undo, std::string& out_text); bool search_string_getter(int idx, const char** label, const char** tooltip); - // For the memory statistics. + // For the memory statistics. const Slic3r::UndoRedo::Stack& undo_redo_stack_main() const; void clear_undo_redo_stack_main(); // Enter / leave the Gizmos specific Undo / Redo stack. To be used by the SLA support point editing gizmo. @@ -291,14 +291,14 @@ class Plater: public wxPanel void set_project_filename(const wxString& filename); bool is_export_gcode_scheduled() const; - + const Selection& get_selection() const; int get_selected_object_idx(); bool is_single_full_object_selection() const; GLCanvas3D* canvas3D(); const GLCanvas3D * canvas3D() const; GLCanvas3D* get_current_canvas3D(); - + void arrange(); void find_new_position(const ModelInstancePtrs &instances); @@ -382,7 +382,7 @@ class Plater: public wxPanel void init_notification_manager(); void bring_instance_forward(); - + // ROII wrapper for suppressing the Undo / Redo snapshot to be taken. class SuppressSnapshots { diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index eb7308db6a..7915de9606 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -98,7 +98,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo } }); txt_filename->SetFocus(); - + if (post_actions.has(PrintHostPostUploadAction::StartPrint)) { auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print")); btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { @@ -116,7 +116,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo if (validate_path(txt_filename->GetValue())) { post_upload_action = PrintHostPostUploadAction::StartSimulation; EndDialog(wxID_OK); - } + } }); } diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 62461cdbe5..ee22801aaa 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -133,6 +133,13 @@ struct CurlGlobalInit std::unique_ptr CurlGlobalInit::instance; +struct form_file +{ + fs::ifstream ifs; + boost::filesystem::ifstream::off_type init_offset; + size_t content_length; +}; + struct Http::priv { enum { @@ -149,7 +156,7 @@ struct Http::priv std::string buffer; // Used for storing file streams added as multipart form parts // Using a deque here because unlike vector it doesn't ivalidate pointers on insertion - std::deque form_files; + std::deque form_files; std::string postfields; std::string error_buffer; // Used for CURLOPT_ERRORBUFFER size_t limit; @@ -173,7 +180,10 @@ struct Http::priv void set_timeout_connect(long timeout); void set_timeout_max(long timeout); - void form_add_file(const char *name, const fs::path &path, const char* filename); + void form_add_file(const char *name, const fs::path &path, const char* filename, boost::filesystem::ifstream::off_type offset, size_t length); + /* mime */ + void mime_form_add_text(const char* name, const char* value); + void mime_form_add_file(const char* name, const char* path); void set_post_body(const fs::path &path); void set_post_body(const std::string &body); void set_put_body(const fs::path &path); @@ -275,15 +285,27 @@ int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double size_t Http::priv::form_file_read_cb(char *buffer, size_t size, size_t nitems, void *userp) { - auto stream = reinterpret_cast(userp); + auto f = reinterpret_cast(userp); try { - stream->read(buffer, size * nitems); + size_t max_read_size = size * nitems; + if (f->content_length == 0) { + // Unlimited + f->ifs.read(buffer, max_read_size); + } else { + unsigned long long read_size = f->ifs.tellg() - f->init_offset; + if (read_size >= f->content_length) { + return 0; + } + + max_read_size = std::min(max_read_size, size_t(f->content_length - read_size)); + f->ifs.read(buffer, max_read_size); + } } catch (const std::exception &) { return CURL_READFUNC_ABORT; } - return stream->gcount(); + return f->ifs.gcount(); } void Http::priv::set_timeout_connect(long timeout) @@ -296,7 +318,7 @@ void Http::priv::set_timeout_max(long timeout) ::curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout); } -void Http::priv::form_add_file(const char *name, const fs::path &path, const char* filename) +void Http::priv::form_add_file(const char *name, const fs::path &path, const char* filename, boost::filesystem::ifstream::off_type offset, size_t length) { // We can't use CURLFORM_FILECONTENT, because curl doesn't support Unicode filenames on Windows // and so we use CURLFORM_STREAM with boost ifstream to read the file. @@ -305,18 +327,21 @@ void Http::priv::form_add_file(const char *name, const fs::path &path, const cha filename = path.string().c_str(); } - form_files.emplace_back(path, std::ios::in | std::ios::binary); - auto &stream = form_files.back(); - stream.seekg(0, std::ios::end); - size_t size = stream.tellg(); - stream.seekg(0); + form_files.emplace_back(form_file{{path, std::ios::in | std::ios::binary}, offset, length}); + auto &f = form_files.back(); + size_t size = length; + if (length == 0) { + f.ifs.seekg(0, std::ios::end); + size = f.ifs.tellg() - offset; + } + f.ifs.seekg(offset); if (filename != nullptr) { ::curl_formadd(&form, &form_end, CURLFORM_COPYNAME, name, CURLFORM_FILENAME, filename, CURLFORM_CONTENTTYPE, "application/octet-stream", - CURLFORM_STREAM, static_cast(&stream), + CURLFORM_STREAM, static_cast(&f), CURLFORM_CONTENTSLENGTH, static_cast(size), CURLFORM_END ); @@ -545,15 +570,35 @@ Http& Http::form_add(const std::string &name, const std::string &contents) return *this; } -Http& Http::form_add_file(const std::string &name, const fs::path &path) +Http& Http::form_add_file(const std::string &name, const fs::path &path, boost::filesystem::ifstream::off_type offset, size_t length) +{ + if (p) { p->form_add_file(name.c_str(), path.c_str(), nullptr, offset, length); } + return *this; +} + + +Http& Http::mime_form_add_text(std::string &name, std::string &value) +{ + if (p) { p->mime_form_add_text(name.c_str(), value.c_str()); } + return *this; +} + +Http& Http::mime_form_add_file(std::string &name, const char* path) +{ + if (p) { p->mime_form_add_file(name.c_str(), path); } + return *this; +} + + +Http& Http::form_add_file(const std::wstring& name, const fs::path& path, boost::filesystem::ifstream::off_type offset, size_t length) { - if (p) { p->form_add_file(name.c_str(), path.c_str(), nullptr); } + if (p) { p->form_add_file((char*)name.c_str(), path.c_str(), nullptr, offset, length); } return *this; } -Http& Http::form_add_file(const std::string &name, const fs::path &path, const std::string &filename) +Http& Http::form_add_file(const std::string &name, const fs::path &path, const std::string &filename, boost::filesystem::ifstream::off_type offset, size_t length) { - if (p) { p->form_add_file(name.c_str(), path.c_str(), filename.c_str()); } + if (p) { p->form_add_file(name.c_str(), path.c_str(), filename.c_str(), offset, length); } return *this; } diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 438b8d31ed..e8c18cf001 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -84,12 +84,18 @@ class Http : public std::enable_shared_from_this { // Add a HTTP multipart form field Http& form_add(const std::string &name, const std::string &contents); // Add a HTTP multipart form file data contents, `name` is the name of the part - Http& form_add_file(const std::string &name, const boost::filesystem::path &path); + Http& form_add_file(const std::string &name, const boost::filesystem::path &path, boost::filesystem::ifstream::off_type offset = 0, size_t length = 0); + // Add a HTTP mime form field + Http& mime_form_add_text(std::string& name, std::string& value); + // Add a HTTP mime form file + Http& mime_form_add_file(std::string& name, const char* path); + // Same as above except also override the file's filename with a wstring type + Http& form_add_file(const std::wstring& name, const boost::filesystem::path& path, boost::filesystem::ifstream::off_type offset = 0, size_t length = 0); // Same as above except also override the file's filename with a custom one - Http& form_add_file(const std::string &name, const boost::filesystem::path &path, const std::string &filename); + Http& form_add_file(const std::string &name, const boost::filesystem::path &path, const std::string &filename, boost::filesystem::ifstream::off_type offset = 0, size_t length = 0); #ifdef WIN32 - // Tells libcurl to ignore certificate revocation checks in case of missing or offline distribution points for those SSL backends where such behavior is present. + // Tells libcurl to ignore certificate revocation checks in case of missing or offline distribution points for those SSL backends where such behavior is present. // This option is only supported for Schannel (the native Windows SSL library). Http& ssl_revoke_best_effort(bool set); #endif // WIN32 diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 840949f3e2..674ccb5267 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -23,6 +23,7 @@ #include "MKS.hpp" #include "SimplyPrint.hpp" #include "../GUI/PrintHostDialogs.hpp" +#include "../GUI/MainFrame.hpp" #include "../GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index 240a568ef6..e05836a4c9 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -28,6 +28,7 @@ ENABLE_ENUM_BITMASK_OPERATORS(PrintHostPostUploadAction); struct PrintHostUpload { + bool use_3mf; boost::filesystem::path source_path; boost::filesystem::path upload_path; @@ -79,6 +80,7 @@ struct PrintHostJob { PrintHostUpload upload_data; std::unique_ptr printhost; + bool switch_to_device_tab{false}; bool cancelled = false; PrintHostJob() {} @@ -86,6 +88,7 @@ struct PrintHostJob PrintHostJob(PrintHostJob &&other) : upload_data(std::move(other.upload_data)) , printhost(std::move(other.printhost)) + , switch_to_device_tab(other.switch_to_device_tab) , cancelled(other.cancelled) {} @@ -97,7 +100,8 @@ struct PrintHostJob PrintHostJob& operator=(PrintHostJob &&other) { upload_data = std::move(other.upload_data); - printhost = std::move(other.printhost); + printhost = std::move(other.printhost); + switch_to_device_tab = other.switch_to_device_tab; cancelled = other.cancelled; return *this; } diff --git a/src/slic3r/Utils/SimplyPrint.cpp b/src/slic3r/Utils/SimplyPrint.cpp index 9c5603c7d7..84d1e979c9 100644 --- a/src/slic3r/Utils/SimplyPrint.cpp +++ b/src/slic3r/Utils/SimplyPrint.cpp @@ -7,17 +7,36 @@ #include "libslic3r/Utils.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" namespace Slic3r { +// to make testing easier +//#define SIMPLYPRINT_TEST + +#ifdef SIMPLYPRINT_TEST +#define URL_BASE_HOME "https://test.simplyprint.io" +#define URL_BASE_API "https://testapi.simplyprint.io" +#else +#define URL_BASE_HOME "https://simplyprint.io" +#define URL_BASE_API "https://api.simplyprint.io" +#endif + static constexpr boost::asio::ip::port_type CALLBACK_PORT = 21328; static const std::string CALLBACK_URL = "http://localhost:21328/callback"; static const std::string RESPONSE_TYPE = "code"; static const std::string CLIENT_ID = "simplyprintorcaslicer"; static const std::string CLIENT_SCOPES = "user.read files.temp_upload"; static const std::string OAUTH_CREDENTIAL_PATH = "simplyprint_oauth.json"; -static const std::string TOKEN_URL = "https://simplyprint.io/api/oauth2/Token"; +static const std::string TOKEN_URL = URL_BASE_API"/oauth2/Token"; +#ifdef SIMPLYPRINT_TEST +static constexpr uint64_t MAX_SINGLE_UPLOAD_FILE_SIZE = 100000ull; // Max file size that can be uploaded in a single http request +#else +static constexpr uint64_t MAX_SINGLE_UPLOAD_FILE_SIZE = 100000000ull; // Max file size that can be uploaded in a single http request +#endif +static const std::string CHUNCK_RECEIVE_URL = URL_BASE_API"/0/files/ChunkReceive"; static std::string generate_verification_code(int code_length = 32) { @@ -55,6 +74,9 @@ static std::string url_encode(const std::vectoruse_bbl_device_tab()) { + // When using bbl device tab, we always need to open external browser + return true; + } + + // Otherwise, if user choose to switch to device tab, then don't bother opening external browser + return !app.app_config->get_bool("open_device_tab_post_upload"); +} + SimplyPrint::SimplyPrint(DynamicPrintConfig* config) { cred_file = (boost::filesystem::path(data_dir()) / OAUTH_CREDENTIAL_PATH).make_preferred().string(); @@ -85,7 +120,7 @@ GUI::OAuthParams SimplyPrint::get_oauth_params() const {"code_challenge", code_challenge}, {"code_challenge_method", "S256"}, }; - const auto login_url = (boost::format("https://simplyprint.io/panel/oauth2/authorize?%s") % url_encode(query_parameters)).str(); + const auto login_url = (boost::format(URL_BASE_HOME"/panel/oauth2/authorize?%s") % url_encode(query_parameters)).str(); return GUI::OAuthParams{ login_url, @@ -94,8 +129,8 @@ GUI::OAuthParams SimplyPrint::get_oauth_params() const CALLBACK_URL, CLIENT_SCOPES, RESPONSE_TYPE, - "https://simplyprint.io/login-success", - "https://simplyprint.io/login-success", + URL_BASE_HOME"/login-success", + URL_BASE_HOME"/login-success", TOKEN_URL, verification_code, state, @@ -224,7 +259,7 @@ bool SimplyPrint::test(wxString& curl_msg) const return do_api_call( [](bool is_retry) { - auto http = Http::get("https://api.simplyprint.io/oauth2/TokenInfo"); + auto http = Http::get(URL_BASE_API"/oauth2/TokenInfo"); http.header("Accept", "application/json"); return http; }, @@ -239,30 +274,31 @@ bool SimplyPrint::test(wxString& curl_msg) const }); } -bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path, + const std::string& chunk_id, + const std::string& filename, + ProgressFn prorgess_fn, + ErrorFn error_fn) const { - if (cred.find("access_token") == cred.end()) { - error_fn(_L("SimplyPrint account not linked. Go to Connect options to set it up.")); - return false; - } - - // If file is over 100 MB, fail - if (boost::filesystem::file_size(upload_data.source_path) > 104857600ull) { - error_fn(_L("File size exceeds the 100MB upload limit. Please upload your file through the panel.")); + if (file_path.empty() == chunk_id.empty()) { + BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid arguments: both file_path and chunk_id are set or not provided"; + error_fn(_L("Internel error")); return false; } - const auto filename = upload_data.upload_path.filename().string(); - return do_api_call( - [&upload_data, &prorgess_fn, &filename](bool is_retry) { - auto http = Http::post("https://simplyprint.io/api/files/TempUpload"); - http.form_add_file("file", upload_data.source_path.string(), filename) - .on_progress([&prorgess_fn](Http::Progress progress, bool& cancel) { prorgess_fn(std::move(progress), cancel); }); + [&file_path, &chunk_id, &prorgess_fn, &filename](bool is_retry) { + auto http = Http::post(URL_BASE_HOME"/api/files/TempUpload"); + if (!file_path.empty()) { + http.form_add_file("file", file_path, filename); + } else { + http.form_add("chunkId", chunk_id); + } + http.on_progress([&prorgess_fn](Http::Progress progress, bool& cancel) { prorgess_fn(std::move(progress), cancel); }); return http; }, - [&error_fn, &filename](std::string body, unsigned status) { + [&error_fn, &filename, this](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File uploaded: HTTP %1%: %2%") % status % body; // Get file UUID @@ -281,8 +317,15 @@ bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Er const std::string uuid = j["uuid"]; // Launch external browser for file importing after uploading - const auto url = "https://simplyprint.io/panel?" + url_encode({{"import", "tmp:" + uuid}, {"filename", filename}}); - wxLaunchDefaultBrowser(url); + const auto url = URL_BASE_HOME"/panel?" + url_encode({{"import", "tmp:" + uuid}, {"filename", filename}}); + + if (should_open_in_external_browser()) { + wxLaunchDefaultBrowser(url); + } else { + const auto mainframe = GUI::wxGetApp().mainframe; + mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor); + mainframe->load_printer_url(url); + } return true; }, @@ -293,4 +336,155 @@ bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Er }); } +bool SimplyPrint::do_chunk_upload(const boost::filesystem::path& file_path, const std::string& filename, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + const auto file_size = boost::filesystem::file_size(file_path); +#ifdef SIMPLYPRINT_TEST + constexpr auto buffer_size = MAX_SINGLE_UPLOAD_FILE_SIZE; +#else + constexpr auto buffer_size = MAX_SINGLE_UPLOAD_FILE_SIZE - 1000000; +#endif + + const auto chunk_amount = (size_t)ceil((double) file_size / buffer_size); + + std::string chunk_id; + std::string delete_token; + + // Tell SimplyPrint that the upload has failed and the chunks should be deleted + // Note: any error happens here won't be notified to the user + const auto clean_up = [this, &chunk_id, &delete_token]() { + if (chunk_id.empty()) { + // The initial upload failed, do nothing + BOOST_LOG_TRIVIAL(warning) << "SimplyPrint: Initial chunk upload failed, skip delete"; + return; + } + + assert(!delete_token.empty()); + + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: Deleting file chunk %s...") % chunk_id; + const std::vector> query_parameters{ + {"id", chunk_id}, + {"temp", "true"}, + {"delete", delete_token}, + }; + const auto url = (boost::format("%s?%s") % CHUNCK_RECEIVE_URL % url_encode(query_parameters)).str(); + do_api_call( + [&url](bool is_retry) { + auto http = Http::get(url); + return http; + }, + [&chunk_id](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File chunk %1% deleted: HTTP %2%: %3%") % chunk_id % status % body; + return true; + }, + [&chunk_id](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(warning) << boost::format("SimplyPrint: Error deleting file chunk %1%: %2%, HTTP %3%, body: `%4%`") % + chunk_id % error % status % body; + return false; + }); + }; + + // Do chunk upload + for (size_t i = 0; i < chunk_amount; i++) { + std::string url; + { + std::vector> query_parameters{ + {"i", std::to_string(i)}, + {"temp", "true"}, + }; + if (i == 0) { + query_parameters.emplace_back("filename", filename); + query_parameters.emplace_back("chunks", std::to_string(chunk_amount)); + query_parameters.emplace_back("totalsize", std::to_string(file_size)); + } else { + query_parameters.emplace_back("id", chunk_id); + } + url = (boost::format("%s?%s") % CHUNCK_RECEIVE_URL % url_encode(query_parameters)).str(); + } + + // Calculate the offset and length of current chunk + const boost::filesystem::ifstream::off_type offset = i * buffer_size; + const size_t length = i == (chunk_amount - 1) ? file_size - offset : buffer_size; + + const bool succ = do_api_call( + [&url, &file_path, &filename, i, chunk_amount, file_size, offset, length, prorgess_fn](bool is_retry) { + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: Start uploading file chunk [%1%/%2%]...") % (i + 1) % chunk_amount; + auto http = Http::post(url); + http.form_add_file("file", file_path, filename, offset, length); + + http.on_progress([&prorgess_fn, file_size, offset](Http::Progress progress, bool& cancel) { + progress.ultotal = file_size; + progress.ulnow += offset; + + prorgess_fn(std::move(progress), cancel); + }); + + return http; + }, + [&error_fn, i, chunk_amount, this, &chunk_id, &delete_token](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File chunk [%1%/%2%] uploaded: HTTP %3%: %4%") % (i + 1) % chunk_amount % status % body; + if (i == 0) { + // First chunk, parse chunk id + const auto j = nlohmann::json::parse(body, nullptr, false, true); + if (j.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on ChunkReceive: " << body; + error_fn(_L("Unknown error")); + return false; + } + + if (j.find("id") == j.end() || j.find("delete_token") == j.end()) { + BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on ChunkReceive: " << body; + error_fn(_L("Unknown error")); + return false; + } + + const unsigned long id = j["id"]; + + chunk_id = std::to_string(id); + delete_token = j["delete_token"]; + } + return true; + }, + [this, &error_fn, i, chunk_amount](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("SimplyPrint: Error uploading file chunk [%1%/%2%]: %3%, HTTP %4%, body: `%5%`") % + (i + 1) % chunk_amount % error % status % body; + error_fn(format_error(body, error, status)); + return false; + }); + + if (!succ) { + clean_up(); + return false; + } + } + + assert(!chunk_id.empty()); + + // Finally, complete the upload using the chunk id + const bool succ = do_temp_upload({}, chunk_id, filename, prorgess_fn, error_fn); + if (!succ) { + clean_up(); + } + + return succ; +} + + +bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + if (cred.find("access_token") == cred.end()) { + error_fn(_L("SimplyPrint account not linked. Go to Connect options to set it up.")); + return false; + } + const auto filename = upload_data.upload_path.filename().string(); + + if (boost::filesystem::file_size(upload_data.source_path) > MAX_SINGLE_UPLOAD_FILE_SIZE) { + // If file is over 100 MB, do chunk upload + return do_chunk_upload(upload_data.source_path, filename, prorgess_fn, error_fn); + } else { + // Normal upload + return do_temp_upload(upload_data.source_path, {}, filename, prorgess_fn, error_fn); + } +} + } // namespace Slic3r diff --git a/src/slic3r/Utils/SimplyPrint.hpp b/src/slic3r/Utils/SimplyPrint.hpp index b2c06a04e6..5a0cd4d59e 100644 --- a/src/slic3r/Utils/SimplyPrint.hpp +++ b/src/slic3r/Utils/SimplyPrint.hpp @@ -15,10 +15,37 @@ class SimplyPrint : public PrintHost void load_oauth_credential(); + /** + * \brief Call the given SimplyPrint API, and if the token expired do a token refresh then retry + * \param build_request the http request builder + * \param on_complete + * \param on_error + * \return whether the API call succeeded + */ bool do_api_call(std::function build_request, std::function on_complete, std::function on_error) const; + /** + * \brief Upload a temp file and open SimplyPrint panel for file importing + * \param file_path for file smaller than 100MB, this is the file path, otherwise must left empty + * \param chunk_id for file greater than 100MB, this is the chunk id returned by the ChunkReceive API, otherwise must left empty + * \param filename the target file name + * \param prorgess_fn + * \param error_fn + * \return whether upload succeeded + */ + bool do_temp_upload(const boost::filesystem::path& file_path, + const std::string& chunk_id, + const std::string& filename, + ProgressFn prorgess_fn, + ErrorFn error_fn) const; + + bool do_chunk_upload(const boost::filesystem::path& file_path, + const std::string& filename, + ProgressFn prorgess_fn, + ErrorFn error_fn) const; + public: SimplyPrint(DynamicPrintConfig* config); ~SimplyPrint() override = default;