From 72e757ebd6ec8767541a37b229370265c510a035 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 25 Apr 2025 17:42:02 +1200 Subject: [PATCH] Multiple directories --- src/config.cpp | 51 +++++++++++--------- src/config.hpp | 7 +-- src/main.cpp | 65 ++++++++++++++++--------- src/servers.cpp | 43 ++++++++--------- src/service_runner.cpp | 6 ++- src/services.cpp | 38 +++++++++------ src/templates.cpp | 92 ++++++++++++++++++++++++++++++++++-- src/templates.hpp | 7 +++ src/utils/directories.cpp | 45 +++++++++++------- src/utils/directories.hpp | 10 ++-- src/utils/envmanager.cpp | 60 ++++++++++++++++++++++- src/utils/envmanager.hpp | 7 ++- templates/example/README.txt | 22 +++++++++ templates/example/_common.sh | 4 ++ 14 files changed, 344 insertions(+), 113 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 6dafcfe..00fd8b1 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -30,17 +30,14 @@ bool config::load_config() { envmanager config_env(config_path); if (!config_env.load()) - { - std::cerr << "Warning: Unable to read configuration file: " << config_path << std::endl; - return false; - } + return false; - mLocalConfigPath = config_env.get_variable_substituted("local.config.directory"); - if (mLocalConfigPath.empty()) - { - std::cerr << "Warning: User directory not set in config" << std::endl; + std::string mDirectories = config_env.get_variable_substituted("local.config.directories"); + if (mDirectories.empty()) return false; - } + + // Split the directories string into a vector of strings + mLocalConfigPaths = string2multi(mDirectories); //std::cout << "Local config path: " << mLocalConfigPath << std::endl; return true; @@ -48,7 +45,7 @@ bool config::load_config() { void config::save_config() { - if (mLocalConfigPath.empty()) + if (mLocalConfigPaths.empty()) { std::cerr << "Warning: Unable to save configuration file, as DropShell has not been initialised."<< std::endl; std::cerr << "Please run 'dropshell init ' to initialise DropShell." << std::endl; @@ -57,36 +54,46 @@ void config::save_config() std::string config_path = get_local_dropshell_config_path(); envmanager config_env(config_path); - config_env.set_variable("local.config.directory", mLocalConfigPath); + + config_env.set_variable("local.config.directories", multi2string(mLocalConfigPaths)); config_env.save(); } bool config::is_config_set() const { - return !mLocalConfigPath.empty(); + return !mLocalConfigPaths.empty(); } -bool config::get_local_config_directory(std::string& path) const { - path = mLocalConfigPath; - return !path.empty(); + +const std::vector & config::get_local_config_directories() const +{ + return mLocalConfigPaths; } -void config::init_local_config_directory(const std::string& path) { +bool config::add_local_config_directory(const std::string &path) +{ if (path.empty()) - throw std::runtime_error("Warning: Unable to initialise local config directory, as the path is empty."); + return false; // Convert to canonical path fs::path abs_path = fs::canonical(path); // The directory must exist if (!fs::exists(abs_path)) { - throw std::runtime_error("The local config directory does not exist: " + abs_path.string()); + std::cerr << "Error: The local config directory does not exist: " << abs_path.string() << std::endl; + return false; } - mLocalConfigPath = abs_path.string(); - save_config(); - std::cout << "Local config directory initialized to: " << abs_path.string() << std::endl; + // Add to config paths if not already there + std::string path_str = abs_path.string(); + if (std::find(mLocalConfigPaths.begin(), mLocalConfigPaths.end(), path_str) == mLocalConfigPaths.end()) { + mLocalConfigPaths.push_back(path_str); + return true; + } + + std::cerr << "Warning: The local config directory is already registered: " << path_str << std::endl; + std::cerr << "No changes made to the DropShell configuration." << std::endl; + return false; } - } // namespace dropshell \ No newline at end of file diff --git a/src/config.hpp b/src/config.hpp index 2ef159f..63a8206 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace dropshell { @@ -13,11 +14,11 @@ class config { bool is_config_set() const; - bool get_local_config_directory(std::string& path) const; - void init_local_config_directory(const std::string& path); + const std::vector & get_local_config_directories() const; + bool add_local_config_directory(const std::string& path); private: - std::string mLocalConfigPath; + std::vector mLocalConfigPaths; }; diff --git a/src/main.cpp b/src/main.cpp index a1abf7f..af71871 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,7 +11,7 @@ #include #include #include - +#include namespace dropshell { void print_version() { @@ -30,7 +30,7 @@ void print_help() { std::cout << std::endl; std::cout << " help Show this help message" << std::endl; std::cout << " version Show version information" << std::endl; - std::cout << " init DIR Initialise the local dropshell directory (local config, backups, etc)" << std::endl; + std::cout << " init DIR Add a local dropshell config directory (you can add several)" << std::endl; std::cout << std::endl; std::cout << std::endl; std::cout << "Server commands:" << std::endl; @@ -44,6 +44,11 @@ void print_help() { std::cout << std::endl; std::cout << "Standard commands: install, backup, uninstall, start, stop" << std::endl; std::cout << std::endl; + std::cout << std::endl; + std::cout << "Creation commands: (apply to the first local config directory)"< 1) { - cmd = argv[1]; + if (argc == 1) { + dropshell::print_help(); + return 0; } + std::string cmd = argv[1]; + + // silently attempt to load the config file. + cfg->load_config(); // don't load old config if we're initializing if (cmd == "init") { std::string lcd; - if (boost::filesystem::exists(dropshell::get_local_dropshell_config_path())) { - std::cerr << "DropShell is already initialised in " << dropshell::get_local_dropshell_config_path() << std::endl; - std::cerr << "Please manually delete this old config file and re-run the init command." << std::endl; - return 1; - } if (argc < 3) { std::cerr << "Error: init command requires a directory argument" << std::endl; return 1; } try { - cfg->init_local_config_directory(argv[2]); + if (!cfg->add_local_config_directory(argv[2])) + return 1; // error already reported + cfg->save_config(); + std::cout << "Config directory added: " << cfg->get_local_config_directories().back() << std::endl; + if (cfg->get_local_config_directories().size() ==1) + std::cout << "DropShell is now initialised and you can add a server with 'dropshell create-server '" << std::endl; + else + { + std::cout << "DropShell will now use all of the following directories for configuration:" << std::endl; + for (const auto& dir : cfg->get_local_config_directories()) { + std::cout << " " << dir << std::endl; + } + std::cout << "You can edit the config file manually at: " << dropshell::get_local_dropshell_config_path() << std::endl; + } return 0; } catch (const std::exception& e) { std::cerr << "Error in init: " << e.what() << std::endl; @@ -111,9 +127,6 @@ int main(int argc, char* argv[]) { return 0; } - // silently attempt to load the config file. - cfg->load_config(); - // auto completion stuff. std::set commands; std::vector servers = dropshell::get_configured_servers(); @@ -126,13 +139,11 @@ int main(int argc, char* argv[]) { if (cmd == "autocomplete_list_commands") { commands.merge(std::set{ - "help","version" + "help","version","init" }); - if (!boost::filesystem::exists(dropshell::get_local_dropshell_config_path())) - commands.insert("init"); if (cfg->is_config_set()) commands.merge(std::set{ - "servers","templates" + "servers","templates","create-service","create-template","create-server" }); for (const auto& command : commands) { @@ -172,9 +183,11 @@ int main(int argc, char* argv[]) { } std::string lcd; - cfg->get_local_config_directory(lcd); - std::cout << "Local config path: " << lcd << std::endl; - + const std::vector & local_config_directories = cfg->get_local_config_directories(); + std::cout << "Config directories: "; + for (auto & dir : local_config_directories) + std::cout << "["<< dir << "] " << std::endl; + std::cout << std::endl;; // No arguments provided if (argc < 2) { dropshell::print_help(); @@ -197,6 +210,14 @@ int main(int argc, char* argv[]) { return 0; } + if (cmd == "create-template") { + if (argc < 3) { + std::cerr << "Error: create-template requires a template name" << std::endl; + return 1; + } + dropshell::create_template(argv[2]); + return 0; + } // handle running a command. for (const auto& command : commands) { if (cmd == command) { diff --git a/src/servers.cpp b/src/servers.cpp index 0097c1a..995206d 100644 --- a/src/servers.cpp +++ b/src/servers.cpp @@ -5,6 +5,7 @@ #include "utils/envmanager.hpp" #include "utils/directories.hpp" #include "services.hpp" +#include "config.hpp" #include #include #include @@ -18,39 +19,37 @@ namespace dropshell { std::vector get_configured_servers() { std::vector servers; - std::string servers_dir = get_local_config_servers_path(); - if (servers_dir.empty()) { + std::vector local_config_directories = get_global_config()->get_local_config_directories(); + if (local_config_directories.empty()) return servers; - } - if (!fs::exists(servers_dir)) { - std::cerr << "Error: Servers directory not found:" << servers_dir << std::endl; - return servers; - } + for (int i = 0; i < local_config_directories.size(); i++) { + std::string servers_dir = get_local_config_servers_path(i); + if (!servers_dir.empty() && fs::exists(servers_dir)) { - for (const auto& entry : fs::directory_iterator(servers_dir)) { - if (fs::is_directory(entry)) { - std::string server_name = entry.path().filename().string(); + for (const auto& entry : fs::directory_iterator(servers_dir)) { + if (fs::is_directory(entry)) { + std::string server_name = entry.path().filename().string(); - server_env env(server_name); - if (!env.is_valid()) { - std::cerr << "Error: Invalid server environment file: " << server_name << std::endl; - continue; + server_env env(server_name); + if (!env.is_valid()) { + std::cerr << "Error: Invalid server environment file: " << entry.path().string() << std::endl; + continue; + } + servers.push_back({ + server_name, + env.get_SSH_HOST(), + env.get_SSH_USER(), + env.get_SSH_PORT() + }); + } } - servers.push_back({ - server_name, - env.get_SSH_HOST(), - env.get_SSH_USER(), - env.get_SSH_PORT() - }); } } return servers; } - - void list_servers() { auto servers = get_configured_servers(); diff --git a/src/service_runner.cpp b/src/service_runner.cpp index 3b49ad1..8b3bb52 100644 --- a/src/service_runner.cpp +++ b/src/service_runner.cpp @@ -275,7 +275,11 @@ bool service_runner::backup() { } // Create backups directory locally if it doesn't exist - std::string local_backups_dir = get_local_config_backups_path(); + std::string local_backups_dir = get_local_config_backups_path(0); + if (local_backups_dir.empty()) { + std::cerr << "Error: Local backups directory not found - is DropShell initialised?" << std::endl; + return false; + } if (!fs::exists(local_backups_dir)) { fs::create_directories(local_backups_dir); } diff --git a/src/services.cpp b/src/services.cpp index 99e5146..9ab256a 100644 --- a/src/services.cpp +++ b/src/services.cpp @@ -2,6 +2,7 @@ #include "utils/envmanager.hpp" #include "utils/directories.hpp" #include "templates.hpp" +#include "config.hpp" #include #include @@ -15,25 +16,32 @@ std::vector get_server_services_info(const std::string& server_name if (server_name.empty()) return services; - std::string serverpath = get_local_config_servers_path(); - if (serverpath.empty()) { - std::cerr << "Error: Server directory not found: " << serverpath << std::endl; + std::vector local_config_directories = get_global_config()->get_local_config_directories(); + if (local_config_directories.empty()) { + std::cerr << "Error: No local config directories found" << std::endl; + std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl; return services; } - fs::path server_dir = fs::path(serverpath) / server_name; - if (!fs::exists(server_dir)) { - std::cerr << "Error: Server directory not found:" << server_dir.string() << std::endl; - return services; - } - for (const auto& entry : fs::directory_iterator(server_dir)) { - if (fs::is_directory(entry)) { - ServiceInfo service = get_service_info(server_name, entry.path().filename().string()); - if (!service.template_name.empty()) { - // std::cout << "Service: " << service.service_name << " found in " << server_dir.string() << ", with template: " << service.template_name << std::endl; - services.push_back(service); - } + + for (int i = 0; i < getNumConfigDirectories(); i++) { + std::string serverpath = get_local_config_servers_path(i); + if (serverpath.empty()) { + std::cerr << "Error: Server directory not found: " << serverpath << std::endl; + return services; } + fs::path server_dir = fs::path(serverpath) / server_name; + if (fs::exists(server_dir)) { + for (const auto& entry : fs::directory_iterator(server_dir)) { + if (fs::is_directory(entry)) { + ServiceInfo service = get_service_info(server_name, entry.path().filename().string()); + if (!service.template_name.empty()) { + services.push_back(service); + } + } + } + } // end of for (int i = 0; i < getNumConfigDirectories(); i++) } + return services; } diff --git a/src/templates.cpp b/src/templates.cpp index fa87fcd..9fe7b18 100644 --- a/src/templates.cpp +++ b/src/templates.cpp @@ -3,6 +3,7 @@ #include "utils/directories.hpp" #include #include +#include #include #include #include @@ -39,9 +40,19 @@ bool get_templates(std::vector& templates) { } } }; - - add_templates_from_dir(get_local_config_templates_path()); - add_templates_from_dir(get_local_system_templates_path()); + + // add templates from the local config directories + for (int i = 0; i < getNumConfigDirectories(); i++) { + std::string path = get_local_config_templates_path(i); + if (path.empty()) { + std::cerr << "Error: Templates directory not found: " << path << std::endl; + return false; + } + add_templates_from_dir(path); + } + + // add templates from the system templates directory + add_templates_from_dir(get_local_system_templates_path()); return true; } @@ -94,6 +105,79 @@ void list_templates() { for (const auto& t : templates) { std::cout << std::left << std::setw(20) << t.name << t.path << std::endl; } -} +} + +void create_template(const std::string& template_name) { + // 1. Create a new directory in the user templates directory + std::vector local_config_directories = get_global_config()->get_local_config_directories(); + + if (local_config_directories.empty()) { + std::cerr << "Error: No local config directories found" << std::endl; + std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl; + return; + } + + template_info info; + if (get_template_info(template_name, info)) { + std::cerr << "Error: Template '" << template_name << "' already exists at " << info.path << std::endl; + return; + } + + std::string user_templates_dir = local_config_directories[0] + "/templates"; + std::string new_template_path = user_templates_dir + "/" + template_name; + + // Create the new template directory + std::filesystem::create_directories(new_template_path); + + // 2. Copy the example template from the system templates directory + std::string system_templates_dir = get_local_system_templates_path(); + std::string example_template_path = system_templates_dir + "/example"; + + if (!std::filesystem::exists(example_template_path)) { + std::cerr << "Error: Example template not found at " << example_template_path << std::endl; + return; + } + + // Copy all files from example template to new template + for (const auto& entry : std::filesystem::recursive_directory_iterator(example_template_path)) { + std::string relative_path = entry.path().string().substr(example_template_path.length()); + std::string target_path = new_template_path + relative_path; + + if (entry.is_directory()) { + std::filesystem::create_directory(target_path); + } else { + std::filesystem::copy_file(entry.path(), target_path); + } + } + + // modify the TEMPLATE=example line in the service.env file to TEMPLATE= + std::string search_string = "TEMPLATE="; + std::string replacement_line = "TEMPLATE=" + template_name; + // replace the line in the example/service.env file with the replacement line + std::string service_env_path = new_template_path + "/example/service.env"; + + + // 3. Print out the README.txt file and the path + std::string readme_path = new_template_path + "/README.txt"; + if (std::filesystem::exists(readme_path)) { + std::cout << "\nREADME contents:" << std::endl; + std::cout << std::string(60, '-') << std::endl; + + std::ifstream readme_file(readme_path); + if (readme_file.is_open()) { + std::string line; + while (std::getline(readme_file, line)) { + std::cout << line << std::endl; + } + readme_file.close(); + } + std::cout << std::string(60, '-') << std::endl; + } else { + std::cout << "No README.txt file found in the template." << std::endl; + } + + std::cout << std::endl; + std::cout << "Template '" << template_name << "' created at " << new_template_path << std::endl; +} } // namespace dropshell \ No newline at end of file diff --git a/src/templates.hpp b/src/templates.hpp index ada160a..3e5d895 100644 --- a/src/templates.hpp +++ b/src/templates.hpp @@ -24,4 +24,11 @@ bool get_template_info(const std::string& name, template_info& info); bool template_command_exists(const std::string& template_name,const std::string& command); void list_templates(); + +// create a template +// 1. create a new directory in the user templates directory +// 2. copy the example template from the system templates directory into the new directory +// 3. print out the README.txt file in the new template directory, and the path to the new template +void create_template(const std::string& template_name); + } // namespace dropshell diff --git a/src/utils/directories.cpp b/src/utils/directories.cpp index ffab664..afed437 100644 --- a/src/utils/directories.cpp +++ b/src/utils/directories.cpp @@ -28,35 +28,41 @@ std::string get_local_system_templates_path() return "/opt/dropshell/templates"; } -std::string get_local_config_path() +int getNumConfigDirectories() { config *cfg = get_global_config(); - std::string user_dir; - if (!cfg->get_local_config_directory(user_dir)) { - return std::string(); - } - return user_dir; + std::vector local_config_directories = cfg->get_local_config_directories(); + return local_config_directories.size(); } -std::string get_local_config_templates_path() +std::string get_local_config_path(int index) { - std::string config_path = get_local_config_path(); + config *cfg = get_global_config(); + std::vector local_config_directories = cfg->get_local_config_directories(); + if (index < 0 || index >= local_config_directories.size()) + return std::string(); + return local_config_directories[index]; +} + +std::string get_local_config_templates_path(int index) +{ + std::string config_path = get_local_config_path(index); if (config_path.empty()) return std::string(); return config_path + "/templates"; } -std::string get_local_config_servers_path() +std::string get_local_config_servers_path(int index) { - std::string config_path = get_local_config_path(); + std::string config_path = get_local_config_path(index); if (config_path.empty()) return std::string(); return config_path + "/servers"; } -std::string get_local_config_backups_path() +std::string get_local_config_backups_path(int index) { - std::string config_path = get_local_config_path(); + std::string config_path = get_local_config_path(index); if (config_path.empty()) return std::string(); return config_path + "/backups"; @@ -64,12 +70,15 @@ std::string get_local_config_backups_path() std::string get_local_server_path(const std::string &server_name) { - if (server_name.empty()) - return std::string(); - std::string config_path = get_local_config_path(); - if (config_path.empty()) - return std::string(); - return config_path + "/servers/" + server_name; + config *cfg = get_global_config(); + std::vector local_config_directories = cfg->get_local_config_directories(); + for (auto &dir : local_config_directories) { + std::string server_path = dir + "/servers/" + server_name; + if (fs::exists(server_path)) { + return server_path; + } + } + return std::string(); } std::string get_local_server_env_path(const std::string &server_name) diff --git a/src/utils/directories.hpp b/src/utils/directories.hpp index 7c55ff2..3a27b58 100644 --- a/src/utils/directories.hpp +++ b/src/utils/directories.hpp @@ -8,10 +8,12 @@ namespace dropshell { // local paths - return empty string on failure std::string get_local_dropshell_config_path(); std::string get_local_system_templates_path(); - std::string get_local_config_path(); - std::string get_local_config_templates_path(); - std::string get_local_config_servers_path(); - std::string get_local_config_backups_path(); + + int getNumConfigDirectories(); + std::string get_local_config_path(int index); + std::string get_local_config_templates_path(int index); + std::string get_local_config_servers_path(int index); + std::string get_local_config_backups_path(int index); std::string get_local_server_path(const std::string &server_name); std::string get_local_server_env_path(const std::string &server_name); diff --git a/src/utils/envmanager.cpp b/src/utils/envmanager.cpp index 1624d4d..92283ba 100644 --- a/src/utils/envmanager.cpp +++ b/src/utils/envmanager.cpp @@ -97,7 +97,7 @@ void envmanager::clear_variables() { m_variables.clear(); } -std::string envmanager::trim(std::string str) const { +std::string trim(std::string str) { // Trim leading whitespace str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) { return !std::isspace(ch); @@ -132,4 +132,62 @@ std::string envmanager::expand_patterns(std::string str) const { return result; } +std::string multi2string(std::vector values) +{ + std::string result; + for (const auto& value : values) { + // remove any " contained in the string value, if present + std::string quoteless_value = value; + quoteless_value.erase(std::remove(quoteless_value.begin(), quoteless_value.end(), '"'), quoteless_value.end()); + result += "\"" + trim(quoteless_value) + "\","; + } + if (!result.empty()) + result.pop_back(); // Remove the last comma + + return result; +} + +std::vector string2multi(std::string values) +{ + std::vector result; + + // Return values separated by commas, but ignore commas within quotes + bool inside_quotes = false; + std::string current_item; + + for (char c : values) { + if (c == '"') { + inside_quotes = !inside_quotes; + } else if (c == ',' && !inside_quotes) { + if (!current_item.empty()) { + // Remove quotes if present + if (current_item.front() == '"' && current_item.back() == '"') { + current_item = current_item.substr(1, current_item.length() - 2); + } + std::string final = trim(current_item); + if (!final.empty()) { + result.push_back(final); + } + current_item.clear(); + } + } else { + current_item += c; + } + } + + // Add the last item if not empty + if (!current_item.empty()) { + // Remove quotes if present + if (current_item.front() == '"' && current_item.back() == '"') { + current_item = current_item.substr(1, current_item.length() - 2); + } + std::string final = trim(current_item); + if (!final.empty()) { + result.push_back(final); + } + } + + return result; +} + } // namespace dropshell diff --git a/src/utils/envmanager.hpp b/src/utils/envmanager.hpp index 149c621..b533953 100644 --- a/src/utils/envmanager.hpp +++ b/src/utils/envmanager.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace dropshell { // envmanager is a class that manages the environment files for the application. @@ -36,7 +37,6 @@ class envmanager { void clear_variables(); private: - std::string trim(std::string str) const; std::string expand_patterns(std::string str) const; private: @@ -44,6 +44,11 @@ class envmanager { std::map m_variables; }; +// utility functions + std::string trim(std::string str); +std::string multi2string(std::vector values); +std::vector string2multi(std::string values); + } // namespace dropshell #endif diff --git a/templates/example/README.txt b/templates/example/README.txt index e69de29..c2f052d 100644 --- a/templates/example/README.txt +++ b/templates/example/README.txt @@ -0,0 +1,22 @@ +DropShell Template Example + +Shell scripts defined in this folder are run as DropShell commands on the remote server (not locally!). +All scripts are passed the server-specific service environment (SSSE) file as an argument. + +The default SSSE file used when a new service is created is in example/service.env. This must exist, +and must at minimum contain the TEMPLATE= variable. + +The backups script gets a second argument, which is the backup file to create (a single tgz file). + +Mandatory scripts are: +- install.sh +- uninstall.sh +- start.sh +- stop.sh + +Optional standard scripts are: +- backup.sh +- status.sh +- ports.sh +- logs.sh + diff --git a/templates/example/_common.sh b/templates/example/_common.sh index 6a5d2cf..8dfab27 100755 --- a/templates/example/_common.sh +++ b/templates/example/_common.sh @@ -63,6 +63,10 @@ grey_end() { } create_and_start_container() { + if [ -z "$1" ] || [ -z "$2" ]; then + die "Template error: create_and_start_container " + fi + local run_cmd="$1" local container_name="$2"