PrusaSlicer/src/slic3r/Utils/PrusaConnect.cpp

340 lines
14 KiB
C++

#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 <boost/log/trivial.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/filesystem.hpp>
#include <curl/curl.h>
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<std::string>("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<StorageInfo> 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<std::string>("name");
const auto path = section.second.get_optional<std::string>("mountpoint");
const auto space = section.second.get_optional<std::string>("free_space");
const auto read_only = section.second.get_optional<bool>("read_only");
const auto ro = section.second.get_optional<bool>("ro"); // In PrusaLink 0.7.0RC2 "read_only" value is stored under "ro".
const auto available = section.second.get_optional<bool>("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<std::string>("team_id");
if (team_id)
{
return *team_id;
}
return {};
}
}