654 lines
23 KiB
Nim
654 lines
23 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/filesystem.nim
|
|
## GoboLinux-style filesystem management with generation integration
|
|
##
|
|
## This module implements the filesystem operations for NimPak, including:
|
|
## - GoboLinux-style /Programs/App/Version directory structure
|
|
## - Atomic symlink management in /System/Index
|
|
## - Generation-aware filesystem operations
|
|
## - Boot integration for generation selection
|
|
|
|
import std/[os, strutils, times, json, tables, sequtils, algorithm, osproc]
|
|
import ./types_fixed
|
|
|
|
type
|
|
FilesystemError* = object of types_fixed.NimPakError
|
|
path*: string
|
|
|
|
EnhancedFilesystemManager* = object
|
|
programsRoot*: string ## /Programs - Package installation directory
|
|
indexRoot*: string ## /System/Index - Symlink directory
|
|
generationsRoot*: string ## /System/Generations - Generation metadata
|
|
currentGeneration*: string ## Current active generation ID
|
|
dryRun*: bool ## Dry run mode for testing
|
|
|
|
GenerationFilesystem* = object
|
|
generation*: Generation
|
|
symlinkMap*: Table[string, string] ## target -> source mapping
|
|
backupPath*: string ## Backup location for rollback
|
|
|
|
# =============================================================================
|
|
# FilesystemManager Creation and Configuration
|
|
# =============================================================================
|
|
|
|
proc newEnhancedFilesystemManager*(programsRoot: string = "/Programs",
|
|
indexRoot: string = "/System/Index",
|
|
generationsRoot: string = "/System/Generations",
|
|
dryRun: bool = false): EnhancedFilesystemManager =
|
|
## Create a new EnhancedFilesystemManager with specified paths
|
|
EnhancedFilesystemManager(
|
|
programsRoot: programsRoot,
|
|
indexRoot: indexRoot,
|
|
generationsRoot: generationsRoot,
|
|
currentGeneration: "", # Will be loaded from filesystem
|
|
dryRun: dryRun
|
|
)
|
|
|
|
proc loadCurrentGeneration*(fm: var EnhancedFilesystemManager): Result[void, FilesystemError] =
|
|
## Load the current generation ID from filesystem
|
|
try:
|
|
let currentGenFile = fm.generationsRoot / "current"
|
|
if fileExists(currentGenFile):
|
|
fm.currentGeneration = readFile(currentGenFile).strip()
|
|
else:
|
|
# No current generation - this is a fresh system
|
|
fm.currentGeneration = ""
|
|
|
|
return ok[void, FilesystemError]()
|
|
except IOError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to load current generation: " & e.msg,
|
|
path: fm.generationsRoot / "current"
|
|
))
|
|
|
|
# =============================================================================
|
|
# Package Installation with Generation Integration
|
|
# =============================================================================
|
|
|
|
proc installPackage*(fm: EnhancedFilesystemManager, pkg: Fragment, generation: Generation): Result[InstallLocation, FilesystemError] =
|
|
## Install a package to the filesystem with generation tracking
|
|
try:
|
|
let programDir = fm.programsRoot / pkg.id.name / pkg.id.version
|
|
|
|
# Create program directory structure
|
|
if not fm.dryRun:
|
|
createDir(programDir)
|
|
createDir(programDir / "bin")
|
|
createDir(programDir / "lib")
|
|
createDir(programDir / "share")
|
|
createDir(programDir / "etc")
|
|
|
|
# Generate symlinks for this package
|
|
var indexLinks: seq[SymlinkPair] = @[]
|
|
|
|
# Scan for binaries to symlink
|
|
let binDir = programDir / "bin"
|
|
if dirExists(binDir):
|
|
for file in walkDir(binDir):
|
|
if file.kind == pcFile:
|
|
let fileName = extractFilename(file.path)
|
|
indexLinks.add(SymlinkPair(
|
|
source: file.path,
|
|
target: fm.indexRoot / "bin" / fileName
|
|
))
|
|
|
|
# Scan for libraries to symlink
|
|
let libDir = programDir / "lib"
|
|
if dirExists(libDir):
|
|
for file in walkDir(libDir):
|
|
if file.kind == pcFile and (file.path.endsWith(".so") or file.path.contains(".so.")):
|
|
let fileName = extractFilename(file.path)
|
|
indexLinks.add(SymlinkPair(
|
|
source: file.path,
|
|
target: fm.indexRoot / "lib" / fileName
|
|
))
|
|
|
|
# Scan for shared data to symlink
|
|
let shareDir = programDir / "share"
|
|
if dirExists(shareDir):
|
|
for subdir in walkDir(shareDir):
|
|
if subdir.kind == pcDir:
|
|
let dirName = extractFilename(subdir.path)
|
|
indexLinks.add(SymlinkPair(
|
|
source: subdir.path,
|
|
target: fm.indexRoot / "share" / dirName
|
|
))
|
|
|
|
let location = InstallLocation(
|
|
programDir: programDir,
|
|
indexLinks: indexLinks
|
|
)
|
|
|
|
return ok[InstallLocation, FilesystemError](location)
|
|
|
|
except OSError as e:
|
|
return err[InstallLocation, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to install package: " & e.msg,
|
|
path: fm.programsRoot / pkg.id.name / pkg.id.version
|
|
))
|
|
|
|
# =============================================================================
|
|
# Atomic Symlink Management
|
|
# =============================================================================
|
|
|
|
proc createSymlinks*(fm: EnhancedFilesystemManager, location: InstallLocation, generation: Generation): Result[void, FilesystemError] =
|
|
## Create symlinks for package installation with generation tracking
|
|
try:
|
|
for link in location.indexLinks:
|
|
let targetDir = parentDir(link.target)
|
|
|
|
if not fm.dryRun:
|
|
# Ensure target directory exists
|
|
if not dirExists(targetDir):
|
|
createDir(targetDir)
|
|
|
|
# Create the symlink (remove existing if present)
|
|
if symlinkExists(link.target) or fileExists(link.target):
|
|
removeFile(link.target)
|
|
|
|
createSymlink(link.source, link.target)
|
|
else:
|
|
echo "DRY RUN: Would create symlink " & link.source & " -> " & link.target
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except OSError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to create symlinks: " & e.msg,
|
|
path: location.programDir
|
|
))
|
|
|
|
proc atomicSymlinkUpdate*(fm: EnhancedFilesystemManager, updates: seq[SymlinkPair], generation: Generation): Result[void, FilesystemError] =
|
|
## Atomically update symlinks with generation tracking and rollback capability
|
|
try:
|
|
# Create backup of current symlink state
|
|
let backupDir = fm.generationsRoot / generation.id / "symlink_backup"
|
|
if not fm.dryRun:
|
|
createDir(backupDir)
|
|
|
|
var backupData: seq[SymlinkPair] = @[]
|
|
|
|
# Phase 1: Backup existing symlinks
|
|
for update in updates:
|
|
if symlinkExists(update.target):
|
|
let currentTarget = expandSymlink(update.target)
|
|
backupData.add(SymlinkPair(
|
|
source: currentTarget,
|
|
target: update.target
|
|
))
|
|
|
|
if not fm.dryRun:
|
|
# Save backup information
|
|
let backupFile = backupDir / extractFilename(update.target) & ".backup"
|
|
writeFile(backupFile, currentTarget)
|
|
elif fileExists(update.target):
|
|
# Handle regular files that need to be replaced
|
|
let backupFile = backupDir / extractFilename(update.target) & ".file"
|
|
if not fm.dryRun:
|
|
copyFile(update.target, backupFile)
|
|
|
|
# Phase 2: Apply new symlinks atomically
|
|
for update in updates:
|
|
let targetDir = parentDir(update.target)
|
|
|
|
if not fm.dryRun:
|
|
# Ensure target directory exists
|
|
if not dirExists(targetDir):
|
|
createDir(targetDir)
|
|
|
|
# Remove existing file/symlink
|
|
if fileExists(update.target) or symlinkExists(update.target):
|
|
removeFile(update.target)
|
|
|
|
# Create new symlink
|
|
createSymlink(update.source, update.target)
|
|
else:
|
|
echo "DRY RUN: Would update symlink " & update.source & " -> " & update.target
|
|
|
|
# Phase 3: Record generation symlink state
|
|
if not fm.dryRun:
|
|
let symlinkStateFile = fm.generationsRoot / generation.id / "symlinks.json"
|
|
let symlinkState = %*{
|
|
"generation": generation.id,
|
|
"timestamp": $generation.timestamp,
|
|
"symlinks": updates.mapIt(%*{
|
|
"source": it.source,
|
|
"target": it.target
|
|
}),
|
|
"backup_location": backupDir
|
|
}
|
|
writeFile(symlinkStateFile, $symlinkState)
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except OSError as e:
|
|
# Attempt rollback on failure
|
|
discard rollbackSymlinks(fm, backupData)
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to update symlinks atomically: " & e.msg,
|
|
path: fm.indexRoot
|
|
))
|
|
|
|
proc rollbackSymlinks*(fm: EnhancedFilesystemManager, backupData: seq[SymlinkPair]): Result[void, FilesystemError] =
|
|
## Rollback symlinks to previous state
|
|
try:
|
|
for backup in backupData:
|
|
if not fm.dryRun:
|
|
# Remove current symlink
|
|
if fileExists(backup.target) or symlinkExists(backup.target):
|
|
removeFile(backup.target)
|
|
|
|
# Restore original symlink
|
|
createSymlink(backup.source, backup.target)
|
|
else:
|
|
echo "DRY RUN: Would rollback symlink " & backup.source & " -> " & backup.target
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except OSError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to rollback symlinks: " & e.msg,
|
|
path: fm.indexRoot
|
|
))
|
|
|
|
# =============================================================================
|
|
# Generation Switching and Management
|
|
# =============================================================================
|
|
|
|
proc switchToGeneration*(fm: var EnhancedFilesystemManager, targetGeneration: Generation): Result[void, FilesystemError] =
|
|
## Switch the system to a specific generation atomically
|
|
try:
|
|
let generationDir = fm.generationsRoot / targetGeneration.id
|
|
let symlinkStateFile = generationDir / "symlinks.json"
|
|
|
|
if not fileExists(symlinkStateFile):
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "Generation symlink state not found: " & targetGeneration.id,
|
|
path: symlinkStateFile
|
|
))
|
|
|
|
# Load generation symlink state
|
|
let symlinkStateJson = parseJson(readFile(symlinkStateFile))
|
|
var targetSymlinks: seq[SymlinkPair] = @[]
|
|
|
|
for linkJson in symlinkStateJson["symlinks"].getElems():
|
|
targetSymlinks.add(SymlinkPair(
|
|
source: linkJson["source"].getStr(),
|
|
target: linkJson["target"].getStr()
|
|
))
|
|
|
|
# Perform atomic symlink update to target generation
|
|
let updateResult = atomicSymlinkUpdate(fm, targetSymlinks, targetGeneration)
|
|
if updateResult.isErr:
|
|
return updateResult
|
|
|
|
# Update current generation pointer
|
|
if not fm.dryRun:
|
|
let currentGenFile = fm.generationsRoot / "current"
|
|
writeFile(currentGenFile, targetGeneration.id)
|
|
|
|
fm.currentGeneration = targetGeneration.id
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except JsonParsingError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: InvalidMetadata,
|
|
msg: "Failed to parse generation metadata: " & e.msg,
|
|
path: fm.generationsRoot / targetGeneration.id
|
|
))
|
|
except IOError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to switch generation: " & e.msg,
|
|
path: fm.generationsRoot / targetGeneration.id
|
|
))
|
|
|
|
proc rollbackToPreviousGeneration*(fm: var EnhancedFilesystemManager): Result[Generation, FilesystemError] =
|
|
## Rollback to the previous generation
|
|
try:
|
|
if fm.currentGeneration.len == 0:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "No current generation to rollback from",
|
|
path: fm.generationsRoot
|
|
))
|
|
|
|
# Load current generation metadata
|
|
let currentGenFile = fm.generationsRoot / fm.currentGeneration / "generation.json"
|
|
if not fileExists(currentGenFile):
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "Current generation metadata not found",
|
|
path: currentGenFile
|
|
))
|
|
|
|
let currentGenJson = parseJson(readFile(currentGenFile))
|
|
let previousGenId = currentGenJson.getOrDefault("previous")
|
|
|
|
if previousGenId.isNil or previousGenId.getStr().len == 0:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "No previous generation available for rollback",
|
|
path: currentGenFile
|
|
))
|
|
|
|
# Load previous generation
|
|
let previousGenFile = fm.generationsRoot / previousGenId.getStr() / "generation.json"
|
|
let previousGenJson = parseJson(readFile(previousGenFile))
|
|
|
|
let previousGeneration = Generation(
|
|
id: previousGenId.getStr(),
|
|
timestamp: previousGenJson["timestamp"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()),
|
|
packages: previousGenJson["packages"].getElems().mapIt(PackageId(
|
|
name: it["name"].getStr(),
|
|
version: it["version"].getStr(),
|
|
stream: parseEnum[PackageStream](it["stream"].getStr())
|
|
)),
|
|
previous: if previousGenJson.hasKey("previous") and not previousGenJson["previous"].isNil:
|
|
some(previousGenJson["previous"].getStr())
|
|
else:
|
|
none(string),
|
|
size: previousGenJson["size"].getInt()
|
|
)
|
|
|
|
# Switch to previous generation
|
|
let switchResult = switchToGeneration(fm, previousGeneration)
|
|
if switchResult.isErr:
|
|
return err[Generation, FilesystemError](switchResult.getError())
|
|
|
|
return ok[Generation, FilesystemError](previousGeneration)
|
|
|
|
except JsonParsingError as e:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: InvalidMetadata,
|
|
msg: "Failed to parse generation metadata: " & e.msg,
|
|
path: fm.generationsRoot
|
|
))
|
|
except IOError as e:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to rollback generation: " & e.msg,
|
|
path: fm.generationsRoot
|
|
))
|
|
|
|
# =============================================================================
|
|
# Generation Export and Import
|
|
# =============================================================================
|
|
|
|
proc exportGeneration*(fm: EnhancedFilesystemManager, generation: Generation, exportPath: string): Result[void, FilesystemError] =
|
|
## Export a generation for system migration
|
|
try:
|
|
let generationDir = fm.generationsRoot / generation.id
|
|
|
|
if not dirExists(generationDir):
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "Generation directory not found: " & generation.id,
|
|
path: generationDir
|
|
))
|
|
|
|
# Create export archive
|
|
let exportCmd = "tar -czf " & exportPath & " -C " & generationDir & " ."
|
|
let result = execProcess(exportCmd, options = {poUsePath})
|
|
|
|
if result.exitCode != 0:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to export generation: " & result.output,
|
|
path: exportPath
|
|
))
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except OSError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to export generation: " & e.msg,
|
|
path: exportPath
|
|
))
|
|
|
|
proc importGeneration*(fm: EnhancedFilesystemManager, importPath: string, newGenerationId: string): Result[Generation, FilesystemError] =
|
|
## Import a generation from archive
|
|
try:
|
|
let generationDir = fm.generationsRoot / newGenerationId
|
|
|
|
if dirExists(generationDir):
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Generation already exists: " & newGenerationId,
|
|
path: generationDir
|
|
))
|
|
|
|
# Create generation directory
|
|
if not fm.dryRun:
|
|
createDir(generationDir)
|
|
|
|
# Extract archive
|
|
let extractCmd = "tar -xzf " & importPath & " -C " & generationDir
|
|
let result = execProcess(extractCmd, options = {poUsePath})
|
|
|
|
if result.exitCode != 0:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to import generation: " & result.output,
|
|
path: importPath
|
|
))
|
|
|
|
# Load generation metadata
|
|
let generationFile = generationDir / "generation.json"
|
|
let generationJson = parseJson(readFile(generationFile))
|
|
|
|
let importedGeneration = Generation(
|
|
id: newGenerationId, # Use new ID
|
|
timestamp: now(), # Update timestamp
|
|
packages: generationJson["packages"].getElems().mapIt(PackageId(
|
|
name: it["name"].getStr(),
|
|
version: it["version"].getStr(),
|
|
stream: parseEnum[PackageStream](it["stream"].getStr())
|
|
)),
|
|
previous: some(fm.currentGeneration), # Link to current generation
|
|
size: generationJson["size"].getInt()
|
|
)
|
|
|
|
return ok[Generation, FilesystemError](importedGeneration)
|
|
|
|
except JsonParsingError as e:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: InvalidMetadata,
|
|
msg: "Failed to parse imported generation: " & e.msg,
|
|
path: importPath
|
|
))
|
|
except IOError as e:
|
|
return err[Generation, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to import generation: " & e.msg,
|
|
path: importPath
|
|
))
|
|
|
|
# =============================================================================
|
|
# Generation Repair and Recovery
|
|
# =============================================================================
|
|
|
|
proc repairGeneration*(fm: EnhancedFilesystemManager, generation: Generation): Result[void, FilesystemError] =
|
|
## Repair a corrupted generation by rebuilding symlinks
|
|
try:
|
|
let generationDir = fm.generationsRoot / generation.id
|
|
let symlinkStateFile = generationDir / "symlinks.json"
|
|
|
|
if not fileExists(symlinkStateFile):
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "Generation symlink state not found for repair: " & generation.id,
|
|
path: symlinkStateFile
|
|
))
|
|
|
|
# Load expected symlink state
|
|
let symlinkStateJson = parseJson(readFile(symlinkStateFile))
|
|
var expectedSymlinks: seq[SymlinkPair] = @[]
|
|
|
|
for linkJson in symlinkStateJson["symlinks"].getElems():
|
|
expectedSymlinks.add(SymlinkPair(
|
|
source: linkJson["source"].getStr(),
|
|
target: linkJson["target"].getStr()
|
|
))
|
|
|
|
# Verify and repair each symlink
|
|
var repairedCount = 0
|
|
for expected in expectedSymlinks:
|
|
let needsRepair = if symlinkExists(expected.target):
|
|
expandSymlink(expected.target) != expected.source
|
|
else:
|
|
true # Missing symlink needs repair
|
|
|
|
if needsRepair:
|
|
if not fm.dryRun:
|
|
# Remove incorrect symlink/file
|
|
if fileExists(expected.target) or symlinkExists(expected.target):
|
|
removeFile(expected.target)
|
|
|
|
# Ensure target directory exists
|
|
let targetDir = parentDir(expected.target)
|
|
if not dirExists(targetDir):
|
|
createDir(targetDir)
|
|
|
|
# Create correct symlink
|
|
createSymlink(expected.source, expected.target)
|
|
else:
|
|
echo "DRY RUN: Would repair symlink " & expected.source & " -> " & expected.target
|
|
|
|
repairedCount += 1
|
|
|
|
if repairedCount > 0:
|
|
echo "Repaired " & $repairedCount & " symlinks in generation " & generation.id
|
|
else:
|
|
echo "Generation " & generation.id & " is healthy - no repairs needed"
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except JsonParsingError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: InvalidMetadata,
|
|
msg: "Failed to parse generation metadata for repair: " & e.msg,
|
|
path: generationDir
|
|
))
|
|
except OSError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to repair generation: " & e.msg,
|
|
path: generationDir
|
|
))
|
|
|
|
# =============================================================================
|
|
# Boot Integration Support
|
|
# =============================================================================
|
|
|
|
proc createBootEntry*(fm: EnhancedFilesystemManager, generation: Generation, bootDir: string = "/boot"): Result[void, FilesystemError] =
|
|
## Create boot entry for generation selection
|
|
try:
|
|
let bootEntryDir = bootDir / "nexus" / "generations"
|
|
if not fm.dryRun:
|
|
createDir(bootEntryDir)
|
|
|
|
let bootEntryFile = bootEntryDir / generation.id & ".conf"
|
|
let bootEntry = """
|
|
title NexusOS Generation """ & generation.id & """
|
|
version """ & generation.id & """
|
|
linux /nexus/kernel
|
|
initrd /nexus/initrd
|
|
options nexus.generation=""" & generation.id & """ root=LABEL=nexus-root
|
|
"""
|
|
|
|
if not fm.dryRun:
|
|
writeFile(bootEntryFile, bootEntry)
|
|
else:
|
|
echo "DRY RUN: Would create boot entry " & bootEntryFile
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except IOError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to create boot entry: " & e.msg,
|
|
path: bootDir
|
|
))
|
|
|
|
proc setDefaultBootGeneration*(fm: EnhancedFilesystemManager, generation: Generation, bootDir: string = "/boot"): Result[void, FilesystemError] =
|
|
## Set the default boot generation
|
|
try:
|
|
let defaultBootFile = bootDir / "nexus" / "default_generation"
|
|
|
|
if not fm.dryRun:
|
|
writeFile(defaultBootFile, generation.id)
|
|
else:
|
|
echo "DRY RUN: Would set default boot generation to " & generation.id
|
|
|
|
return ok[void, FilesystemError]()
|
|
|
|
except IOError as e:
|
|
return err[void, FilesystemError](FilesystemError(
|
|
code: FileWriteError,
|
|
msg: "Failed to set default boot generation: " & e.msg,
|
|
path: bootDir
|
|
))
|
|
|
|
# =============================================================================
|
|
# Utility Functions
|
|
# =============================================================================
|
|
|
|
proc getGenerationFilesystemInfo*(fm: EnhancedFilesystemManager, generation: Generation): Result[GenerationFilesystem, FilesystemError] =
|
|
## Get detailed filesystem information for a generation
|
|
try:
|
|
let generationDir = fm.generationsRoot / generation.id
|
|
let symlinkStateFile = generationDir / "symlinks.json"
|
|
|
|
if not fileExists(symlinkStateFile):
|
|
return err[GenerationFilesystem, FilesystemError](FilesystemError(
|
|
code: PackageNotFound,
|
|
msg: "Generation filesystem state not found: " & generation.id,
|
|
path: symlinkStateFile
|
|
))
|
|
|
|
let symlinkStateJson = parseJson(readFile(symlinkStateFile))
|
|
var symlinkMap: Table[string, string] = initTable[string, string]()
|
|
|
|
for linkJson in symlinkStateJson["symlinks"].getElems():
|
|
let target = linkJson["target"].getStr()
|
|
let source = linkJson["source"].getStr()
|
|
symlinkMap[target] = source
|
|
|
|
let backupPath = symlinkStateJson.getOrDefault("backup_location")
|
|
let backupLocation = if backupPath.isNil: "" else: backupPath.getStr()
|
|
|
|
let genFs = GenerationFilesystem(
|
|
generation: generation,
|
|
symlinkMap: symlinkMap,
|
|
backupPath: backupLocation
|
|
)
|
|
|
|
return ok[GenerationFilesystem, FilesystemError](genFs)
|
|
|
|
except JsonParsingError as e:
|
|
return err[GenerationFilesystem, FilesystemError](FilesystemError(
|
|
code: InvalidMetadata,
|
|
msg: "Failed to parse generation filesystem info: " & e.msg,
|
|
path: generationDir
|
|
))
|
|
except IOError as e:
|
|
return err[GenerationFilesystem, FilesystemError](FilesystemError(
|
|
code: FileReadError,
|
|
msg: "Failed to load generation filesystem info: " & e.msg,
|
|
path: generationDir
|
|
)) |