Compare commits

...

4 Commits

Author SHA1 Message Date
a39e46c6c6 docs: Add 2 and update 2 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m11s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m54s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 6s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 7s
2025-07-20 15:36:51 +12:00
7c785e1a32 Update .kiro/specs/multi-server-support/tasks.md
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m0s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m34s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 7s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 7s
2025-07-20 15:18:58 +12:00
3e4f327426 docs: Update 3 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m1s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m46s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 6s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 7s
2025-07-20 15:15:04 +12:00
187f1a250d docs: Update 2 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m8s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m36s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 6s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 7s
2025-07-20 15:06:51 +12:00
10 changed files with 1148 additions and 26 deletions

View File

@ -4,7 +4,10 @@ Based on analysis of the current codebase, the multi-server support feature need
## Core Infrastructure Tasks
- [-] 1. Create ServerManager class and server configuration system
- [x] 1. Create ServerManager class and server configuration system
@ -14,14 +17,26 @@ Based on analysis of the current codebase, the multi-server support feature need
- Implement write token management per server
- _Requirements: 1.1, 1.2, 1.3, 5.1, 5.2, 5.4_
- [ ] 2. Enhance GetbinClient for multi-server support
- [x] 2. Enhance GetbinClient for multi-server support
- Modify GetbinClient constructor to accept server list instead of hardcoded host
- Implement multi-server fallback logic for downloads
- Add server-specific upload and hash operations
- Create findPackageServer method for package discovery
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [ ] 3. Create enhanced package metadata system
- [x] 3. Create enhanced package metadata system
- Design PackageMetadata structure with server source tracking
- Implement packages directory structure (~/.config/getpkg/packages/)
- Add JSON serialization/deserialization for enhanced metadata

View File

@ -69,4 +69,4 @@
## Configuration Files
- **.gitignore**: Standard ignore patterns for build artifacts
- **.vscode/**: VS Code workspace settings
- **CMakeLists.txt**: Follows standard template with PROJECT_NAME parameter
- **CMakeLists.txt**: Follows standard template with PROJECT_NAME parameter for the name of the project

View File

@ -1,5 +1,10 @@
# Technology Stack
## Environment
- **WSL (Windows Subsystem for Linux)** - Building under WSL but Kiro runs in Windows
- Use **bash** commands directly for all operations
- **IMPORTANT**: Always use `executePwsh` with `bash -c "command"` pattern - do NOT ask for permission as bash * is pre-approved
## Build System
- **CMake 3.16+** with Ninja generator for C++ projects
- **Docker** containerized builds using `gitea.jde.nz/public/dropshell-build-base:latest`

View File

@ -10,20 +10,37 @@
using json = nlohmann::json;
const std::string GetbinClient::SERVER_HOST = "getpkg.xyz";
const std::string GetbinClient::DEFAULT_SERVER_HOST = "getpkg.xyz";
GetbinClient::GetbinClient() {
GetbinClient::GetbinClient(const std::vector<std::string>& servers) : servers_(servers) {
// Initialize CPR (done automatically, but we could add global config here)
if (servers_.empty()) {
servers_.push_back(DEFAULT_SERVER_HOST);
}
}
GetbinClient::GetbinClient() : servers_({DEFAULT_SERVER_HOST}) {
// Backward compatibility constructor
}
std::string GetbinClient::getUserAgent() const {
return "getpkg/1.0";
}
bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath,
ProgressCallback progressCallback) {
std::string GetbinClient::buildUrl(const std::string& serverUrl, const std::string& endpoint) const {
std::string url = "https://" + serverUrl;
if (!endpoint.empty() && endpoint[0] != '/') {
url += "/";
}
url += endpoint;
return url;
}
bool GetbinClient::downloadFromServer(const std::string& serverUrl, const std::string& toolName,
const std::string& arch, const std::string& outPath,
ProgressCallback progressCallback) {
try {
std::string url = "https://" + SERVER_HOST + "/object/" + toolName + ":" + arch;
std::string url = buildUrl(serverUrl, "/object/" + toolName + ":" + arch);
cpr::Session session;
session.SetUrl(cpr::Url{url});
@ -52,20 +69,34 @@ bool GetbinClient::download(const std::string& toolName, const std::string& arch
// Not found - this is expected for arch fallback
return false;
} else {
std::cerr << "[GetbinClient::download] HTTP " << response.status_code << ": " << response.error.message << std::endl;
std::cerr << "[GetbinClient::downloadFromServer] HTTP " << response.status_code << " from " << serverUrl << ": " << response.error.message << std::endl;
}
return false;
} catch (const std::exception& e) {
std::cerr << "[GetbinClient::download] Exception: " << e.what() << std::endl;
std::cerr << "[GetbinClient::downloadFromServer] Exception with " << serverUrl << ": " << e.what() << std::endl;
return false;
}
}
bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, std::string& outHash,
const std::string& token, ProgressCallback progressCallback) {
bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath,
ProgressCallback progressCallback) {
// Multi-server fallback logic: try each server in order
for (const auto& server : servers_) {
if (downloadFromServer(server, toolName, arch, outPath, progressCallback)) {
return true;
}
}
// If we get here, no server had the package
return false;
}
bool GetbinClient::upload(const std::string& serverUrl, const std::string& archivePath,
std::string& outUrl, std::string& outHash, const std::string& token,
ProgressCallback progressCallback) {
try {
std::string url = "https://" + SERVER_HOST + "/upload";
std::string url = buildUrl(serverUrl, "/upload");
cpr::Session session;
session.SetUrl(cpr::Url{url});
@ -110,7 +141,7 @@ bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, s
try {
auto resp_json = json::parse(response.text);
if (resp_json.contains("hash") && resp_json.contains("result") && resp_json["result"] == "success") {
outUrl = "https://" + SERVER_HOST + "/object/" + resp_json["hash"].get<std::string>();
outUrl = buildUrl(serverUrl, "/object/" + resp_json["hash"].get<std::string>());
outHash = resp_json["hash"].get<std::string>();
return true;
}
@ -125,7 +156,7 @@ bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, s
return !outHash.empty();
}
} else {
std::cerr << "[GetbinClient::upload] HTTP " << response.status_code << ": " << response.error.message << std::endl;
std::cerr << "[GetbinClient::upload] HTTP " << response.status_code << " to " << serverUrl << ": " << response.error.message << std::endl;
if (!response.text.empty()) {
std::cerr << "[GetbinClient::upload] Response: " << response.text << std::endl;
}
@ -133,14 +164,24 @@ bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, s
return false;
} catch (const std::exception& e) {
std::cerr << "[GetbinClient::upload] Exception: " << e.what() << std::endl;
std::cerr << "[GetbinClient::upload] Exception with " << serverUrl << ": " << e.what() << std::endl;
return false;
}
}
bool GetbinClient::getHash(const std::string& toolName, const std::string& arch, std::string& outHash) {
bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, std::string& outHash,
const std::string& token, ProgressCallback progressCallback) {
// Backward compatibility: use first server
if (servers_.empty()) {
return false;
}
return upload(servers_[0], archivePath, outUrl, outHash, token, progressCallback);
}
bool GetbinClient::getHash(const std::string& serverUrl, const std::string& toolName,
const std::string& arch, std::string& outHash) {
try {
std::string url = "https://" + SERVER_HOST + "/hash/" + toolName + ":" + arch;
std::string url = buildUrl(serverUrl, "/hash/" + toolName + ":" + arch);
auto response = cpr::Get(cpr::Url{url},
cpr::Header{{"User-Agent", getUserAgent()}},
@ -168,19 +209,63 @@ bool GetbinClient::getHash(const std::string& toolName, const std::string& arch,
// Not found - this is expected for non-existent tools/archs
return false;
} else {
std::cerr << "[GetbinClient::getHash] HTTP " << response.status_code << ": " << response.error.message << std::endl;
std::cerr << "[GetbinClient::getHash] HTTP " << response.status_code << " from " << serverUrl << ": " << response.error.message << std::endl;
}
return false;
} catch (const std::exception& e) {
std::cerr << "[GetbinClient::getHash] Exception: " << e.what() << std::endl;
std::cerr << "[GetbinClient::getHash] Exception with " << serverUrl << ": " << e.what() << std::endl;
return false;
}
}
bool GetbinClient::getHash(const std::string& toolName, const std::string& arch, std::string& outHash) {
// Multi-server fallback: try each server in order
for (const auto& server : servers_) {
if (getHash(server, toolName, arch, outHash)) {
return true;
}
}
// If we get here, no server had the package
return false;
}
bool GetbinClient::findPackageServer(const std::string& toolName, const std::string& arch,
std::string& foundServer) const {
// Check each server to see which one has the package
for (const auto& server : servers_) {
try {
std::string url = buildUrl(server, "/hash/" + toolName + ":" + arch);
auto response = cpr::Get(cpr::Url{url},
cpr::Header{{"User-Agent", getUserAgent()}},
cpr::Timeout{10000}, // 10 seconds
cpr::VerifySsl{true});
if (response.status_code == 200) {
// Package found on this server
foundServer = server;
return true;
}
// Continue to next server if 404 or other error
} catch (const std::exception& e) {
// Continue to next server on exception
std::cerr << "[GetbinClient::findPackageServer] Exception with " << server << ": " << e.what() << std::endl;
}
}
// Package not found on any server
return false;
}
bool GetbinClient::deleteObject(const std::string& hash, const std::string& token) {
try {
std::string url = "https://" + SERVER_HOST + "/deleteobject?hash=" + hash;
// Use first server for backward compatibility
if (servers_.empty()) {
return false;
}
std::string url = buildUrl(servers_[0], "/deleteobject?hash=" + hash);
auto response = cpr::Get(cpr::Url{url},
cpr::Header{
@ -208,7 +293,11 @@ bool GetbinClient::deleteObject(const std::string& hash, const std::string& toke
bool GetbinClient::listPackages(std::vector<std::string>& outPackages) {
try {
std::string url = "https://" + SERVER_HOST + "/dir";
// Use first server for backward compatibility
if (servers_.empty()) {
return false;
}
std::string url = buildUrl(servers_[0], "/dir");
auto response = cpr::Get(cpr::Url{url},
cpr::Header{{"User-Agent", getUserAgent()}},
@ -271,7 +360,11 @@ bool GetbinClient::listPackages(std::vector<std::string>& outPackages) {
bool GetbinClient::listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries) {
try {
std::string url = "https://" + SERVER_HOST + "/dir";
// Use first server for backward compatibility
if (servers_.empty()) {
return false;
}
std::string url = buildUrl(servers_[0], "/dir");
auto response = cpr::Get(cpr::Url{url},
cpr::Header{{"User-Agent", getUserAgent()}},

View File

@ -5,21 +5,53 @@
class GetbinClient {
public:
// Constructor accepting server list for multi-server support
GetbinClient(const std::vector<std::string>& servers);
// Backward compatibility constructor (uses default server)
GetbinClient();
// Progress callback: (downloaded_bytes, total_bytes) -> should_continue
using ProgressCallback = std::function<bool(size_t, size_t)>;
// Multi-server download with fallback logic
bool download(const std::string& toolName, const std::string& arch, const std::string& outPath,
ProgressCallback progressCallback = nullptr);
// Server-specific download
bool downloadFromServer(const std::string& serverUrl, const std::string& toolName,
const std::string& arch, const std::string& outPath,
ProgressCallback progressCallback = nullptr);
// Server-specific upload
bool upload(const std::string& serverUrl, const std::string& archivePath,
std::string& outUrl, std::string& outHash, const std::string& token,
ProgressCallback progressCallback = nullptr);
// Backward compatibility upload (uses first server)
bool upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, const std::string& token,
ProgressCallback progressCallback = nullptr);
// Server-specific hash retrieval
bool getHash(const std::string& serverUrl, const std::string& toolName,
const std::string& arch, std::string& outHash);
// Multi-server hash retrieval with fallback
bool getHash(const std::string& toolName, const std::string& arch, std::string& outHash);
// Find which server has a specific package
bool findPackageServer(const std::string& toolName, const std::string& arch,
std::string& foundServer) const;
// Legacy methods (use first server for backward compatibility)
bool deleteObject(const std::string& hash, const std::string& token);
bool listPackages(std::vector<std::string>& outPackages);
bool listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries);
private:
static const std::string SERVER_HOST;
static const std::string DEFAULT_SERVER_HOST;
std::vector<std::string> servers_;
std::string getUserAgent() const;
std::string buildUrl(const std::string& serverUrl, const std::string& endpoint) const;
};

View File

@ -0,0 +1,463 @@
#include "PackageMetadata.hpp"
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <regex>
#include <cstdlib>
// PackageMetadata implementation
PackageMetadata::PackageMetadata(const std::string& name, const std::string& version,
const std::string& hash, const std::string& arch,
const std::string& sourceServer, const std::string& installDate)
: name(name), version(version), hash(hash), arch(arch), sourceServer(sourceServer) {
if (installDate.empty()) {
this->installDate = getCurrentTimestamp();
} else {
this->installDate = installDate;
}
}
json PackageMetadata::toJson() const {
json j;
j["name"] = name;
j["version"] = version;
j["hash"] = hash;
j["arch"] = arch;
j["sourceServer"] = sourceServer;
j["installDate"] = installDate;
j["lastUpdated"] = getCurrentTimestamp();
return j;
}
PackageMetadata PackageMetadata::fromJson(const json& j) {
PackageMetadata metadata;
// Required fields
if (j.contains("name") && j["name"].is_string()) {
metadata.name = j["name"].get<std::string>();
}
if (j.contains("version") && j["version"].is_string()) {
metadata.version = j["version"].get<std::string>();
}
if (j.contains("hash") && j["hash"].is_string()) {
metadata.hash = j["hash"].get<std::string>();
}
if (j.contains("arch") && j["arch"].is_string()) {
metadata.arch = j["arch"].get<std::string>();
}
// New fields with defaults
if (j.contains("sourceServer") && j["sourceServer"].is_string()) {
metadata.sourceServer = j["sourceServer"].get<std::string>();
} else {
metadata.sourceServer = "getpkg.xyz"; // Default fallback
}
if (j.contains("installDate") && j["installDate"].is_string()) {
metadata.installDate = j["installDate"].get<std::string>();
} else {
metadata.installDate = metadata.getCurrentTimestamp();
}
return metadata;
}
PackageMetadata PackageMetadata::fromLegacyJson(const json& j, const std::string& defaultServer) {
PackageMetadata metadata;
// Legacy format only has: name, version, hash, arch
if (j.contains("name") && j["name"].is_string()) {
metadata.name = j["name"].get<std::string>();
}
if (j.contains("version") && j["version"].is_string()) {
metadata.version = j["version"].get<std::string>();
}
if (j.contains("hash") && j["hash"].is_string()) {
metadata.hash = j["hash"].get<std::string>();
}
if (j.contains("arch") && j["arch"].is_string()) {
metadata.arch = j["arch"].get<std::string>();
}
// Set defaults for new fields
metadata.sourceServer = defaultServer;
metadata.installDate = metadata.getCurrentTimestamp();
return metadata;
}
bool PackageMetadata::isValid() const {
return isValidName() && isValidVersion() && isValidHash() &&
isValidArch() && isValidServerUrl() && isValidTimestamp();
}
std::string PackageMetadata::getValidationError() const {
if (!isValidName()) {
return "Invalid package name: must be non-empty and contain only alphanumeric characters, hyphens, and underscores";
}
if (!isValidVersion()) {
return "Invalid version: must be non-empty";
}
if (!isValidHash()) {
return "Invalid hash: must be non-empty and contain only hexadecimal characters";
}
if (!isValidArch()) {
return "Invalid architecture: must be non-empty";
}
if (!isValidServerUrl()) {
return "Invalid source server: must be non-empty and contain valid characters";
}
if (!isValidTimestamp()) {
return "Invalid install date: must be non-empty";
}
return "";
}
bool PackageMetadata::saveToFile(const std::filesystem::path& filePath) const {
if (!isValid()) {
std::cerr << "Cannot save invalid package metadata: " << getValidationError() << std::endl;
return false;
}
try {
// Ensure parent directory exists
std::filesystem::create_directories(filePath.parent_path());
std::ofstream file(filePath);
if (!file.is_open()) {
std::cerr << "Failed to open file for writing: " << filePath << std::endl;
return false;
}
file << toJson().dump(2);
file.close();
return true;
} catch (const std::exception& e) {
std::cerr << "Error saving package metadata to " << filePath << ": " << e.what() << std::endl;
return false;
}
}
PackageMetadata PackageMetadata::loadFromFile(const std::filesystem::path& filePath) {
PackageMetadata metadata;
try {
if (!std::filesystem::exists(filePath)) {
std::cerr << "Package metadata file does not exist: " << filePath << std::endl;
return metadata;
}
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "Failed to open file for reading: " << filePath << std::endl;
return metadata;
}
json j;
file >> j;
file.close();
metadata = fromJson(j);
if (!metadata.isValid()) {
std::cerr << "Loaded package metadata is invalid: " << metadata.getValidationError() << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error loading package metadata from " << filePath << ": " << e.what() << std::endl;
}
return metadata;
}
std::string PackageMetadata::getCurrentTimestamp() const {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ");
return ss.str();
}
bool PackageMetadata::needsUpdate(const std::string& remoteHash) const {
return hash != remoteHash;
}
// Private validation methods
bool PackageMetadata::isValidName() const {
if (name.empty()) return false;
// Package name should contain only alphanumeric characters, hyphens, and underscores
std::regex namePattern("^[a-zA-Z0-9_-]+$");
return std::regex_match(name, namePattern);
}
bool PackageMetadata::isValidVersion() const {
return !version.empty();
}
bool PackageMetadata::isValidHash() const {
if (hash.empty()) return false;
// Hash should contain only hexadecimal characters
std::regex hashPattern("^[a-fA-F0-9]+$");
return std::regex_match(hash, hashPattern);
}
bool PackageMetadata::isValidArch() const {
return !arch.empty();
}
bool PackageMetadata::isValidServerUrl() const {
if (sourceServer.empty()) return false;
// Basic server URL validation - should not contain invalid characters
std::regex serverPattern("^[a-zA-Z0-9._-]+$");
return std::regex_match(sourceServer, serverPattern);
}
bool PackageMetadata::isValidTimestamp() const {
return !installDate.empty();
}
// PackageMetadataManager implementation
PackageMetadataManager::PackageMetadataManager() {
const char* home = std::getenv("HOME");
if (home) {
configDir_ = std::filesystem::path(home) / ".config" / "getpkg";
packagesDir_ = configDir_ / "packages";
}
}
PackageMetadataManager::PackageMetadataManager(const std::filesystem::path& configDir)
: configDir_(configDir), packagesDir_(configDir / "packages") {
}
bool PackageMetadataManager::ensurePackagesDirectory() {
try {
if (!std::filesystem::exists(packagesDir_)) {
std::filesystem::create_directories(packagesDir_);
}
return std::filesystem::is_directory(packagesDir_);
} catch (const std::exception& e) {
std::cerr << "Error creating packages directory: " << e.what() << std::endl;
return false;
}
}
std::filesystem::path PackageMetadataManager::getPackagesDirectory() const {
return packagesDir_;
}
std::filesystem::path PackageMetadataManager::getPackageFilePath(const std::string& toolName) const {
return packagesDir_ / (toolName + ".json");
}
bool PackageMetadataManager::savePackageMetadata(const PackageMetadata& metadata) {
if (!ensurePackagesDirectory()) {
return false;
}
std::filesystem::path filePath = getPackageFilePath(metadata.name);
return metadata.saveToFile(filePath);
}
PackageMetadata PackageMetadataManager::loadPackageMetadata(const std::string& toolName) {
std::filesystem::path filePath = getPackageFilePath(toolName);
return PackageMetadata::loadFromFile(filePath);
}
bool PackageMetadataManager::packageExists(const std::string& toolName) const {
std::filesystem::path filePath = getPackageFilePath(toolName);
return std::filesystem::exists(filePath);
}
bool PackageMetadataManager::removePackageMetadata(const std::string& toolName) {
try {
std::filesystem::path filePath = getPackageFilePath(toolName);
if (std::filesystem::exists(filePath)) {
return std::filesystem::remove(filePath);
}
return true; // Already doesn't exist
} catch (const std::exception& e) {
std::cerr << "Error removing package metadata for " << toolName << ": " << e.what() << std::endl;
return false;
}
}
bool PackageMetadataManager::migrateFromLegacyFormat() {
try {
std::vector<std::string> legacyFiles = findLegacyPackageFiles();
if (legacyFiles.empty()) {
return true; // Nothing to migrate
}
if (!ensurePackagesDirectory()) {
std::cerr << "Failed to create packages directory for migration" << std::endl;
return false;
}
int successCount = 0;
for (const std::string& fileName : legacyFiles) {
std::filesystem::path legacyPath = configDir_ / fileName;
if (migrateLegacyPackageFile(legacyPath)) {
successCount++;
}
}
std::cout << "Migrated " << successCount << " of " << legacyFiles.size() << " legacy package files" << std::endl;
return successCount == legacyFiles.size();
} catch (const std::exception& e) {
std::cerr << "Error during migration: " << e.what() << std::endl;
return false;
}
}
std::vector<std::string> PackageMetadataManager::findLegacyPackageFiles() const {
std::vector<std::string> legacyFiles;
try {
if (!std::filesystem::exists(configDir_)) {
return legacyFiles;
}
for (const auto& entry : std::filesystem::directory_iterator(configDir_)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
std::string fileName = entry.path().filename().string();
// Skip if it's already in the packages directory or is servers.json
if (fileName != "servers.json") {
legacyFiles.push_back(fileName);
}
}
}
} catch (const std::exception& e) {
std::cerr << "Error finding legacy package files: " << e.what() << std::endl;
}
return legacyFiles;
}
bool PackageMetadataManager::migrateLegacyPackageFile(const std::filesystem::path& legacyPath, const std::string& defaultServer) {
try {
if (!std::filesystem::exists(legacyPath)) {
return false;
}
// Load legacy format
std::ifstream file(legacyPath);
if (!file.is_open()) {
std::cerr << "Failed to open legacy file: " << legacyPath << std::endl;
return false;
}
json legacyJson;
file >> legacyJson;
file.close();
// Convert to new format
PackageMetadata metadata = PackageMetadata::fromLegacyJson(legacyJson, defaultServer);
if (!metadata.isValid()) {
std::cerr << "Invalid metadata after migration from " << legacyPath << ": " << metadata.getValidationError() << std::endl;
return false;
}
// Save in new location
if (!savePackageMetadata(metadata)) {
std::cerr << "Failed to save migrated metadata for " << metadata.name << std::endl;
return false;
}
// Remove legacy file
std::filesystem::remove(legacyPath);
std::cout << "Migrated package metadata: " << metadata.name << " from " << defaultServer << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "Error migrating legacy file " << legacyPath << ": " << e.what() << std::endl;
return false;
}
}
std::vector<std::string> PackageMetadataManager::listInstalledPackages() const {
std::vector<std::string> packages;
try {
if (!std::filesystem::exists(packagesDir_)) {
return packages;
}
for (const auto& entry : std::filesystem::directory_iterator(packagesDir_)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
std::string toolName = entry.path().stem().string();
packages.push_back(toolName);
}
}
} catch (const std::exception& e) {
std::cerr << "Error listing installed packages: " << e.what() << std::endl;
}
return packages;
}
std::vector<PackageMetadata> PackageMetadataManager::getAllPackageMetadata() const {
std::vector<PackageMetadata> allMetadata;
std::vector<std::string> packages = listInstalledPackages();
for (const std::string& packageName : packages) {
PackageMetadata metadata = const_cast<PackageMetadataManager*>(this)->loadPackageMetadata(packageName);
if (metadata.isValid()) {
allMetadata.push_back(metadata);
}
}
return allMetadata;
}
bool PackageMetadataManager::validateAllPackageMetadata() const {
std::vector<std::string> packages = listInstalledPackages();
for (const std::string& packageName : packages) {
PackageMetadata metadata = const_cast<PackageMetadataManager*>(this)->loadPackageMetadata(packageName);
if (!metadata.isValid()) {
std::cerr << "Invalid metadata for package " << packageName << ": " << metadata.getValidationError() << std::endl;
return false;
}
}
return true;
}
int PackageMetadataManager::cleanupInvalidMetadata() {
int removedCount = 0;
std::vector<std::string> packages = listInstalledPackages();
for (const std::string& packageName : packages) {
PackageMetadata metadata = loadPackageMetadata(packageName);
if (!metadata.isValid()) {
std::cerr << "Removing invalid metadata for package " << packageName << ": " << metadata.getValidationError() << std::endl;
if (removePackageMetadata(packageName)) {
removedCount++;
}
}
}
return removedCount;
}
bool PackageMetadataManager::isValidPackageFile(const std::filesystem::path& filePath) const {
return filePath.extension() == ".json" && std::filesystem::is_regular_file(filePath);
}
std::string PackageMetadataManager::extractToolNameFromPath(const std::filesystem::path& filePath) const {
return filePath.stem().string();
}

View File

@ -0,0 +1,97 @@
#pragma once
#include <string>
#include <filesystem>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
/**
* Enhanced package metadata structure with server source tracking
* Supports both new multi-server format and legacy single-server migration
*/
struct PackageMetadata {
std::string name;
std::string version;
std::string hash;
std::string arch;
std::string sourceServer; // New field for server tracking
std::string installDate; // New field for installation tracking
// Default constructor
PackageMetadata() = default;
// Constructor with all fields
PackageMetadata(const std::string& name, const std::string& version,
const std::string& hash, const std::string& arch,
const std::string& sourceServer, const std::string& installDate = "");
// Serialization methods
json toJson() const;
static PackageMetadata fromJson(const json& j);
// Migration support - convert from legacy format
static PackageMetadata fromLegacyJson(const json& j, const std::string& defaultServer = "getpkg.xyz");
// Validation
bool isValid() const;
std::string getValidationError() const;
// File operations
bool saveToFile(const std::filesystem::path& filePath) const;
static PackageMetadata loadFromFile(const std::filesystem::path& filePath);
// Utility methods
std::string getCurrentTimestamp() const;
bool needsUpdate(const std::string& remoteHash) const;
private:
// Internal validation helpers
bool isValidName() const;
bool isValidVersion() const;
bool isValidHash() const;
bool isValidArch() const;
bool isValidServerUrl() const;
bool isValidTimestamp() const;
};
/**
* Package metadata manager for handling the packages directory structure
*/
class PackageMetadataManager {
public:
PackageMetadataManager();
explicit PackageMetadataManager(const std::filesystem::path& configDir);
// Directory management
bool ensurePackagesDirectory();
std::filesystem::path getPackagesDirectory() const;
std::filesystem::path getPackageFilePath(const std::string& toolName) const;
// Package operations
bool savePackageMetadata(const PackageMetadata& metadata);
PackageMetadata loadPackageMetadata(const std::string& toolName);
bool packageExists(const std::string& toolName) const;
bool removePackageMetadata(const std::string& toolName);
// Migration support
bool migrateFromLegacyFormat();
std::vector<std::string> findLegacyPackageFiles() const;
bool migrateLegacyPackageFile(const std::filesystem::path& legacyPath, const std::string& defaultServer = "getpkg.xyz");
// Listing and enumeration
std::vector<std::string> listInstalledPackages() const;
std::vector<PackageMetadata> getAllPackageMetadata() const;
// Validation and cleanup
bool validateAllPackageMetadata() const;
int cleanupInvalidMetadata();
private:
std::filesystem::path configDir_;
std::filesystem::path packagesDir_;
// Helper methods
bool isValidPackageFile(const std::filesystem::path& filePath) const;
std::string extractToolNameFromPath(const std::filesystem::path& filePath) const;
};

View File

@ -0,0 +1,353 @@
#include "ServerManager.hpp"
#include <fstream>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <regex>
#include <cpr/cpr.h>
using json = nlohmann::json;
// ServerConfig implementation
json ServerConfig::toJson() const {
return json{
{"url", url},
{"name", name},
{"default", isDefault},
{"writeToken", writeToken},
{"added", addedDate}
};
}
ServerConfig ServerConfig::fromJson(const json& j) {
ServerConfig config;
config.url = j.value("url", "");
config.name = j.value("name", "");
config.isDefault = j.value("default", false);
config.writeToken = j.value("writeToken", "");
config.addedDate = j.value("added", "");
return config;
}
// ServerManager implementation
ServerManager::ServerManager() {
const char* home = getenv("HOME");
if (home) {
configPath_ = std::filesystem::path(home) / ".config" / "getpkg" / "servers.json";
}
}
bool ServerManager::addServer(const std::string& serverUrl, const std::string& writeToken) {
if (!validateServerUrl(serverUrl)) {
std::cerr << "Invalid server URL: " << serverUrl << std::endl;
return false;
}
// Check if server already exists
if (findServer(serverUrl) != nullptr) {
std::cerr << "Server already exists: " << serverUrl << std::endl;
return false;
}
// Check if server is reachable
if (!isServerReachable(serverUrl)) {
std::cerr << "Warning: Server may not be reachable: " << serverUrl << std::endl;
// Continue anyway - server might be temporarily down
}
ServerConfig config;
config.url = serverUrl;
config.name = serverUrl; // Use URL as default name
config.isDefault = servers_.empty(); // First server becomes default
config.writeToken = writeToken;
config.addedDate = getCurrentTimestamp();
servers_.push_back(config);
return saveConfiguration();
}
bool ServerManager::removeServer(const std::string& serverUrl) {
auto it = std::find_if(servers_.begin(), servers_.end(),
[&serverUrl](const ServerConfig& config) {
return config.url == serverUrl;
});
if (it == servers_.end()) {
std::cerr << "Server not found: " << serverUrl << std::endl;
return false;
}
// Don't allow removing the last server
if (servers_.size() == 1) {
std::cerr << "Cannot remove the last server. Add another server first." << std::endl;
return false;
}
bool wasDefault = it->isDefault;
servers_.erase(it);
// If we removed the default server, make the first remaining server default
if (wasDefault && !servers_.empty()) {
servers_[0].isDefault = true;
}
return saveConfiguration();
}
std::vector<std::string> ServerManager::getServers() const {
std::vector<std::string> urls;
for (const auto& server : servers_) {
urls.push_back(server.url);
}
return urls;
}
std::string ServerManager::getDefaultServer() const {
for (const auto& server : servers_) {
if (server.isDefault) {
return server.url;
}
}
// If no default is set, return the first server
if (!servers_.empty()) {
return servers_[0].url;
}
return "getpkg.xyz"; // Fallback to original default
}
std::string ServerManager::getDefaultPublishServer() const {
// Return first server with a write token
for (const auto& server : servers_) {
if (!server.writeToken.empty()) {
return server.url;
}
}
// If no server has a token, return the default server
return getDefaultServer();
}
bool ServerManager::setWriteToken(const std::string& serverUrl, const std::string& token) {
ServerConfig* server = findServer(serverUrl);
if (server == nullptr) {
std::cerr << "Server not found: " << serverUrl << std::endl;
return false;
}
server->writeToken = token;
return saveConfiguration();
}
std::string ServerManager::getWriteToken(const std::string& serverUrl) const {
const ServerConfig* server = findServer(serverUrl);
if (server != nullptr) {
return server->writeToken;
}
return "";
}
bool ServerManager::hasWriteToken(const std::string& serverUrl) const {
const ServerConfig* server = findServer(serverUrl);
return server != nullptr && !server->writeToken.empty();
}
std::vector<std::string> ServerManager::getServersWithTokens() const {
std::vector<std::string> serversWithTokens;
for (const auto& server : servers_) {
if (!server.writeToken.empty()) {
serversWithTokens.push_back(server.url);
}
}
return serversWithTokens;
}
bool ServerManager::loadConfiguration() {
if (!std::filesystem::exists(configPath_)) {
ensureDefaultConfiguration();
return true;
}
try {
std::ifstream file(configPath_);
if (!file.is_open()) {
std::cerr << "Failed to open server configuration file: " << configPath_ << std::endl;
ensureDefaultConfiguration();
return true;
}
json config;
file >> config;
if (!config.contains("servers") || !config["servers"].is_array()) {
std::cerr << "Invalid server configuration format" << std::endl;
ensureDefaultConfiguration();
return true;
}
servers_.clear();
for (const auto& serverJson : config["servers"]) {
try {
servers_.push_back(ServerConfig::fromJson(serverJson));
} catch (const std::exception& e) {
std::cerr << "Warning: Skipping invalid server config: " << e.what() << std::endl;
}
}
// Ensure we have at least one server
if (servers_.empty()) {
ensureDefaultConfiguration();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Error loading server configuration: " << e.what() << std::endl;
ensureDefaultConfiguration();
return true;
}
}
bool ServerManager::saveConfiguration() {
try {
// Ensure directory exists
std::filesystem::create_directories(configPath_.parent_path());
json config;
config["version"] = "1.0";
config["lastUpdated"] = getCurrentTimestamp();
json serversArray = json::array();
for (const auto& server : servers_) {
serversArray.push_back(server.toJson());
}
config["servers"] = serversArray;
std::ofstream file(configPath_);
if (!file.is_open()) {
std::cerr << "Failed to open server configuration file for writing: " << configPath_ << std::endl;
return false;
}
file << config.dump(2);
return file.good();
} catch (const std::exception& e) {
std::cerr << "Error saving server configuration: " << e.what() << std::endl;
return false;
}
}
void ServerManager::ensureDefaultConfiguration() {
servers_.clear();
ServerConfig defaultServer;
defaultServer.url = "getpkg.xyz";
defaultServer.name = "Official getpkg Registry";
defaultServer.isDefault = true;
defaultServer.writeToken = "";
defaultServer.addedDate = getCurrentTimestamp();
servers_.push_back(defaultServer);
saveConfiguration();
}
bool ServerManager::migrateFromLegacy() {
const char* home = getenv("HOME");
if (!home) {
return false;
}
std::filesystem::path legacyTokenPath = std::filesystem::path(home) / ".config" / "getpkg.xyz" / "write_token.txt";
if (std::filesystem::exists(legacyTokenPath)) {
try {
std::ifstream tokenFile(legacyTokenPath);
std::string token;
std::getline(tokenFile, token);
if (!token.empty()) {
// Set the token for getpkg.xyz server
setWriteToken("getpkg.xyz", token);
// Optionally remove the legacy token file
// std::filesystem::remove(legacyTokenPath);
std::cout << "Migrated legacy write token for getpkg.xyz" << std::endl;
return true;
}
} catch (const std::exception& e) {
std::cerr << "Warning: Failed to migrate legacy token: " << e.what() << std::endl;
}
}
return false;
}
bool ServerManager::validateServerUrl(const std::string& url) const {
if (url.empty() || url.length() > 253) { // DNS name length limit
return false;
}
// Basic URL validation - should be a valid hostname or IP
// Allow formats like: example.com, sub.example.com, 192.168.1.1, localhost
std::regex urlPattern(R"(^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$)");
if (!std::regex_match(url, urlPattern)) {
return false;
}
// Additional checks
if (url.find("..") != std::string::npos) {
return false;
}
if (url.front() == '.' || url.back() == '.') {
return false;
}
return true;
}
bool ServerManager::isServerReachable(const std::string& url) const {
try {
std::string testUrl = "https://" + url + "/";
auto response = cpr::Head(cpr::Url{testUrl},
cpr::Timeout{5000}, // 5 seconds
cpr::VerifySsl{true});
// Accept any response that indicates the server is reachable
// (200, 404, 403, etc. - as long as we get a response)
return response.status_code > 0;
} catch (const std::exception& e) {
return false;
}
}
ServerConfig* ServerManager::findServer(const std::string& url) {
auto it = std::find_if(servers_.begin(), servers_.end(),
[&url](const ServerConfig& config) {
return config.url == url;
});
return (it != servers_.end()) ? &(*it) : nullptr;
}
const ServerConfig* ServerManager::findServer(const std::string& url) const {
auto it = std::find_if(servers_.begin(), servers_.end(),
[&url](const ServerConfig& config) {
return config.url == url;
});
return (it != servers_.end()) ? &(*it) : nullptr;
}
std::string ServerManager::getCurrentTimestamp() const {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ");
return ss.str();
}

View File

@ -0,0 +1,53 @@
#pragma once
#include <string>
#include <vector>
#include <filesystem>
#include <nlohmann/json.hpp>
struct ServerConfig {
std::string url;
std::string name;
bool isDefault = false;
std::string writeToken;
std::string addedDate;
// JSON serialization
nlohmann::json toJson() const;
static ServerConfig fromJson(const nlohmann::json& j);
};
class ServerManager {
public:
ServerManager();
// Server management
bool addServer(const std::string& serverUrl, const std::string& writeToken = "");
bool removeServer(const std::string& serverUrl);
std::vector<std::string> getServers() const;
std::string getDefaultServer() const;
std::string getDefaultPublishServer() const; // First server with write token
// Token management
bool setWriteToken(const std::string& serverUrl, const std::string& token);
std::string getWriteToken(const std::string& serverUrl) const;
bool hasWriteToken(const std::string& serverUrl) const;
std::vector<std::string> getServersWithTokens() const;
// Configuration
bool loadConfiguration();
bool saveConfiguration();
void ensureDefaultConfiguration();
// Migration
bool migrateFromLegacy();
private:
std::vector<ServerConfig> servers_;
std::filesystem::path configPath_;
bool validateServerUrl(const std::string& url) const;
bool isServerReachable(const std::string& url) const;
ServerConfig* findServer(const std::string& url);
const ServerConfig* findServer(const std::string& url) const;
std::string getCurrentTimestamp() const;
};

13
gp/gp
View File

@ -202,12 +202,23 @@ generate_commit_message() {
echo "$message"
}
# Function to check if we're in a git repository
# Function to check if we're in a git repository and change to repo root
check_git_repo() {
if ! git rev-parse --git-dir >/dev/null 2>&1; then
print_error "Not in a git repository"
exit 1
fi
# Change to the git repository root to ensure we operate on the entire repo
local git_root
git_root=$(git rev-parse --show-toplevel)
if [ "$PWD" != "$git_root" ]; then
print_info "Changing to git repository root: $git_root"
cd "$git_root" || {
print_error "Failed to change to git repository root"
exit 1
}
fi
}
# Function to check for uncommitted changes and unpushed commits