#include "PrusaConnect.hpp" #include "Http.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/UserAccount.hpp" #include #include #include #include #include #include namespace fs = boost::filesystem; namespace pt = boost::property_tree; namespace Slic3r { namespace { std::string escape_string(const std::string& unescaped) { std::string ret_val; CURL* curl = curl_easy_init(); if (curl) { char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); if (decoded) { ret_val = std::string(decoded); curl_free(decoded); } curl_easy_cleanup(curl); } return ret_val; } std::string escape_path_by_element(const boost::filesystem::path& path) { std::string ret_val = escape_string(path.filename().string()); boost::filesystem::path parent(path.parent_path()); while (!parent.empty() && parent.string() != "/") // "/" check is for case "/file.gcode" was inserted. Then boost takes "/" as parent_path. { ret_val = escape_string(parent.filename().string()) + "/" + ret_val; parent = parent.parent_path(); } return ret_val; } } PrusaConnectNew::PrusaConnectNew(DynamicPrintConfig *config) : m_uuid(config->opt_string("print_host")) , m_team_id(config->opt_string("printhost_apikey")) {} const char* PrusaConnectNew::get_name() const { return "PrusaConnectNew"; } bool PrusaConnectNew::test(wxString& curl_msg) const { // Test is not used by upload and gets list of files on a device. const std::string name = get_name(); std::string url = GUI::format("https://dev.connect.prusa3d.com/app/teams/%1%/files?printer_uuid=%2%", m_team_id, m_uuid); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); BOOST_LOG_TRIVIAL(info) << GUI::format("%1%: Get files/raw at: %2%", name, url); bool res = true; auto http = Http::get(std::move(url)); http.header("Authorization", "Bearer " + access_token); 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; curl_msg = format_error(body, error, status); }) .on_complete([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Got files/raw: %2%") % name % body; }) .perform_sync(); return res; } bool PrusaConnectNew::init_upload(PrintHostUpload upload_data, std::string& out) const { // Register upload. Then upload must be performed immediately with returned "id" bool res = true; boost::system::error_code ec; boost::uintmax_t size = boost::filesystem::file_size(upload_data.source_path, ec); const std::string name = get_name(); const std::string file_size = std::to_string(size); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); //const std::string upload_path = upload_data.upload_path.generic_string(); const std::string upload_filename = upload_data.upload_path.filename().string(); std::string url = GUI::format("%1%/app/users/teams/%2%/uploads", get_host(), m_team_id); const std::string request_body_json = GUI::format( "{" "\"filename\": \"%1%\", " "\"size\": %2%, " "\"path\": \"%3%\", " "\"force\": true, " "\"printer_uuid\": \"%4%\"" "}" , upload_filename , file_size , upload_data.upload_path.generic_string() , m_uuid ); BOOST_LOG_TRIVIAL(info) << "Register upload to "<< name<<". Url: " << url << "\nBody: " << request_body_json; Http http = Http::post(std::move(url)); http.header("Authorization", "Bearer " + access_token) .header("Content-Type", "application/json") .set_post_body(request_body_json) .on_complete([&](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << boost::format("%1%: File upload registered: HTTP %2%: %3%") % name % status % body; out = body; }) .on_error([&](std::string body, std::string error, unsigned status) { BOOST_LOG_TRIVIAL(error) << body; BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error registering file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; res = false; out = GUI::into_u8(format_error(body, error, status)); }) .perform_sync(); return res; } bool PrusaConnectNew::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const { std::string init_out; if (!init_upload(upload_data, init_out)) { error_fn(std::move(GUI::from_u8(init_out))); return false; } // init reply format: {"id": 1234, "team_id": 12345, "name": "filename.gcode", "size": 123, "hash": "QhE0LD76vihC-F11Jfx9rEqGsk4.", "state": "INITIATED", "source": "CONNECT_USER", "path": "/usb/filename.bgcode"} std::string upload_id; try { std::stringstream ss(init_out); pt::ptree ptree; pt::read_json(ss, ptree); const auto id_opt = ptree.get_optional("id"); if (!id_opt) { error_fn(std::move(_L("Failed to extract upload id from server reply."))); return false; } upload_id = *id_opt; } catch (const std::exception&) { error_fn(std::move(_L("Failed to extract upload id from server reply."))); return false; } const std::string name = get_name(); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); const std::string escaped_upload_path = upload_data.storage + "/" + escape_path_by_element(upload_data.upload_path.string()); const std::string set_ready = upload_data.set_ready.empty() ? "" : "&set_ready=" + upload_data.set_ready; const std::string position = upload_data.position.empty() ? "" : "&position=" + upload_data.position; const std::string wait_until = upload_data.wait_until.empty() ? "" : "&wait_until=" + upload_data.wait_until; const std::string url = GUI::format( "%1%/app/teams/%2%/files/raw" "?upload_id=%3%" "&force=true" "&printer_uuid=%4%" "&path=%5%" "%6%" "%7%" "%8%" , get_host(), m_team_id, upload_id, m_uuid, escaped_upload_path, set_ready, position, wait_until); bool res = true; BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") % name % upload_data.source_path % url % upload_data.upload_path.filename().string() % upload_data.upload_path.parent_path().string() % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false"); Http http = Http::put(std::move(url)); http.set_put_body(upload_data.source_path) .header("Content-Type", "text/x.gcode") .header("Authorization", "Bearer " + access_token) .on_complete([&](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(info) << 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) { progress_fn(std::move(progress), cancel); if (cancel) { // Upload was canceled BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled"; res = false; } }) .perform_sync(); return res; } bool PrusaConnectNew::get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const { const char* name = get_name(); bool res = true; std::string url = GUI::format("%1%/app/printers/%2%/storages", get_host(), m_uuid); const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token(); wxString error_msg; struct StorageInfo { wxString path; wxString name; bool read_only = false; long long free_space = -1; }; std::vector storage; BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get storage at: %2%") % name % url; wxString wlang = GUI::wxGetApp().current_language_code(); std::string lang = GUI::format(wlang.SubString(0, 1)); auto http = Http::get(std::move(url)); http.header("Authorization", "Bearer " + access_token) .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([&](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got storage: %2%") % name % body; // {"storages": [{"mountpoint": "/usb", "name": "usb", "free_space": 16340844544, "type": "USB", "is_sfn": true, "read_only": false, "file_count": 1}]} 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 != "storages") { res = false; return; } // each storage has own subtree of storage_list for (const auto& section : ptree.front().second) { const auto name = section.second.get_optional("name"); const auto path = section.second.get_optional("mountpoint"); const auto space = section.second.get_optional("free_space"); const auto read_only = section.second.get_optional("read_only"); const auto ro = section.second.get_optional("ro"); // In PrusaLink 0.7.0RC2 "read_only" value is stored under "ro". const auto available = section.second.get_optional("available"); if (path && (!available || *available)) { StorageInfo si; si.path = boost::nowide::widen(*path); si.name = name ? boost::nowide::widen(*name) : wxString(); // If read_only is missing, assume it is NOT read only. // si.read_only = read_only ? *read_only : false; // version without "ro" si.read_only = (read_only ? *read_only : (ro ? *ro : false)); si.free_space = space ? std::stoll(*space) : 1; // If free_space is missing, assume there is free space. storage.emplace_back(std::move(si)); } } } catch (const std::exception&) { res = false; } }) .perform_sync(); for (const auto& si : storage) { if (!si.read_only && si.free_space > 0) { storage_path.push_back(si.path); storage_name.push_back(si.name); } } if (res && storage_path.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 += GUI::format_wxstr(si.read_only ? // TRN %1% = storage path _L("%1% : read only") : // TRN %1% = storage path _L("%1% : no free space"), si.path) + L"\n"; } } // TRN %1% = host std::string message = GUI::format(_L("Upload has failed. There is no suitable storage found at %1%. "), get_host()) + GUI::into_u8(error_msg); BOOST_LOG_TRIVIAL(error) << message; throw Slic3r::IOError(message); } return res; } wxString PrusaConnectNew::get_test_ok_msg() const { return _L("Test OK."); } wxString PrusaConnectNew::get_test_failed_msg(wxString& msg) const { return _L("Test NOK."); } std::string PrusaConnectNew::get_team_id(const std::string& data) const { boost::property_tree::ptree ptree; try { std::stringstream ss(data); boost::property_tree::read_json(ss, ptree); } catch (const std::exception&) { return {}; } const auto team_id = ptree.get_optional("team_id"); if (team_id) { return *team_id; } return {}; } }