Multiple directories

This commit is contained in:
Your Name 2025-04-25 17:42:02 +12:00
parent 4d3523a346
commit 72e757ebd6
14 changed files with 344 additions and 113 deletions

View File

@ -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 <path>' 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<std::string> & 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

View File

@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <vector>
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<std::string> & get_local_config_directories() const;
bool add_local_config_directory(const std::string& path);
private:
std::string mLocalConfigPath;
std::vector<std::string> mLocalConfigPaths;
};

View File

@ -11,7 +11,7 @@
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
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)"<<std::endl;
std::cout << " create-template TEMPLATE" << std::endl;
std::cout << " create-server SERVER" << std::endl;
std::cout << " create-service SERVER SERVICE" << std::endl;
}
} // namespace dropshell
@ -73,27 +78,38 @@ int main(int argc, char* argv[]) {
try {
dropshell::config *cfg = dropshell::get_global_config();
// Handle commands
std::string cmd;
if (argc > 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 <server-name>'" << 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<std::string> commands;
std::vector<dropshell::ServerInfo> servers = dropshell::get_configured_servers();
@ -126,13 +139,11 @@ int main(int argc, char* argv[]) {
if (cmd == "autocomplete_list_commands") {
commands.merge(std::set<std::string>{
"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<std::string>{
"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<std::string> & 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) {

View File

@ -5,6 +5,7 @@
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "services.hpp"
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <iomanip>
@ -18,39 +19,37 @@ namespace dropshell {
std::vector<ServerInfo> get_configured_servers() {
std::vector<ServerInfo> servers;
std::string servers_dir = get_local_config_servers_path();
if (servers_dir.empty()) {
std::vector<std::string> 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();

View File

@ -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);
}

View File

@ -2,6 +2,7 @@
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "templates.hpp"
#include "config.hpp"
#include <boost/filesystem.hpp>
#include <iostream>
@ -15,25 +16,32 @@ std::vector<ServiceInfo> 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<std::string> 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;
}

View File

@ -3,6 +3,7 @@
#include "utils/directories.hpp"
#include <filesystem>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
@ -39,9 +40,19 @@ bool get_templates(std::vector<template_info>& 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<std::string> 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=<template_name>
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

View File

@ -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

View File

@ -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<std::string> 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<std::string> 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<std::string> 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)

View File

@ -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);

View File

@ -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<std::string> 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<std::string> string2multi(std::string values)
{
std::vector<std::string> 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

View File

@ -3,6 +3,7 @@
#include <string>
#include <map>
#include <vector>
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<std::string, std::string> m_variables;
};
// utility functions
std::string trim(std::string str);
std::string multi2string(std::vector<std::string> values);
std::vector<std::string> string2multi(std::string values);
} // namespace dropshell
#endif

View File

@ -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=<template_name> 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

View File

@ -63,6 +63,10 @@ grey_end() {
}
create_and_start_container() {
if [ -z "$1" ] || [ -z "$2" ]; then
die "Template error: create_and_start_container <run_cmd> <container_name>"
fi
local run_cmd="$1"
local container_name="$2"