diff --git a/source/src/commands/destroy.cpp b/source/src/commands/destroy.cpp index 62c72c7..5ac6fc6 100644 --- a/source/src/commands/destroy.cpp +++ b/source/src/commands/destroy.cpp @@ -79,6 +79,8 @@ namespace dropshell LocalServiceInfo service_info; service_info = get_service_info(server, service); bool service_valid = SIvalid(service_info); + if (service_valid) + gTemplateManager().pull_for_template(service_info.template_name); if (!service_valid) warning << "No valid service definition found for " << service << std::endl; diff --git a/source/src/commands/help.cpp b/source/src/commands/help.cpp index c16e317..084378e 100644 --- a/source/src/commands/help.cpp +++ b/source/src/commands/help.cpp @@ -158,7 +158,8 @@ int help_handler(const CommandContext& ctx) { show_command("create-template"); info << std::endl; show_command("validate-template"); - show_command("publish-template"); + info << std::endl; + show_command("pull"); } return 0; } diff --git a/source/src/commands/install.cpp b/source/src/commands/install.cpp index c0e7e65..b10c255 100644 --- a/source/src/commands/install.cpp +++ b/source/src/commands/install.cpp @@ -90,6 +90,9 @@ namespace dropshell maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server); + // Pull latest template from git if applicable + gTemplateManager().pull_for_template(service_info.template_name); + // Check if template exists template_info tinfo = gTemplateManager().get_template_info(service_info.template_name); if (!tinfo.is_set()) diff --git a/source/src/commands/list-templates.cpp b/source/src/commands/list-templates.cpp index 2d05584..508eb2d 100644 --- a/source/src/commands/list-templates.cpp +++ b/source/src/commands/list-templates.cpp @@ -27,7 +27,7 @@ struct ListTemplatesCommandRegister { "list-templates", "List all available templates and their sources.", R"( -List all available templates from configured local paths and registries. +List all available templates from template_paths.json sources. list-templates Show template names and where they come from. )" }); diff --git a/source/src/commands/publish-template.cpp b/source/src/commands/publish-template.cpp deleted file mode 100644 index 2b76200..0000000 --- a/source/src/commands/publish-template.cpp +++ /dev/null @@ -1,601 +0,0 @@ -#include "command_registry.hpp" -#include "config.hpp" -#include "templates.hpp" -#include "utils/utils.hpp" -#include "utils/directories.hpp" -#include "utils/hash.hpp" -#include "shared_commands.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dropshell { - -void publish_template_autocomplete(const CommandContext& ctx); -int publish_template_handler(const CommandContext& ctx); - -static std::vector publish_template_name_list = {"publish-template", "publish"}; - -// Static registration -struct PublishTemplateCommandRegister { - PublishTemplateCommandRegister() { - CommandRegistry::instance().register_command({ - publish_template_name_list, - publish_template_handler, - publish_template_autocomplete, - false, // hidden - true, // requires_config - true, // requires_install - 1, // min_args - 3, // max_args (--all + optional registry + directory) - "publish-template [--all] [REGISTRY] DIRECTORY", - "Publish a template to a template registry.", - R"HELP( -Publishes a template directory to a template registry. - -Usage: - ds publish-template DIRECTORY - ds publish-template REGISTRY_NAME DIRECTORY - ds publish-template --all DIRECTORY - ds publish-template --all REGISTRY_NAME DIRECTORY - -Arguments: - --all Publish all templates in subdirectories of DIRECTORY. - Only publishes subdirectories containing template_info.env - REGISTRY_NAME Optional. Name of the template registry to publish to. - If not specified, uses the first registry with a token. - DIRECTORY Path to the template directory (or parent directory with --all). - -The template is validated before publishing. Two tags are created: - - YYYYMMDD (e.g., 20251228) - - latest - -Authentication: - Token is resolved in this order: - 1. Token configured in dropshell.json for the registry - 2. SOS_WRITE_TOKEN environment variable (if single registry defined) - -Requirements: - - Template must pass validation - - Valid authentication token - - curl must be available - -Example: - ds publish-template ./my-template - ds publish-template main ./my-template - ds publish-template --all ./templates-dir - SOS_WRITE_TOKEN=xxx ds publish-template ./my-template - )HELP" - }); - } -} publish_template_command_register; - - -void publish_template_autocomplete(const CommandContext& ctx) { - if (ctx.args.size() == 0) { - rawout << "--all" << std::endl; - // List registry names - std::vector registries = gConfig().get_template_registry_urls(); - for (const auto& reg : registries) { - if (!reg.token.empty()) { - rawout << reg.name << std::endl; - } - } - } -} - -// Get current date as YYYYMMDD (UTC) -static std::string get_date_tag() { - auto now = std::time(nullptr); - auto tm = *std::gmtime(&now); - std::ostringstream oss; - oss << std::put_time(&tm, "%Y%m%d"); - return oss.str(); -} - -// Create tarball of template -static bool create_tarball(const std::string& template_dir, const std::string& tarball_path) { - std::filesystem::path dir_path(template_dir); - std::string template_name = dir_path.filename().string(); - std::string parent_dir = dir_path.parent_path().string(); - - std::string cmd = "tar -czf \"" + tarball_path + "\" -C \"" + parent_dir + "\" \"" + template_name + "\" 2>/dev/null"; - int ret = system(cmd.c_str()); - - return ret == 0 && std::filesystem::exists(tarball_path); -} - -// Run curl and capture output -static bool run_curl(const std::string& cmd, std::string& output, int& http_code) { - // Append -w to get HTTP status code - std::string full_cmd = cmd + " -w '\\nHTTP_CODE:%{http_code}' 2>&1"; - - FILE* pipe = popen(full_cmd.c_str(), "r"); - if (!pipe) return false; - - char buffer[512]; - output.clear(); - while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { - output += buffer; - } - - int ret = pclose(pipe); - - // Extract HTTP code from output - size_t code_pos = output.find("HTTP_CODE:"); - if (code_pos != std::string::npos) { - http_code = std::stoi(output.substr(code_pos + 10)); - output = output.substr(0, code_pos); - // Remove trailing newline - while (!output.empty() && (output.back() == '\n' || output.back() == '\r')) { - output.pop_back(); - } - } else { - http_code = 0; - } - - return ret == 0 || http_code == 200; -} - -// Check if file already exists on server by hash -static bool check_file_exists(const std::string& server_url, const std::string& file_hash) { - std::string cmd = "curl -s \"" + server_url + "/exists/" + file_hash + "\""; - std::string output; - int http_code; - - if (!run_curl(cmd, output, http_code)) { - return false; - } - - // Response is JSON: {"exists": true} or {"exists": false} - return output.find("\"exists\":true") != std::string::npos || - output.find("\"exists\": true") != std::string::npos; -} - -// Get unpacked hash for a template:tag from server metadata -// Returns empty string if not found or error -static std::string get_remote_unpacked_hash(const std::string& server_url, const std::string& labeltag) { - std::string cmd = "curl -s \"" + server_url + "/meta/" + labeltag + "\""; - std::string output; - int http_code; - - if (!run_curl(cmd, output, http_code) || http_code != 200) { - return ""; - } - - // Parse unpackedhash from JSON response - // Look for "unpackedhash":"" or "unpackedhash": "" - size_t pos = output.find("\"unpackedhash\""); - if (pos == std::string::npos) { - return ""; - } - - // Find the colon and opening quote - pos = output.find(':', pos); - if (pos == std::string::npos) return ""; - - pos = output.find('"', pos); - if (pos == std::string::npos) return ""; - - pos++; // skip opening quote - size_t end = output.find('"', pos); - if (end == std::string::npos) return ""; - - return output.substr(pos, end - pos); -} - -// Upload a new file to the server -static bool upload_file(const std::string& server_url, const std::string& token, - const std::string& file_path, const std::vector& labeltags, - const std::string& unpacked_hash) { - // Build labeltags JSON array - std::ostringstream labeltags_json; - labeltags_json << "["; - for (size_t i = 0; i < labeltags.size(); i++) { - if (i > 0) labeltags_json << ","; - labeltags_json << "\"" << labeltags[i] << "\""; - } - labeltags_json << "]"; - - // Build metadata JSON - std::ostringstream metadata; - metadata << "{\"labeltags\":" << labeltags_json.str() - << ",\"unpackedhash\":\"" << unpacked_hash << "\"" - << ",\"description\":\"Published by dropshell\"}"; - - // Build curl command for multipart upload - std::string cmd = "curl -s -X PUT " - "-H \"Authorization: Bearer " + token + "\" " - "-F \"file=@" + file_path + "\" " - "-F 'metadata=" + metadata.str() + "' " - "\"" + server_url + "/upload\""; - - std::string output; - int http_code; - - if (!run_curl(cmd, output, http_code)) { - error << "Upload failed: " << output << std::endl; - return false; - } - - if (http_code != 200) { - error << "Upload failed with HTTP " << http_code << ": " << output << std::endl; - return false; - } - - return true; -} - -// Update metadata for an existing file -static bool update_metadata(const std::string& server_url, const std::string& token, - const std::string& file_hash, const std::vector& labeltags, - const std::string& unpacked_hash) { - // Build labeltags JSON array - std::ostringstream labeltags_json; - labeltags_json << "["; - for (size_t i = 0; i < labeltags.size(); i++) { - if (i > 0) labeltags_json << ","; - labeltags_json << "\"" << labeltags[i] << "\""; - } - labeltags_json << "]"; - - // Build metadata JSON - std::ostringstream metadata; - metadata << "{\"labeltags\":" << labeltags_json.str() - << ",\"unpackedhash\":\"" << unpacked_hash << "\"" - << ",\"description\":\"Published by dropshell\"}"; - - // Build the full request body - std::ostringstream body; - body << "{\"hash\":\"" << file_hash << "\",\"metadata\":" << metadata.str() << "}"; - - // Build curl command for JSON update - std::string cmd = "curl -s -X PUT " - "-H \"Authorization: Bearer " + token + "\" " - "-H \"Content-Type: application/json\" " - "-d '" + body.str() + "' " - "\"" + server_url + "/update\""; - - std::string output; - int http_code; - - if (!run_curl(cmd, output, http_code)) { - error << "Update failed: " << output << std::endl; - return false; - } - - if (http_code != 200) { - error << "Update failed with HTTP " << http_code << ": " << output << std::endl; - return false; - } - - return true; -} - -// Publish a file to the registry (upload or update metadata if exists) -static bool publish_to_registry(const std::string& server_url, const std::string& token, - const std::string& file_path, const std::vector& labeltags, - const std::string& unpacked_hash) { - // Calculate SHA256 hash of the tarball for deduplication - std::string file_hash = hash_file(file_path); - if (file_hash.empty()) { - error << "Failed to calculate file hash" << std::endl; - return false; - } - - // Check if file already exists on server - bool exists = check_file_exists(server_url, file_hash); - - if (exists) { - // File exists, just update metadata (adds new labeltags) - return update_metadata(server_url, token, file_hash, labeltags, unpacked_hash); - } else { - // File doesn't exist, upload it - return upload_file(server_url, token, file_path, labeltags, unpacked_hash); - } -} - -// Check if a directory is a valid template (has template_info.env at root) -static bool is_valid_template_dir(const std::filesystem::path& dir_path) { - return std::filesystem::exists(dir_path / "template_info.env"); -} - -// Publish a single template directory -// Returns: 0 = success, 1 = error, 2 = skipped (not a template), 3 = unchanged (already up to date) -static int publish_single_template(const std::string& template_dir, const std::string& server_url, - const std::string& token, bool quiet = false, bool skip_if_unchanged = false) { - std::filesystem::path dir_path(template_dir); - std::string template_name = dir_path.filename().string(); - - if (!quiet) { - maketitle("Publishing template: " + template_name); - info << "Directory: " << template_dir << std::endl; - std::cout << std::endl; - } - - // Validate template - if (!quiet) info << "=== Validating Template ===" << std::endl; - if (!template_manager::test_template(template_dir)) { - error << "Template '" << template_name << "' validation failed." << std::endl; - if (!quiet) info << "Run: ds validate-template " << template_dir << std::endl; - return 1; - } - if (!quiet) info << " ✓ Template is valid" << std::endl; - if (!quiet) std::cout << std::endl; - - // Calculate directory hash - if (!quiet) info << "=== Calculating Hash ===" << std::endl; - std::string unpacked_hash = hash_directory_recursive(template_dir); - if (unpacked_hash.empty()) { - error << "Failed to calculate directory hash for " << template_name << std::endl; - return 1; - } - if (!quiet) info << " Hash: " << unpacked_hash << std::endl; - - // Check if unchanged (compare with remote :latest) - if (skip_if_unchanged) { - std::string remote_hash = get_remote_unpacked_hash(server_url, template_name + ":latest"); - if (!remote_hash.empty() && remote_hash == unpacked_hash) { - if (!quiet) { - info << " ✓ Unchanged (matches remote)" << std::endl; - std::cout << std::endl; - } - return 3; // unchanged - } - } - if (!quiet) std::cout << std::endl; - - // Create temp directory and tarball - std::string temp_dir = std::filesystem::temp_directory_path().string() + "/dropshell-publish-" + std::to_string(getpid()); - std::filesystem::create_directories(temp_dir); - - // Cleanup on exit - struct TempDirCleaner { - std::string path; - ~TempDirCleaner() { - std::filesystem::remove_all(path); - } - } cleaner{temp_dir}; - - if (!quiet) info << "=== Creating Package ===" << std::endl; - std::string tarball_path = temp_dir + "/" + template_name + ".tgz"; - if (!create_tarball(template_dir, tarball_path)) { - error << "Failed to create tarball for " << template_name << std::endl; - return 1; - } - - auto file_size = std::filesystem::file_size(tarball_path); - if (!quiet) info << " Created " << template_name << ".tgz (" << (file_size / 1024) << " KB)" << std::endl; - if (!quiet) std::cout << std::endl; - - // Publish with tags - if (!quiet) info << "=== Publishing to Registry ===" << std::endl; - - std::string date_tag = get_date_tag(); - std::vector labeltags = { - template_name + ":" + date_tag, - template_name + ":latest" - }; - - if (!quiet) info << " Tags: " << date_tag << ", latest" << std::endl; - if (!quiet) info << " Uploading..." << std::endl; - - if (!publish_to_registry(server_url, token, tarball_path, labeltags, unpacked_hash)) { - error << "Failed to publish template " << template_name << std::endl; - return 1; - } - - if (!quiet) { - info << " ✓ Published successfully" << std::endl; - std::cout << std::endl; - } - - return 0; -} - -int publish_template_handler(const CommandContext& ctx) { - bool publish_all = false; - std::string registry_name; - std::string target_dir; - - // Parse arguments - check for --all flag - std::vector args; - for (const auto& arg : ctx.args) { - if (arg == "--all" || arg == "-a" || arg == "all") { - publish_all = true; - } else { - args.push_back(arg); - } - } - - // Parse remaining arguments - if (args.size() == 1) { - target_dir = args[0]; - } else if (args.size() == 2) { - registry_name = args[0]; - target_dir = args[1]; - } else if (args.empty()) { - error << "Usage: ds publish-template [--all] [REGISTRY] DIRECTORY" << std::endl; - return 1; - } else { - error << "Too many arguments" << std::endl; - error << "Usage: ds publish-template [--all] [REGISTRY] DIRECTORY" << std::endl; - return 1; - } - - // Resolve directory to absolute path - std::filesystem::path dir_path(target_dir); - if (dir_path.is_relative()) { - dir_path = std::filesystem::current_path() / dir_path; - } - dir_path = std::filesystem::canonical(dir_path); - target_dir = dir_path.string(); - - // Check directory exists - if (!std::filesystem::exists(dir_path) || !std::filesystem::is_directory(dir_path)) { - error << "Directory not found: " << target_dir << std::endl; - return 1; - } - - // Find registry and token - std::vector registries = gConfig().get_template_registry_urls(); - tRegistryEntry* selected_registry = nullptr; - std::string effective_token; - - if (registry_name.empty()) { - // Find first registry with a token - for (auto& reg : registries) { - if (!reg.token.empty()) { - selected_registry = ® - effective_token = reg.token; - break; - } - } - - // Fallback: if no registry has a token, but there's exactly one registry - // and SOS_WRITE_TOKEN is set, use that - if (!selected_registry && registries.size() == 1) { - const char* env_token = std::getenv("SOS_WRITE_TOKEN"); - if (env_token && env_token[0] != '\0') { - selected_registry = ®istries[0]; - effective_token = env_token; - info << "Using SOS_WRITE_TOKEN environment variable for authentication" << std::endl; - } - } - - if (!selected_registry) { - error << "No template registry with a token found" << std::endl; - info << "Either:" << std::endl; - info << " - Add a token to a registry in dropshell.json" << std::endl; - info << " - Set SOS_WRITE_TOKEN environment variable (if only one registry defined)" << std::endl; - return 1; - } - } else { - // Find registry by name - for (auto& reg : registries) { - if (reg.name == registry_name) { - selected_registry = ® - break; - } - } - if (!selected_registry) { - error << "Registry not found: " << registry_name << std::endl; - return 1; - } - - // Use token from config, or fall back to SOS_WRITE_TOKEN env var - if (!selected_registry->token.empty()) { - effective_token = selected_registry->token; - } else { - const char* env_token = std::getenv("SOS_WRITE_TOKEN"); - if (env_token && env_token[0] != '\0') { - effective_token = env_token; - info << "Using SOS_WRITE_TOKEN environment variable for authentication" << std::endl; - } else { - error << "Registry '" << registry_name << "' does not have a token configured" << std::endl; - info << "Set token in dropshell.json or SOS_WRITE_TOKEN environment variable" << std::endl; - return 1; - } - } - } - - // Build server URL (ensure https://) - std::string server_url = selected_registry->url; - if (server_url.substr(0, 8) != "https://" && server_url.substr(0, 7) != "http://") { - server_url = "https://" + server_url; - } - // Remove trailing slash if present - if (!server_url.empty() && server_url.back() == '/') { - server_url.pop_back(); - } - - if (publish_all) { - // Publish all templates in subdirectories - maketitle("Publishing all templates"); - info << "Parent directory: " << target_dir << std::endl; - info << "Registry: " << selected_registry->name << " (" << server_url << ")" << std::endl; - std::cout << std::endl; - - // Find all subdirectories that are valid templates - std::vector template_dirs; - for (const auto& entry : std::filesystem::directory_iterator(dir_path)) { - if (entry.is_directory() && is_valid_template_dir(entry.path())) { - template_dirs.push_back(entry.path()); - } - } - - // Sort alphabetically - std::sort(template_dirs.begin(), template_dirs.end()); - - if (template_dirs.empty()) { - error << "No valid templates found in " << target_dir << std::endl; - info << "Templates must have template_info.env" << std::endl; - return 1; - } - - info << "Found " << template_dirs.size() << " template(s) to publish:" << std::endl; - for (const auto& tdir : template_dirs) { - info << " - " << tdir.filename().string() << std::endl; - } - std::cout << std::endl; - - int success_count = 0; - int unchanged_count = 0; - int fail_count = 0; - std::vector failed_templates; - - for (const auto& tdir : template_dirs) { - int result = publish_single_template(tdir.string(), server_url, effective_token, false, true); - if (result == 0) { - success_count++; - } else if (result == 3) { - unchanged_count++; - } else { - fail_count++; - failed_templates.push_back(tdir.filename().string()); - } - } - - // Summary - std::cout << std::endl; - maketitle("Publish Summary"); - if (success_count > 0) { - info << "Published: " << success_count << " template(s)" << std::endl; - } - if (unchanged_count > 0) { - info << "Unchanged: " << unchanged_count << " template(s)" << std::endl; - } - if (fail_count > 0) { - error << "Failed: " << fail_count << " template(s)" << std::endl; - for (const auto& name : failed_templates) { - error << " - " << name << std::endl; - } - return 1; - } - - return 0; - } else { - // Single template publish - info << "Registry: " << selected_registry->name << " (" << server_url << ")" << std::endl; - - int result = publish_single_template(target_dir, server_url, effective_token, false); - - if (result == 0) { - std::string template_name = dir_path.filename().string(); - maketitle("Publish Complete"); - info << "Template '" << template_name << "' published successfully!" << std::endl; - info << "Tags: " << get_date_tag() << ", latest" << std::endl; - info << "URL: " << server_url << "/" << template_name << ":latest" << std::endl; - } - - return result; - } -} - -} // namespace dropshell diff --git a/source/src/commands/pull.cpp b/source/src/commands/pull.cpp new file mode 100644 index 0000000..bee2631 --- /dev/null +++ b/source/src/commands/pull.cpp @@ -0,0 +1,44 @@ +#include "command_registry.hpp" +#include "templates.hpp" +#include "utils/output.hpp" + +namespace dropshell { + +int pull_handler(const CommandContext& ctx); + +static std::vector pull_name_list = {"pull"}; + +struct PullCommandRegister { + PullCommandRegister() { + CommandRegistry::instance().register_command({ + pull_name_list, + pull_handler, + nullptr, + false, // hidden + true, // requires_config + false, // requires_install + 0, // min_args + 0, // max_args + "pull", + "Pull/clone all git-backed template sources.", + R"( +Pull the latest changes for all git-backed template sources +defined in template_paths.json. + +If a source directory doesn't exist and has a git URL, it will be cloned. +If it exists with a .git directory, it will be pulled. +Local-only sources (no git URL) are skipped. + )" + }); + } +} pull_command_register; + +int pull_handler(const CommandContext& ctx) { + if (!gTemplateManager().is_loaded()) { + error << "Template manager not loaded" << std::endl; + return 1; + } + return gTemplateManager().pull_all() ? 0 : 1; +} + +} // namespace dropshell diff --git a/source/src/commands/restoredata.cpp b/source/src/commands/restoredata.cpp index c028cfb..adf6b5c 100644 --- a/source/src/commands/restoredata.cpp +++ b/source/src/commands/restoredata.cpp @@ -105,6 +105,8 @@ namespace dropshell return 1; } + // Pull latest template from git if applicable + gTemplateManager().pull_for_template(service_info.template_name); if (!gTemplateManager().template_command_exists(service_info.template_name, "backup") || !gTemplateManager().template_command_exists(service_info.template_name, "restore")) diff --git a/source/src/config.cpp b/source/src/config.cpp index 39b705d..30a19f9 100644 --- a/source/src/config.cpp +++ b/source/src/config.cpp @@ -46,37 +46,26 @@ bool config::load_config() { // load json config file. // Validate the config format - check for required and supported fields std::set allowed_fields = { "server_definition_paths", - "template_local_paths", - "template_registries", "backups_path", "log_level", "disabled_servers" }; - + std::set deprecated_fields = { "template_registry_URLs", - "template_upload_token" + "template_upload_token", + "template_registries", + "template_local_paths" }; - + // Check for deprecated fields for (const auto& field : deprecated_fields) { if (mConfig.contains(field)) { - error << "Config file contains deprecated field '" << field << "'" << std::endl; - error << "Please update your config file to the new format." << std::endl; - if (field == "template_registry_URLs") { - error << "Replace 'template_registry_URLs' with 'template_registries' using the format:" << std::endl; - error << " \"template_registries\": [" << std::endl; - error << " {" << std::endl; - error << " \"name\": \"main\"," << std::endl; - error << " \"url\": \"https://templates.dropshell.app\"," << std::endl; - error << " \"token\": \"\"" << std::endl; - error << " }" << std::endl; - error << " ]" << std::endl; - } - return false; + warning << "Config file contains deprecated field '" << field << "' - ignoring it." << std::endl; + mConfig.erase(field); } } - + // Check for unknown fields for (auto& [key, value] : mConfig.items()) { if (allowed_fields.find(key) == allowed_fields.end()) { @@ -92,25 +81,7 @@ bool config::load_config() { // load json config file. return false; } } - - // Validate template_registries format if present - if (mConfig.contains("template_registries")) { - if (!mConfig["template_registries"].is_array()) { - error << "'template_registries' must be an array" << std::endl; - return false; - } - for (const auto& registry : mConfig["template_registries"]) { - if (!registry.is_object()) { - error << "Each registry in 'template_registries' must be an object" << std::endl; - return false; - } - if (!registry.contains("name") || !registry.contains("url")) { - error << "Each registry must have 'name' and 'url' fields" << std::endl; - return false; - } - } - } - + // Validate log_level if present if (mConfig.contains("log_level")) { if (!mConfig["log_level"].is_string()) { @@ -131,15 +102,6 @@ bool config::load_config() { // load json config file. return true; } -void _append(std::vector & a, const std::vector & b) { - if (b.empty()) - return; - if (a.empty()) - a = b; - else - a.insert(std::end(a), std::begin(b), std::end(b)); -} - bool config::save_config() { std::string config_path = localfile::dropshell_json(); @@ -160,19 +122,9 @@ bool config::save_config() mConfig["server_definition_paths"] = { dropshell_base + "/servers" }; - mConfig["template_local_paths"] = { - dropshell_base + "/local_templates" - }; - mConfig["template_registries"] = nlohmann::json::array({ - nlohmann::json::object({ - {"name", "main"}, - {"url", "https://templates.dropshell.app"}, - {"token", ""} - }) - }); mConfig["backups_path"] = dropshell_base + "/backups"; mConfig["log_level"] = "info"; // Default log level - } + } config_file << mConfig.dump(4); config_file.close(); @@ -181,15 +133,13 @@ bool config::save_config() for (auto [key,value] : mConfig.items()) { debug << " " << key << ": " << value << std::endl; } - + return true; } bool config::create_aux_directories() { - std::vector paths; - _append(paths, get_local_template_paths()); - _append(paths, get_local_server_definition_paths()); + std::vector paths = get_local_server_definition_paths(); for (auto & p : paths) if (!std::filesystem::exists(p)) { @@ -200,7 +150,7 @@ bool config::create_aux_directories() } bool config::is_config_set() const -{ +{ return mIsConfigSet; } @@ -209,27 +159,6 @@ bool config::is_agent_installed() return std::filesystem::exists(localfile::bb64()); } -std::vector config::get_template_registry_urls() { - nlohmann::json template_registries = mConfig["template_registries"]; - std::vector registries; - for (auto ®istry : template_registries) { - if (registry.is_object() && !registry.empty()) - registries.push_back(tRegistryEntry(registry)); - } - return registries; -} - -std::vector config::get_local_template_paths() -{ - nlohmann::json template_local_paths = mConfig["template_local_paths"]; - std::vector paths; - for (auto &path : template_local_paths) { - if (path.is_string() && !path.empty()) - paths.push_back(path); - } - return paths; -} - std::vector config::get_local_server_definition_paths() { nlohmann::json server_definition_paths = mConfig["server_definition_paths"]; std::vector paths; @@ -248,14 +177,6 @@ std::string config::get_server_create_path() return paths[0]; } -std::string config::get_template_create_path() -{ - std::vector paths = get_local_template_paths(); - if (paths.empty()) - return ""; - return paths[0]; -} - std::string config::get_backups_path() { nlohmann::json backups_path = mConfig["backups_path"]; @@ -267,39 +188,11 @@ std::string config::get_backups_path() return ""; } -dropshell::tRegistryEntry::tRegistryEntry(nlohmann::json json) -{ - valid = false; - if (json.is_object() && !json.empty()) { - for (auto &[key, value] : json.items()) { - if (value.is_string() && !value.empty()) - switch (switchhash(key.c_str())) { - case switchhash("name"): - name = value; - break; - case switchhash("url"): - url = value; - break; - case switchhash("token"): - token = value; - break; - default: - break; - } - } - valid = (!url.empty()&&!name.empty()); // token can be empty. - } -} - -tRegistryEntry::~tRegistryEntry() -{ -} - std::string config::get_log_level() const { if (!mIsConfigSet || !mConfig.contains("log_level")) return "info"; // Default log level - + return mConfig["log_level"]; } @@ -376,4 +269,4 @@ std::vector config::get_disabled_servers() const return result; } -} // namespace dropshell \ No newline at end of file +} // namespace dropshell \ No newline at end of file diff --git a/source/src/config.hpp b/source/src/config.hpp index 022b276..9021e82 100644 --- a/source/src/config.hpp +++ b/source/src/config.hpp @@ -8,19 +8,6 @@ namespace dropshell { -class tRegistryEntry { - - public: - tRegistryEntry(nlohmann::json json); - ~tRegistryEntry(); - - public: - std::string name; - std::string url; - std::string token; - bool valid; -}; - class config { public: config(); @@ -33,14 +20,11 @@ class config { bool is_config_set() const; static bool is_agent_installed(); - std::vector get_template_registry_urls(); - std::vector get_local_template_paths(); std::vector get_local_server_definition_paths(); std::string get_server_create_path(); - std::string get_template_create_path(); std::string get_backups_path(); - + std::string get_log_level() const; void apply_log_level(); @@ -57,4 +41,4 @@ class config { config & gConfig(); -} // namespace dropshell \ No newline at end of file +} // namespace dropshell \ No newline at end of file diff --git a/source/src/services.cpp b/source/src/services.cpp index 74ffffc..be6f025 100644 --- a/source/src/services.cpp +++ b/source/src/services.cpp @@ -23,7 +23,7 @@ namespace dropshell !service_info.user.empty(); } - std::vector get_server_services_info(const std::string &server_name, bool skip_update) + std::vector get_server_services_info(const std::string &server_name) { std::vector services; @@ -49,7 +49,7 @@ namespace dropshell std::string dirname = entry.path().filename().string(); if (dirname.empty() || dirname[0] == '.' || dirname[0] == '_') continue; - auto service = get_service_info(server_name, dirname, skip_update); + auto service = get_service_info(server_name, dirname); if (!service.local_service_path.empty()) services.push_back(service); else @@ -72,7 +72,7 @@ namespace dropshell return it->second == "true"; } - LocalServiceInfo get_service_info(const std::string &server_name, const std::string &service_name, bool skip_update) + LocalServiceInfo get_service_info(const std::string &server_name, const std::string &service_name) { if (server_name.empty() || service_name.empty()) @@ -114,15 +114,12 @@ namespace dropshell } service.template_name = it->second; - tinfo = gTemplateManager().get_template_info(service.template_name,skip_update); + tinfo = gTemplateManager().get_template_info(service.template_name); if (!tinfo.is_set()) { - // Template not found - this means it's not available locally OR from registry - error << "Template '" << service.template_name << "' not found locally or in registry" << std::endl; + error << "Template '" << service.template_name << "' not found in local template paths" << std::endl; return LocalServiceInfo(); } - - // Template is available (either locally or downloaded from registry) service.local_template_path = tinfo.local_template_path(); { // set the user. @@ -208,70 +205,4 @@ namespace dropshell return backups; } - // bool get_all_service_env_vars(const std::string &server_name, const std::string &service_name, ordered_env_vars &all_env_vars) - // { - // clear_vars(all_env_vars); - - // if (localpath::service(server_name, service_name).empty() || !fs::exists(localpath::service(server_name, service_name))) - // { - // error << "Service not found: " << service_name << " on server " << server_name << std::endl; - // return false; - // } - - // // Lambda function to load environment variables from a file - // auto load_env_file = [&all_env_vars](const std::string &file) - // { - // if (!file.empty() && std::filesystem::exists(file)) - // { - // ordered_env_vars env_vars; - // envmanager env_manager(file); - // env_manager.load(); - // env_manager.get_all_variables(env_vars); - // merge_vars(all_env_vars, env_vars); - // } - // else - // warning << "Expected environment file not found: " << file << std::endl; - // }; - - - // // add in some simple variables first, as others below may depend on/use these in bash. - // set_var(all_env_vars, "SERVER", server_name); - // set_var(all_env_vars, "SERVICE", service_name); - // set_var(all_env_vars, "DOCKER_CLI_HINTS", "false"); // turn off docker junk. - - // // Load environment files - // load_env_file(localfile::service_env(server_name, service_name)); - - // std::string template_name = get_var(all_env_vars, "TEMPLATE"); - // if (template_name.empty()) - // { - // error << "TEMPLATE variable not defined in service " << service_name << " on server " << server_name << std::endl; - // return false; - // } - // auto tinfo = gTemplateManager().get_template_info(template_name, true); // skip updates. - // if (!tinfo.is_set()) - // { - // // Template is not available locally or from registry - // error << "Template '" << template_name << "' not found locally or in registry" << std::endl; - // return false; - // } - // ASSERT(std::filesystem::exists(tinfo.local_template_info_env_path())); - // load_env_file(tinfo.local_template_info_env_path()); - - // std::string user = get_var(all_env_vars, "SSH_USER"); - // if (user.empty()) - // { - // error << "SSH_USER variable not defined in service " << service_name << " on server " << server_name << std::endl; - // info << "This variable definition is always required, and usually set in the "< #include #include -//#include #include "utils/ordered_env.hpp" namespace dropshell { @@ -18,15 +17,13 @@ namespace dropshell { bool requires_host_root; bool requires_docker; bool requires_docker_root; - //bool service_template_hash_match; }; - - bool SIvalid(const LocalServiceInfo& service_info); - - // if skip_update, don't check for updates to the service template. - std::vector get_server_services_info(const std::string& server_name, bool skip_update=false); - LocalServiceInfo get_service_info(const std::string& server_name, const std::string& service_name, bool skip_update=false); + bool SIvalid(const LocalServiceInfo& service_info); + + std::vector get_server_services_info(const std::string& server_name); + + LocalServiceInfo get_service_info(const std::string& server_name, const std::string& service_name); std::set get_used_commands(const std::string& server_name, const std::string& service_name); // list all backups for a given service (across all servers) diff --git a/source/src/templates.cpp b/source/src/templates.cpp index a740186..7b978c1 100644 --- a/source/src/templates.cpp +++ b/source/src/templates.cpp @@ -11,419 +11,303 @@ #include "utils/directories.hpp" #include "utils/utils.hpp" #include "utils/output.hpp" + #include "utils/execute.hpp" #include "templates.hpp" - #include "config.hpp" + #include "config.hpp" #include "utils/hash.hpp" namespace dropshell { // ------------------------------------------------------------------------------------------------ - // template_source_local + // template_manager — loading and resolution // ------------------------------------------------------------------------------------------------ - std::set template_source_local::get_template_list() { - std::set templates; - - // Helper function to add templates from a directory - auto add_templates_from_dir = [&templates](const std::string& dir_path) { - if (!std::filesystem::exists(dir_path)) - return; - - for (const auto& entry : std::filesystem::directory_iterator(dir_path)) - if (entry.is_directory()) - templates.insert(entry.path().filename().string()); - }; - - add_templates_from_dir(mLocalPath); - return templates; - } - - bool template_source_local::has_template(const std::string& template_name) { - std::filesystem::path path = mLocalPath / template_name; - return (std::filesystem::exists(path)); - } - - bool template_source_local::template_command_exists(const std::string& template_name, const std::string& command) { - std::filesystem::path path = mLocalPath / template_name / (command+".sh"); - return std::filesystem::exists(path); - } - - template_info template_source_local::get_template_info(const std::string& template_name, bool skip_update) { - std::filesystem::path path = mLocalPath / template_name; - - if (!std::filesystem::exists(path)) - return template_info(); - - return template_info( - template_name, - mLocalPath.string(), - path, - skip_update - ); - } - - - - // ------------------------------------------------------------------------------------------------ - // template_source_registry - // ------------------------------------------------------------------------------------------------ - - std::set template_source_registry::get_template_list() + void template_manager::load_sources() { - // Query the registry for available templates - // The registry should return a JSON list of available templates - std::string list_url = mRegistry.url + "/dir"; - - nlohmann::json json_response; - - // For HTTPS URLs, use curl to fetch the JSON - if (list_url.substr(0, 8) == "https://") { - std::string temp_file = "/tmp/dropshell_registry_list_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); - std::string cmd = "curl -fsSL " + quote(list_url) + " -o " + quote(temp_file) + " 2>/dev/null"; - int result = system(cmd.c_str()); - - if (result == 0) { - try { - std::ifstream file(temp_file); - if (file.is_open()) { - file >> json_response; - file.close(); - } - } catch (...) { - // Failed to parse JSON - } - std::filesystem::remove(temp_file); - } - } else { - json_response = get_json_from_url(list_url); - } - - std::set templates; - if (json_response.is_null() || !json_response.contains("entries")) { - warning << "Failed to get template list from registry: " << mRegistry.name << std::endl; - return templates; - } - - // Parse the entries array to extract unique template names - // Only process entries that have labeltags (skip untagged files) - int total_entries = 0; - int skipped_entries = 0; - for (const auto& entry : json_response["entries"]) { - total_entries++; - if (entry.contains("labeltags") && entry["labeltags"].is_array() && !entry["labeltags"].empty()) { - for (const auto& label : entry["labeltags"]) { - // Extract template name from label (format: "template:version") - std::string label_str = label.get(); - size_t colon_pos = label_str.find(':'); - if (colon_pos != std::string::npos) { - std::string template_name = label_str.substr(0, colon_pos); - templates.insert(template_name); - } - } - } else { - skipped_entries++; - // Entry has no labeltags or empty labeltags - skip it - debug << "Skipping registry entry without labeltags" << std::endl; - } - } - - if (skipped_entries > 0) { - debug << "Registry " << mRegistry.name << ": Processed " << (total_entries - skipped_entries) - << " tagged entries, skipped " << skipped_entries << " untagged entries" << std::endl; - } - - return templates; - } + ASSERT(mSources.empty(), "Template manager already loaded (sources are not empty)."); + ASSERT(gConfig().is_config_set(), "Config not set."); + ASSERT(!mLoaded, "Template manager already loaded."); - bool template_source_registry::has_template(const std::string& template_name) - { - // First check if we have it cached - std::filesystem::path cache_dir = get_cache_dir(); - std::filesystem::path template_cache_dir = cache_dir / template_name; - if (std::filesystem::exists(template_cache_dir)) { - // Template found in cache - return true; - } - - // Check if template exists in registry - std::string check_url = mRegistry.url + "/exists/" + template_name + ":latest"; - - // For HTTPS URLs, use curl to fetch the JSON - nlohmann::json json_response; - if (check_url.substr(0, 8) == "https://") { - // Create a temporary file for the response - std::string temp_file = "/tmp/dropshell_registry_check_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); - std::string cmd = "curl -fsSL " + quote(check_url) + " -o " + quote(temp_file) + " 2>/dev/null"; - int result = system(cmd.c_str()); - - if (result == 0) { - try { - std::ifstream file(temp_file); - if (file.is_open()) { - file >> json_response; - file.close(); - } - } catch (const std::exception& e) { - warning << "Failed to parse JSON response from " << check_url << ": " << e.what() << std::endl; - } - std::filesystem::remove(temp_file); - } else { - // curl failed - network issue or server down - return std::filesystem::exists(template_cache_dir); - } - } else { - json_response = get_json_from_url(check_url); - } - - if (!json_response.is_null() && json_response.contains("exists")) { - return json_response["exists"].get(); - } - - // If registry check failed but we have cache, use cache - return std::filesystem::exists(template_cache_dir); - } + // Read template_paths.json from each server_definition_path + auto server_def_paths = gConfig().get_local_server_definition_paths(); + for (const auto& sdp : server_def_paths) { + std::filesystem::path json_path = std::filesystem::path(sdp) / filenames::template_paths_json; + if (!std::filesystem::exists(json_path)) + continue; - template_info template_source_registry::get_template_info(const std::string& template_name, bool skip_update) - { - // Get cache directory - std::filesystem::path cache_dir = get_cache_dir(); - std::filesystem::path template_cache_dir = cache_dir / template_name; - std::filesystem::path template_json_file = cache_dir / (template_name + ".json"); - - // Create cache directory if it doesn't exist - if (!std::filesystem::exists(cache_dir)) { - std::filesystem::create_directories(cache_dir); - } - - // If we have a cached version and can't reach the registry, use the cache - bool have_cache = std::filesystem::exists(template_cache_dir) && std::filesystem::exists(template_json_file); - - // Check if template exists (in cache or registry) - if (!has_template(template_name)) { - return template_info(); - } - - // skip_update: don't bother updating anything - if we have a cached version use that. - if (skip_update && have_cache) { - return template_info( - template_name, - "Registry: " + mRegistry.name + " (cached)", - template_cache_dir, - skip_update - ); - } + std::ifstream f(json_path); + if (!f.is_open()) { + warning << "Could not open " << json_path << std::endl; + continue; + } - // Get metadata from registry to check version - std::string meta_url = mRegistry.url + "/meta/" + template_name + ":latest"; - - nlohmann::json registry_metadata; - - // For HTTPS URLs, use curl to fetch the JSON - if (meta_url.substr(0, 8) == "https://") { - std::string temp_file = "/tmp/dropshell_registry_meta_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); - std::string cmd = "curl -fsSL " + quote(meta_url) + " -o " + quote(temp_file) + " 2>/dev/null"; - int result = system(cmd.c_str()); - - if (result == 0) { - try { - std::ifstream file(temp_file); - if (file.is_open()) { - file >> registry_metadata; - file.close(); - } - } catch (...) { - // Failed to parse JSON - } - std::filesystem::remove(temp_file); - } - } else { - registry_metadata = get_json_from_url(meta_url); - } - - if (registry_metadata.is_null()) { - // If we can't get metadata from registry but have cache, use cache - if (have_cache) { - info << "Registry unavailable, using cached template: " << template_name << std::endl; - return template_info( - template_name, - "Registry: " + mRegistry.name + " (cached)", - template_cache_dir, - skip_update - ); - } - warning << "Failed to get metadata for template: " << template_name << std::endl; - return template_info(); - } - - // Check if we need to download/update the template - bool need_download = true; - std::string registry_version = "unknown"; - std::string registry_unpacked_hash = ""; - - // Extract version and hash from registry metadata - if (registry_metadata.contains("metadata")) { - auto& metadata = registry_metadata["metadata"]; - if (metadata.contains("version")) { - registry_version = metadata["version"].get(); - } - // REQUIRED: unpackedhash - the hash of extracted contents - if (metadata.contains("unpackedhash")) { - registry_unpacked_hash = metadata["unpackedhash"].get(); - //debug << "Found unpackedhash in metadata: " << registry_hash << std::endl; - } else { - // unpackedhash is required for security - error << "Template '" << template_name << "' from registry '" << mRegistry.name - << "' does not provide unpackedhash for integrity verification." << std::endl; - error << "This template cannot be downloaded for security reasons." << std::endl; - error << "Please contact the registry administrator to update the template metadata." << std::endl; - return template_info(); - } - } - - // Check if we have a cached version - if (std::filesystem::exists(template_json_file)) { + nlohmann::json j; try { - std::ifstream cache_file(template_json_file); - nlohmann::json cache_json; - cache_file >> cache_json; - cache_file.close(); - - // Compare versions or hashes - if (cache_json.contains("unpacked_hash") && !registry_unpacked_hash.empty()) - if (cache_json["unpacked_hash"].get() == registry_unpacked_hash) - need_download = false; + j = nlohmann::json::parse(f); + } catch (nlohmann::json::parse_error& ex) { + error << "Failed to parse " << json_path << ": " << ex.what() << std::endl; + continue; + } + + if (!j.is_array()) { + error << json_path << " must be a JSON array" << std::endl; + continue; + } + + for (const auto& entry : j) { + if (!entry.is_string() || entry.get().empty()) + continue; + std::string s = entry.get(); + TemplateSource src; + // Split on first ':' — Linux paths never contain colons + size_t colon = s.find(':'); + if (colon != std::string::npos) { + src.local_path = s.substr(0, colon); + src.git_url = s.substr(colon + 1); + } else { + src.local_path = s; } - catch (...) { - // If reading cache fails, re-download - need_download = true; + mSources.push_back(src); } } - - // Download and extract if needed - if (need_download) { - info << "Downloading template '" << template_name << "' from registry..." << std::endl; - - // Download the .tgz file - std::string download_url = mRegistry.url + "/" + template_name + ":latest"; - std::filesystem::path temp_tgz = cache_dir / (template_name + ".tgz"); - - if (!download_file(download_url, temp_tgz.string())) { - error << "Failed to download template: " << template_name << std::endl; - return template_info(); - } - - // Remove old template directory if it exists - if (std::filesystem::exists(template_cache_dir)) { - std::filesystem::remove_all(template_cache_dir); - } - - // Extract the .tgz file - std::string extract_cmd = "tar -xzf " + quote(temp_tgz.string()) + " -C " + quote(cache_dir.string()); - int result = system(extract_cmd.c_str()); - if (result != 0) { - error << "Failed to extract template: " << template_name << std::endl; - std::filesystem::remove(temp_tgz); - return template_info(); - } - - // Clean up the .tgz file - std::filesystem::remove(temp_tgz); - - // Calculate actual hash of extracted template - std::string actual_unpacked_hash = hash_directory_recursive(template_cache_dir.string()); - - // Verify the extracted template hash matches what registry claimed - // unpackedhash is required, so registry_hash should always be set here - if (registry_unpacked_hash.empty()) { - // This shouldn't happen as we check for it above, but handle it just in case - error << "Internal error: unpackedhash was not properly set" << std::endl; - std::filesystem::remove_all(template_cache_dir); - return template_info(); - } - - if (actual_unpacked_hash != registry_unpacked_hash) { - error << "Template hash verification failed!" << std::endl; - error << "Expected unpacked hash: " << registry_unpacked_hash << std::endl; - error << "Actual unpacked hash: " << actual_unpacked_hash << std::endl; - error << "The downloaded template '" << template_name << "' may be corrupted or tampered with." << std::endl; - - // Remove the corrupted template - std::filesystem::remove_all(template_cache_dir); - return template_info(); - } - - info << "Template extracted successfully. SHA256: " << actual_unpacked_hash << std::endl; - - std::filesystem::path template_info_env_path = template_cache_dir / filenames::template_info_env; - if (!std::filesystem::exists(template_info_env_path)) { - error << "template_info.env doesn't exist in template '" << template_name << "'" << std::endl; - std::filesystem::remove_all(template_cache_dir); - return template_info(); - } - - // Update cache JSON file - nlohmann::json cache_json; - cache_json["template"] = template_name; - cache_json["version"] = registry_version; - cache_json["unpacked_hash"] = actual_unpacked_hash; // Store actual calculated hash - cache_json["registry"] = mRegistry.name; - cache_json["last_updated"] = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - - std::ofstream cache_file(template_json_file); - cache_file << cache_json.dump(4); - cache_file.close(); - - info << "Template '" << template_name << "' downloaded and cached successfully" << std::endl; - } - - // Return template info pointing to the cached template - return template_info( - template_name, - "Registry: " + mRegistry.name, - template_cache_dir, - skip_update - ); + + // Resolve templates from all sources + resolve_templates(); + mLoaded = true; } - bool template_source_registry::template_command_exists(const std::string& template_name, const std::string& command) + void template_manager::resolve_templates() { - // Get template info to ensure it's downloaded and cached - auto tinfo = get_template_info(template_name, false); - if (!tinfo.is_set()) { - return false; - } - - // Check if the command script exists in the cached template - std::filesystem::path script_path = tinfo.local_template_path() / (command + ".sh"); - return std::filesystem::exists(script_path); + mTemplateMap.clear(); + for (size_t i = 0; i < mSources.size(); ++i) + resolve_source(i); } - std::filesystem::path template_source_registry::get_cache_dir() + void template_manager::resolve_source(size_t source_index) { - return localpath::template_cache(); + const auto& src = mSources[source_index]; + if (!std::filesystem::exists(src.local_path) || !std::filesystem::is_directory(src.local_path)) { + if (!src.git_url.empty()) + debug << "Source path does not exist (run 'dropshell pull' to clone): " << src.local_path << std::endl; + else + warning << "Source path does not exist: " << src.local_path << std::endl; + return; + } + + // Try to read dropshell-templates.list + std::filesystem::path list_file = std::filesystem::path(src.local_path) / filenames::dropshell_templates_list; + std::vector template_dirs; + + if (std::filesystem::exists(list_file)) { + template_dirs = parse_list_file(src.local_path); + } else { + // Discover templates by searching for template_info.env + template_dirs = discover_templates(src.local_path); + // Write the discovered list so next time is fast + if (!template_dirs.empty()) + write_list_file(src.local_path, template_dirs); + } + + // Remove stale entries for this source from mTemplateMap before re-adding + for (auto it = mTemplateMap.begin(); it != mTemplateMap.end(); ) { + if (it->second.source_index == source_index) + it = mTemplateMap.erase(it); + else + ++it; + } + + // Add to map (first-wins across sources) + for (const auto& tdir : template_dirs) { + std::filesystem::path full_path = std::filesystem::path(src.local_path) / tdir; + if (!std::filesystem::exists(full_path) || !std::filesystem::is_directory(full_path)) { + debug << "Template directory does not exist: " << full_path << std::endl; + continue; + } + std::string name = full_path.filename().string(); + if (mTemplateMap.find(name) == mTemplateMap.end()) { + mTemplateMap[name] = ResolvedTemplate{full_path, source_index}; + } + } } + std::vector template_manager::parse_list_file(const std::string& local_path) const + { + std::vector result; + std::filesystem::path list_file = std::filesystem::path(local_path) / filenames::dropshell_templates_list; + std::ifstream f(list_file); + if (!f.is_open()) + return result; + std::string line; + while (std::getline(f, line)) { + // Trim whitespace + size_t start = line.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) continue; + size_t end = line.find_last_not_of(" \t\r\n"); + line = line.substr(start, end - start + 1); + // Skip comments and blank lines + if (line.empty() || line[0] == '#') continue; + result.push_back(line); + } + return result; + } + std::vector template_manager::discover_templates(const std::string& local_path) const + { + std::vector result; + std::filesystem::path root(local_path); + for (auto it = std::filesystem::recursive_directory_iterator(root, std::filesystem::directory_options::skip_permission_denied); + it != std::filesystem::recursive_directory_iterator(); ++it) + { + if (!it->is_directory()) continue; + + // Skip .git directories + if (it->path().filename() == ".git") { + it.disable_recursion_pending(); + continue; + } + + // Check if this directory contains template_info.env + if (std::filesystem::exists(it->path() / filenames::template_info_env)) { + // Get relative path from root + std::string rel = std::filesystem::relative(it->path(), root).string(); + result.push_back(rel); + // Don't recurse into template directories (templates don't nest) + it.disable_recursion_pending(); + } + } + return result; + } + + void template_manager::write_list_file(const std::string& local_path, const std::vector& template_dirs) const + { + std::filesystem::path list_file = std::filesystem::path(local_path) / filenames::dropshell_templates_list; + std::ofstream f(list_file); + if (!f.is_open()) { + debug << "Could not write " << list_file << std::endl; + return; + } + f << "# Auto-generated by dropshell — template directories relative to this file" << std::endl; + for (const auto& dir : template_dirs) + f << dir << std::endl; + info << "Wrote " << list_file << " (" << template_dirs.size() << " templates)" << std::endl; + } // ------------------------------------------------------------------------------------------------ - // template_manager + // template_manager — git operations + // ------------------------------------------------------------------------------------------------ + + bool template_manager::git_clone_source(const TemplateSource& source) + { + if (source.git_url.empty()) return false; + info << "Cloning " << source.git_url << " → " << source.local_path << std::endl; + + // Create parent directory if needed + std::filesystem::path parent = std::filesystem::path(source.local_path).parent_path(); + if (!std::filesystem::exists(parent)) + std::filesystem::create_directories(parent); + + std::string cmd = "git clone " + quote(source.git_url) + " " + quote(source.local_path) + " 2>&1"; + std::string output; + ordered_env_vars empty_env; + bool ok = execute_local_command(".", cmd, empty_env, &output, cMode::Silent); + if (!ok) + error << "git clone failed: " << output << std::endl; + else + info << "Cloned successfully." << std::endl; + return ok; + } + + bool template_manager::git_pull_source(const TemplateSource& source) + { + if (!std::filesystem::exists(std::filesystem::path(source.local_path) / ".git")) + return false; + + // Verify remote URL matches if git_url is set + if (!source.git_url.empty()) { + std::string remote_output; + ordered_env_vars empty_env; + std::string check_cmd = "git -C " + quote(source.local_path) + " remote get-url origin 2>/dev/null"; + execute_local_command(".", check_cmd, empty_env, &remote_output, cMode::Silent); + // Trim + while (!remote_output.empty() && (remote_output.back() == '\n' || remote_output.back() == '\r')) + remote_output.pop_back(); + if (!remote_output.empty() && remote_output != source.git_url) { + warning << "Remote URL mismatch for " << source.local_path << std::endl; + warning << " Expected: " << source.git_url << std::endl; + warning << " Actual: " << remote_output << std::endl; + return false; + } + } + + debug << "Pulling " << source.local_path << std::endl; + std::string cmd = "git -C " + quote(source.local_path) + " pull --ff-only 2>&1"; + std::string output; + ordered_env_vars empty_env; + bool ok = execute_local_command(".", cmd, empty_env, &output, cMode::Silent); + if (!ok) + warning << "git pull failed for " << source.local_path << ": " << output << std::endl; + return ok; + } + + bool template_manager::pull_all() + { + bool any_changes = false; + for (const auto& src : mSources) { + if (!std::filesystem::exists(src.local_path)) { + if (!src.git_url.empty()) { + if (git_clone_source(src)) + any_changes = true; + } else { + warning << "Source path does not exist and no git URL configured: " << src.local_path << std::endl; + } + } else if (std::filesystem::exists(std::filesystem::path(src.local_path) / ".git")) { + if (git_pull_source(src)) + any_changes = true; + } + // else: local-only, no .git — skip + } + + // Re-resolve all templates after pulling + resolve_templates(); + return true; + } + + bool template_manager::pull_for_template(const std::string& template_name) + { + auto it = mTemplateMap.find(template_name); + if (it == mTemplateMap.end()) + return false; + + const auto& src = mSources[it->second.source_index]; + if (src.git_url.empty()) + return true; // local-only, nothing to pull + + if (!std::filesystem::exists(std::filesystem::path(src.local_path) / ".git")) + return true; // no .git dir, nothing to pull + + git_pull_source(src); + // Re-resolve this source's templates + resolve_source(it->second.source_index); + return true; + } + + // ------------------------------------------------------------------------------------------------ + // template_manager — queries // ------------------------------------------------------------------------------------------------ void template_manager::list_templates() const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); + ASSERT(mLoaded, "Template manager not loaded."); auto templates = get_template_list(); - + if (templates.empty()) { std::cout << "No templates found." << std::endl; return; } - - std::cout << "Available templates:" << std::endl; - // print templates. - std::cout << std::string(60, '-') << std::endl; + std::cout << "Available templates:" << std::endl; + std::cout << std::string(60, '-') << std::endl; bool first = true; for (const auto& t : templates) { std::cout << (first ? "" : ", ") << t; @@ -435,63 +319,61 @@ std::set template_manager::get_template_list() const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); + ASSERT(mLoaded, "Template manager not loaded."); std::set templates; - for (const auto& source : mSources) { - auto source_templates = source->get_template_list(); - templates.insert(source_templates.begin(), source_templates.end()); - } + for (const auto& [name, _] : mTemplateMap) + templates.insert(name); return templates; } std::vector> template_manager::get_template_list_with_source() const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); + ASSERT(mLoaded, "Template manager not loaded."); std::vector> result; - std::set seen; - for (const auto& source : mSources) { - auto source_templates = source->get_template_list(); - for (const auto& t : source_templates) { - if (!seen.insert(t).second) - continue; - if (source->template_command_exists(t, "install")) - result.push_back({t, source->get_description()}); - } + for (const auto& [name, resolved] : mTemplateMap) { + // Only include templates that have install.sh + if (!std::filesystem::exists(resolved.template_dir / "install.sh")) + continue; + const auto& src = mSources[resolved.source_index]; + std::string desc = src.git_url.empty() + ? "Local: " + src.local_path + : "Git: " + src.local_path; + result.push_back({name, desc}); } return result; } bool template_manager::has_template(const std::string &template_name) const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); - template_source_interface* source = get_source(template_name); - if (!source) - return false; - return true; + ASSERT(mLoaded, "Template manager not loaded."); + return mTemplateMap.count(template_name) > 0; } - template_info template_manager::get_template_info(const std::string &template_name, bool skip_update) const + template_info template_manager::get_template_info(const std::string &template_name) const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); - template_source_interface* source = get_source(template_name); - if (source) - return source->get_template_info(template_name, skip_update); - - // fail - return template_info(); -} + ASSERT(mLoaded, "Template manager not loaded."); + auto it = mTemplateMap.find(template_name); + if (it == mTemplateMap.end()) + return template_info(); + const auto& src = mSources[it->second.source_index]; + return template_info(template_name, src.local_path, it->second.template_dir); + } bool template_manager::template_command_exists(const std::string &template_name, const std::string &command) const { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); - template_source_interface* source = get_source(template_name); - if (!source) { + ASSERT(mLoaded, "Template manager not loaded."); + auto it = mTemplateMap.find(template_name); + if (it == mTemplateMap.end()) { error << "Template '" << template_name << "' not found" << std::endl; return false; } - return source->template_command_exists(template_name, command); + return std::filesystem::exists(it->second.template_dir / (command + ".sh")); } + // ------------------------------------------------------------------------------------------------ + // template_manager — create template + // ------------------------------------------------------------------------------------------------ + // Helper function to write a file with content static bool write_template_file(const std::string& path, const std::string& content, bool executable = false) { std::ofstream file(path); @@ -519,27 +401,29 @@ } // 1. Check template doesn't already exist - auto tinfo = get_template_info(template_name, false); + auto tinfo = get_template_info(template_name); if (tinfo.is_set()) { error << "Template '" << template_name << "' already exists at " << tinfo.locationID() << std::endl; return false; } // 2. Determine where to create the template - auto local_template_paths = gConfig().get_local_template_paths(); - if (local_template_paths.empty()) { - error << "No local template paths found" << std::endl; - info << "Run 'dropshell edit' to add one to the DropShell config" << std::endl; + if (mSources.empty()) { + error << "No template sources configured" << std::endl; + info << "Create a template_paths.json in your server definition path" << std::endl; return false; } - std::string new_template_path = local_template_paths[0] + "/" + template_name; + std::string base_path = mSources[0].local_path; + if (!std::filesystem::exists(base_path)) { + error << "Template source path does not exist: " << base_path << std::endl; + return false; + } + std::string new_template_path = base_path + "/" + template_name; // 3. Create directory structure std::filesystem::create_directories(new_template_path + "/config"); - // 4. Generate template files inline (self-contained, no external dependencies) - - // template_info.env + // 4. Generate template files std::string template_info_env = R"TMPL(# Template metadata - DO NOT EDIT # This file is replaced when the template is updated. @@ -558,7 +442,6 @@ DATA_VOLUME="${CONTAINER_NAME}_data" )TMPL"; if (!write_template_file(new_template_path + "/" + filenames::template_info_env, template_info_env)) return false; - // config/service.env std::string service_env = R"TMPL(# Service identification (REQUIRED) CONTAINER_NAME=)TMPL" + template_name + R"TMPL( @@ -570,95 +453,55 @@ HTTP_PORT=8080 )TMPL"; if (!write_template_file(new_template_path + "/config/" + filenames::service_env, service_env)) return false; - // install.sh std::string install_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Check required environment variables _check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" - -# Check Docker is available _check_docker_installed || _die "Docker test failed" - -# Pull the Docker image docker pull -q "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" - -# Stop any existing container bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true - -# Remove old container _remove_container "$CONTAINER_NAME" 2>/dev/null || true - -# Start the new container bash "$SCRIPT_DIR/start.sh" || _die "Failed to start container" - echo "Installation of ${CONTAINER_NAME} complete" )BASH"; if (!write_template_file(new_template_path + "/install.sh", install_sh, true)) return false; - // uninstall.sh std::string uninstall_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - _check_required_env_vars "CONTAINER_NAME" - -# Stop the container bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true - -# Remove the container (but preserve data volumes!) _remove_container "$CONTAINER_NAME" || _die "Failed to remove container" - -# CRITICAL: Never remove data volumes in uninstall.sh! -# Data volumes must be preserved for potential reinstallation -# Only destroy.sh should remove volumes - echo "Uninstallation of ${CONTAINER_NAME} complete" echo "Note: Data volumes have been preserved. To remove all data, use destroy.sh" )BASH"; if (!write_template_file(new_template_path + "/uninstall.sh", uninstall_sh, true)) return false; - // start.sh std::string start_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" - _check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" "HTTP_PORT" - -# Create data volume if it doesn't exist docker volume create "$DATA_VOLUME" 2>/dev/null || true - -# Start the container docker run -d \ --name "$CONTAINER_NAME" \ --restart unless-stopped \ -p "${HTTP_PORT}:80" \ -v "${DATA_VOLUME}:/usr/share/nginx/html" \ "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to start container" - echo "Container ${CONTAINER_NAME} started on port ${HTTP_PORT}" )BASH"; if (!write_template_file(new_template_path + "/start.sh", start_sh, true)) return false; - // stop.sh std::string stop_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" - _check_required_env_vars "CONTAINER_NAME" - docker stop "$CONTAINER_NAME" 2>/dev/null || true - echo "Container ${CONTAINER_NAME} stopped" )BASH"; if (!write_template_file(new_template_path + "/stop.sh", stop_sh, true)) return false; - // status.sh (REQUIRED for dropshell list command) std::string status_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" - _check_required_env_vars "CONTAINER_NAME" - -# Check if container is running if docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then echo "Running" else @@ -667,58 +510,22 @@ fi )BASH"; if (!write_template_file(new_template_path + "/status.sh", status_sh, true)) return false; - // logs.sh std::string logs_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" - _check_required_env_vars "CONTAINER_NAME" - docker logs "$CONTAINER_NAME" "$@" )BASH"; if (!write_template_file(new_template_path + "/logs.sh", logs_sh, true)) return false; - // README.txt - std::string readme = "Template: " + template_name + R"TMPL( + // 5. Update dropshell-templates.list + std::filesystem::path list_file = std::filesystem::path(base_path) / filenames::dropshell_templates_list; + std::ofstream lf(list_file, std::ios::app); + if (lf.is_open()) { + lf << template_name << std::endl; + lf.close(); + } -This template was created by 'dropshell create-template'. - -QUICK START ------------ -1. Edit config/service.env to customize your deployment -2. Edit template_info.env if you need different Docker settings -3. Modify the scripts as needed for your use case -4. Run 'dropshell validate ' to check for issues - -REQUIRED FILES --------------- -- template_info.env : Template metadata (do not edit) -- config/service.env : Service configuration (edit this!) -- install.sh : Installation script -- uninstall.sh : Uninstallation script (preserves data) -- status.sh : Status check (required for 'dropshell list') - -OPTIONAL FILES --------------- -- start.sh : Start the service -- stop.sh : Stop the service -- logs.sh : View logs -- backup.sh : Backup data -- restore.sh : Restore data -- destroy.sh : Remove service AND data (use with caution!) - -BEST PRACTICES --------------- -1. Always source common.sh: source "${AGENT_PATH}/common.sh" -2. Check required vars: _check_required_env_vars "VAR1" "VAR2" -3. Handle errors: command || _die "Error message" -4. NEVER remove data volumes in uninstall.sh -5. Run 'dropshell validate' before publishing - -For full documentation, see: dropshell help templates -)TMPL"; - if (!write_template_file(new_template_path + "/README.txt", readme)) return false; - - // 5. Print summary + // 6. Print summary std::cout << "\nTemplate '" << template_name << "' created at " << new_template_path << std::endl; std::cout << std::string(60, '-') << std::endl; std::cout << "Next steps:" << std::endl; @@ -730,40 +537,25 @@ For full documentation, see: dropshell help templates return test_template(new_template_path); } - void template_manager::load_sources() - { - ASSERT(mSources.empty(), "Template manager already loaded (sources are not empty)."); - ASSERT(gConfig().is_config_set(), "Config not set."); - ASSERT(!mLoaded, "Template manager already loaded."); - - // Add local template sources only if the paths exist and are directories - auto local_template_paths = gConfig().get_local_template_paths(); - for (const auto& path : local_template_paths) { - if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) { - mSources.push_back(std::make_unique(path)); - } else { - info << "Skipping non-existent or invalid local template path: " << path << std::endl; - } - } - - // Add registry sources - these should always be added - std::vector registry_urls = gConfig().get_template_registry_urls(); - for (const tRegistryEntry & url : registry_urls) { - mSources.push_back(std::make_unique(url)); - } - - mLoaded = true; - } + // ------------------------------------------------------------------------------------------------ + // template_manager — sources display + // ------------------------------------------------------------------------------------------------ void template_manager::print_sources() const { - std::cout << "Template sources: "; - for (const auto& source : mSources) { - std::cout << "[" << source->get_description() << "] "; + std::cout << "Template sources:" << std::endl; + for (const auto& src : mSources) { + std::cout << " [" << src.local_path; + if (!src.git_url.empty()) + std::cout << " ← " << src.git_url; + std::cout << "]" << std::endl; } - std::cout << std::endl; } + // ------------------------------------------------------------------------------------------------ + // template_manager — validation (static) + // ------------------------------------------------------------------------------------------------ + bool template_manager::required_file(std::string path, std::string template_name) { if (!std::filesystem::exists(path)) { @@ -773,17 +565,6 @@ For full documentation, see: dropshell help templates return true; } - template_source_interface *template_manager::get_source(const std::string &template_name) const - { - ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); - for (const auto& source : mSources) { - if (source->has_template(template_name)) { - return source.get(); - } - } - return nullptr; - } - bool template_manager::test_template(const std::string &template_path) { if (template_path.empty()) @@ -803,10 +584,9 @@ For full documentation, see: dropshell help templates }; for (const auto& file : required_files) { - if (!required_file(template_path + "/" + file, template_name)) + if (!required_file(template_path + "/" + file, template_name)) return false; - // check if file is executable, if it ends in .sh std::string suffix=".sh"; if (file.find(suffix) == file.size() - suffix.size()) { @@ -815,9 +595,8 @@ For full documentation, see: dropshell help templates if ((perms & std::filesystem::perms::owner_exec) == std::filesystem::perms::none) error << file << " is not executable" << std::endl; } - } + } - // ------------------------------------------------------------ // check required variables in template_info.env ordered_env_vars all_env_vars; std::vector env_files = { @@ -825,13 +604,11 @@ For full documentation, see: dropshell help templates filenames::template_info_env }; for (const auto& file : env_files) { - { // load service.env from the service on this machine. ordered_env_vars env_vars; envmanager env_manager(template_path + "/" + file); env_manager.load(); env_manager.get_all_variables(env_vars); merge_vars(all_env_vars, env_vars); - } } std::vector required_vars = { @@ -846,7 +623,7 @@ For full documentation, see: dropshell help templates if (it == all_env_vars.end()) { error << "Required variable "<< required_var<<" not defined in " << template_path << std::endl; return false; - } + } } return true; @@ -859,7 +636,6 @@ For full documentation, see: dropshell help templates bool all_passed = true; - // Find all .sh files for (const auto& entry : std::filesystem::recursive_directory_iterator(template_path)) { if (entry.is_regular_file() && entry.path().extension() == ".sh") { std::string script_path = entry.path().string(); @@ -874,9 +650,8 @@ For full documentation, see: dropshell help templates char buffer[256]; std::string output; - while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) output += buffer; - } int result = pclose(pipe); if (result != 0 || !output.empty()) { @@ -890,13 +665,17 @@ For full documentation, see: dropshell help templates return all_passed; } + // ------------------------------------------------------------------------------------------------ + // Singletons and template_info + // ------------------------------------------------------------------------------------------------ + template_manager & gTemplateManager() { static template_manager instance; return instance; } - template_info::template_info(const std::string &template_name, const std::string &location_id, const std::filesystem::path &local_template_path, bool skip_update) : + template_info::template_info(const std::string &template_name, const std::string &location_id, const std::filesystem::path &local_template_path) : mTemplateName(template_name), mLocationID(location_id), mTemplateLocalPath(local_template_path), @@ -908,19 +687,16 @@ For full documentation, see: dropshell help templates error << "Template path does not exist: " << local_template_path << std::endl; return; } - - // if (!skip_update) - // mHash = hash_directory_recursive(local_template_path); } std::filesystem::path template_info::local_template_service_env_path() { - return mTemplateLocalPath / "config" / filenames::service_env ; + return mTemplateLocalPath / "config" / filenames::service_env; } std::filesystem::path template_info::local_template_info_env_path() { - return mTemplateLocalPath / filenames::template_info_env ; + return mTemplateLocalPath / filenames::template_info_env; } } // namespace dropshell diff --git a/source/src/templates.hpp b/source/src/templates.hpp index ffa3f83..9556307 100644 --- a/source/src/templates.hpp +++ b/source/src/templates.hpp @@ -1,26 +1,31 @@ +#pragma once + #include #include #include -#include +#include #include #include "config.hpp" -#define JSON_INLINE_ALL -#include - namespace dropshell { -typedef enum template_source_type { - TEMPLATE_SOURCE_TYPE_LOCAL, - TEMPLATE_SOURCE_TYPE_REGISTRY, - TEMPLATE_SOURCE_NOT_SET -} template_source_type; +// Represents one entry from template_paths.json +struct TemplateSource { + std::string local_path; // Git checkout root or local directory + std::string git_url; // Optional, empty if local-only +}; + +// Represents one resolved template +struct ResolvedTemplate { + std::filesystem::path template_dir; + size_t source_index; // Index into mSources +}; class template_info { public: template_info() : mIsSet(false) {} - template_info(const std::string& template_name, const std::string& location_id, const std::filesystem::path& local_template_path, bool skip_update); + template_info(const std::string& template_name, const std::string& location_id, const std::filesystem::path& local_template_path); virtual ~template_info() {} bool is_set() const { return mIsSet; } std::string name() const { return mTemplateName; } @@ -33,55 +38,11 @@ class template_info { private: std::string mTemplateName; std::string mLocationID; - std::filesystem::path mTemplateLocalPath; // source or cache. + std::filesystem::path mTemplateLocalPath; bool mTemplateValid; bool mIsSet; }; -class template_source_interface { - public: - virtual ~template_source_interface() {} - virtual std::set get_template_list() = 0; - virtual bool has_template(const std::string& template_name) = 0; - virtual template_info get_template_info(const std::string& template_name, bool skip_update=false) = 0; - virtual bool template_command_exists(const std::string& template_name,const std::string& command) = 0; - - virtual std::string get_description() = 0; -}; - -class template_source_registry : public template_source_interface { - public: - template_source_registry(tRegistryEntry registry) : mRegistry(registry) {} - - ~template_source_registry() {} - - std::set get_template_list(); - bool has_template(const std::string& template_name); - template_info get_template_info(const std::string& template_name, bool skip_update=false); - bool template_command_exists(const std::string& template_name,const std::string& command); - - std::string get_description() { return "Registry: " + mRegistry.name + " (" + mRegistry.url + ")"; } - private: - std::filesystem::path get_cache_dir(); - private: - tRegistryEntry mRegistry; - std::vector mTemplates; // cached list. -}; - -class template_source_local : public template_source_interface { - public: - template_source_local(std::string local_path) : mLocalPath(local_path) {} - ~template_source_local() {} - std::set get_template_list(); - bool has_template(const std::string& template_name); - template_info get_template_info(const std::string& template_name, bool skip_update=false); - bool template_command_exists(const std::string& template_name,const std::string& command); - - std::string get_description() { return "Local: " + mLocalPath.string(); } - private: - std::filesystem::path mLocalPath; -}; - class template_manager { public: template_manager() : mLoaded(false) {} @@ -89,9 +50,9 @@ class template_manager { std::set get_template_list() const; bool has_template(const std::string& template_name) const; - template_info get_template_info(const std::string& template_name, bool skip_update=false) const; // skip_update = don't check for updates. + template_info get_template_info(const std::string& template_name) const; - bool template_command_exists(const std::string& template_name,const std::string& command) const; + bool template_command_exists(const std::string& template_name, const std::string& command) const; bool create_template(const std::string& template_name) const; static bool test_template(const std::string& template_path); static bool check_template_shell_scripts_syntax(const std::string& template_path); @@ -105,16 +66,26 @@ class template_manager { bool is_loaded() const { return mLoaded; } int get_source_count() const { return mSources.size(); } + // Git pull operations + bool pull_all(); + bool pull_for_template(const std::string& template_name); + private: static bool required_file(std::string path, std::string template_name); - template_source_interface* get_source(const std::string& template_name) const; + void resolve_templates(); + void resolve_source(size_t source_index); + std::vector parse_list_file(const std::string& local_path) const; + std::vector discover_templates(const std::string& local_path) const; + void write_list_file(const std::string& local_path, const std::vector& template_dirs) const; + bool git_pull_source(const TemplateSource& source); + bool git_clone_source(const TemplateSource& source); private: bool mLoaded; - mutable std::vector> mSources; + std::vector mSources; + std::map mTemplateMap; }; template_manager & gTemplateManager(); - } // namespace dropshell diff --git a/source/src/utils/directories.cpp b/source/src/utils/directories.cpp index 5c1538f..fa5324b 100644 --- a/source/src/utils/directories.cpp +++ b/source/src/utils/directories.cpp @@ -128,11 +128,6 @@ namespace dropshell return dropshell_dir() + "/temp_files"; } - std::string template_cache() - { - return dropshell_dir() + "/template_cache"; - } - std::string template_example() { return agent_local() + "/template_example"; @@ -145,7 +140,6 @@ namespace dropshell dropshell_dir(), agent_local(), agent_remote(), - template_cache(), backups(), temp_files()}; for (auto &p : gConfig().get_local_server_definition_paths()) diff --git a/source/src/utils/directories.hpp b/source/src/utils/directories.hpp index 0d5ef7d..b82974b 100644 --- a/source/src/utils/directories.hpp +++ b/source/src/utils/directories.hpp @@ -20,14 +20,6 @@ namespace dropshell { // |-- agent-remote // |-- (remote agent files) // |-- temp_files - // |-- template_cache - // | |-- .json - // | |-- - // | |-- (...script files...) - // | |-- template_info.env - // | |-- config - // | |-- service.env - // | |-- (...other service config files...) // backups_path // |-- katie-_-squashkiwi-_-squashkiwi-test-_-2025-04-28_21-23-59.tgz @@ -49,6 +41,8 @@ namespace dropshell { static const std::string server_json = "server.json"; static const std::string dropshell_json = "dropshell.json"; static const std::string ds_run = "ds_run.sh"; + static const std::string template_paths_json = "template_paths.json"; + static const std::string dropshell_templates_list = "dropshell-templates.list"; } // namespace filenames. namespace localfile { @@ -77,7 +71,6 @@ namespace dropshell { std::string backups(); std::string temp_files(); - std::string template_cache(); bool create_directories(); } // namespace local