Compare commits
7 Commits
v2025.0720
...
v2025.0720
Author | SHA1 | Date | |
---|---|---|---|
938f4ac323 | |||
c507b1405e | |||
2ab0483ecb | |||
a39e46c6c6 | |||
7c785e1a32 | |||
3e4f327426 | |||
187f1a250d |
@ -4,7 +4,10 @@ Based on analysis of the current codebase, the multi-server support feature need
|
|||||||
|
|
||||||
## Core Infrastructure Tasks
|
## 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
|
- Implement write token management per server
|
||||||
- _Requirements: 1.1, 1.2, 1.3, 5.1, 5.2, 5.4_
|
- _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
|
- Modify GetbinClient constructor to accept server list instead of hardcoded host
|
||||||
- Implement multi-server fallback logic for downloads
|
- Implement multi-server fallback logic for downloads
|
||||||
- Add server-specific upload and hash operations
|
- Add server-specific upload and hash operations
|
||||||
- Create findPackageServer method for package discovery
|
- Create findPackageServer method for package discovery
|
||||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
- _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
|
- Design PackageMetadata structure with server source tracking
|
||||||
- Implement packages directory structure (~/.config/getpkg/packages/)
|
- Implement packages directory structure (~/.config/getpkg/packages/)
|
||||||
- Add JSON serialization/deserialization for enhanced metadata
|
- Add JSON serialization/deserialization for enhanced metadata
|
||||||
@ -30,7 +45,12 @@ Based on analysis of the current codebase, the multi-server support feature need
|
|||||||
|
|
||||||
## Migration and Compatibility Tasks
|
## Migration and Compatibility Tasks
|
||||||
|
|
||||||
- [ ] 4. Implement migration system for existing installations
|
- [x] 4. Implement migration system for existing installations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Create MigrationManager class for legacy data handling
|
- Create MigrationManager class for legacy data handling
|
||||||
- Implement automatic migration from single-server to multi-server config
|
- Implement automatic migration from single-server to multi-server config
|
||||||
- Migrate existing package JSON files to packages subdirectory
|
- Migrate existing package JSON files to packages subdirectory
|
||||||
@ -38,7 +58,13 @@ Based on analysis of the current codebase, the multi-server support feature need
|
|||||||
- Add migration error handling and rollback capabilities
|
- Add migration error handling and rollback capabilities
|
||||||
- _Requirements: 4.4, 4.5, 6.1, 6.2, 6.3, 6.5_
|
- _Requirements: 4.4, 4.5, 6.1, 6.2, 6.3, 6.5_
|
||||||
|
|
||||||
- [ ] 5. Ensure backward compatibility
|
- [x] 5. Ensure backward compatibility
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Implement default server configuration (getpkg.xyz) when no config exists
|
- Implement default server configuration (getpkg.xyz) when no config exists
|
||||||
- Maintain existing CLI behavior for users without custom server configuration
|
- Maintain existing CLI behavior for users without custom server configuration
|
||||||
- Preserve existing token storage location compatibility
|
- Preserve existing token storage location compatibility
|
||||||
@ -47,14 +73,22 @@ Based on analysis of the current codebase, the multi-server support feature need
|
|||||||
|
|
||||||
## CLI Integration Tasks
|
## CLI Integration Tasks
|
||||||
|
|
||||||
- [ ] 6. Add server management commands to main.cpp
|
- [x] 6. Add server management commands to main.cpp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Implement `getpkg server add <url>` command
|
- Implement `getpkg server add <url>` command
|
||||||
- Implement `getpkg server remove <url>` command
|
- Implement `getpkg server remove <url>` command
|
||||||
- Implement `getpkg server list` command
|
- Implement `getpkg server list` command
|
||||||
- Add server URL validation and user feedback
|
- Add server URL validation and user feedback
|
||||||
- _Requirements: 1.1, 1.2, 1.3_
|
- _Requirements: 1.1, 1.2, 1.3_
|
||||||
|
|
||||||
- [ ] 7. Update existing commands for multi-server support
|
- [-] 7. Update existing commands for multi-server support
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Modify install command to use ServerManager and multi-server GetbinClient
|
- Modify install command to use ServerManager and multi-server GetbinClient
|
||||||
- Update publish command to support --server option and default server selection
|
- Update publish command to support --server option and default server selection
|
||||||
- Update unpublish command to support --server option and default server selection
|
- Update unpublish command to support --server option and default server selection
|
||||||
|
@ -69,4 +69,4 @@
|
|||||||
## Configuration Files
|
## Configuration Files
|
||||||
- **.gitignore**: Standard ignore patterns for build artifacts
|
- **.gitignore**: Standard ignore patterns for build artifacts
|
||||||
- **.vscode/**: VS Code workspace settings
|
- **.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
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# Technology Stack
|
# 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
|
## Build System
|
||||||
- **CMake 3.16+** with Ninja generator for C++ projects
|
- **CMake 3.16+** with Ninja generator for C++ projects
|
||||||
- **Docker** containerized builds using `gitea.jde.nz/public/dropshell-build-base:latest`
|
- **Docker** containerized builds using `gitea.jde.nz/public/dropshell-build-base:latest`
|
||||||
|
@ -10,20 +10,37 @@
|
|||||||
|
|
||||||
using json = nlohmann::json;
|
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)
|
// 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 {
|
std::string GetbinClient::getUserAgent() const {
|
||||||
return "getpkg/1.0";
|
return "getpkg/1.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath,
|
std::string GetbinClient::buildUrl(const std::string& serverUrl, const std::string& endpoint) const {
|
||||||
ProgressCallback progressCallback) {
|
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 {
|
try {
|
||||||
std::string url = "https://" + SERVER_HOST + "/object/" + toolName + ":" + arch;
|
std::string url = buildUrl(serverUrl, "/object/" + toolName + ":" + arch);
|
||||||
|
|
||||||
cpr::Session session;
|
cpr::Session session;
|
||||||
session.SetUrl(cpr::Url{url});
|
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
|
// Not found - this is expected for arch fallback
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} 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;
|
return false;
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, std::string& outHash,
|
bool GetbinClient::download(const std::string& toolName, const std::string& arch, const std::string& outPath,
|
||||||
const std::string& token, ProgressCallback progressCallback) {
|
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 {
|
try {
|
||||||
std::string url = "https://" + SERVER_HOST + "/upload";
|
std::string url = buildUrl(serverUrl, "/upload");
|
||||||
|
|
||||||
cpr::Session session;
|
cpr::Session session;
|
||||||
session.SetUrl(cpr::Url{url});
|
session.SetUrl(cpr::Url{url});
|
||||||
@ -110,7 +141,7 @@ bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, s
|
|||||||
try {
|
try {
|
||||||
auto resp_json = json::parse(response.text);
|
auto resp_json = json::parse(response.text);
|
||||||
if (resp_json.contains("hash") && resp_json.contains("result") && resp_json["result"] == "success") {
|
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>();
|
outHash = resp_json["hash"].get<std::string>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -125,7 +156,7 @@ bool GetbinClient::upload(const std::string& archivePath, std::string& outUrl, s
|
|||||||
return !outHash.empty();
|
return !outHash.empty();
|
||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (!response.text.empty()) {
|
||||||
std::cerr << "[GetbinClient::upload] Response: " << response.text << std::endl;
|
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;
|
return false;
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
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 {
|
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},
|
auto response = cpr::Get(cpr::Url{url},
|
||||||
cpr::Header{{"User-Agent", getUserAgent()}},
|
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
|
// Not found - this is expected for non-existent tools/archs
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} 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;
|
return false;
|
||||||
} catch (const std::exception& e) {
|
} 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;
|
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) {
|
bool GetbinClient::deleteObject(const std::string& hash, const std::string& token) {
|
||||||
try {
|
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},
|
auto response = cpr::Get(cpr::Url{url},
|
||||||
cpr::Header{
|
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) {
|
bool GetbinClient::listPackages(std::vector<std::string>& outPackages) {
|
||||||
try {
|
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},
|
auto response = cpr::Get(cpr::Url{url},
|
||||||
cpr::Header{{"User-Agent", getUserAgent()}},
|
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) {
|
bool GetbinClient::listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries) {
|
||||||
try {
|
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},
|
auto response = cpr::Get(cpr::Url{url},
|
||||||
cpr::Header{{"User-Agent", getUserAgent()}},
|
cpr::Header{{"User-Agent", getUserAgent()}},
|
||||||
|
@ -5,21 +5,53 @@
|
|||||||
|
|
||||||
class GetbinClient {
|
class GetbinClient {
|
||||||
public:
|
public:
|
||||||
|
// Constructor accepting server list for multi-server support
|
||||||
|
GetbinClient(const std::vector<std::string>& servers);
|
||||||
|
|
||||||
|
// Backward compatibility constructor (uses default server)
|
||||||
GetbinClient();
|
GetbinClient();
|
||||||
|
|
||||||
// Progress callback: (downloaded_bytes, total_bytes) -> should_continue
|
// Progress callback: (downloaded_bytes, total_bytes) -> should_continue
|
||||||
using ProgressCallback = std::function<bool(size_t, size_t)>;
|
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,
|
bool download(const std::string& toolName, const std::string& arch, const std::string& outPath,
|
||||||
ProgressCallback progressCallback = nullptr);
|
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,
|
bool upload(const std::string& archivePath, std::string& outUrl, std::string& outHash, const std::string& token,
|
||||||
ProgressCallback progressCallback = nullptr);
|
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);
|
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 deleteObject(const std::string& hash, const std::string& token);
|
||||||
bool listPackages(std::vector<std::string>& outPackages);
|
bool listPackages(std::vector<std::string>& outPackages);
|
||||||
bool listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries);
|
bool listAllEntries(std::vector<std::pair<std::string, std::vector<std::string>>>& outEntries);
|
||||||
|
|
||||||
private:
|
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 getUserAgent() const;
|
||||||
|
std::string buildUrl(const std::string& serverUrl, const std::string& endpoint) const;
|
||||||
};
|
};
|
||||||
|
575
getpkg/src/MigrationManager.cpp
Normal file
575
getpkg/src/MigrationManager.cpp
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
#include "MigrationManager.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
MigrationManager::MigrationManager() {
|
||||||
|
const char* home = std::getenv("HOME");
|
||||||
|
if (home) {
|
||||||
|
configDir_ = std::filesystem::path(home) / ".config" / "getpkg";
|
||||||
|
packagesDir_ = configDir_ / PACKAGES_DIRECTORY_NAME;
|
||||||
|
backupDir_ = configDir_ / BACKUP_DIRECTORY_NAME;
|
||||||
|
legacyTokenDir_ = configDir_ / DEFAULT_SERVER_URL;
|
||||||
|
|
||||||
|
packageManager_ = std::make_unique<PackageMetadataManager>(configDir_);
|
||||||
|
serverManager_ = std::make_unique<ServerManager>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrationManager::MigrationManager(const std::filesystem::path& configDir)
|
||||||
|
: configDir_(configDir),
|
||||||
|
packagesDir_(configDir / PACKAGES_DIRECTORY_NAME),
|
||||||
|
backupDir_(configDir / BACKUP_DIRECTORY_NAME),
|
||||||
|
legacyTokenDir_(configDir / DEFAULT_SERVER_URL) {
|
||||||
|
|
||||||
|
packageManager_ = std::make_unique<PackageMetadataManager>(configDir);
|
||||||
|
serverManager_ = std::make_unique<ServerManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::needsMigration() const {
|
||||||
|
// Check if we have legacy configuration that needs migration
|
||||||
|
bool hasLegacyConfig = hasLegacyServerConfiguration() || hasLegacyPackageFiles();
|
||||||
|
bool hasNewConfig = hasNewFormatConfiguration();
|
||||||
|
bool hasPackagesDir = std::filesystem::exists(packagesDir_);
|
||||||
|
|
||||||
|
// Need migration if:
|
||||||
|
// 1. We have legacy config (token file or package files in root config dir)
|
||||||
|
// 2. We have new config but no packages directory (incomplete migration)
|
||||||
|
return hasLegacyConfig || (hasNewConfig && !hasPackagesDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::performMigration() {
|
||||||
|
lastResult_ = MigrationResult();
|
||||||
|
|
||||||
|
logInfo("Starting migration from single-server to multi-server configuration");
|
||||||
|
|
||||||
|
// Create backup before starting migration
|
||||||
|
if (!createBackup()) {
|
||||||
|
logError("Failed to create backup before migration");
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create packages directory
|
||||||
|
if (!createPackagesDirectory()) {
|
||||||
|
logError("Failed to create packages directory");
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastResult_.packageDirectoryCreated = true;
|
||||||
|
|
||||||
|
// Step 2: Migrate server configuration
|
||||||
|
if (!migrateServerConfiguration()) {
|
||||||
|
logError("Failed to migrate server configuration");
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastResult_.serverConfigMigrated = true;
|
||||||
|
|
||||||
|
// Step 3: Migrate package metadata
|
||||||
|
if (!migratePackageMetadata()) {
|
||||||
|
logError("Failed to migrate package metadata");
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Validate migration
|
||||||
|
if (!validateMigration()) {
|
||||||
|
logError("Migration validation failed");
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Clean up legacy files (optional, keep backup)
|
||||||
|
// We don't delete legacy files immediately to allow rollback
|
||||||
|
|
||||||
|
lastResult_.success = true;
|
||||||
|
logInfo("Migration completed successfully");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Migration failed with exception: " + std::string(e.what()));
|
||||||
|
lastResult_.success = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::migrateServerConfiguration() {
|
||||||
|
try {
|
||||||
|
// Load existing server configuration or create default
|
||||||
|
if (!serverManager_->loadConfiguration()) {
|
||||||
|
logWarning("Failed to load existing server configuration, creating default");
|
||||||
|
serverManager_->ensureDefaultConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate legacy token file if it exists
|
||||||
|
if (!migrateLegacyTokenFile()) {
|
||||||
|
logWarning("Failed to migrate legacy token file (may not exist)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the configuration to ensure it's in the new format
|
||||||
|
if (!serverManager_->saveConfiguration()) {
|
||||||
|
logError("Failed to save server configuration");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("Server configuration migrated successfully");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Error migrating server configuration: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::migratePackageMetadata() {
|
||||||
|
try {
|
||||||
|
// Find legacy package files in the config directory
|
||||||
|
std::vector<std::filesystem::path> legacyFiles = findFilesWithExtension(configDir_, ".json");
|
||||||
|
|
||||||
|
// Filter out non-package files
|
||||||
|
std::vector<std::filesystem::path> packageFiles;
|
||||||
|
for (const auto& file : legacyFiles) {
|
||||||
|
std::string filename = file.filename().string();
|
||||||
|
// Skip servers.json and any files already in packages directory
|
||||||
|
if (filename != SERVERS_CONFIG_FILENAME && file.parent_path() == configDir_) {
|
||||||
|
packageFiles.push_back(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastResult_.totalPackages = packageFiles.size();
|
||||||
|
|
||||||
|
if (packageFiles.empty()) {
|
||||||
|
logInfo("No legacy package files found to migrate");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("Found " + std::to_string(packageFiles.size()) + " legacy package files to migrate");
|
||||||
|
|
||||||
|
// Migrate each package file
|
||||||
|
for (const auto& packageFile : packageFiles) {
|
||||||
|
if (migrateLegacyPackageFile(packageFile)) {
|
||||||
|
lastResult_.migratedPackages++;
|
||||||
|
logInfo("Migrated package file: " + packageFile.filename().string());
|
||||||
|
} else {
|
||||||
|
logError("Failed to migrate package file: " + packageFile.filename().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("Migrated " + std::to_string(lastResult_.migratedPackages) + " of " +
|
||||||
|
std::to_string(lastResult_.totalPackages) + " package files");
|
||||||
|
|
||||||
|
return lastResult_.migratedPackages == lastResult_.totalPackages;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Error migrating package metadata: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::createPackagesDirectory() {
|
||||||
|
return safeDirectoryCreate(packagesDir_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::validateMigration() const {
|
||||||
|
try {
|
||||||
|
// Validate server configuration
|
||||||
|
if (!validateServerConfiguration()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate package metadata
|
||||||
|
if (!validatePackageMetadata()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate directory structure
|
||||||
|
if (!validateDirectoryStructure()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error during migration validation: " << e.what() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::canRollback() const {
|
||||||
|
return std::filesystem::exists(backupDir_) && std::filesystem::is_directory(backupDir_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::performRollback() {
|
||||||
|
if (!canRollback()) {
|
||||||
|
logError("Cannot rollback: no backup found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logInfo("Starting rollback to previous configuration");
|
||||||
|
|
||||||
|
// Restore from backup
|
||||||
|
if (!restoreFromBackup()) {
|
||||||
|
logError("Failed to restore from backup");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("Rollback completed successfully");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Rollback failed with exception: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::createBackup() {
|
||||||
|
try {
|
||||||
|
// Create backup directory with timestamp
|
||||||
|
std::string timestamp = generateBackupTimestamp();
|
||||||
|
std::filesystem::path timestampedBackupDir = backupDir_ / timestamp;
|
||||||
|
|
||||||
|
if (!safeDirectoryCreate(timestampedBackupDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup existing configuration files
|
||||||
|
std::filesystem::path serversConfigPath = configDir_ / SERVERS_CONFIG_FILENAME;
|
||||||
|
if (std::filesystem::exists(serversConfigPath)) {
|
||||||
|
safeFileCopy(serversConfigPath, timestampedBackupDir / SERVERS_CONFIG_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup legacy token directory
|
||||||
|
if (std::filesystem::exists(legacyTokenDir_)) {
|
||||||
|
std::filesystem::path backupTokenDir = timestampedBackupDir / DEFAULT_SERVER_URL;
|
||||||
|
safeDirectoryCreate(backupTokenDir);
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(legacyTokenDir_)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
safeFileCopy(entry.path(), backupTokenDir / entry.path().filename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup existing package files
|
||||||
|
std::vector<std::filesystem::path> packageFiles = findFilesWithExtension(configDir_, ".json");
|
||||||
|
for (const auto& file : packageFiles) {
|
||||||
|
if (file.parent_path() == configDir_) {
|
||||||
|
safeFileCopy(file, timestampedBackupDir / file.filename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup packages directory if it exists
|
||||||
|
if (std::filesystem::exists(packagesDir_)) {
|
||||||
|
std::filesystem::path backupPackagesDir = timestampedBackupDir / PACKAGES_DIRECTORY_NAME;
|
||||||
|
safeDirectoryCreate(backupPackagesDir);
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(packagesDir_)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
safeFileCopy(entry.path(), backupPackagesDir / entry.path().filename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("Backup created at: " + timestampedBackupDir.string());
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to create backup: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::restoreFromBackup() {
|
||||||
|
try {
|
||||||
|
// Find the most recent backup
|
||||||
|
if (!std::filesystem::exists(backupDir_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path latestBackup;
|
||||||
|
std::filesystem::file_time_type latestTime{};
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(backupDir_)) {
|
||||||
|
if (entry.is_directory()) {
|
||||||
|
auto writeTime = entry.last_write_time();
|
||||||
|
if (writeTime > latestTime) {
|
||||||
|
latestTime = writeTime;
|
||||||
|
latestBackup = entry.path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestBackup.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore files from backup
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(latestBackup)) {
|
||||||
|
std::filesystem::path targetPath = configDir_ / entry.path().filename();
|
||||||
|
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
safeFileCopy(entry.path(), targetPath);
|
||||||
|
} else if (entry.is_directory()) {
|
||||||
|
// Restore directory recursively
|
||||||
|
std::filesystem::remove_all(targetPath);
|
||||||
|
std::filesystem::copy(entry.path(), targetPath, std::filesystem::copy_options::recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to restore from backup: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helper methods
|
||||||
|
|
||||||
|
bool MigrationManager::hasLegacyServerConfiguration() const {
|
||||||
|
// Check for legacy token file
|
||||||
|
std::filesystem::path legacyTokenPath = legacyTokenDir_ / LEGACY_TOKEN_FILENAME;
|
||||||
|
return std::filesystem::exists(legacyTokenPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::hasLegacyPackageFiles() const {
|
||||||
|
// Check for JSON files directly in config directory (not in packages subdirectory)
|
||||||
|
std::vector<std::filesystem::path> jsonFiles = findFilesWithExtension(configDir_, ".json");
|
||||||
|
|
||||||
|
for (const auto& file : jsonFiles) {
|
||||||
|
std::string filename = file.filename().string();
|
||||||
|
// If it's not servers.json and it's in the config directory (not packages), it's legacy
|
||||||
|
if (filename != SERVERS_CONFIG_FILENAME && file.parent_path() == configDir_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::hasNewFormatConfiguration() const {
|
||||||
|
std::filesystem::path serversConfigPath = configDir_ / SERVERS_CONFIG_FILENAME;
|
||||||
|
return std::filesystem::exists(serversConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::migrateLegacyTokenFile() {
|
||||||
|
std::filesystem::path legacyTokenPath = legacyTokenDir_ / LEGACY_TOKEN_FILENAME;
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(legacyTokenPath)) {
|
||||||
|
return true; // Nothing to migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream tokenFile(legacyTokenPath);
|
||||||
|
std::string token;
|
||||||
|
std::getline(tokenFile, token);
|
||||||
|
tokenFile.close();
|
||||||
|
|
||||||
|
if (!token.empty()) {
|
||||||
|
// Set the token for the default server
|
||||||
|
if (serverManager_->setWriteToken(DEFAULT_SERVER_URL, token)) {
|
||||||
|
logInfo("Migrated legacy write token for " + std::string(DEFAULT_SERVER_URL));
|
||||||
|
|
||||||
|
// Move the legacy token file to backup (don't delete immediately)
|
||||||
|
std::filesystem::path backupTokenPath = backupDir_ / "legacy_tokens" / DEFAULT_SERVER_URL / LEGACY_TOKEN_FILENAME;
|
||||||
|
safeDirectoryCreate(backupTokenPath.parent_path());
|
||||||
|
safeFileMove(legacyTokenPath, backupTokenPath);
|
||||||
|
|
||||||
|
// Remove the legacy directory if it's empty
|
||||||
|
try {
|
||||||
|
if (std::filesystem::is_empty(legacyTokenDir_)) {
|
||||||
|
std::filesystem::remove(legacyTokenDir_);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
// Ignore errors when removing empty directory
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to migrate legacy token file: " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::migrateLegacyPackageFile(const std::filesystem::path& legacyPath) {
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists(legacyPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load legacy format
|
||||||
|
std::ifstream file(legacyPath);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
logError("Failed to open legacy file: " + legacyPath.string());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json legacyJson;
|
||||||
|
file >> legacyJson;
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Convert to new format
|
||||||
|
PackageMetadata metadata = PackageMetadata::fromLegacyJson(legacyJson, DEFAULT_SERVER_URL);
|
||||||
|
|
||||||
|
if (!metadata.isValid()) {
|
||||||
|
logError("Invalid metadata after migration from " + legacyPath.string() + ": " + metadata.getValidationError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save in new location
|
||||||
|
if (!packageManager_->savePackageMetadata(metadata)) {
|
||||||
|
logError("Failed to save migrated metadata for " + metadata.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move legacy file to backup (don't delete immediately)
|
||||||
|
std::filesystem::path backupPath = backupDir_ / "legacy_packages" / legacyPath.filename();
|
||||||
|
safeDirectoryCreate(backupPath.parent_path());
|
||||||
|
safeFileMove(legacyPath, backupPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Error migrating legacy file " + legacyPath.string() + ": " + std::string(e.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::validateServerConfiguration() const {
|
||||||
|
try {
|
||||||
|
// Check if servers.json exists and is valid
|
||||||
|
std::filesystem::path serversConfigPath = configDir_ / SERVERS_CONFIG_FILENAME;
|
||||||
|
if (!std::filesystem::exists(serversConfigPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the configuration
|
||||||
|
auto tempServerManager = std::make_unique<ServerManager>();
|
||||||
|
if (!tempServerManager->loadConfiguration()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have at least one server
|
||||||
|
std::vector<std::string> servers = tempServerManager->getServers();
|
||||||
|
return !servers.empty();
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::validatePackageMetadata() const {
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists(packagesDir_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all package metadata files
|
||||||
|
return packageManager_->validateAllPackageMetadata();
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::validateDirectoryStructure() const {
|
||||||
|
// Check that packages directory exists and is accessible
|
||||||
|
return std::filesystem::exists(packagesDir_) && std::filesystem::is_directory(packagesDir_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MigrationManager::logError(const std::string& message) const {
|
||||||
|
std::cerr << "[MIGRATION ERROR] " << message << std::endl;
|
||||||
|
lastResult_.errors.push_back(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MigrationManager::logWarning(const std::string& message) const {
|
||||||
|
std::cerr << "[MIGRATION WARNING] " << message << std::endl;
|
||||||
|
lastResult_.warnings.push_back(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MigrationManager::logInfo(const std::string& message) const {
|
||||||
|
std::cout << "[MIGRATION INFO] " << message << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::safeFileMove(const std::filesystem::path& source, const std::filesystem::path& destination) {
|
||||||
|
try {
|
||||||
|
// Ensure destination directory exists
|
||||||
|
std::filesystem::create_directories(destination.parent_path());
|
||||||
|
|
||||||
|
// Move the file
|
||||||
|
std::filesystem::rename(source, destination);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to move file from " + source.string() + " to " + destination.string() + ": " + e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::safeFileCopy(const std::filesystem::path& source, const std::filesystem::path& destination) {
|
||||||
|
try {
|
||||||
|
// Ensure destination directory exists
|
||||||
|
std::filesystem::create_directories(destination.parent_path());
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
std::filesystem::copy_file(source, destination, std::filesystem::copy_options::overwrite_existing);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to copy file from " + source.string() + " to " + destination.string() + ": " + e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MigrationManager::safeDirectoryCreate(const std::filesystem::path& directory) {
|
||||||
|
try {
|
||||||
|
std::filesystem::create_directories(directory);
|
||||||
|
return std::filesystem::exists(directory) && std::filesystem::is_directory(directory);
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Failed to create directory " + directory.string() + ": " + e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> MigrationManager::findFilesWithExtension(const std::filesystem::path& directory, const std::string& extension) const {
|
||||||
|
std::vector<std::filesystem::path> files;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!std::filesystem::exists(directory)) {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||||
|
if (entry.is_regular_file() && entry.path().extension() == extension) {
|
||||||
|
files.push_back(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
logError("Error finding files with extension " + extension + " in " + directory.string() + ": " + e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MigrationManager::generateBackupTimestamp() 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%d_%H%M%S");
|
||||||
|
return ss.str();
|
||||||
|
}
|
100
getpkg/src/MigrationManager.hpp
Normal file
100
getpkg/src/MigrationManager.hpp
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include "PackageMetadata.hpp"
|
||||||
|
#include "ServerManager.hpp"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration manager for handling the transition from single-server to multi-server configuration
|
||||||
|
* Handles migration of server configuration, package metadata, and directory structure
|
||||||
|
*/
|
||||||
|
class MigrationManager {
|
||||||
|
public:
|
||||||
|
MigrationManager();
|
||||||
|
explicit MigrationManager(const std::filesystem::path& configDir);
|
||||||
|
|
||||||
|
// Main migration interface
|
||||||
|
bool needsMigration() const;
|
||||||
|
bool performMigration();
|
||||||
|
|
||||||
|
// Migration status and reporting
|
||||||
|
struct MigrationResult {
|
||||||
|
bool success = false;
|
||||||
|
int migratedPackages = 0;
|
||||||
|
int totalPackages = 0;
|
||||||
|
bool serverConfigMigrated = false;
|
||||||
|
bool packageDirectoryCreated = false;
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
std::vector<std::string> warnings;
|
||||||
|
};
|
||||||
|
|
||||||
|
MigrationResult getLastMigrationResult() const { return lastResult_; }
|
||||||
|
|
||||||
|
// Individual migration components (for testing and granular control)
|
||||||
|
bool migrateServerConfiguration();
|
||||||
|
bool migratePackageMetadata();
|
||||||
|
bool createPackagesDirectory();
|
||||||
|
bool validateMigration() const;
|
||||||
|
|
||||||
|
// Rollback capabilities
|
||||||
|
bool canRollback() const;
|
||||||
|
bool performRollback();
|
||||||
|
|
||||||
|
// Backup and restore
|
||||||
|
bool createBackup();
|
||||||
|
bool restoreFromBackup();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path configDir_;
|
||||||
|
std::filesystem::path packagesDir_;
|
||||||
|
std::filesystem::path backupDir_;
|
||||||
|
std::filesystem::path legacyTokenDir_;
|
||||||
|
|
||||||
|
std::unique_ptr<PackageMetadataManager> packageManager_;
|
||||||
|
std::unique_ptr<ServerManager> serverManager_;
|
||||||
|
|
||||||
|
mutable MigrationResult lastResult_;
|
||||||
|
|
||||||
|
// Migration detection helpers
|
||||||
|
bool hasLegacyServerConfiguration() const;
|
||||||
|
bool hasLegacyPackageFiles() const;
|
||||||
|
bool hasNewFormatConfiguration() const;
|
||||||
|
|
||||||
|
// Migration implementation helpers
|
||||||
|
bool migrateLegacyTokenFile();
|
||||||
|
bool migrateLegacyPackageFile(const std::filesystem::path& legacyPath);
|
||||||
|
bool movePackageFilesToSubdirectory();
|
||||||
|
bool updatePackageMetadataFormat();
|
||||||
|
bool cleanupLegacyFiles();
|
||||||
|
|
||||||
|
// Backup and rollback helpers
|
||||||
|
bool backupLegacyConfiguration();
|
||||||
|
bool backupExistingConfiguration();
|
||||||
|
std::string generateBackupTimestamp() const;
|
||||||
|
|
||||||
|
// Validation helpers
|
||||||
|
bool validateServerConfiguration() const;
|
||||||
|
bool validatePackageMetadata() const;
|
||||||
|
bool validateDirectoryStructure() const;
|
||||||
|
|
||||||
|
// Error handling and logging
|
||||||
|
void logError(const std::string& message) const;
|
||||||
|
void logWarning(const std::string& message) const;
|
||||||
|
void logInfo(const std::string& message) const;
|
||||||
|
|
||||||
|
// File system utilities
|
||||||
|
bool safeFileMove(const std::filesystem::path& source, const std::filesystem::path& destination);
|
||||||
|
bool safeFileCopy(const std::filesystem::path& source, const std::filesystem::path& destination);
|
||||||
|
bool safeDirectoryCreate(const std::filesystem::path& directory);
|
||||||
|
std::vector<std::filesystem::path> findFilesWithExtension(const std::filesystem::path& directory, const std::string& extension) const;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
static constexpr const char* LEGACY_TOKEN_FILENAME = "write_token.txt";
|
||||||
|
static constexpr const char* SERVERS_CONFIG_FILENAME = "servers.json";
|
||||||
|
static constexpr const char* PACKAGES_DIRECTORY_NAME = "packages";
|
||||||
|
static constexpr const char* BACKUP_DIRECTORY_NAME = "migration_backup";
|
||||||
|
static constexpr const char* DEFAULT_SERVER_URL = "getpkg.xyz";
|
||||||
|
};
|
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;
|
||||||
|
};
|
353
getpkg/src/ServerManager.cpp
Normal file
353
getpkg/src/ServerManager.cpp
Normal 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();
|
||||||
|
}
|
53
getpkg/src/ServerManager.hpp
Normal file
53
getpkg/src/ServerManager.hpp
Normal 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;
|
||||||
|
};
|
@ -57,6 +57,8 @@
|
|||||||
#include "BashrcEditor.hpp"
|
#include "BashrcEditor.hpp"
|
||||||
#include "DropshellScriptManager.hpp"
|
#include "DropshellScriptManager.hpp"
|
||||||
#include "GetbinClient.hpp"
|
#include "GetbinClient.hpp"
|
||||||
|
#include "MigrationManager.hpp"
|
||||||
|
#include "ServerManager.hpp"
|
||||||
#include "archive_tgz.hpp"
|
#include "archive_tgz.hpp"
|
||||||
#include "hash.hpp"
|
#include "hash.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -1127,6 +1129,15 @@ void show_help() {
|
|||||||
std::cout << " clean Clean up orphaned configs and symlinks" << std::endl;
|
std::cout << " clean Clean up orphaned configs and symlinks" << std::endl;
|
||||||
std::cout << " Removes unused config files and dangling symlinks" << std::endl;
|
std::cout << " Removes unused config files and dangling symlinks" << std::endl;
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
|
std::cout << " server add <url> Add a new package server" << std::endl;
|
||||||
|
std::cout << " Adds a server to the configuration for package discovery" << std::endl;
|
||||||
|
std::cout << std::endl;
|
||||||
|
std::cout << " server remove <url> Remove a package server" << std::endl;
|
||||||
|
std::cout << " Removes a server from the configuration" << std::endl;
|
||||||
|
std::cout << std::endl;
|
||||||
|
std::cout << " server list List all configured servers" << std::endl;
|
||||||
|
std::cout << " Shows all servers with their status and write token info" << std::endl;
|
||||||
|
std::cout << std::endl;
|
||||||
std::cout << " version Show getpkg version" << std::endl;
|
std::cout << " version Show getpkg version" << std::endl;
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
std::cout << " help Show this help message" << std::endl;
|
std::cout << " help Show this help message" << std::endl;
|
||||||
@ -1140,6 +1151,9 @@ void show_help() {
|
|||||||
std::cout << " getpkg unpublish myapp:x86_64 Remove only x86_64 version" << std::endl;
|
std::cout << " getpkg unpublish myapp:x86_64 Remove only x86_64 version" << std::endl;
|
||||||
std::cout << " getpkg uninstall myapp Remove myapp from system" << std::endl;
|
std::cout << " getpkg uninstall myapp Remove myapp from system" << std::endl;
|
||||||
std::cout << " getpkg update Update everything" << std::endl;
|
std::cout << " getpkg update Update everything" << std::endl;
|
||||||
|
std::cout << " getpkg server add packages.example.com Add a custom package server" << std::endl;
|
||||||
|
std::cout << " getpkg server remove packages.example.com Remove a package server" << std::endl;
|
||||||
|
std::cout << " getpkg server list List all configured servers" << std::endl;
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
std::cout << "ENVIRONMENT:" << std::endl;
|
std::cout << "ENVIRONMENT:" << std::endl;
|
||||||
std::cout << " SOS_WRITE_TOKEN Auth token for publishing tools" << std::endl;
|
std::cout << " SOS_WRITE_TOKEN Auth token for publishing tools" << std::endl;
|
||||||
@ -1150,6 +1164,163 @@ void show_help() {
|
|||||||
std::cout << " ~/.local/bin/getpkg/ Installed tool binaries" << std::endl;
|
std::cout << " ~/.local/bin/getpkg/ Installed tool binaries" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int server_command(int argc, char* argv[]) {
|
||||||
|
if (argc < 3) {
|
||||||
|
std::cerr << "Usage: getpkg server <add|remove|list> [args...]" << std::endl;
|
||||||
|
std::cerr << " getpkg server add <url> Add a new server" << std::endl;
|
||||||
|
std::cerr << " getpkg server remove <url> Remove a server" << std::endl;
|
||||||
|
std::cerr << " getpkg server list List all configured servers" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string subcommand = argv[2];
|
||||||
|
ServerManager serverManager;
|
||||||
|
|
||||||
|
// Load existing configuration
|
||||||
|
if (!serverManager.loadConfiguration()) {
|
||||||
|
std::cerr << "Failed to load server configuration" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == "add") {
|
||||||
|
if (argc < 4) {
|
||||||
|
std::cerr << "Usage: getpkg server add <url>" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serverUrl = argv[3];
|
||||||
|
|
||||||
|
// Validate server URL format
|
||||||
|
if (serverUrl.empty()) {
|
||||||
|
std::cerr << "Error: Server URL cannot be empty" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove protocol if provided (we'll add it internally)
|
||||||
|
if (serverUrl.find("http://") == 0) {
|
||||||
|
serverUrl = serverUrl.substr(7);
|
||||||
|
} else if (serverUrl.find("https://") == 0) {
|
||||||
|
serverUrl = serverUrl.substr(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash if present
|
||||||
|
if (!serverUrl.empty() && serverUrl.back() == '/') {
|
||||||
|
serverUrl.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Adding server: " << serverUrl << std::endl;
|
||||||
|
|
||||||
|
if (serverManager.addServer(serverUrl)) {
|
||||||
|
std::cout << "Successfully added server: " << serverUrl << std::endl;
|
||||||
|
|
||||||
|
// Ask if user wants to add a write token
|
||||||
|
std::cout << "Would you like to add a write token for this server? (y/N): ";
|
||||||
|
std::string response;
|
||||||
|
std::getline(std::cin, response);
|
||||||
|
|
||||||
|
if (response == "y" || response == "Y" || response == "yes" || response == "Yes") {
|
||||||
|
std::cout << "Enter write token for " << serverUrl << ": ";
|
||||||
|
std::string token;
|
||||||
|
std::getline(std::cin, token);
|
||||||
|
|
||||||
|
if (!token.empty()) {
|
||||||
|
if (serverManager.setWriteToken(serverUrl, token)) {
|
||||||
|
std::cout << "Write token added successfully" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Failed to save write token" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Failed to add server: " << serverUrl << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (subcommand == "remove") {
|
||||||
|
if (argc < 4) {
|
||||||
|
std::cerr << "Usage: getpkg server remove <url>" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serverUrl = argv[3];
|
||||||
|
|
||||||
|
// Remove protocol if provided
|
||||||
|
if (serverUrl.find("http://") == 0) {
|
||||||
|
serverUrl = serverUrl.substr(7);
|
||||||
|
} else if (serverUrl.find("https://") == 0) {
|
||||||
|
serverUrl = serverUrl.substr(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash if present
|
||||||
|
if (!serverUrl.empty() && serverUrl.back() == '/') {
|
||||||
|
serverUrl.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Removing server: " << serverUrl << std::endl;
|
||||||
|
|
||||||
|
if (serverManager.removeServer(serverUrl)) {
|
||||||
|
std::cout << "Successfully removed server: " << serverUrl << std::endl;
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Failed to remove server: " << serverUrl << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (subcommand == "list") {
|
||||||
|
std::vector<std::string> servers = serverManager.getServers();
|
||||||
|
|
||||||
|
if (servers.empty()) {
|
||||||
|
std::cout << "No servers configured" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << std::endl;
|
||||||
|
std::cout << "Configured servers:" << std::endl;
|
||||||
|
std::cout << "+" << std::string(30, '-') << "+" << std::string(12, '-') << "+" << std::string(15, '-') << "+" << std::endl;
|
||||||
|
std::cout << "|" << std::setw(30) << std::left << " Server URL"
|
||||||
|
<< "|" << std::setw(12) << std::left << " Default"
|
||||||
|
<< "|" << std::setw(15) << std::left << " Write Token"
|
||||||
|
<< "|" << std::endl;
|
||||||
|
std::cout << "+" << std::string(30, '-') << "+" << std::string(12, '-') << "+" << std::string(15, '-') << "+" << std::endl;
|
||||||
|
|
||||||
|
std::string defaultServer = serverManager.getDefaultServer();
|
||||||
|
|
||||||
|
for (const auto& server : servers) {
|
||||||
|
bool isDefault = (server == defaultServer);
|
||||||
|
bool hasToken = serverManager.hasWriteToken(server);
|
||||||
|
|
||||||
|
std::string displayUrl = server;
|
||||||
|
if (displayUrl.length() > 29) {
|
||||||
|
displayUrl = displayUrl.substr(0, 26) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "|" << std::setw(30) << std::left << (" " + displayUrl)
|
||||||
|
<< "|" << std::setw(12) << std::left << (isDefault ? " Yes" : " No")
|
||||||
|
<< "|" << std::setw(15) << std::left << (hasToken ? " Yes" : " No")
|
||||||
|
<< "|" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "+" << std::string(30, '-') << "+" << std::string(12, '-') << "+" << std::string(15, '-') << "+" << std::endl;
|
||||||
|
std::cout << std::endl;
|
||||||
|
std::cout << "Total servers: " << servers.size() << std::endl;
|
||||||
|
|
||||||
|
// Show default publish server if different from default
|
||||||
|
std::string defaultPublishServer = serverManager.getDefaultPublishServer();
|
||||||
|
if (defaultPublishServer != defaultServer) {
|
||||||
|
std::cout << "Default publish server: " << defaultPublishServer << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
std::cerr << "Unknown server subcommand: " << subcommand << std::endl;
|
||||||
|
std::cerr << "Use 'getpkg server' for usage information." << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int autocomplete_command(int argc, char* argv[]) {
|
int autocomplete_command(int argc, char* argv[]) {
|
||||||
std::vector<std::string> args(argv + 2, argv + argc);
|
std::vector<std::string> args(argv + 2, argv + argc);
|
||||||
|
|
||||||
@ -1165,6 +1336,7 @@ int autocomplete_command(int argc, char* argv[]) {
|
|||||||
std::cout << "hash\n";
|
std::cout << "hash\n";
|
||||||
std::cout << "list\n";
|
std::cout << "list\n";
|
||||||
std::cout << "clean\n";
|
std::cout << "clean\n";
|
||||||
|
std::cout << "server\n";
|
||||||
std::cout << "help\n";
|
std::cout << "help\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1179,6 +1351,35 @@ int autocomplete_command(int argc, char* argv[]) {
|
|||||||
} else if (subcommand == "uninstall") {
|
} else if (subcommand == "uninstall") {
|
||||||
// For uninstall, list installed tools
|
// For uninstall, list installed tools
|
||||||
std::filesystem::path configDir = std::filesystem::path(std::getenv("HOME")) / ".config" / "getpkg";
|
std::filesystem::path configDir = std::filesystem::path(std::getenv("HOME")) / ".config" / "getpkg";
|
||||||
|
if (std::filesystem::exists(configDir)) {
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(configDir)) {
|
||||||
|
if (entry.path().extension() == ".json") {
|
||||||
|
std::cout << entry.path().stem().string() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else if (subcommand == "server") {
|
||||||
|
// Handle server subcommand autocompletion
|
||||||
|
if (args.size() == 1) {
|
||||||
|
// Show server subcommands
|
||||||
|
std::cout << "add\n";
|
||||||
|
std::cout << "remove\n";
|
||||||
|
std::cout << "list\n";
|
||||||
|
} else if (args.size() == 2 && args[1] == "remove") {
|
||||||
|
// For server remove, list configured servers
|
||||||
|
ServerManager serverManager;
|
||||||
|
if (serverManager.loadConfiguration()) {
|
||||||
|
std::vector<std::string> servers = serverManager.getServers();
|
||||||
|
for (const auto& server : servers) {
|
||||||
|
std::cout << server << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else if (subcommand == "unpublish") {
|
||||||
|
// For unpublish, we could suggest installed tools
|
||||||
|
std::filesystem::path configDir = std::filesystem::path(std::getenv("HOME")) / ".config" / "getpkg";
|
||||||
if (std::filesystem::exists(configDir)) {
|
if (std::filesystem::exists(configDir)) {
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(configDir)) {
|
for (const auto& entry : std::filesystem::directory_iterator(configDir)) {
|
||||||
if (entry.path().extension() == ".json") {
|
if (entry.path().extension() == ".json") {
|
||||||
@ -1229,9 +1430,76 @@ int autocomplete_command(int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration check and execution
|
||||||
|
bool checkAndPerformMigration() {
|
||||||
|
try {
|
||||||
|
MigrationManager migrationManager;
|
||||||
|
|
||||||
|
if (migrationManager.needsMigration()) {
|
||||||
|
std::cout << "Migrating getpkg configuration to multi-server format..." << std::endl;
|
||||||
|
|
||||||
|
if (migrationManager.performMigration()) {
|
||||||
|
auto result = migrationManager.getLastMigrationResult();
|
||||||
|
std::cout << "Migration completed successfully!" << std::endl;
|
||||||
|
|
||||||
|
if (result.migratedPackages > 0) {
|
||||||
|
std::cout << " - Migrated " << result.migratedPackages << " package(s)" << std::endl;
|
||||||
|
}
|
||||||
|
if (result.serverConfigMigrated) {
|
||||||
|
std::cout << " - Updated server configuration" << std::endl;
|
||||||
|
}
|
||||||
|
if (result.packageDirectoryCreated) {
|
||||||
|
std::cout << " - Created packages directory structure" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.warnings.empty()) {
|
||||||
|
std::cout << "Migration warnings:" << std::endl;
|
||||||
|
for (const auto& warning : result.warnings) {
|
||||||
|
std::cout << " - " << warning << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
auto result = migrationManager.getLastMigrationResult();
|
||||||
|
std::cerr << "Migration failed!" << std::endl;
|
||||||
|
|
||||||
|
if (!result.errors.empty()) {
|
||||||
|
std::cerr << "Migration errors:" << std::endl;
|
||||||
|
for (const auto& error : result.errors) {
|
||||||
|
std::cerr << " - " << error << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrationManager.canRollback()) {
|
||||||
|
std::cerr << "Attempting rollback..." << std::endl;
|
||||||
|
if (migrationManager.performRollback()) {
|
||||||
|
std::cerr << "Rollback successful. Configuration restored to previous state." << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Rollback failed. Manual intervention may be required." << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // No migration needed
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Migration error: " << e.what() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // end anonymous namespace
|
} // end anonymous namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
// Perform migration check before any other operations
|
||||||
|
if (!checkAndPerformMigration()) {
|
||||||
|
std::cerr << "Failed to migrate configuration. Some functionality may not work correctly." << std::endl;
|
||||||
|
// Continue execution but warn user
|
||||||
|
}
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
show_help();
|
show_help();
|
||||||
return 0;
|
return 0;
|
||||||
@ -1259,6 +1527,8 @@ int main(int argc, char* argv[]) {
|
|||||||
return list_packages(argc, argv);
|
return list_packages(argc, argv);
|
||||||
} else if (command == "clean") {
|
} else if (command == "clean") {
|
||||||
return clean_tool(argc, argv);
|
return clean_tool(argc, argv);
|
||||||
|
} else if (command == "server") {
|
||||||
|
return server_command(argc, argv);
|
||||||
} else if (command == "help") {
|
} else if (command == "help") {
|
||||||
show_help();
|
show_help();
|
||||||
} else {
|
} else {
|
||||||
|
13
gp/gp
13
gp/gp
@ -202,12 +202,23 @@ generate_commit_message() {
|
|||||||
echo "$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() {
|
check_git_repo() {
|
||||||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
print_error "Not in a git repository"
|
print_error "Not in a git repository"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# Function to check for uncommitted changes and unpushed commits
|
||||||
|
Reference in New Issue
Block a user