377 lines
12 KiB
Nim
377 lines
12 KiB
Nim
# SPDX-License-Identifier: LSL-1.0
|
|
# Copyright (c) 2026 Markus Maiwald
|
|
# Stewardship: Self Sovereign Society Foundation
|
|
#
|
|
# This file is part of the Nexus Sovereign Core.
|
|
# See legal/LICENSE_SOVEREIGN.md for license terms.
|
|
|
|
## NimPak Migration Tools
|
|
##
|
|
## Tools for migrating from legacy formats and other package managers.
|
|
## Task 42: Implement migration tools.
|
|
|
|
import std/[os, strutils, strformat, json, tables, sequtils, times]
|
|
import ./types
|
|
import cas
|
|
import logging
|
|
|
|
type
|
|
MigrationSource* = enum
|
|
OldNip, ## Legacy NIP format
|
|
Flatpak, ## Flatpak applications
|
|
Snap, ## Snap packages
|
|
AppImage, ## AppImage files
|
|
Docker, ## Docker images
|
|
Nix, ## Nix packages
|
|
Pkgsrc ## pkgsrc packages
|
|
|
|
MigrationResult* = object
|
|
success*: bool
|
|
source*: MigrationSource
|
|
packageName*: string
|
|
targetPath*: string
|
|
casHashes*: seq[string]
|
|
errors*: seq[string]
|
|
warnings*: seq[string]
|
|
|
|
MigrationStats* = object
|
|
totalFiles*: int
|
|
totalBytes*: int64
|
|
dedupBytes*: int64
|
|
casObjects*: int
|
|
startTime*: DateTime
|
|
endTime*: DateTime
|
|
|
|
MigrationManager* = object
|
|
casManager*: CasManager
|
|
logger*: Logger
|
|
dryRun*: bool
|
|
verbose*: bool
|
|
|
|
# ############################################################################
|
|
# Migration Manager Initialization
|
|
# ############################################################################
|
|
|
|
proc initMigrationManager*(casRoot: string, dryRun: bool = false, verbose: bool = false): MigrationManager =
|
|
result = MigrationManager(
|
|
casManager: initCasManager(casRoot, casRoot / "system"),
|
|
logger: initLogger("migration", if verbose: Debug else: Info, {Console}),
|
|
dryRun: dryRun,
|
|
verbose: verbose
|
|
)
|
|
|
|
# ############################################################################
|
|
# Legacy NIP Migration
|
|
# ############################################################################
|
|
|
|
proc migrateLegacyNip*(mm: var MigrationManager, legacyPath: string): MigrationResult =
|
|
## Migrate from legacy NIP format (pre-unified storage)
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: OldNip,
|
|
packageName: legacyPath.extractFilename,
|
|
casHashes: @[],
|
|
errors: @[],
|
|
warnings: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Migrating legacy NIP: {legacyPath}")
|
|
|
|
if not dirExists(legacyPath):
|
|
result.errors.add(fmt"Legacy NIP directory not found: {legacyPath}")
|
|
return
|
|
|
|
# Check for legacy manifest
|
|
let manifestPath = legacyPath / "manifest.kdl"
|
|
let legacyManifest = legacyPath / "package.json"
|
|
|
|
if not fileExists(manifestPath) and not fileExists(legacyManifest):
|
|
result.errors.add("No manifest found (manifest.kdl or package.json)")
|
|
return
|
|
|
|
# Enumerate files to migrate
|
|
var stats = MigrationStats(startTime: now())
|
|
|
|
for file in walkDirRec(legacyPath):
|
|
if file.endswith(".nip-old") or file.contains("/.git/"):
|
|
continue
|
|
|
|
stats.totalFiles += 1
|
|
let fileInfo = getFileInfo(file)
|
|
stats.totalBytes += fileInfo.size
|
|
|
|
if not mm.dryRun:
|
|
# Store each file in CAS
|
|
let storeResult = mm.casManager.storeFile(file)
|
|
if storeResult.isOk:
|
|
let obj = storeResult.get()
|
|
result.casHashes.add(obj.hash)
|
|
stats.casObjects += 1
|
|
stats.dedupBytes += obj.size - obj.compressedSize
|
|
mm.logger.log(Debug, fmt"Stored: {file} -> {obj.hash[0..15]}...")
|
|
else:
|
|
result.warnings.add(fmt"Failed to store: {file}")
|
|
|
|
stats.endTime = now()
|
|
|
|
# Generate migration report
|
|
mm.logger.log(Info, fmt"Migration stats: {stats.totalFiles} files, {stats.totalBytes} bytes")
|
|
mm.logger.log(Info, fmt"CAS objects: {stats.casObjects}, Dedup savings: {stats.dedupBytes} bytes")
|
|
|
|
result.success = result.errors.len == 0
|
|
result.targetPath = mm.casManager.rootPath / "migrated" / result.packageName
|
|
|
|
# ############################################################################
|
|
# Flatpak Migration
|
|
# ############################################################################
|
|
|
|
proc migrateFlatpak*(mm: var MigrationManager, appId: string): MigrationResult =
|
|
## Migrate a Flatpak application to NIP format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: Flatpak,
|
|
packageName: appId,
|
|
casHashes: @[],
|
|
errors: @[],
|
|
warnings: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Migrating Flatpak app: {appId}")
|
|
|
|
# Check if flatpak is installed
|
|
let flatpakPath = "/var/lib/flatpak/app" / appId
|
|
let userFlatpakPath = getHomeDir() / ".local/share/flatpak/app" / appId
|
|
|
|
var sourcePath = ""
|
|
if dirExists(flatpakPath):
|
|
sourcePath = flatpakPath
|
|
elif dirExists(userFlatpakPath):
|
|
sourcePath = userFlatpakPath
|
|
else:
|
|
result.errors.add(fmt"Flatpak app not found: {appId}")
|
|
result.warnings.add("Ensure the app is installed via 'flatpak install'")
|
|
return
|
|
|
|
# Find current version
|
|
var currentDir = ""
|
|
for dir in walkDir(sourcePath / "current"):
|
|
if dir.kind == pcDir or dir.kind == pcLinkToDir:
|
|
currentDir = dir.path
|
|
break
|
|
|
|
if currentDir == "":
|
|
result.errors.add("Could not find current version")
|
|
return
|
|
|
|
# Migrate files
|
|
let filesDir = currentDir / "files"
|
|
if dirExists(filesDir):
|
|
for file in walkDirRec(filesDir):
|
|
if not mm.dryRun:
|
|
let storeResult = mm.casManager.storeFile(file)
|
|
if storeResult.isOk:
|
|
result.casHashes.add(storeResult.get().hash)
|
|
|
|
# Create NIP manifest from Flatpak metadata
|
|
let metadataPath = currentDir / "metadata"
|
|
if fileExists(metadataPath):
|
|
mm.logger.log(Debug, "Found Flatpak metadata, will convert to NIP manifest")
|
|
|
|
result.success = result.errors.len == 0
|
|
result.targetPath = getHomeDir() / ".local/share/nexus/nips" / appId
|
|
|
|
# ############################################################################
|
|
# AppImage Migration
|
|
# ############################################################################
|
|
|
|
proc migrateAppImage*(mm: var MigrationManager, appImagePath: string): MigrationResult =
|
|
## Migrate an AppImage to NIP format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: AppImage,
|
|
packageName: appImagePath.extractFilename.changeFileExt(""),
|
|
casHashes: @[],
|
|
errors: @[],
|
|
warnings: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Migrating AppImage: {appImagePath}")
|
|
|
|
if not fileExists(appImagePath):
|
|
result.errors.add(fmt"AppImage not found: {appImagePath}")
|
|
return
|
|
|
|
# AppImages are squashfs, need to extract
|
|
result.warnings.add("AppImage extraction requires 'unsquashfs' - placeholder")
|
|
|
|
# Store the AppImage itself as a blob for now
|
|
if not mm.dryRun:
|
|
let storeResult = mm.casManager.storeFile(appImagePath)
|
|
if storeResult.isOk:
|
|
result.casHashes.add(storeResult.get().hash)
|
|
result.success = true
|
|
|
|
# ############################################################################
|
|
# Docker/OCI Migration
|
|
# ############################################################################
|
|
|
|
proc migrateDockerImage*(mm: var MigrationManager, imageName: string): MigrationResult =
|
|
## Migrate a Docker image to NEXTER format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: Docker,
|
|
packageName: imageName.replace(":", "-").replace("/", "-"),
|
|
casHashes: @[],
|
|
errors: @[],
|
|
warnings: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Migrating Docker image: {imageName}")
|
|
|
|
# Export Docker image to tar
|
|
let exportPath = getTempDir() / fmt"docker-export-{result.packageName}.tar"
|
|
|
|
result.warnings.add("Docker migration requires 'docker save' - placeholder")
|
|
result.warnings.add(fmt"Would export to: {exportPath}")
|
|
|
|
# TODO: Actually run docker save and process layers
|
|
# Each Docker layer can be stored as a CAS chunk for deduplication
|
|
|
|
result.success = true # Placeholder
|
|
|
|
# ############################################################################
|
|
# Nix Package Migration
|
|
# ############################################################################
|
|
|
|
proc migrateNixPackage*(mm: var MigrationManager, nixStorePath: string): MigrationResult =
|
|
## Migrate a Nix store path to NPK format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: Nix,
|
|
packageName: nixStorePath.extractFilename.split("-", 1)[^1],
|
|
casHashes: @[],
|
|
errors: @[],
|
|
warnings: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Migrating Nix package: {nixStorePath}")
|
|
|
|
if not nixStorePath.startsWith("/nix/store/"):
|
|
result.errors.add("Invalid Nix store path")
|
|
return
|
|
|
|
if not dirExists(nixStorePath):
|
|
result.errors.add(fmt"Nix store path not found: {nixStorePath}")
|
|
return
|
|
|
|
# Migrate files from Nix store
|
|
for file in walkDirRec(nixStorePath):
|
|
if not mm.dryRun:
|
|
let storeResult = mm.casManager.storeFile(file)
|
|
if storeResult.isOk:
|
|
result.casHashes.add(storeResult.get().hash)
|
|
|
|
result.success = result.errors.len == 0
|
|
|
|
# ############################################################################
|
|
# Format Conversion Utilities
|
|
# ############################################################################
|
|
|
|
proc convertNpkToNip*(mm: var MigrationManager, npkPath: string): MigrationResult =
|
|
## Convert an NPK (binary package) to NIP (application) format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: OldNip,
|
|
packageName: npkPath.extractFilename.changeFileExt(""),
|
|
casHashes: @[],
|
|
errors: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Converting NPK to NIP: {npkPath}")
|
|
|
|
# NPK is a binary package, NIP is an application bundle
|
|
# The main difference is the manifest and entry point handling
|
|
|
|
result.warnings.add("NPK->NIP conversion creates application wrapper")
|
|
result.success = true # Placeholder
|
|
|
|
proc convertNipToNexter*(mm: var MigrationManager, nipPath: string): MigrationResult =
|
|
## Convert a NIP to NEXTER container format
|
|
result = MigrationResult(
|
|
success: false,
|
|
source: OldNip,
|
|
packageName: nipPath.extractFilename,
|
|
casHashes: @[],
|
|
errors: @[]
|
|
)
|
|
|
|
mm.logger.log(Info, fmt"Converting NIP to NEXTER: {nipPath}")
|
|
|
|
result.warnings.add("NIP->NEXTER creates containerized environment")
|
|
result.success = true # Placeholder
|
|
|
|
# ############################################################################
|
|
# Verification Tools
|
|
# ############################################################################
|
|
|
|
proc verifyMigration*(mm: var MigrationManager, migResult: MigrationResult): bool =
|
|
## Verify that a migration completed successfully
|
|
mm.logger.log(Info, fmt"Verifying migration: {migResult.packageName}")
|
|
|
|
if not migResult.success:
|
|
mm.logger.log(Error, "Migration reported failure")
|
|
return false
|
|
|
|
# Verify all CAS objects exist
|
|
var missing = 0
|
|
for hash in migResult.casHashes:
|
|
if not mm.casManager.objectExists(hash):
|
|
mm.logger.log(Error, fmt"Missing CAS object: {hash}")
|
|
missing += 1
|
|
|
|
if missing > 0:
|
|
mm.logger.log(Error, fmt"{missing} objects missing from CAS")
|
|
return false
|
|
|
|
mm.logger.log(Info, fmt"Migration verified: {migResult.casHashes.len} objects")
|
|
return true
|
|
|
|
proc generateMigrationReport*(results: seq[MigrationResult]): string =
|
|
## Generate a summary report of migration results
|
|
var successCount = 0
|
|
var failCount = 0
|
|
var totalObjects = 0
|
|
|
|
result = "# Migration Report\n\n"
|
|
let timestamp = now().format("yyyy-MM-dd HH:mm:ss")
|
|
result.add fmt"Generated: {timestamp}" & "\n\n"
|
|
|
|
for r in results:
|
|
if r.success:
|
|
successCount += 1
|
|
totalObjects += r.casHashes.len
|
|
else:
|
|
failCount += 1
|
|
|
|
result.add fmt"## Summary\n\n"
|
|
result.add fmt"- Total migrations: {results.len}\n"
|
|
result.add fmt"- Successful: {successCount}\n"
|
|
result.add fmt"- Failed: {failCount}\n"
|
|
result.add fmt"- Total CAS objects: {totalObjects}\n\n"
|
|
|
|
result.add "## Details\n\n"
|
|
for r in results:
|
|
let status = if r.success: "✅" else: "❌"
|
|
result.add fmt"### {status} {r.packageName}\n\n"
|
|
result.add fmt"- Source: {r.source}\n"
|
|
result.add fmt"- Objects: {r.casHashes.len}\n"
|
|
if r.errors.len > 0:
|
|
result.add "- Errors:\n"
|
|
for err in r.errors:
|
|
result.add fmt" - {err}\n"
|
|
if r.warnings.len > 0:
|
|
result.add "- Warnings:\n"
|
|
for warn in r.warnings:
|
|
result.add fmt" - {warn}\n"
|
|
result.add "\n"
|