mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-02 07:30:41 +08:00
340 lines
14 KiB
C++
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 {};
|
|
}
|
|
|
|
}
|