nip/src/nimpak/migration.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"