From 0d03ed37271ca50405a9eb521a8f944cd50fc7bb Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 29 Aug 2022 11:08:14 +0200 Subject: [PATCH] Perform PUT instead of POST for PrusaLink if it is possible. Getting storage from PrusaLink before upload. Allows user to choose storage and to have different storage on each printer. --- src/slic3r/GUI/Plater.cpp | 16 +- src/slic3r/GUI/PrintHostDialogs.cpp | 26 ++- src/slic3r/GUI/PrintHostDialogs.hpp | 4 +- src/slic3r/Utils/OctoPrint.cpp | 323 ++++++++++++++++++++++++++++ src/slic3r/Utils/OctoPrint.hpp | 18 +- src/slic3r/Utils/PrintHost.hpp | 6 +- 6 files changed, 384 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 45884cc9d9..140e16ac09 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6151,12 +6151,24 @@ void Plater::send_gcode() wxBusyCursor wait; upload_job.printhost->get_groups(groups); } - - PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups); + // PrusaLink specific: Query the server for the list of file groups. + wxArrayString storage; + { + wxBusyCursor wait; + try { + upload_job.printhost->get_storage(storage); + } catch (const Slic3r::IOError& ex) { + show_error(this, ex.what(), false); + return; + } + } + + PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage); if (dlg.ShowModal() == wxID_OK) { upload_job.upload_data.upload_path = dlg.filename(); upload_job.upload_data.post_action = dlg.post_action(); upload_job.upload_data.group = dlg.group(); + upload_job.upload_data.storage = dlg.storage(); p->export_gcode(fs::path(), false, std::move(upload_job)); } diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index b53fe5c474..20356b9bdb 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -35,12 +35,14 @@ namespace GUI { static const char *CONFIG_KEY_PATH = "printhost_path"; static const char *CONFIG_KEY_GROUP = "printhost_group"; +static const char* CONFIG_KEY_STORAGE = "printhost_storage"; -PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups) +PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUploadActions post_actions, const wxArrayString &groups, const wxArrayString& storage) : MsgDialog(static_cast(wxGetApp().mainframe), _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"), 0) // Set style = 0 to avoid default creation of the "OK" button. // All buttons will be added later in this constructor , txt_filename(new wxTextCtrl(this, wxID_ANY)) , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr) + , combo_storage(!storage.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, storage, wxCB_READONLY) : nullptr) , post_upload_action(PrintHostPostUploadAction::None) { #ifdef __APPLE__ @@ -65,6 +67,17 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo combo_groups->SetValue(recent_group); } + if (combo_storage != nullptr) { + // PrusaLink specific: User needs to choose a storage + auto* label_group = new wxStaticText(this, wxID_ANY, _L("Choose target storage:")); + content_sizer->Add(label_group); + content_sizer->Add(combo_storage, 0, wxBOTTOM, 2 * VERT_SPACING); + combo_storage->SetValue(storage.front()); + wxString recent_storage = from_u8(app_config->get("recent", CONFIG_KEY_STORAGE)); + if (!recent_storage.empty()) + combo_storage->SetValue(recent_storage); + } + wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH)); if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') { recent_path += '/'; @@ -162,6 +175,13 @@ std::string PrintHostSendDialog::group() const } } +std::string PrintHostSendDialog::storage() const +{ + if (!combo_storage) + return std::string(); + return boost::nowide::narrow(combo_storage->GetValue()); +} + void PrintHostSendDialog::EndModal(int ret) { if (ret == wxID_OK) { @@ -180,6 +200,10 @@ void PrintHostSendDialog::EndModal(int ret) wxString group = combo_groups->GetValue(); app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group)); } + if (combo_storage != nullptr) { + wxString storage = combo_storage->GetValue(); + app_config->set("recent", CONFIG_KEY_STORAGE, into_u8(storage)); + } } MsgDialog::EndModal(ret); diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index ff3eb60125..1bcd703db0 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -26,15 +26,17 @@ namespace GUI { class PrintHostSendDialog : public GUI::MsgDialog { public: - PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups); + PrintHostSendDialog(const boost::filesystem::path &path, PrintHostPostUploadActions post_actions, const wxArrayString& groups, const wxArrayString& storage); boost::filesystem::path filename() const; PrintHostPostUploadAction post_action() const; std::string group() const; + std::string storage() const; virtual void EndModal(int ret) override; private: wxTextCtrl *txt_filename; wxComboBox *combo_groups; + wxComboBox* combo_storage; PrintHostPostUploadAction post_upload_action; wxString m_valid_suffix; }; diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 250b16b4ac..11c7d243c9 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -364,4 +365,326 @@ void PrusaLink::set_auth(Http& http) const } } +#if 0 +bool PrusaLink::version_check(const boost::optional& version_text) const +{ + // version_text is in format OctoPrint 1.2.3 + // true (= use PUT) should return: + // PrusaLink 0.7+ + + try { + if (!version_text) + throw Slic3r::RuntimeError("no version_text was given"); + + std::vector name_and_version; + boost::algorithm::split(name_and_version, *version_text, boost::is_any_of(" ")); + + if (name_and_version.size() != 2) + throw Slic3r::RuntimeError("invalid version_text"); + + Semver semver(name_and_version[1]); // throws Slic3r::RuntimeError when unable to parse + if (name_and_version.front() == "PrusaLink" && semver >= Semver(0, 7, 0)) + return true; + } catch (const Slic3r::RuntimeError& ex) { + BOOST_LOG_TRIVIAL(error) << std::string("Print host version check failed: ") + ex.what(); + } + + return false; +} +# endif + +bool PrusaLink::get_storage(wxArrayString& output) const +{ + const char* name = get_name(); + + bool res = true; + auto url = make_url("api/v1/storage"); + wxString error_msg; + + struct StorageInfo{ + wxString name; + bool read_only; + long long free_space; + }; + std::vector storage; + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get storage at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting storage: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_msg = L"\n\n" + boost::nowide::widen(error); + res = false; + // If status is 0, the communication with the printer has failed completely (most likely a timeout), if the status is <= 400, it is an error returned by the pritner. + // If 0, we can show error to the user now, as we know the communication has failed. (res = true will do the trick.) + // if not 0, we must not show error, as not all printers support api/v1/storage endpoint. + // So we must be extra careful here, or we might be showing errors on perfectly fine communication. + if (status == 0) + res = true; + + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got storage: %2%") % name % body; + try + { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + // what if there is more structure added in the future? Enumerate all elements? + if (ptree.front().first != "storage_list") { + res = false; + return; + } + // each storage has own subtree of storage_list + for (const auto& section : ptree.front().second) { + const auto path = section.second.get_optional("path"); + const auto space = section.second.get_optional("free_space"); + if (path) + { + StorageInfo si; + si.name = boost::nowide::widen(*path); + si.read_only = !space; + si.free_space = space ? std::stoll(*space) : 0; + storage.emplace_back(std::move(si)); + } + } + } + catch (const std::exception&) + { + res = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + +#endif // WIN32 + .perform_sync(); + + for (const auto& si : storage) { + if (!si.read_only && si.free_space > 0) + output.push_back(si.name); + } + + if (res && output.empty()) + { + if (!storage.empty()) { // otherwise error_msg is already filled + error_msg = L"\n\n" + _L("Storages found:") + L" \n"; + for (const auto& si : storage) { + error_msg += si.name + L" : " + (si.read_only ? _L("read only") : _L("no free space")) + L"\n"; + } + } + std::string message = GUI::format(_L("Upload has failed. There is no suitable storage found at %1%.%2%"), m_host, error_msg); + BOOST_LOG_TRIVIAL(error) << message; + throw Slic3r::IOError(message); + } + + return res; +} + +bool PrusaLink::test_with_method_check(wxString& msg, bool& use_put) const +{ + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + + const char* name = get_name(); + + bool res = true; + auto url = make_url("api/version"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; + + try { + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + + if (!ptree.get_optional("api")) { + res = false; + return; + } + + const auto text = ptree.get_optional("text"); + res = validate_version_text(text); + if (!res) { + msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "OctoPrint")).str()); + use_put = false; + return; + } + + // find capabilities subtree and read upload-by-put + for (const auto& section : ptree) { + if (section.first == "capabilities") { + const auto put_upload = section.second.get_optional("upload-by-put"); + if (put_upload) + use_put = *put_upload; + break; + } + } + + } + catch (const std::exception&) { + res = false; + msg = "Could not parse server response"; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + + return res; +} + +bool PrusaLink::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + const char* name = get_name(); + + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + + // If test fails, test_msg_or_host_ip contains the error message. + // Otherwise on Windows it contains the resolved IP address of the host. + wxString test_msg_or_host_ip; + bool use_put = false; + if (!test_with_method_check(test_msg_or_host_ip, use_put)) { + error_fn(std::move(test_msg_or_host_ip)); + return false; + } + + // set here use_put = false; to forbid using PUT instead of POST + + std::string url; + bool res = true; + + std::string storage_path = (use_put ? "api/v1" : "api/files"); + storage_path += (upload_data.storage.empty() ? "/local" : upload_data.storage); + +#ifdef WIN32 + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || GUI::get_app_config()->get("allow_ip_resolve") != "1") +#endif // _WIN32 + { + // If https is entered we assume signed ceritificate is being used + // IP resolving will not happen - it could resolve into address not being specified in cert + url = make_url(storage_path); + } +#ifdef WIN32 + else { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Curl uses easy_getinfo to get ip address of last successful transaction. + // If it got the address use it instead of the stored in "host" variable. + // This new address returns in "test_msg_or_host_ip" variable. + // Solves troubles of uploades failing with name address. + // in original address (m_host) replace host for resolved ip + url = substitute_host(make_url(storage_path), GUI::into_u8(test_msg_or_host_ip)); + BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; + } +#endif // _WIN32 + + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%, method: %7%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + % (use_put ? "PUT" : "POST"); + + + if (use_put) + return put_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn); + return post_inner(std::move(upload_data), std::move(url), name, prorgess_fn, error_fn); +} + +bool PrusaLink::put_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + bool res = true; + + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + + url += "/" + upload_filename.string(); + + auto http = Http::put(std::move(url)); + set_auth(http); + // This is ugly, but works. There was an error at PrusaLink side that accepts any string at Print-After-Upload as true, thus False was also triggering print after upload. + if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) + http.header("Print-After-Upload", "True"); + + http.set_put_body(upload_data.source_path) +// .header("Print-After-Upload", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "True" : "False") + .header("Content-Type", "text/x.gcode") + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled"; + res = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) +#endif + .perform_sync(); + return res; +} +bool PrusaLink::post_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn) const +{ + bool res = true; + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + auto http = Http::post(std::move(url)); + set_auth(http); + http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") + .form_add("path", upload_parent_path.string()) // XXX: slashes on windows ??? + .form_add_file("file", upload_data.source_path.string(), upload_filename.string()) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled"; + res = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) +#endif + .perform_sync(); + return res; +} + + } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index 262efe9ff5..77681e296c 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -22,10 +22,10 @@ public: const char* get_name() const override; - bool test(wxString &curl_msg) const override; + virtual bool test(wxString &curl_msg) const override; wxString get_test_ok_msg () const override; wxString get_test_failed_msg (wxString &msg) const override; - bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + virtual bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; bool has_auto_discovery() const override { return true; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } @@ -36,7 +36,6 @@ public: protected: virtual bool validate_version_text(const boost::optional &version_text) const; -private: std::string m_host; std::string m_apikey; std::string m_cafile; @@ -83,17 +82,28 @@ public: wxString get_test_failed_msg(wxString& msg) const override; PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + // gets possible storage to be uploaded to. This allows different printer to have different storage. F.e. local vs sdcard vs usb. + virtual bool get_storage(wxArrayString& /* storage */) const override; protected: bool validate_version_text(const boost::optional& version_text) const override; -private: void set_auth(Http& http) const override; +private: + bool test_with_method_check(wxString& curl_msg, bool& use_put) const; + bool put_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn) const; + bool post_inner(PrintHostUpload upload_data, std::string url, const std::string& name, ProgressFn prorgess_fn, ErrorFn error_fn) const; + // Host authorization type. AuthorizationType m_authorization_type; // username and password for HTTP Digest Authentization (RFC RFC2617) std::string m_username; std::string m_password; + +#if 0 + bool version_check(const boost::optional& version_text) const; +#endif }; } diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index dd22e60b7d..8db7224bc7 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -32,7 +32,8 @@ struct PrintHostUpload boost::filesystem::path upload_path; std::string group; - + std::string storage; + PrintHostPostUploadAction post_action { PrintHostPostUploadAction::None }; }; @@ -61,6 +62,9 @@ public: // Returns false if not supported. May throw HostNetworkError. virtual bool get_groups(wxArrayString & /* groups */) const { return false; } virtual bool get_printers(wxArrayString & /* printers */) const { return false; } + // Support for PrusaLink uploading to different storage. Not supported by other print hosts. + // Returns false if not supported or fail. + virtual bool get_storage(wxArrayString& /* storage */) const { return false; } static PrintHost* get_print_host(DynamicPrintConfig *config);