Compare commits
3 Commits
v2025.0720
...
v2025.0720
Author | SHA1 | Date | |
---|---|---|---|
a39e46c6c6 | |||
7c785e1a32 | |||
3e4f327426 |
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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()}},
|
||||
|
@ -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;
|
||||
};
|
||||
|
463
getpkg/src/PackageMetadata.cpp
Normal file
463
getpkg/src/PackageMetadata.cpp
Normal 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();
|
||||
}
|
97
getpkg/src/PackageMetadata.hpp
Normal file
97
getpkg/src/PackageMetadata.hpp
Normal 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;
|
||||
};
|
Reference in New Issue
Block a user