#include #include #include #include #include #include #include #include #include "utils/envmanager.hpp" #include "utils/directories.hpp" #include "utils/utils.hpp" #include "utils/output.hpp" #include "templates.hpp" #include "config.hpp" #include "utils/hash.hpp" namespace dropshell { // ------------------------------------------------------------------------------------------------ // template_source_local // ------------------------------------------------------------------------------------------------ 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() { // 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; } 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); } 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 ); } // 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)) { 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; } catch (...) { // If reading cache fails, re-download need_download = true; } } // 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; ASSERT( std::filesystem::exists( template_info_env_path ), "template_info.env doesn't exist in the template." ); // 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 ); } bool template_source_registry::template_command_exists(const std::string& template_name, const std::string& command) { // 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); } std::filesystem::path template_source_registry::get_cache_dir() { return localpath::template_cache(); } // ------------------------------------------------------------------------------------------------ // template_manager // ------------------------------------------------------------------------------------------------ void template_manager::list_templates() const { ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); 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; bool first = true; for (const auto& t : templates) { std::cout << (first ? "" : ", ") << t; first = false; } std::cout << std::endl; std::cout << std::string(60, '-') << std::endl; } std::set template_manager::get_template_list() const { ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found."); std::set templates; for (const auto& source : mSources) { auto source_templates = source->get_template_list(); templates.insert(source_templates.begin(), source_templates.end()); } return templates; } 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; } template_info template_manager::get_template_info(const std::string &template_name, bool skip_update) 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(); } 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) { error << "Template '" << template_name << "' not found" << std::endl; return false; } return source->template_command_exists(template_name, command); } // 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); if (!file.is_open()) { error << "Failed to create file: " << path << std::endl; return false; } file << content; file.close(); if (executable) { std::filesystem::permissions(path, std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec | std::filesystem::perms::others_read | std::filesystem::perms::others_exec); } return true; } bool template_manager::create_template(const std::string &template_name) const { if (!legal_service_name(template_name)) { error << "Template name contains illegal characters: " << template_name << std::endl; return false; } // 1. Check template doesn't already exist auto tinfo = get_template_info(template_name, false); 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; return false; } std::string new_template_path = local_template_paths[0] + "/" + template_name; // 3. Create directory structure std::filesystem::create_directories(new_template_path + "/config"); // 4. Generate template files inline (self-contained, no external dependencies) // config/.template_info.env std::string template_info_env = R"TMPL(# Template identifier - MUST match the directory name TEMPLATE=)TMPL" + template_name + R"TMPL( # Requirements REQUIRES_HOST_ROOT=false REQUIRES_DOCKER=true REQUIRES_DOCKER_ROOT=false # Docker image settings IMAGE_REGISTRY="docker.io" IMAGE_REPO="nginx" IMAGE_TAG="alpine" # Volume definitions DATA_VOLUME="${CONTAINER_NAME}_data" )TMPL"; if (!write_template_file(new_template_path + "/config/" + 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( # Server settings (REQUIRED by dropshell) SSH_USER="root" # Service-specific settings 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 "$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 echo "Stopped" 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( This template was created by 'dropshell create-template'. QUICK START ----------- 1. Edit config/service.env to customize your deployment 2. Edit config/.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 -------------- - config/.template_info.env : Template metadata (don't change TEMPLATE=) - 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 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; std::cout << " 1. Edit " << new_template_path << "/config/service.env" << std::endl; std::cout << " 2. Customize the scripts for your application" << std::endl; std::cout << " 3. Run: dropshell validate " << new_template_path << std::endl; std::cout << std::string(60, '-') << std::endl; 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; } void template_manager::print_sources() const { std::cout << "Template sources: "; for (const auto& source : mSources) { std::cout << "[" << source->get_description() << "] "; } std::cout << std::endl; } bool template_manager::required_file(std::string path, std::string template_name) { if (!std::filesystem::exists(path)) { error << path << " file not found in template - REQUIRED." << template_name << std::endl; return false; } 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()) return false; if (!std::filesystem::exists(template_path)) return false; std::string template_name = std::filesystem::path(template_path).filename().string(); std::vector required_files = { "config/" + filenames::service_env, filenames::template_info_env, "install.sh", "uninstall.sh" }; for (const auto& file : required_files) { 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()) { std::filesystem::path path = template_path + "/" + file; auto perms = std::filesystem::status(path).permissions(); if ((perms & std::filesystem::perms::owner_exec) == std::filesystem::perms::none) error << file << " is not executable" << std::endl; } } // ------------------------------------------------------------ // check TEMPLATE= line. ordered_env_vars all_env_vars; std::vector env_files = { "config/" + filenames::service_env, 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 = { "REQUIRES_HOST_ROOT", "REQUIRES_DOCKER", "REQUIRES_DOCKER_ROOT" }; for (const auto & required_var : required_vars) { auto it = find_var(all_env_vars, required_var); if (it == all_env_vars.end()) { error << "Required variable "<< required_var<<" not defined in " << template_path << std::endl; return false; } } return true; } bool template_manager::check_template_shell_scripts_syntax(const std::string &template_path) { if (template_path.empty() || !std::filesystem::exists(template_path)) return false; 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(); std::string command = "bash -n " + script_path + " 2>&1"; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { error << "Failed to run bash -n on " << entry.path().filename() << std::endl; all_passed = false; continue; } char buffer[256]; std::string output; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { output += buffer; } int result = pclose(pipe); if (result != 0 || !output.empty()) { error << "Syntax error in " << entry.path().filename() << ":" << std::endl; error << output << std::endl; all_passed = false; } } } return all_passed; } 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) : mTemplateName(template_name), mLocationID(location_id), mTemplateLocalPath(local_template_path), mTemplateValid(template_manager::test_template(local_template_path.string())), mIsSet(!template_name.empty() && !location_id.empty() && !local_template_path.empty()) { if (!std::filesystem::exists(local_template_path)) { 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_info_env_path() { return mTemplateLocalPath / filenames::template_info_env ; } } // namespace dropshell