diff --git a/.kiro/specs/multi-server-support/tasks.md b/.kiro/specs/multi-server-support/tasks.md index 32fa8a0..d712ca2 100644 --- a/.kiro/specs/multi-server-support/tasks.md +++ b/.kiro/specs/multi-server-support/tasks.md @@ -45,7 +45,10 @@ Based on analysis of the current codebase, the multi-server support feature need ## Migration and Compatibility Tasks -- [ ] 4. Implement migration system for existing installations +- [-] 4. Implement migration system for existing installations + + + - Create MigrationManager class for legacy data handling - Implement automatic migration from single-server to multi-server config - Migrate existing package JSON files to packages subdirectory diff --git a/getpkg/src/MigrationManager.cpp b/getpkg/src/MigrationManager.cpp new file mode 100644 index 0000000..8a5b28e --- /dev/null +++ b/getpkg/src/MigrationManager.cpp @@ -0,0 +1,575 @@ +#include "MigrationManager.hpp" +#include +#include +#include +#include +#include +#include +#include + +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(configDir_); + serverManager_ = std::make_unique(); + } +} + +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(configDir); + serverManager_ = std::make_unique(); +} + +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 legacyFiles = findFilesWithExtension(configDir_, ".json"); + + // Filter out non-package files + std::vector 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 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 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(); + if (!tempServerManager->loadConfiguration()) { + return false; + } + + // Check that we have at least one server + std::vector 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 MigrationManager::findFilesWithExtension(const std::filesystem::path& directory, const std::string& extension) const { + std::vector 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(); +} \ No newline at end of file diff --git a/getpkg/src/MigrationManager.hpp b/getpkg/src/MigrationManager.hpp new file mode 100644 index 0000000..54c1687 --- /dev/null +++ b/getpkg/src/MigrationManager.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#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 errors; + std::vector 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 packageManager_; + std::unique_ptr 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 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"; +}; \ No newline at end of file diff --git a/getpkg/src/main.cpp b/getpkg/src/main.cpp index 9ffcb14..3e2649f 100644 --- a/getpkg/src/main.cpp +++ b/getpkg/src/main.cpp @@ -57,6 +57,7 @@ #include "BashrcEditor.hpp" #include "DropshellScriptManager.hpp" #include "GetbinClient.hpp" +#include "MigrationManager.hpp" #include "archive_tgz.hpp" #include "hash.hpp" #include @@ -1229,9 +1230,76 @@ int autocomplete_command(int argc, char* argv[]) { 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 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) { show_help(); return 0;