From 68124f94fc59e687945238e61f4e7e7d0ba8435d Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Dec 2025 11:15:41 +1300 Subject: [PATCH] Update source/src/commands/help.cpp --- source/src/commands/help.cpp | 1 + source/src/commands/publish-template.cpp | 346 +++++++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 source/src/commands/publish-template.cpp diff --git a/source/src/commands/help.cpp b/source/src/commands/help.cpp index b04b4e9..4702034 100644 --- a/source/src/commands/help.cpp +++ b/source/src/commands/help.cpp @@ -156,6 +156,7 @@ int help_handler(const CommandContext& ctx) { show_command("create-template"); info << std::endl; show_command("validate"); + show_command("publish-template"); } return 0; } diff --git a/source/src/commands/publish-template.cpp b/source/src/commands/publish-template.cpp new file mode 100644 index 0000000..57435a0 --- /dev/null +++ b/source/src/commands/publish-template.cpp @@ -0,0 +1,346 @@ +#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 + +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 (directory required) + 2, // max_args (optional registry name + directory) + "publish-template [REGISTRY] DIRECTORY", + "Publish a template to a template registry.", + R"HELP( +Publishes a template directory to a template registry using sos. + +Usage: + ds publish-template DIRECTORY + ds publish-template REGISTRY_NAME DIRECTORY + +Arguments: + 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 to publish. + +The template is validated before publishing. Two tags are created: + - YYYYMMDD (e.g., 20251228) + - latest + +Requirements: + - Template must pass validation + - Registry must have a token configured in dropshell.json + - curl must be available (for sos download) + +Example: + ds publish-template ./my-template + ds publish-template main ./my-template + )HELP" + }); + } +} publish_template_command_register; + + +void publish_template_autocomplete(const CommandContext& ctx) { + if (ctx.args.size() == 0) { + // 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 +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(); +} + +// Get architecture string +static std::string get_arch() { + FILE* pipe = popen("uname -m", "r"); + if (!pipe) return "x86_64"; + + char buffer[128]; + std::string result; + if (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + result = buffer; + // Remove trailing newline + if (!result.empty() && result.back() == '\n') { + result.pop_back(); + } + } + pclose(pipe); + + if (result == "aarch64" || result == "arm64") { + return "aarch64"; + } + return "x86_64"; +} + +// Download a tool to temp directory +static bool download_tool(const std::string& tool_name, const std::string& temp_dir, std::string& tool_path) { + std::string arch = get_arch(); + tool_path = temp_dir + "/" + tool_name; + + std::string url = "https://getbin.xyz/" + tool_name + ":latest-" + arch; + std::string cmd = "curl -fsSL -o \"" + tool_path + "\" \"" + url + "\" 2>/dev/null"; + + int ret = system(cmd.c_str()); + if (ret != 0) { + return false; + } + + // Make executable + std::string chmod_cmd = "chmod +x \"" + tool_path + "\""; + system(chmod_cmd.c_str()); + + return std::filesystem::exists(tool_path); +} + +// 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); +} + +// Upload using sos +static bool upload_with_sos(const std::string& sos_path, const std::string& server, + const std::string& tarball, const std::string& label_tag, + const std::string& hash, const std::string& token) { + // Set environment variable for token + setenv("SOS_WRITE_TOKEN", token.c_str(), 1); + + std::string cmd = "\"" + sos_path + "\" upload " + server + " \"" + tarball + "\" \"" + label_tag + "\" --metadata \"unpackedhash=" + hash + "\" 2>&1"; + + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) return false; + + char buffer[512]; + std::string output; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + output += buffer; + } + + int ret = pclose(pipe); + + if (ret != 0) { + error << output << std::endl; + return false; + } + + return true; +} + +int publish_template_handler(const CommandContext& ctx) { + std::string registry_name; + std::string template_dir; + + // Parse arguments + if (ctx.args.size() == 1) { + // Only directory provided + template_dir = ctx.args[0]; + } else if (ctx.args.size() == 2) { + // Registry name and directory provided + registry_name = ctx.args[0]; + template_dir = ctx.args[1]; + } else { + error << "Usage: ds publish-template [REGISTRY] DIRECTORY" << std::endl; + return 1; + } + + // Resolve template directory to absolute path + std::filesystem::path dir_path(template_dir); + if (dir_path.is_relative()) { + dir_path = std::filesystem::current_path() / dir_path; + } + dir_path = std::filesystem::canonical(dir_path); + template_dir = dir_path.string(); + + // Check directory exists + if (!std::filesystem::exists(dir_path) || !std::filesystem::is_directory(dir_path)) { + error << "Directory not found: " << template_dir << std::endl; + return 1; + } + + std::string template_name = dir_path.filename().string(); + + // Find registry + std::vector registries = gConfig().get_template_registry_urls(); + tRegistryEntry* selected_registry = nullptr; + + if (registry_name.empty()) { + // Find first registry with a token + for (auto& reg : registries) { + if (!reg.token.empty()) { + selected_registry = ® + break; + } + } + if (!selected_registry) { + error << "No template registry with a token found in config" << std::endl; + info << "Add a token to a registry in dropshell.json or specify a registry name" << 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; + } + if (selected_registry->token.empty()) { + error << "Registry '" << registry_name << "' does not have a token configured" << std::endl; + return 1; + } + } + + // Extract server from URL (remove https:// prefix) + std::string server = selected_registry->url; + if (server.substr(0, 8) == "https://") { + server = server.substr(8); + } else if (server.substr(0, 7) == "http://") { + server = server.substr(7); + } + // Remove trailing slash if present + if (!server.empty() && server.back() == '/') { + server.pop_back(); + } + + maketitle("Publishing template: " + template_name); + info << "Directory: " << template_dir << std::endl; + info << "Registry: " << selected_registry->name << " (" << server << ")" << std::endl; + std::cout << std::endl; + + // Step 1: Validate template + info << "=== Validating Template ===" << std::endl; + if (!template_manager::test_template(template_dir)) { + error << "Template validation failed. Please fix issues before publishing." << std::endl; + info << "Run: ds validate " << template_dir << std::endl; + return 1; + } + info << " ✓ Template is valid" << std::endl; + std::cout << std::endl; + + // Step 2: Create temp directory + 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}; + + // Step 3: Calculate hash (using built-in hash function) + info << "=== Calculating Hash ===" << std::endl; + std::string hash = hash_directory_recursive(template_dir); + if (hash.empty()) { + error << "Failed to calculate directory hash" << std::endl; + return 1; + } + info << " Hash: " << hash << std::endl; + std::cout << std::endl; + + // Step 4: Download sos tool + info << "=== Downloading Tools ===" << std::endl; + + std::string sos_path; + + info << " Downloading sos..." << std::endl; + if (!download_tool("sos", temp_dir, sos_path)) { + error << "Failed to download sos" << std::endl; + return 1; + } + info << " ✓ sos downloaded" << std::endl; + std::cout << std::endl; + + // Step 5: Create tarball + 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" << std::endl; + return 1; + } + + auto file_size = std::filesystem::file_size(tarball_path); + info << " Created " << template_name << ".tgz (" << (file_size / 1024) << " KB)" << std::endl; + std::cout << std::endl; + + // Step 6: Upload with tags + info << "=== Uploading to Registry ===" << std::endl; + + std::string date_tag = get_date_tag(); + + // Upload with date tag + std::string label_date = template_name + ":" + date_tag; + info << " Uploading " << label_date << "..." << std::endl; + if (!upload_with_sos(sos_path, server, tarball_path, label_date, hash, selected_registry->token)) { + error << "Failed to upload " << label_date << std::endl; + return 1; + } + info << " ✓ Uploaded " << label_date << std::endl; + + // Upload with latest tag + std::string label_latest = template_name + ":latest"; + info << " Uploading " << label_latest << "..." << std::endl; + if (!upload_with_sos(sos_path, server, tarball_path, label_latest, hash, selected_registry->token)) { + error << "Failed to upload " << label_latest << std::endl; + return 1; + } + info << " ✓ Uploaded " << label_latest << std::endl; + std::cout << std::endl; + + // Summary + maketitle("Publish Complete"); + info << "Template '" << template_name << "' published successfully!" << std::endl; + info << "Tags: " << date_tag << ", latest" << std::endl; + info << "URL: https://" << server << "/" << template_name << ":latest" << std::endl; + + return 0; +} + +} // namespace dropshell