583 lines
18 KiB
C++
583 lines
18 KiB
C++
#include "middleware.h"
|
|
|
|
#include "file.h"
|
|
#include "main-window.h"
|
|
#include "md-parser.h"
|
|
#include <cmark-gfm.h>
|
|
#include <glibmm.h>
|
|
#include <glibmm/main.h>
|
|
|
|
/**
|
|
* Middleware constructor
|
|
*/
|
|
Middleware::Middleware(MainWindow& main_window, const std::string& timeout)
|
|
: main_window_(main_window),
|
|
// Threading:
|
|
request_thread_(nullptr),
|
|
status_thread_(nullptr),
|
|
is_request_thread_done_(false),
|
|
keep_request_thread_running_(true),
|
|
is_status_thread_done_(false),
|
|
// IPFS:
|
|
ipfs_host_("localhost"),
|
|
ipfs_port_(5001),
|
|
ipfs_timeout_(timeout),
|
|
ipfs_fetch_(ipfs_host_, ipfs_port_, ipfs_timeout_),
|
|
ipfs_status_(ipfs_host_, ipfs_port_, ipfs_timeout_),
|
|
ipfs_number_of_peers_(0),
|
|
ipfs_repo_size_(0),
|
|
ipfs_incoming_rate_("0.0"),
|
|
ipfs_outgoing_rate_("0.0"),
|
|
// Request & Response:
|
|
wait_page_visible_(false)
|
|
{
|
|
// Hook up signals to Main Window methods
|
|
request_started_.connect(sigc::mem_fun(main_window, &MainWindow::started_request));
|
|
request_finished_.connect(sigc::mem_fun(main_window, &MainWindow::finished_request));
|
|
|
|
// First update status manually (with slight delay), after that the timer below will take care of updates
|
|
Glib::signal_timeout().connect_once(sigc::mem_fun(this, &Middleware::do_ipfs_status_update_once), 550);
|
|
|
|
// Create a timer, triggers every 4 seconds
|
|
status_timer_handler_ = Glib::signal_timeout().connect_seconds(sigc::mem_fun(this, &Middleware::do_ipfs_status_update), 4);
|
|
}
|
|
|
|
/**
|
|
* Destructor
|
|
*/
|
|
Middleware::~Middleware()
|
|
{
|
|
status_timer_handler_.disconnect();
|
|
abort_request();
|
|
abort_status();
|
|
}
|
|
|
|
/**
|
|
* Fetch document from disk or IPFS, using threading
|
|
* \param path File path that needs to be opened (either from disk or IPFS network)
|
|
* \param is_set_address_bar If true update the address bar with the file path (default: true)
|
|
* \param is_history_request Set to true if this is an history request call: back/forward (default: false)
|
|
* \param is_disable_editor If true the editor will be disabled if needed (default: true)
|
|
* \param is_parse_content If true the content received will be parsed and displayed as markdown syntax (default: true),
|
|
* set to false if you want to editor the content
|
|
*/
|
|
void Middleware::do_request(const std::string& path, bool is_set_address_bar, bool is_history_request, bool is_disable_editor, bool is_parse_content)
|
|
{
|
|
// Stop any on-going request first, if applicable
|
|
abort_request();
|
|
|
|
if (request_thread_ == nullptr)
|
|
{
|
|
std::string title;
|
|
if (path.empty() && request_path_.starts_with("file://"))
|
|
{
|
|
title = File::get_filename(request_path_); // During refresh
|
|
}
|
|
else if (path.starts_with("file://"))
|
|
{
|
|
title = File::get_filename(path);
|
|
}
|
|
// Update main window widgets
|
|
main_window_.pre_request(path, title, is_set_address_bar, is_history_request, is_disable_editor);
|
|
|
|
// Start thread
|
|
request_thread_ = new std::thread(&Middleware::process_request, this, path, is_parse_content);
|
|
}
|
|
else
|
|
{
|
|
std::cerr << "ERROR: Could not start request thread. Something went wrong." << std::endl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Add current content to IPFS
|
|
* \param path file path in IPFS
|
|
* \return Content identifier (CID)
|
|
*/
|
|
std::string Middleware::do_add(const std::string& path)
|
|
{
|
|
// TODO: We should run this within a seperate thread, to avoid blocking the main thread.
|
|
// See also the other status calls we are making, but maybe we should use ipfs_fetch_ anyway.
|
|
return ipfs_status_.add(path, get_content());
|
|
}
|
|
|
|
/**
|
|
* \brief Write file to disk
|
|
* \param path file path to disk
|
|
* \param is_set_address_and_title If true update the address bar & title (default: true)
|
|
*/
|
|
void Middleware::do_write(const std::string& path, bool is_set_address_and_title)
|
|
{
|
|
File::write(path, get_content());
|
|
main_window_.post_write("file://" + path, File::get_filename(path), is_set_address_and_title);
|
|
}
|
|
|
|
/**
|
|
* \brief Set current plain-text content (not parsed)
|
|
*/
|
|
void Middleware::set_content(const Glib::ustring& content)
|
|
{
|
|
current_content_ = content;
|
|
}
|
|
|
|
/**
|
|
* \brief Get current plain content (not parsed)
|
|
* \return content as string
|
|
*/
|
|
Glib::ustring Middleware::get_content() const
|
|
{
|
|
return current_content_;
|
|
}
|
|
|
|
/**
|
|
* \brief Current content parser middleware.
|
|
* Note: Do not forget to free the document: cmark_node_free(root_node;
|
|
* \return AST structure (of type cmark_node)
|
|
*/
|
|
cmark_node* Middleware::parse_content() const
|
|
{
|
|
return Parser::parse_content(current_content_);
|
|
}
|
|
|
|
/**
|
|
* \brief Reset state
|
|
*/
|
|
void Middleware::reset_content_and_path()
|
|
{
|
|
current_content_ = "";
|
|
request_path_ = "";
|
|
final_request_path_ = "";
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS number of peers
|
|
* \return number of peers (size_t)
|
|
*/
|
|
std::size_t Middleware::get_ipfs_number_of_peers() const
|
|
{
|
|
return ipfs_number_of_peers_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS repository size
|
|
* \return repo size (int)
|
|
*/
|
|
int Middleware::get_ipfs_repo_size() const
|
|
{
|
|
return ipfs_repo_size_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS repository path
|
|
* \return repo path (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_repo_path() const
|
|
{
|
|
return ipfs_repo_path_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS Incoming rate
|
|
* \return incoming rate (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_incoming_rate() const
|
|
{
|
|
return ipfs_incoming_rate_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS Outgoing rate
|
|
* \return outgoing rate (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_outgoing_rate() const
|
|
{
|
|
return ipfs_outgoing_rate_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS version
|
|
* \return version (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_version() const
|
|
{
|
|
return ipfs_version_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS Client ID
|
|
* \return client ID (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_client_id() const
|
|
{
|
|
return ipfs_client_id_;
|
|
}
|
|
|
|
/**
|
|
* \brief Get IPFS Client Public key
|
|
* \return public key (string)
|
|
*/
|
|
std::string Middleware::get_ipfs_client_public_key() const
|
|
{
|
|
return ipfs_client_public_key_;
|
|
}
|
|
|
|
/************************************************
|
|
* Private methods
|
|
************************************************/
|
|
|
|
/**
|
|
* \brief Get the file from disk or IPFS network, from the provided path,
|
|
* parse the content, and display the document.
|
|
* Call this method with empty path, will use the previous request_path_ (thus refresh).
|
|
* \param path File path that needs to be fetched (from disk or IPFS network)
|
|
* \param isParseContent Set to true if you want to parse and display the content as markdown syntax (from disk or IPFS
|
|
* network), set to false if you want to edit the content
|
|
*/
|
|
void Middleware::process_request(const std::string& path, bool isParseContent)
|
|
{
|
|
request_started_.emit(); // Emit started for Main Window
|
|
// Reset private variables
|
|
current_content_ = "";
|
|
wait_page_visible_ = false;
|
|
|
|
// Do not update the request_path_ when path is empty,
|
|
// this is used for refreshing the page
|
|
if (!path.empty())
|
|
{
|
|
request_path_ = path;
|
|
}
|
|
|
|
if (request_path_.empty())
|
|
{
|
|
std::cerr << "Info: Empty request path." << std::endl;
|
|
}
|
|
// Handle homepage
|
|
else if (request_path_.compare("about:home") == 0)
|
|
{
|
|
Glib::signal_idle().connect_once(sigc::mem_fun(main_window_, &MainWindow::show_homepage));
|
|
}
|
|
// Handle disk or IPFS file paths
|
|
else
|
|
{
|
|
// Check if CID
|
|
if (request_path_.starts_with("ipfs://"))
|
|
{
|
|
final_request_path_ = request_path_;
|
|
final_request_path_.erase(0, 7);
|
|
fetch_from_ipfs(isParseContent);
|
|
}
|
|
else if ((request_path_.length() == 46) && request_path_.starts_with("Qm"))
|
|
{
|
|
// CIDv0
|
|
final_request_path_ = request_path_;
|
|
fetch_from_ipfs(isParseContent);
|
|
}
|
|
else if (request_path_.starts_with("file://"))
|
|
{
|
|
final_request_path_ = request_path_;
|
|
final_request_path_.erase(0, 7);
|
|
open_from_disk(isParseContent);
|
|
}
|
|
else
|
|
{
|
|
// IPFS as fallback / CIDv1
|
|
final_request_path_ = request_path_;
|
|
fetch_from_ipfs(isParseContent);
|
|
}
|
|
}
|
|
|
|
request_finished_.emit(); // Emit finished for Main Window
|
|
is_request_thread_done_ = true; // mark thread as done
|
|
}
|
|
|
|
/**
|
|
* \brief Helper method for process_request(), display markdown file from IPFS network.
|
|
* Runs in a seperate thread.
|
|
* \param isParseContent Set to true if you want to parse and display the content as markdown syntax (from disk or IPFS
|
|
* network), set to false if you want to edit the content
|
|
*/
|
|
void Middleware::fetch_from_ipfs(bool isParseContent)
|
|
{
|
|
try
|
|
{
|
|
std::stringstream contents;
|
|
ipfs_fetch_.fetch(final_request_path_, &contents);
|
|
// If the thread stops, don't brother to parse the file/update the GTK window
|
|
if (keep_request_thread_running_)
|
|
{
|
|
// Retrieve content to string
|
|
Glib::ustring content = contents.str();
|
|
// Only set content if valid UTF-8
|
|
if (validate_utf8(content) && keep_request_thread_running_)
|
|
{
|
|
set_content(content);
|
|
if (isParseContent)
|
|
{
|
|
// TODO: Maybe we want to abort the parser when keep_request_thread_running_ = false,
|
|
// depending time the parser is taking?
|
|
cmark_node* doc = parse_content();
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_document), doc));
|
|
}
|
|
else
|
|
{
|
|
// Directly display the plain markdown content
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_text), get_content()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "😵 File will not be displayed ",
|
|
"File is not valid UTF-8 encoded, like a markdown or text file."));
|
|
}
|
|
}
|
|
}
|
|
catch (const std::runtime_error& error)
|
|
{
|
|
std::string errorMessage = std::string(error.what());
|
|
// Ignore error reporting when the request was aborted
|
|
if (errorMessage != "Request was aborted")
|
|
{
|
|
std::cerr << "ERROR: IPFS request failed, with message: " << errorMessage << std::endl;
|
|
if (errorMessage.starts_with("HTTP request failed with status code"))
|
|
{
|
|
std::string message;
|
|
// Remove text until ':\n'
|
|
errorMessage.erase(0, errorMessage.find(':') + 2);
|
|
if (!errorMessage.empty() && errorMessage != "")
|
|
{
|
|
try
|
|
{
|
|
auto content = nlohmann::json::parse(errorMessage);
|
|
message = "Message: " + content.value("Message", "");
|
|
if (message.starts_with("context deadline exceeded"))
|
|
{
|
|
message += ". Time-out is set to: " + ipfs_timeout_;
|
|
}
|
|
message += ".\n\n";
|
|
}
|
|
catch (const nlohmann::json::parse_error& parseError)
|
|
{
|
|
std::cerr << "ERROR: Could not parse at byte: " << parseError.byte << std::endl;
|
|
}
|
|
}
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message),
|
|
"🎂 We're having trouble finding this site.",
|
|
message + "You could try to reload the page or try increase the time-out (see --help)."));
|
|
}
|
|
else if (errorMessage.starts_with("Couldn't connect to server: Failed to connect to localhost"))
|
|
{
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "⌛ Please wait...",
|
|
"IPFS daemon is still spinnng-up, page will automatically refresh..."));
|
|
wait_page_visible_ = true; // Please wait page is shown (auto-refresh when network is up)
|
|
}
|
|
else
|
|
{
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "❌ Something went wrong",
|
|
"Error message: " + std::string(error.what())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Helper method for process_request(), display markdown file from disk.
|
|
* Runs in a seperate thread.
|
|
* \param isParseContent Set to true if you want to parse and display the content as markdown syntax (from disk or IPFS
|
|
* network), set to false if you want to edit the content
|
|
*/
|
|
void Middleware::open_from_disk(bool isParseContent)
|
|
{
|
|
try
|
|
{
|
|
// TODO: Abort file read if keep_request_thread_running_ = false and throw runtime error, to stop futher execution
|
|
// eg. when you are reading a very big file from disk.
|
|
const Glib::ustring content = File::read(final_request_path_);
|
|
// If the thread stops, don't brother to parse the file/update the GTK window
|
|
if (keep_request_thread_running_)
|
|
{
|
|
// Only set content if valid UTF-8
|
|
if (validate_utf8(content))
|
|
{
|
|
set_content(content);
|
|
if (isParseContent)
|
|
{
|
|
cmark_node* doc = parse_content();
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_document), doc));
|
|
}
|
|
else
|
|
{
|
|
// Directly set the plain markdown content
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_text), get_content()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Glib::signal_idle().connect_once(sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "😵 File will not be displayed ",
|
|
"File is not valid UTF-8 encoded, like a markdown file or text file."));
|
|
}
|
|
}
|
|
}
|
|
catch (const std::ios_base::failure& error)
|
|
{
|
|
std::cerr << "ERROR: Could not read file: " << final_request_path_ << ". Message: " << error.what() << ".\nError code: " << error.code()
|
|
<< std::endl;
|
|
Glib::signal_idle().connect_once(
|
|
sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "🎂 Could not read file", "Message: " + std::string(error.what())));
|
|
}
|
|
catch (const std::runtime_error& error)
|
|
{
|
|
std::cerr << "ERROR: File request failed, file: " << final_request_path_ << ". Message: " << error.what() << std::endl;
|
|
Glib::signal_idle().connect_once(
|
|
sigc::bind(sigc::mem_fun(main_window_, &MainWindow::set_message), "🎂 File not found", "Message: " + std::string(error.what())));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \brief Validate if text is valid UTF-8.
|
|
* \param text String that needs to be validated
|
|
* \return true if valid UTF-8
|
|
*/
|
|
bool Middleware::validate_utf8(const Glib::ustring& text) const
|
|
{
|
|
return text.validate();
|
|
}
|
|
|
|
/**
|
|
* \brief Simple wrapper of the method below with void return
|
|
*/
|
|
void Middleware::do_ipfs_status_update_once()
|
|
{
|
|
do_ipfs_status_update();
|
|
}
|
|
|
|
/**
|
|
* \brief Timeout slot: Update the IPFS connection status every x seconds.
|
|
* Process requests inside a seperate thread, to avoid blocking the GUI thread.
|
|
* \return always true, when running as a GTK timeout handler
|
|
*/
|
|
bool Middleware::do_ipfs_status_update()
|
|
{
|
|
// Stop any on-going status calls first, if applicable
|
|
abort_status();
|
|
|
|
if (status_thread_ == nullptr)
|
|
{
|
|
status_thread_ = new std::thread(&Middleware::process_ipfs_status, this);
|
|
}
|
|
// Keep going (never disconnect the timer)
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Process the IPFS status calls.
|
|
* Runs inside a thread.
|
|
*/
|
|
void Middleware::process_ipfs_status()
|
|
{
|
|
std::lock_guard<std::mutex> guard(status_mutex_);
|
|
try
|
|
{
|
|
ipfs_number_of_peers_ = ipfs_status_.get_nr_peers();
|
|
if (ipfs_number_of_peers_ > 0)
|
|
{
|
|
// Auto-refresh page if needed (when 'Please wait' page was shown)
|
|
if (wait_page_visible_)
|
|
Glib::signal_idle().connect_once(sigc::mem_fun(main_window_, &MainWindow::refresh_request));
|
|
|
|
std::map<std::string, std::variant<int, std::string>> repoStats = ipfs_status_.get_repo_stats();
|
|
ipfs_repo_size_ = std::get<int>(repoStats.at("repo-size"));
|
|
ipfs_repo_path_ = std::get<std::string>(repoStats.at("path"));
|
|
|
|
std::map<std::string, float> rates = ipfs_status_.get_bandwidth_rates();
|
|
char buf[32];
|
|
ipfs_incoming_rate_ = std::string(buf, std::snprintf(buf, sizeof buf, "%.1f", rates.at("in") / 1000.0));
|
|
ipfs_outgoing_rate_ = std::string(buf, std::snprintf(buf, sizeof buf, "%.1f", rates.at("out") / 1000.0));
|
|
}
|
|
else
|
|
{
|
|
ipfs_repo_size_ = 0;
|
|
ipfs_repo_path_ = "";
|
|
ipfs_incoming_rate_ = "0.0";
|
|
ipfs_outgoing_rate_ = "0.0";
|
|
}
|
|
|
|
if (ipfs_client_id_.empty())
|
|
ipfs_client_id_ = ipfs_status_.get_client_id();
|
|
if (ipfs_client_public_key_.empty())
|
|
ipfs_client_public_key_ = ipfs_status_.get_client_public_key();
|
|
if (ipfs_version_.empty())
|
|
ipfs_version_ = ipfs_status_.get_version();
|
|
|
|
// Trigger update of all status fields, in a thread-safe manner
|
|
Glib::signal_idle().connect_once(sigc::mem_fun(main_window_, &MainWindow::update_status_popover_and_icon));
|
|
}
|
|
catch (const std::runtime_error& error)
|
|
{
|
|
std::string errorMessage = std::string(error.what());
|
|
if (errorMessage != "Request was aborted")
|
|
{
|
|
// Assume no connection or connection lost; display disconnected
|
|
ipfs_number_of_peers_ = 0;
|
|
ipfs_repo_size_ = 0;
|
|
ipfs_repo_path_ = "";
|
|
ipfs_incoming_rate_ = "0.0";
|
|
ipfs_outgoing_rate_ = "0.0";
|
|
Glib::signal_idle().connect_once(sigc::mem_fun(main_window_, &MainWindow::update_status_popover_and_icon));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort request call and stop the thread, if applicable.
|
|
*/
|
|
void Middleware::abort_request()
|
|
{
|
|
if (request_thread_ && request_thread_->joinable())
|
|
{
|
|
if (is_request_thread_done_)
|
|
{
|
|
request_thread_->join();
|
|
}
|
|
else
|
|
{
|
|
// Trigger the thread to stop now.
|
|
// We call the abort method of the IPFS client.
|
|
ipfs_fetch_.abort();
|
|
keep_request_thread_running_ = false;
|
|
request_thread_->join();
|
|
// Reset states, allowing new threads with new API requests/calls
|
|
ipfs_fetch_.reset();
|
|
keep_request_thread_running_ = true;
|
|
}
|
|
delete request_thread_;
|
|
request_thread_ = nullptr;
|
|
is_request_thread_done_ = false; // reset
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort status calls and stop the thread, if applicable.
|
|
*/
|
|
void Middleware::abort_status()
|
|
{
|
|
if (status_thread_ && status_thread_->joinable())
|
|
{
|
|
if (is_status_thread_done_)
|
|
{
|
|
status_thread_->join();
|
|
}
|
|
else
|
|
{
|
|
// Trigger the thread to stop now.
|
|
// We call the abort method of the IPFS client.
|
|
ipfs_status_.abort();
|
|
status_thread_->join();
|
|
// Reset states, allowing new threads with new API status calls
|
|
ipfs_status_.reset();
|
|
}
|
|
delete status_thread_;
|
|
status_thread_ = nullptr;
|
|
is_status_thread_done_ = false; // reset
|
|
}
|
|
}
|