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.
This commit is contained in:
David Kocik 2022-08-29 11:08:14 +02:00
parent 09997bcd65
commit 0d03ed3727
6 changed files with 384 additions and 9 deletions

View File

@ -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));
}

View File

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

View File

@ -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;
};

View File

@ -8,6 +8,7 @@
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/split.hpp>
#include <curl/curl.h>
@ -364,4 +365,326 @@ void PrusaLink::set_auth(Http& http) const
}
}
#if 0
bool PrusaLink::version_check(const boost::optional<std::string>& 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<std::string> 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<StorageInfo> 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<std::string>("path");
const auto space = section.second.get_optional<std::string>("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<std::string>("api")) {
res = false;
return;
}
const auto text = ptree.get_optional<std::string>("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<bool>("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;
}
}

View File

@ -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<std::string> &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<std::string>& 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<std::string>& version_text) const;
#endif
};
}

View File

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