From 8ccecf09785c532c98c6065432a6399901402584 Mon Sep 17 00:00:00 2001 From: Francois Michaut Date: Sat, 20 Jun 2026 09:50:23 -0400 Subject: [PATCH 1/2] Initial Config creation + Execute Submenu - Address some clang-tidy warnings - Add interactive creation of config file - Implement Execute submenu --- .clang-tidy | 4 + CMakeLists.txt | 3 +- dependencies/CMakeLists.txt | 4 +- source/cli/interactive.cpp | 532 ++++++++++++++++++++---------------- source/cli/main.cpp | 96 +++++-- source/cli/utils.cpp | 35 +++ 6 files changed, 407 insertions(+), 267 deletions(-) create mode 100644 source/cli/utils.cpp diff --git a/.clang-tidy b/.clang-tidy index a5ed099..8cdf55f 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -7,13 +7,17 @@ Checks: ' -google-readability-todo, -cppcoreguidelines-init-variables, -cppcoreguidelines-pro-type-reinterpret-cast, +-cppcoreguidelines-avoid-do-while, -llvm-namespace-comment, -readability-implicit-bool-conversion, -google-explicit-constructor, -abseil-string-find-str-contains, -readability-avoid-return-with-void-value, -readability-convert-member-functions-to-static, +-misc-use-anonymous-namespace, +-llvm-else-after-return, +-readability-else-after-return, -google-readability-braces-around-statements, -google-readability-namespace-comments, -hicpp-special-member-functions, diff --git a/CMakeLists.txt b/CMakeLists.txt index 07721f2..27cd812 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Sat Jan 15 01:19:13 2022 Francois Michaut -## Last update Sun Aug 24 18:59:20 2025 Francois Michaut +## Last update Fri May 22 18:43:01 2026 Francois Michaut ## ## CMakeLists.txt : Top level CMake ## @@ -89,6 +89,7 @@ if (BUILD_CLI) add_executable(file-share-cli source/cli/main.cpp source/cli/interactive.cpp + source/cli/utils.cpp ) target_compile_definitions(file-share-cli PRIVATE _LARGEFILE_SOURCE _FILE_OFFSET_BITS=64) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 25e24f2..274eaa3 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Wed Jul 10 10:27:39 2024 Francois Michaut -## Last update Sun Aug 24 20:34:12 2025 Francois Michaut +## Last update Sat May 23 17:02:38 2026 Francois Michaut ## ## CMakeLists.txt : CMake to fetch and build the dependecies of the FileShare executable ## @@ -14,7 +14,7 @@ include(FetchContent) FetchContent_Declare( argparse GIT_REPOSITORY https://github.com/p-ranav/argparse.git - GIT_TAG af442b4da0cd7a07b56fa709bd16571889dc7fda + GIT_TAG 3eda91b2e1ce7d569f84ba295507c4cd8fd96910 ) FetchContent_Declare( SFML diff --git a/source/cli/interactive.cpp b/source/cli/interactive.cpp index 0465d41..bbd138f 100644 --- a/source/cli/interactive.cpp +++ b/source/cli/interactive.cpp @@ -1,38 +1,48 @@ /* -** Project Project FileShare, 2025 +** Project FileShare, 2025 ** ** Author Francois Michaut ** ** Started on Wed Jul 23 13:49:52 2025 Francois Michaut -** Last update Sun Aug 24 19:57:35 2025 Francois Michaut +** Last update Wed May 27 12:06:41 2026 Francois Michaut ** ** interactive.cpp : Interractive Mode logic */ +#include "FileShare/Peer/Peer.hpp" +#include "FileShare/Peer/PeerBase.hpp" +#include "FileShare/Peer/PreAuthPeer.hpp" #include "FileShareVersion.hpp" #include #include #include +#include #include #include -#include #include -#include -#include #include #include #include +#include #include -#include #include +/* NOLINTBEGIN */ extern bool server_run; void signal_handler(int sig); -enum class InteractiveStateMachine { +void string_trim(std::string &str); +void string_lowercase(std::string &str); + +void interactive_mode(FileShare::Server &server); +auto interactive_user_question(const std::string_view &question) -> bool; +auto interactive_user_input(const std::string_view &question) -> std::string; +/* NOLINTEND */ + +enum class InteractiveStateMachine : std::uint8_t { MAIN_MENU = 0x00, EXIT_MENU = 0x01, CONFIG_MENU = 0x05, @@ -46,7 +56,7 @@ enum class InteractiveStateMachine { TRANSFERS_MENU = 0x30, }; -auto operator<<(std::ostream &out, InteractiveStateMachine state) -> std::ostream & { +static auto operator<<(std::ostream &out, InteractiveStateMachine state) -> std::ostream & { switch (state) { case InteractiveStateMachine::MAIN_MENU: out << "MAIN"; @@ -79,92 +89,102 @@ auto operator<<(std::ostream &out, InteractiveStateMachine state) -> std::ostrea return out; } +struct SubMenuCommandState { + std::string subcommand; + std::stringstream args; +}; + struct InteractiveState { InteractiveState(FileShare::Server &server) : server(server) {} + bool should_prompt = true; + FileShare::Server &server; std::vector pending_events; std::weak_ptr selected_peer; + // TODO: That's a HACK. Find a better solution + // Idea: Give a unique auto-increment int ID to each peer instance ? + std::vector peers; + std::vector pending_peers; + InteractiveStateMachine state = InteractiveStateMachine::MAIN_MENU; + SubMenuCommandState sub_menu_state; }; -using CommandMap = std::unordered_map>; -using ConfigCommandMap = std::unordered_map>; +using CommandMap = std::unordered_map>; +using ExecuteCommandMap = std::unordered_map>; +using ConfigCommandMap = std::unordered_map>; static void display_interactive_help() { - std::cout << "Available Commands are :" << '\n'; - std::cout << "\t" << "- HELP: Display this help message." << '\n'; - std::cout << "\t" << "- EXIT|QUIT: Stop the program." << '\n'; + std::cout << "Available Commands are :\n"; + std::cout << "\t" << "- HELP: Display this help message.\n"; + std::cout << "\t" << "- EXIT|QUIT: Stop the program.\n"; std::cout << "\t" << "- SERVER (ON|OFF): Toggle the Server ON or OFF.\n\t\t If Server is OFF, " "no external peers can connect, but you can still initiate connections (existing " - "connections will not be terminated)." << '\n'; + "connections will not be terminated).\n"; std::cout << "\t" << "- CONFIG (SERVER|DEFAULT|) [SUBCOMMAND] []...: " - "(Interractive) Modify configuration on the fly. Available subcommands :" << '\n'; - std::cout << "\t\t" << "- SHOW: (default) Display the selected configuration." << '\n'; - std::cout << "\t\t" << "- SET : Set a simple configuration value." << '\n'; + "(Interractive) Modify configuration on the fly. Available subcommands :\n"; + std::cout << "\t\t" << "- SHOW: (default) Display the selected configuration.\n"; + std::cout << "\t\t" << "- SET : Set a simple configuration value.\n"; std::cout << "\t\t" << "- SAVE []: Save the config to a file at PATH. " - "Overwrites file if exists. If PATH is empty, will save into the original file." << '\n'; + "Overwrites file if exists. If PATH is empty, will save into the original file.\n"; std::cout << "\t\t" << "- LOAD []: Replace the current config with the one in the " - "config file at PATH. If PATH is empty, will load from the default location." << '\n'; + "config file at PATH. If PATH is empty, will load from the default location.\n"; // TODO: FileMap config support - std::cout << "\t" << "- PEER []...: Manage peers. Available subcommands :" - << '\n'; - std::cout << "\t\t" << "- LIST: List currently connected peers." << '\n'; - std::cout << "\t\t" << "- CONNECT []: Connect to a new peer." << '\n'; + std::cout << "\t" << "- PEER []...: Manage peers. Available subcommands :\n"; + std::cout << "\t\t" << "- LIST: List currently connected peers.\n"; + std::cout << "\t\t" << "- CONNECT []: Connect to a new peer.\n"; std::cout << "\t\t" << "- DISCONNECT []: (Interractive) Disconnect from a connected peer " - "or reject a connection request." << '\n'; + "or reject a connection request.\n"; std::cout << "\t\t" << "- ACCEPT []: (Interractive) Accept a connection request from an " - "external peer." << '\n'; + "external peer.\n"; std::cout << "\t\t" << "- SELECT []: (Interractive) Promote a peer as the 'selected' one." "\n\t\t\t If connected to multiple peers, pre-selecting a peer allows to send multiple " - "commands without having to select it each time." << '\n'; + "commands without having to select it each time.\n"; // TODO: Where do I select Peer if multiple and None selected ? std::cout << "\t" << "- EXECUTE []...: (Interractive) Execute a command on a " - "selected peer. Will prompt to select a peer if none selected. Available commands :" - << '\n'; + "selected peer. Will prompt to select a peer if none selected. Available commands :\n"; std::cout << "\t\t" << "- LIST_FILES []: List availables files on the peer. Can be " - "Downloaded using RECEIVE_FILE." << '\n'; - std::cout << "\t\t" << "- RECEIVE_FILE : Request a file download" << '\n'; - std::cout << "\t\t" << "- SEND_FILE : Send a file to the peer." << '\n'; + "Downloaded using RECEIVE_FILE.\n"; + std::cout << "\t\t" << "- RECEIVE_FILE : Request a file download\n"; + std::cout << "\t\t" << "- SEND_FILE : Send a file to the peer.\n"; - std::cout << "\t" << "- TRANSFERS: Display ongoing Downloads and Uploads transfers and their " - "progress." << '\n'; + std::cout << "\t" << "- TRANSFERS (LIST|ACCEPT): (Interractive) Display or manage transfers:\n"; + std::cout << "\t\t" << "- LIST: Display ongoing Downloads and Uploads transfers and their " + "progress, as well as pending transfer requests.\n"; + std::cout << "\t\t" << "- ACCEPT : Accept a pending transfer request.\n"; - std::cout << "\n" << "Interractive commands will prompt for arguments selection or subsequent " - "required inputs and/or display choices to select from." << '\n'; + std::cout << "\nInterractive commands will prompt for arguments selection and/or display " + "choices to select from.\n"; std::cout << "Some commands accept their arguments optionally and can be both interactive " - "and non-interactive based on if all their arguments have been provided." << '\n'; - std::cout << "Command names and arguments are case-insensitive" << '\n'; + "and non-interactive based on if all their arguments have been provided.\n"; + std::cout << "Command names and arguments are case-insensitive\n"; std::cout << std::flush; } -void lowercase(std::string &str) { - std::transform(str.begin(), str.end(), str.begin(), [](unsigned char chr){ return std::tolower(chr); }); -} - -auto operator<<(std::ostream &out, const FileShare::Config &config) -> std::ostream & { - return out << '{' << '\n' << - '\t' << "DOWNLOAD_FOLDER = " << config.get_downloads_folder() << '\n' << +static auto operator<<(std::ostream &out, const FileShare::Config &config) -> std::ostream & { + return out << "{\n" << + "\tDOWNLOAD_FOLDER = " << config.get_downloads_folder() << '\n' << '}'; } -auto operator<<(std::ostream &out, const FileShare::ServerConfig &config) -> std::ostream & { - return out << '{' << '\n' << - '\t' << "DEVICE_UUID = " << config.get_uuid() << '\n' << - '\t' << "DEVICE_NAME = " << config.get_device_name() << '\n' << - '\t' << "PRIVATE_KEYS_DIR = " << config.get_private_keys_dir() << '\n' << - '\t' << "PRIVATE_KEY_NAME = " << config.get_private_key_name() << '\n' << +static auto operator<<(std::ostream &out, const FileShare::ServerConfig &config) -> std::ostream & { + return out << "{\n" << + "\tDEVICE_UUID = " << config.get_uuid() << '\n' << + "\tDEVICE_NAME = " << config.get_device_name() << '\n' << + "\tPRIVATE_KEYS_DIR = " << config.get_private_keys_dir() << '\n' << + "\tPRIVATE_KEY_NAME = " << config.get_private_key_name() << '\n' << '}'; } -static auto get_peer(InteractiveState &state, std::size_t index, bool include_pending = false, bool include_connected = true) -> std::shared_ptr { - auto pending_peers = state.server.get_pending_peers(); - auto peers = state.server.get_peers(); +static auto get_peer(InteractiveState &state, std::size_t index, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { + auto pending_peers = state.pending_peers; + auto peers = state.peers; std::size_t total_peers = (include_pending ? pending_peers.size() : 0) + (include_connected ? peers.size() : 0); std::string_view peer_prefix = include_pending && !include_connected ? " pending" : ( !include_pending && include_connected ? " connected" : "" @@ -173,27 +193,47 @@ static auto get_peer(InteractiveState &state, std::size_t index, bool include_pe if (index <= pending_peers.size()) { auto peer = *std::next(pending_peers.begin(), static_cast(index - 1)); - return peer.second; + return peer; } if (index - pending_peers.size() <= peers.size()) { auto peer = *std::next(peers.begin(), static_cast(index - pending_peers.size() - 1)); - return peer.second; + return peer; } - std::cerr << "Cannot find peer number " << index << " - there is only " << total_peers << peer_prefix << " peers" << std::endl; + std::cerr << "Cannot find peer number " << index << " - there is only " << total_peers << peer_prefix << " peers\n"; return nullptr; } +static auto get_peer(InteractiveState &state, std::stringstream &ss, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { + std::size_t index = 0; + + if (!(ss >> index) || index == 0) { + std::cerr << "Invalid Input ! Please enter a valid peer ID.\n"; + } + return get_peer(state, index, include_pending, include_connected); +} + +static auto get_peer(InteractiveState &state, const std::string &input, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { + std::stringstream ss{input}; + + return get_peer(state, ss, include_pending, include_connected); +} + +static auto get_peer(InteractiveState &state, std::string_view input, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { + // TODO: Find a better way to use string_view + return get_peer(state, std::string(input), include_pending, include_connected); +} + // TODO: Check for extra args and fail // TODO: Simplify this mess of copy-pasted commands -static void exit_command(InteractiveState &state, std::istream &args) { +static void exit_command(InteractiveState &state, std::stringstream &args) { // TODO: Prompt if pending transfers server_run = false; } -static void server_command(InteractiveState &state, std::istream &args) { +static void server_command(InteractiveState &state, std::stringstream &args) { FileShare::Utils::ci_string arg; bool current_state = !state.server.disabled(); bool new_state; @@ -201,7 +241,7 @@ static void server_command(InteractiveState &state, std::istream &args) { args >> arg; if (arg.empty()) { - std::cout << "Server is currently " << (current_state ? "ON" : "OFF") << std::endl; + std::cout << "Server is currently " << (current_state ? "ON" : "OFF") << '\n'; return; } @@ -210,16 +250,16 @@ static void server_command(InteractiveState &state, std::istream &args) { } else if (arg == "OFF") { new_state = false; } else { - std::cerr << "'" << arg << "' is not a valid server status. Please input either ON or OFF." << std::endl; + std::cerr << "'" << arg << "' is not a valid server status. Please input either ON or OFF.\n"; return; } if (new_state == current_state) { - std::cout << "Server is already " << (current_state ? "ON" : "OFF") << std::endl; + std::cout << "Server is already " << (current_state ? "ON" : "OFF") << '\n'; return; } state.server.set_disabled(!new_state); - std::cout << "Server is now " << (new_state ? "ON" : "OFF") << std::endl; + std::cout << "Server is now " << (new_state ? "ON" : "OFF") << '\n'; } // TODO: FIXME: Solve this mess of copy-pasted config commands with https://isocpp.org/wiki/faq/pointers-to-members#functionoids @@ -227,28 +267,21 @@ static void server_command(InteractiveState &state, std::istream &args) { // Config, and one for ServerConfig in the constructor. The interface of the functionoid would allow // to call all the methods we need, and throw an error if the given method is not supported by that // type of Config. -static void config_show_command(InteractiveState &state, std::string_view config, std::istream &args) { +static void config_show_command(InteractiveState &state, std::string_view config, std::stringstream &args) { if (config == "server") { - std::cout << state.server.get_config() << std::endl; + std::cout << state.server.get_config() << '\n'; } else if (config == "default") { - std::cout << state.server.get_peer_config() << std::endl; + std::cout << state.server.get_peer_config() << '\n'; } else { - std::size_t index = 0; - auto result = std::from_chars(config.data(), config.data() + config.size(), index); - - if (result.ec == std::errc()) { - auto peer = std::dynamic_pointer_cast(get_peer(state, index)); + auto peer = std::dynamic_pointer_cast(get_peer(state, config)); - if (peer) { - std::cout << peer->get_config() << std::endl; - } - } else { - std::cerr << "Please input a valid Peer ID" << std::endl; + if (peer) { + std::cout << peer->get_config() << '\n'; } } } -static void config_set_command(InteractiveState &state, std::string_view config, std::istream &args) { +static void config_set_command(InteractiveState &state, std::string_view config, std::stringstream &args) { using ConfigSetter = std::function; using ConfigSetterMap = std::unordered_map; static const ConfigSetterMap setter_map = { @@ -263,17 +296,17 @@ static void config_set_command(InteractiveState &state, std::string_view config, args >> name; args >> value; // TODO: Currently doesn't accept spaces in value. Look into std::quoted for that maybe ? - lowercase(name); + string_lowercase(name); if (config == "server") { if (name == "device_uuid") { - std::cerr << "DEVICE_UUID is read-only." << std::endl; + std::cerr << "DEVICE_UUID is read-only.\n"; return; } auto fun = setter_map.find(name); if (fun == setter_map.end()) { - std::cerr << "Unknown Config property " << std::quoted(name, '\'') << std::endl; + std::cerr << "Unknown Config property " << std::quoted(name, '\'') << '\n'; return; } (*fun).second(&state.server.get_config(), value); @@ -281,29 +314,22 @@ static void config_set_command(InteractiveState &state, std::string_view config, } if (name != "download_folder") { - std::cerr << "Unkown Config property " << std::quoted(name, '\'') << std::endl; + std::cerr << "Unkown Config property " << std::quoted(name, '\'') << '\n'; return; } if (config == "default") { state.server.get_peer_config().set_downloads_folder(value); } else { - std::size_t index = 0; - auto result = std::from_chars(config.data(), config.data() + config.size(), index); + auto peer = std::dynamic_pointer_cast(get_peer(state, config)); - if (result.ec == std::errc()) { - auto peer = std::dynamic_pointer_cast(get_peer(state, index)); - - if (peer) { - peer->get_config().set_downloads_folder(value); - } - } else { - std::cerr << "Please input a valid Peer ID" << std::endl; + if (peer) { + peer->get_config().set_downloads_folder(value); } } } -static void config_save_command(InteractiveState &state, std::string_view config, std::istream &args) { +static void config_save_command(InteractiveState &state, std::string_view config, std::stringstream &args) { std::string path; args >> path; @@ -313,27 +339,19 @@ static void config_save_command(InteractiveState &state, std::string_view config } else if (config == "default") { state.server.get_peer_config().save(path); } else { - std::size_t index = 0; - auto result = std::from_chars(config.data(), config.data() + config.size(), index); - - if (result.ec == std::errc()) { - auto peer = std::dynamic_pointer_cast(get_peer(state, index)); + auto peer = std::dynamic_pointer_cast(get_peer(state, config)); - if (peer) { - peer->get_config().save(path); - } - } else { - std::cerr << "Please input a valid Peer ID" << std::endl; + if (peer) { + peer->get_config().save(path); } } - } -static void config_load_command(InteractiveState &state, std::string_view config, std::istream &args) { - std::cerr << "TODO" << std::endl; +static void config_load_command(InteractiveState &state, std::string_view config, std::stringstream &args) { + std::cerr << "TODO\n"; } -static void config_command(InteractiveState &state, std::istream &args) { +static void config_command(InteractiveState &state, std::stringstream &args) { static const ConfigCommandMap commands_map = { {"show", &config_show_command}, {"set", &config_set_command}, @@ -347,11 +365,11 @@ static void config_command(InteractiveState &state, std::istream &args) { args >> config; args >> subcommand; - lowercase(config); - lowercase(subcommand); + string_lowercase(config); + string_lowercase(subcommand); if (config.empty()) { - std::cerr << "Please select the type of Config you would like to operate on." << std::endl; + std::cerr << "Please select the type of Config you would like to operate on.\n"; return; } if (subcommand.empty()) { @@ -360,7 +378,7 @@ static void config_command(InteractiveState &state, std::istream &args) { auto fun = commands_map.find(subcommand); if (fun == commands_map.end()) { - std::cerr << "Unknown Config Subcommand " << std::quoted(subcommand, '\'') << std::endl; + std::cerr << "Unknown Config Subcommand " << std::quoted(subcommand, '\'') << '\n'; return; } return (*fun).second(state, config, args); @@ -375,17 +393,23 @@ static void peer_list_command(InteractiveState &state, bool include_pending = fa std::cout << "Currently connected Peers : " << total_peers << '\n'; if (include_pending) { + state.pending_peers.clear(); + state.pending_peers.reserve(pending_peers.size()); for (const auto &peer : pending_peers) { // TODO: Display more stuff about peer std::cout << "\t" << idx << ". PENDING_ACCEPT - " << peer.second->get_device_uuid() << '\n'; + state.pending_peers.emplace_back(peer.second); idx++; } } if (include_connected) { + state.peers.clear(); + state.peers.reserve(peers.size()); for (const auto &peer : peers) { // TODO: Display more stuff about peer std::cout << "\t" << idx << ". - " << peer.second->get_device_uuid() << '\n'; + state.peers.emplace_back(peer.second); idx++; } } @@ -393,89 +417,78 @@ static void peer_list_command(InteractiveState &state, bool include_pending = fa std::cout << std::flush; } -static void peer_connect_command(InteractiveState &state, std::istream &args) { - std::string ip; +static void select_peer(InteractiveState &state, bool include_pending = false, bool include_connected = true) { + peer_list_command(state, include_pending, include_connected); + std::cout << "Input the ID of the peer to select : " << std::flush; + state.should_prompt = false; +} + +static void peer_connect_command(InteractiveState &state, std::stringstream &args) { + std::string ip_address; std::uint16_t port = 0; - args >> ip; - if (ip.empty()) { - std::cerr << "Missing required argument " << std::endl; + args >> ip_address; + if (ip_address.empty()) { + std::cerr << "Missing required argument \n"; return; } - if (!args.eof()) { - args >> std::ws; - } if (args.eof()) { port = 12345; } else { args >> port; } if (args.fail() || port == 0) { - std::cerr << "Please input a valid port number" << std::endl; + std::cerr << "Please input a valid port number\n"; return; } - state.server.connect(CppSockets::EndpointV4(ip.c_str(), port)); + try { + state.server.connect(CppSockets::EndpointV4(ip_address.c_str(), port)); + } catch (const std::runtime_error &e) { + std::cerr << "Failed to connect to " << ip_address << ':' << port << + ". Error: " << e.what() << '\n'; + } } -static void peer_accept_command(InteractiveState &state, std::istream &args) { - std::size_t index = 0; +static void peer_accept_command(InteractiveState &state, std::stringstream &args) { auto pending_peers = state.server.get_pending_peers(); - if (!args.eof()) { - args >> std::ws; - } if (args.eof()) { state.state = InteractiveStateMachine::ACCEPT_MENU; peer_list_command(state, true, false); return; } - args >> index; - if (args.fail() || index == 0) { - std::cerr << "Please input a valid number" << std::endl; - return; - } - auto peer = std::dynamic_pointer_cast(get_peer(state, index, true, false)); + auto peer = std::dynamic_pointer_cast(get_peer(state, args, true, false)); if (peer) { state.server.accept_peer(peer); } } -static void peer_disconnect_command(InteractiveState &state, std::istream &args) { - std::size_t index = 0; - - if (!args.eof()) { - args >> std::ws; - } +static void peer_disconnect_command(InteractiveState &state, std::stringstream &args) { if (args.eof()) { state.state = InteractiveStateMachine::DISCONNECT_MENU; peer_list_command(state, true); return; } - args >> index; - if (args.fail() || index == 0) { - std::cerr << "Please input a valid number" << std::endl; - return; - } - auto peer = get_peer(state, index, true, false); + auto peer = get_peer(state, args, true, false); if (peer) { peer->disconnect(); } } -static void peer_select_command(InteractiveState &state, std::istream &args) { +static void peer_select_command(InteractiveState &state, std::stringstream &args) { peer_list_command(state); // TODO: Need to disable it if it disconnects -> We need events for that too - // TODO: Activate Peer ID (De-Activate current peer if executed twice.) + // TODO: Activate Peer ID (De-Activate current peer if already selected.) // Also displays currently active peer if any, and mentions the De-Activation feature in prompt } -static void peer_command(InteractiveState &state, std::istream &args) { +static void peer_command(InteractiveState &state, std::stringstream &args) { static const CommandMap commands_map = { - {"list", [](InteractiveState &state, std::istream &){ peer_list_command(state, true); }}, + {"list", [](InteractiveState &state, std::stringstream &){ peer_list_command(state, true); }}, {"connect", &peer_connect_command}, {"disconnect", &peer_disconnect_command}, {"accept", &peer_accept_command}, @@ -485,133 +498,110 @@ static void peer_command(InteractiveState &state, std::istream &args) { std::string subcommand; args >> subcommand; - lowercase(subcommand); + string_lowercase(subcommand); auto fun = commands_map.find(subcommand); if (fun == commands_map.end()) { - std::cerr << "Unknown Peer Subcommand " << std::quoted(subcommand, '\'') << std::endl; + std::cerr << "Unknown Peer Subcommand " << std::quoted(subcommand, '\'') << '\n'; return; } return (*fun).second(state, args); } -static void execute_list_files_command(InteractiveState &state, std::istream &args) { - auto selected_peer = state.selected_peer.lock(); +static void execute_list_files_command(FileShare::Peer_ptr &selected_peer, InteractiveState &state, std::stringstream &args) { FileShare::Protocol::Response> result; std::string path; args >> path; - if (selected_peer) { - result = selected_peer->list_files(path); - } else { - auto peers = state.server.get_peers(); - - if (peers.size() > 1) { - state.state = InteractiveStateMachine::EXECUTE_MENU; - // TODO: Store command state (args) to be re-executed when we get the peer - return; - } - if (peers.size() == 1) { - result = peers.begin()->second->list_files(path); - } else { - std::cerr << "No Connected Peers" << std::endl; - return; - } - } + result = selected_peer->list_files(path); std::cout << "Listing files in " << std::quoted(path) << " - got " << result.code << ". " << - result.response->size() << " Files : " << '\n'; + result.response->size() << " Files : \n"; for (const auto &file : *result.response) { std::cout << '\t' << file.file_type << ": " << file.path << '\n'; } } -static void execute_receive_file_command(InteractiveState &state, std::istream &args) { - auto selected_peer = state.selected_peer.lock(); +static void execute_receive_file_command(FileShare::Peer_ptr &selected_peer, InteractiveState &state, std::stringstream &args) { std::string path; FileShare::Protocol::Response result; args >> path; if (path.empty()) { - std::cerr << "The filepath of the file to receive is required" << std::endl; + std::cerr << "The filepath of the file to receive is required\n"; return; } - if (selected_peer) { - result = selected_peer->receive_file(path); - } else { - auto peers = state.server.get_peers(); - - if (peers.size() > 1) { - state.state = InteractiveStateMachine::EXECUTE_MENU; - // TODO: Store command state (args) to be re-executed when we get the peer - return; - } - if (peers.size() == 1) { - result = peers.begin()->second->receive_file(path); - } else { - std::cerr << "No Connected Peers" << std::endl; - return; - } - } - std::cout << "Receive File status: " << result.code << std::endl; + result = selected_peer->receive_file(path); + std::cout << "Receive File status: " << result.code << '\n'; } -static void execute_send_file_command(InteractiveState &state, std::istream &args) { - auto selected_peer = state.selected_peer.lock(); +static void execute_send_file_command(FileShare::Peer_ptr &selected_peer, InteractiveState &state, std::stringstream &args) { std::string path; FileShare::Protocol::Response result; - args >> path; + args >> std::quoted(path); if (path.empty()) { - std::cerr << "The filepath of the file to send is required" << std::endl; + std::cerr << "The filepath of the file to send is required\n"; return; } path = FileShare::Utils::resolve_home_component(path); - if (selected_peer) { - result = selected_peer->send_file(path); - } else { - auto peers = state.server.get_peers(); - - if (peers.size() > 1) { - state.state = InteractiveStateMachine::EXECUTE_MENU; - // TODO: Store command state (args) to be re-executed when we get the peer - return; - } - if (peers.size() == 1) { - result = peers.begin()->second->send_file(path); - } else { - std::cerr << "No Connected Peers" << std::endl; - return; - } - } - std::cout << "Send File status: " << result.code << std::endl; + result = selected_peer->send_file(path); + std::cout << "Send File status: " << result.code << '\n'; } -static void execute_command(InteractiveState &state, std::istream &args) { - // TODO: Execute CMD ARG1 [ARG2...]; If multiple clients connected, will need to prompt for select. - static const CommandMap commands_map = { +static auto get_execute_subcommand(const std::string &subcommand) -> std::optional { + static const ExecuteCommandMap commands_map = { {"list_files", &execute_list_files_command}, {"receive_file", &execute_receive_file_command}, {"send_file", &execute_send_file_command} }; + auto fun = commands_map.find(subcommand); + + if (fun == commands_map.end()) { + std::cerr << "Unknown Execute Subcommand " << std::quoted(subcommand, '\'') << '\n'; + return {}; + } + + return *fun; +} + +static void execute_command(InteractiveState &state, std::stringstream &args) { std::string subcommand; args >> subcommand; - lowercase(subcommand); - - auto fun = commands_map.find(subcommand); + string_lowercase(subcommand); - if (fun == commands_map.end()) { - std::cerr << "Unknown Execute Subcommand " << std::quoted(subcommand, '\'') << std::endl; + auto fun = get_execute_subcommand(subcommand); + if (!fun.has_value()) { return; } - return (*fun).second(state, args); + + auto selected_peer = state.selected_peer.lock(); + + if (!selected_peer) { + auto peers = state.server.get_peers(); + + if (peers.size() > 1) { + state.state = InteractiveStateMachine::EXECUTE_MENU; + state.sub_menu_state = SubMenuCommandState{.subcommand = subcommand, .args = std::move(args)}; + std::cout << "Multiple peers are currently connected. Please select a peer to execute the command on :\n"; + select_peer(state); + return; + } + if (peers.size() == 1) { + selected_peer = peers.begin()->second; + } else { + std::cerr << "No Connected Peers\n"; + return; + } + } + return (*fun).second(selected_peer, state, args); } -static void transfers_command(InteractiveState &state, std::istream &args) { +static void transfers_command(InteractiveState &state, std::stringstream &args) { // TODO: List Transfers } @@ -619,7 +609,6 @@ static void handle_main_menu(InteractiveState &state, const std::string &input) static const CommandMap commands_map = { {"help", [](InteractiveState &, std::istream &){ display_interactive_help(); }}, {"exit", &exit_command}, - {"quit", &exit_command}, {"server", &server_command}, {"config", &config_command}, {"peer", &peer_command}, @@ -631,43 +620,59 @@ static void handle_main_menu(InteractiveState &state, const std::string &input) std::string cmd; ss >> cmd; - lowercase(cmd); + string_lowercase(cmd); if (cmd == "?") { cmd = "help"; + } else if (cmd == "quit") { + cmd = "exit"; } auto fun = commands_map.find(cmd); if (fun == commands_map.end()) { - std::cerr << "Unknown Command " << std::quoted(input, '\'') << std::endl; - std::cerr << "Type HELP or '?' for help." << std::endl; + std::cerr << "Unknown Command " << std::quoted(input, '\'') << '\n'; + std::cerr << "Type HELP or '?' for help.\n"; return; } return (*fun).second(state, ss); } +// TODO: Test this +static void handle_execute_menu(InteractiveState &state, const std::string &input) { + auto peer = std::dynamic_pointer_cast(get_peer(state, input)); + + if (peer) { + auto fun = get_execute_subcommand(state.sub_menu_state.subcommand); + + (*fun).second(peer, state, state.sub_menu_state.args); + state.sub_menu_state = {}; + state.state = InteractiveStateMachine::MAIN_MENU; + } +} + static void handle_interactive_input(InteractiveState &state, const std::string &input) { switch (state.state) { case InteractiveStateMachine::MAIN_MENU: return handle_main_menu(state, input); + case InteractiveStateMachine::EXECUTE_MENU: + return handle_execute_menu(state, input); case InteractiveStateMachine::EXIT_MENU: case InteractiveStateMachine::CONFIG_MENU: case InteractiveStateMachine::DISCONNECT_MENU: case InteractiveStateMachine::ACCEPT_MENU: case InteractiveStateMachine::SELECT_MENU: - case InteractiveStateMachine::EXECUTE_MENU: case InteractiveStateMachine::TRANSFERS_MENU: - std::cout << "TODO - OTHER STATES" << std::endl; + std::cout << "TODO - OTHER STATES\n"; state.state = InteractiveStateMachine::MAIN_MENU; return; } } // TODO: Pretty Colors -static void display_prompt(InteractiveState &state, bool &should_prompt) { - if (!should_prompt) { +static void display_prompt(InteractiveState &state) { + if (!state.should_prompt) { return; } std::stringstream ss; @@ -686,7 +691,7 @@ static void display_prompt(InteractiveState &state, bool &should_prompt) { ss << (empty ? "" : ", ") << "Pending Requests: " << pending_requests; empty = false; - std::cout << "TODO: REMOVE - Accepting all pending requests" << std::endl; + std::cout << "TODO: REMOVE - Accepting all pending requests\n"; for (auto &event : state.pending_events) { if (event.REQUEST) { auto peer = std::dynamic_pointer_cast(event.peer()); @@ -703,28 +708,28 @@ static void display_prompt(InteractiveState &state, bool &should_prompt) { std::cout << state.state << " "; } std::cout << "> " << std::flush; - should_prompt = false; + state.should_prompt = false; } void interactive_mode(FileShare::Server &server) { InteractiveState state(server); - FileShare::Server::PeerAcceptCallback accept_cb = [&](FileShare::Server &, std::shared_ptr &) { + FileShare::Server::PeerAcceptCallback accept_cb = [&](FileShare::Server &, FileShare::PreAuthPeer_ptr &) { return false; // We will handle acceptation ourselves }; - FileShare::Server::PeerRequestCallback request_cb = [&](FileShare::Server &, std::shared_ptr &client, FileShare::Protocol::Request &request) { + FileShare::Server::PeerRequestCallback request_cb = [&](FileShare::Server &, FileShare::Peer_ptr &client, FileShare::Protocol::Request &request) { state.pending_events.emplace_back(FileShare::Server::Event::REQUEST, client, request); }; std::array read_buff = {0}; - struct timespec timeout = {1, 0}; // 1s + struct timespec timeout = {.tv_sec = 1, .tv_nsec = 0}; // 1s std::vector fds; - fds.emplace_back(pollfd({server.get_socket().get_fd(), POLLIN, 0})); - struct pollfd *stdin_fd = &fds.emplace_back(pollfd({STDIN_FILENO, POLLIN, 0})); - bool should_prompt = true; + fds.emplace_back(pollfd({.fd = server.get_socket().get_fd(), .events = POLLIN, .revents = 0})); + struct pollfd *stdin_fd = &fds.emplace_back(pollfd({.fd = STDIN_FILENO, .events = POLLIN, .revents = 0})); int nb_ready = 0; - std::signal(SIGINT, signal_handler); + // Can return old sig_handler or SIG_ERR on failure, but we dont care either way + (void)std::signal(SIGINT, signal_handler); std::cout << "Welcome to the FileShare CLI !" << "\n\n"; // TODO: Find more stuff to say on startup @@ -733,13 +738,13 @@ void interactive_mode(FileShare::Server &server) { server_run = true; while (server_run) { - display_prompt(state, should_prompt); + display_prompt(state); // TODO: Avoid having 2 polls (here + server) nb_ready = FileShare::Utils::poll(fds, &timeout); if (nb_ready == 0) { - continue; + continue; // Timeout, TODO: Refresh screen if needed } // if (nb_ready < 0) { // // TODO handle Signals or Regular Errors @@ -755,9 +760,9 @@ void interactive_mode(FileShare::Server &server) { std::cout << buffer; } - should_prompt = true; + state.should_prompt = true; if (buffer.find('\n') == std::string::npos) { - std::cout << std::endl; + std::cout << '\n'; } std::stringstream ss(buffer); do { @@ -769,7 +774,7 @@ void interactive_mode(FileShare::Server &server) { } } while(ss); } else { - std::cout << "PULLUP -> Quitting..." << std::endl; + std::cout << "PULLUP -> Quitting...\n"; break; } } @@ -790,3 +795,44 @@ void interactive_mode(FileShare::Server &server) { stdin_fd = &fds.emplace_back(pollfd({.fd = STDIN_FILENO, .events = POLLIN, .revents = 0})); } } + +// TODO: Use ncurses (And PDCurses ?) to remove line-buffering +auto interactive_user_question(const std::string_view &question) -> bool { + std::string input; + + do { + std::cout << question << " (y/n) " << std::flush; + std::getline(std::cin, input); + + string_trim(input); + + if (input == "y" || input == "yes") { + return true; + } + + // TODO: Handle EOF differently + if (input == "n" || input == "no" || std::cin.eof()) { + return false; + } + + std::cout << "\nPlease answer with y/Y or n/N.\n"; + } while (true); +} + +auto interactive_user_input(const std::string_view &question) -> std::string { + std::string input; + + do { + std::cout << question << ": " << std::flush; + std::getline(std::cin, input); + + string_trim(input); + if (!input.empty() || !std::cin) { // EOF or error + break; + } + + std::cout << "Input cannot be empty. Please try again.\n"; + } while (true); + + return input; +} diff --git a/source/cli/main.cpp b/source/cli/main.cpp index fe58f47..d05a6c6 100644 --- a/source/cli/main.cpp +++ b/source/cli/main.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat Nov 11 11:06:03 2023 Francois Michaut -** Last update Sun Aug 24 20:04:30 2025 Francois Michaut +** Last update Sat May 23 17:24:04 2026 Francois Michaut ** ** main.cpp : Main entry point of FileShare CLI */ @@ -12,6 +12,7 @@ #include "FileShareVersion.hpp" #include +#include #include #include @@ -21,6 +22,9 @@ #include #include +// TODO: Check if we can use _CRT_NONSTDC_NO_DEPRECATE under windows +#include + #define DESCRIPTION "" // TODO #define SERVER_ARG "--server" @@ -34,21 +38,26 @@ void interactive_mode(FileShare::Server &server); +auto interactive_user_question(const std::string_view &question) -> bool; +auto interactive_user_input(const std::string_view &question) -> std::string; + static auto setup_args() -> std::shared_ptr { auto parser = std::make_shared("file-share-cli", FILE_SHARE_VERSION); parser->add_description(DESCRIPTION); + // TODO: Add argument groups, and group arguments better + parser->add_argument("-s", SERVER_ARG) .flag() .help("Enable server, allowing other peers to connect. The program will keep running until killed."); parser->add_argument(SERVER_CONFIG_ARG) - .default_value("") + .metavar("PATH") .help("Speficy the path to the server config file."); parser->add_argument(CONFIG_ARG) - .default_value("") + .metavar("PATH") .help("Speficy the path to the peer config file."); parser->add_argument(DEFAULT_SERVER_CONFIG_ARG) @@ -60,11 +69,13 @@ static auto setup_args() -> std::shared_ptr { .help("Use the default peer config."); parser->add_argument("-c", CONNECT_ARG) - .nargs(1, 2) - .help("Connect to an external server. Format '-c ip [port]'. Required to execute commands."); + .nargs(1, 2) // TODO: Instead of using nargs, use a custom action() that would parse the IP and Port in an IPv4 Endpoint. Also change format to ip[:port] + .metavar("IP [PORT]") // TODO: Remove the extra "..." that argparse adds + .help("Connect to an external server. Format: '-c ip [port]'. Required to execute commands."); parser->add_argument("-e", EXECUTE_ARG) .append() + .metavar("COMMAND") .help("Add a command to be executed. Repeat the flag to add multiple commands."); parser->add_argument("-i", INTERACTIVE_ARG) @@ -76,6 +87,7 @@ static auto setup_args() -> std::shared_ptr { return parser; } +// TODO: Replace this by calling the corresponding functions in interactive.cpp static void execute_command(std::shared_ptr &peer, const std::string& cmd) { std::stringstream ss(cmd); std::string str; @@ -134,29 +146,70 @@ static void run_server(FileShare::Server &server) { } } -static auto get_server_config(std::shared_ptr &parser) -> FileShare::ServerConfig { - auto server_config_path = parser->get(SERVER_CONFIG_ARG); +template auto get_config(std::shared_ptr &parser, std::string_view config_arg, std::string_view default_config_arg, Config default_config) -> Config { + auto config_path = parser->present(config_arg); - if (parser->get(DEFAULT_SERVER_CONFIG_ARG)) { - return FileShare::Server::default_config(); + if (parser->get(default_config_arg)) { + return default_config; } - return FileShare::ServerConfig::load(server_config_path); + if (config_path) { + return Config::load(config_path.value()); + } + return Config::load(); // Load default config path } -static auto get_default_peer_config(std::shared_ptr &parser) -> FileShare::Config { - auto config_path = parser->get(CONFIG_ARG); +static auto get_configs(std::shared_ptr &parser, bool is_a_tty) -> std::pair { + FileShare::ServerConfig server_config; + FileShare::Config peer_config; + bool first_time_create_configs = false; + + try { + server_config = get_config(parser, SERVER_CONFIG_ARG, DEFAULT_SERVER_CONFIG_ARG, FileShare::Server::default_config()); + } catch (const FileShare::Errors::Config::NotFoundError &error) { + if (parser->is_used(SERVER_CONFIG_ARG)) { + throw error; + } + if (!is_a_tty) { + std::cerr << "Server config not found and cannot ask to create it in non-interactive mode. Please create a config file or use " DEFAULT_SERVER_CONFIG_ARG " to create a new one with the default settings." << std::endl; + throw error; + } + + first_time_create_configs = interactive_user_question("Server config not found. Do you want to create a new one with default settings?"); - if (parser->get(DEFAULT_CONFIG_ARG)) { - return FileShare::Server::default_peer_config(); + if (first_time_create_configs) { + std::string device_name = interactive_user_input("Enter a distinguisable name for this device (will be visible by other peers)"); + + if (device_name.empty()) { // User closed STDIN -> TODO Handle this better + throw std::runtime_error("Device name cannot be empty"); + } + server_config = FileShare::Server::default_config(); + server_config.set_device_name(device_name); + std::cout << "Selected device name: " << std::quoted(device_name) << std::endl; + server_config.save(); + } else { + throw error; + } } - return FileShare::Config::load(config_path); + try { + peer_config = get_config(parser, CONFIG_ARG, DEFAULT_CONFIG_ARG, FileShare::Server::default_peer_config()); + } catch (const FileShare::Errors::Config::NotFoundError &error) { + if (parser->is_used(CONFIG_ARG)) { + throw error; + } + if (!first_time_create_configs) { + std::cerr << "Default peer config not found. Check the permissions of the file or use " DEFAULT_CONFIG_ARG " to create a new one with default settings." << std::endl; + throw error; + } + peer_config = FileShare::Server::default_peer_config(); + peer_config.save(); + } + + return {server_config, peer_config}; } -static void run(std::shared_ptr &parser) { +static void run(std::shared_ptr &parser, bool is_a_tty) { auto cmds = parser->get>(EXECUTE_ARG); - - FileShare::ServerConfig config = get_server_config(parser); - FileShare::Config peer_config = get_default_peer_config(parser); + auto [config, peer_config] = get_configs(parser, is_a_tty); config.set_server_disabled(!parser->get(SERVER_ARG)); FileShare::Server server(config, peer_config); @@ -178,7 +231,7 @@ static void run(std::shared_ptr &parser) { } } } - if (parser->get(INTERACTIVE_ARG)) { + if (parser->get(INTERACTIVE_ARG) || (is_a_tty && !parser->is_used(EXECUTE_ARG))) { interactive_mode(server); } else if (!server.disabled()) { std::cout << "Running Server: Listening on " << server.get_server_endpoint().to_string() << " (Press CTRL+C to stop)" << std::endl; @@ -188,6 +241,7 @@ static void run(std::shared_ptr &parser) { auto main(int argc, char *argv[]) -> int { std::shared_ptr parser = setup_args(); + bool is_a_tty = isatty(fileno(stdin)); try { parser->parse_args(argc, argv); @@ -204,6 +258,6 @@ auto main(int argc, char *argv[]) -> int { return 1; } - run(parser); + run(parser, is_a_tty); return 0; } diff --git a/source/cli/utils.cpp b/source/cli/utils.cpp new file mode 100644 index 0000000..87852b5 --- /dev/null +++ b/source/cli/utils.cpp @@ -0,0 +1,35 @@ +/* +** Project FileShare, 2026 +** +** Author Francois Michaut +** +** Started on Fri May 22 17:10:27 2026 Francois Michaut +** Last update Tue May 26 05:34:41 2026 Francois Michaut +** +** utils.cpp : Utility functions +*/ + +#include +#include +#include + +void string_trim(std::string &str) { + bool found_first_non_space = false; + const auto is_space_wrapper = [](auto chr) { return std::isspace(chr); }; + const auto remove_first_spaces = [&](auto chr) { + if (!std::isspace(chr)) { + found_first_non_space = true; + } + return !found_first_non_space; + }; + + const auto last_non_space = std::ranges::find_if_not(str.rbegin(), str.rend(), is_space_wrapper); + const auto tmp_end = std::remove_if(str.begin(), last_non_space.base(), remove_first_spaces); + const auto tmp_str = std::string_view(str.begin(), tmp_end); + + str.erase(tmp_end, str.end()); +} + +void string_lowercase(std::string &str) { + std::ranges::transform(str, str.begin(), [](unsigned char chr){ return std::tolower(chr); }); +} From c4a8e0453f59312136dca9e2937a00498feaffce Mon Sep 17 00:00:00 2001 From: Francois Michaut Date: Sat, 20 Jun 2026 09:51:01 -0400 Subject: [PATCH 2/2] CLI - Implement Transfers command - Allow to list pending transfers (not ongoing ones yet) - Allow to accept incomming transfer requests --- CMakeLists.txt | 4 +- dependencies/CMakeLists.txt | 4 +- source/cli/interactive.cpp | 116 +++++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 27cd812..7ea8488 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Sat Jan 15 01:19:13 2022 Francois Michaut -## Last update Fri May 22 18:43:01 2026 Francois Michaut +## Last update Fri Jun 12 01:02:32 2026 Francois Michaut ## ## CMakeLists.txt : Top level CMake ## @@ -30,7 +30,7 @@ endif() project(FileShare VERSION 0.1.0 LANGUAGES C CXX) configure_file(include/FileShareVersion.hpp.in FileShareVersion.hpp) -add_subdirectory(dependencies) +add_subdirectory(dependencies EXCLUDE_FROM_ALL) include_directories( "${CMAKE_CURRENT_BINARY_DIR}" # For the configured Version file diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 274eaa3..089ba99 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Wed Jul 10 10:27:39 2024 Francois Michaut -## Last update Sat May 23 17:02:38 2026 Francois Michaut +## Last update Sat Jun 20 10:49:33 2026 Francois Michaut ## ## CMakeLists.txt : CMake to fetch and build the dependecies of the FileShare executable ## @@ -40,7 +40,7 @@ FetchContent_Declare( FetchContent_Declare( fsp GIT_REPOSITORY https://github.com/FileShare-Project/libfsp.git - GIT_TAG 822ed4ce6ad4229652b5b4269ff70fbf845ea1ed + GIT_TAG 5c02001451f5d611a5ba205fc6252e0ae1f2f9a6 ) FetchContent_MakeAvailable(fsp argparse SFML sqlite_orm tgui) diff --git a/source/cli/interactive.cpp b/source/cli/interactive.cpp index bbd138f..96095fc 100644 --- a/source/cli/interactive.cpp +++ b/source/cli/interactive.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Jul 23 13:49:52 2025 Francois Michaut -** Last update Wed May 27 12:06:41 2026 Francois Michaut +** Last update Sat Jun 20 10:39:22 2026 Francois Michaut ** ** interactive.cpp : Interractive Mode logic */ @@ -12,6 +12,7 @@ #include "FileShare/Peer/Peer.hpp" #include "FileShare/Peer/PeerBase.hpp" #include "FileShare/Peer/PreAuthPeer.hpp" +#include "FileShare/Protocol/RequestData.hpp" #include "FileShareVersion.hpp" #include @@ -153,10 +154,11 @@ static void display_interactive_help() { std::cout << "\t\t" << "- RECEIVE_FILE : Request a file download\n"; std::cout << "\t\t" << "- SEND_FILE : Send a file to the peer.\n"; - std::cout << "\t" << "- TRANSFERS (LIST|ACCEPT): (Interractive) Display or manage transfers:\n"; - std::cout << "\t\t" << "- LIST: Display ongoing Downloads and Uploads transfers and their " + std::cout << "\t" << "- TRANSFERS (LIST|ACCEPT): Display or manage transfers:\n"; + std::cout << "\t\t" << "- LIST: (default) Display ongoing Downloads and Uploads transfers and their " "progress, as well as pending transfer requests.\n"; std::cout << "\t\t" << "- ACCEPT : Accept a pending transfer request.\n"; + std::cout << "\t\t" << "- REJECT : Reject a pending transfer request.\n"; std::cout << "\nInterractive commands will prompt for arguments selection and/or display " "choices to select from.\n"; @@ -179,9 +181,38 @@ static auto operator<<(std::ostream &out, const FileShare::ServerConfig &config) "\tDEVICE_NAME = " << config.get_device_name() << '\n' << "\tPRIVATE_KEYS_DIR = " << config.get_private_keys_dir() << '\n' << "\tPRIVATE_KEY_NAME = " << config.get_private_key_name() << '\n' << + "\tSERVER_ENDPOINT = " << config.get_server_endpoint().to_string() << '\n' << '}'; } +static void display_transfer(const FileShare::Protocol::Request &request, const FileShare::Peer_ptr &peer, std::size_t index) { + std::cout << index << ". From: " << peer->get_device_uuid() << ": " + << request.code << " - "; + switch (request.code) { + case FileShare::Protocol::CommandCode::SEND_FILE: { + auto data = std::dynamic_pointer_cast(request.request); + + std::cout << data->filepath << '\n'; + break; + } + case FileShare::Protocol::CommandCode::RECEIVE_FILE: { + auto data = std::dynamic_pointer_cast(request.request); + + std::cout << data->filepath << '\n'; + break; + } + case FileShare::Protocol::CommandCode::LIST_FILES: { + auto data = std::dynamic_pointer_cast(request.request); + + std::cout << data->folderpath << '\n'; + break; + } + default: { + std::cerr << "Unhandled request type: " << request.code << '\n'; + } + } +} + static auto get_peer(InteractiveState &state, std::size_t index, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { auto pending_peers = state.pending_peers; auto peers = state.peers; @@ -190,13 +221,16 @@ static auto get_peer(InteractiveState &state, std::size_t index, bool include_pe !include_pending && include_connected ? " connected" : "" ); - if (index <= pending_peers.size()) { + if (include_pending && index <= pending_peers.size()) { auto peer = *std::next(pending_peers.begin(), static_cast(index - 1)); return peer; } - if (index - pending_peers.size() <= peers.size()) { - auto peer = *std::next(peers.begin(), static_cast(index - pending_peers.size() - 1)); + if (include_pending && include_connected) { + index -= pending_peers.size(); + } + if (include_connected && index <= peers.size()) { + auto peer = *std::next(peers.begin(), static_cast(index - 1)); return peer; } @@ -214,8 +248,8 @@ static auto get_peer(InteractiveState &state, std::stringstream &ss, bool includ return get_peer(state, index, include_pending, include_connected); } -static auto get_peer(InteractiveState &state, const std::string &input, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { - std::stringstream ss{input}; +static auto get_peer(InteractiveState &state, std::string input, bool include_pending = false, bool include_connected = true) -> FileShare::PeerBase_ptr { + std::stringstream ss{std::move(input)}; return get_peer(state, ss, include_pending, include_connected); } @@ -287,7 +321,10 @@ static void config_set_command(InteractiveState &state, std::string_view config, static const ConfigSetterMap setter_map = { {"device_name", &FileShare::ServerConfig::set_device_name}, {"private_keys_dir", &FileShare::ServerConfig::set_private_keys_dir}, - {"private_key_name", &FileShare::ServerConfig::set_private_key_name} + {"private_key_name", &FileShare::ServerConfig::set_private_key_name}, + {"server_endpoint", [](FileShare::ServerConfig *config, std::string value) -> FileShare::ServerConfig & { + return config->set_server_endpoint(value); + }} }; std::string name; @@ -470,7 +507,7 @@ static void peer_disconnect_command(InteractiveState &state, std::stringstream & peer_list_command(state, true); return; } - auto peer = get_peer(state, args, true, false); + auto peer = get_peer(state, args, true, true); if (peer) { peer->disconnect(); @@ -602,7 +639,53 @@ static void execute_command(InteractiveState &state, std::stringstream &args) { } static void transfers_command(InteractiveState &state, std::stringstream &args) { - // TODO: List Transfers + std::string subcommand; + + args >> subcommand; + string_lowercase(subcommand); + + if (subcommand.empty()) { + subcommand = "list"; + } + if (subcommand == "list") { + std::cout << "TODO: List Ongoing Transfers\n"; + + std::size_t index = 1; + std::cout << "Pending Transfer Requests :\n"; + for (auto &event : state.pending_events) { + if (!event.is_request()) { + continue; + } + auto request = event.request().value(); + auto peer = std::dynamic_pointer_cast(event.peer()); + + display_transfer(request, peer, index); + index++; + } + } else if (subcommand == "accept" || subcommand == "reject") { + std::size_t index = 0; + + if (!(args >> index) || index == 0 || index > state.pending_events.size()) { + std::cerr << "Invalid Input ! Please enter a valid transfer ID.\n"; + return; + } + + auto iter = std::next(state.pending_events.begin(), index - 1); + auto &event = *iter; + auto peer = std::dynamic_pointer_cast(event.peer()); + + if (subcommand == "accept") { + peer->respond_to_request(event.request().value(), FileShare::Protocol::StatusCode::STATUS_OK); + std::cout << "Transfer accepted.\n"; + } else { + peer->respond_to_request(event.request().value(), FileShare::Protocol::StatusCode::FORBIDDEN); + std::cout << "Transfer rejected.\n"; + } + + state.pending_events.erase(iter); + } else { + std::cerr << "Unknown Transfers Subcommand " << std::quoted(subcommand, '\'') << '\n'; + } } static void handle_main_menu(InteractiveState &state, const std::string &input) { @@ -690,16 +773,6 @@ static void display_prompt(InteractiveState &state) { if (pending_requests > 0) { ss << (empty ? "" : ", ") << "Pending Requests: " << pending_requests; empty = false; - - std::cout << "TODO: REMOVE - Accepting all pending requests\n"; - for (auto &event : state.pending_events) { - if (event.REQUEST) { - auto peer = std::dynamic_pointer_cast(event.peer()); - - peer->respond_to_request(event.request().value(), FileShare::Protocol::StatusCode::STATUS_OK); - } - } - state.pending_events.clear(); } std::cout << ss.str() << (empty ? "" : ".\n"); @@ -780,7 +853,6 @@ void interactive_mode(FileShare::Server &server) { } if (nb_ready > 0) { server.process_events(accept_cb, request_cb); // Server poll + handle events - // TODO: Smth happened on server -> \r and refresh screen }