fasttree-sys 0.2.0

Surowe bindingi FFI (extern "C") do biblioteki FastTree (Nim). Nie używaj bezpośrednio — patrz crate `fasttree`.
import std/[os, json, strformat, times, strutils, sequtils]
import ./store
import ./manifest
import ./oci
import ./composefs
import ./layers
import ./gc
import ./overlay

proc rootDir(): string =
  getEnv("FASTTREE_ROOT", "/var/lib/fasttree")

proc ensureLayout() =
  for d in ["store", "deployments", "manifests", "layers-cache"]:
    createDir(rootDir() / d)

proc sanitizeTag(reference: string): string =
  ## Nazwa manifestu/wdrożenia na dysku — referencja OCI może zawierać
  ## znaki niebezpieczne jako nazwa pliku (np. "@sha256:..."), więc
  ## zamieniamy dwukropek na podkreślnik (tak samo jak dla nazw plików warstw).
  reference.replace(":", "_")

proc cmdPull*(imageRef: string) =
  ensureLayout()
  echo &"[fasttree] pull {imageRef}"
  let s = openStore(rootDir() / "store")
  let ref0 = parseImageRef(imageRef)
  let tag = sanitizeTag(ref0.reference)

  # 1. Pobierz (lub użyj z cache'u) wszystkie warstwy obrazu, w kolejności
  #    dół -> góra. Cache na poziomie blobów OCI jest niezależny od
  #    deduplikacji na poziomie chunków, która nastąpi w kroku 3.
  let resolved = resolveImageLayers(ref0, rootDir() / "layers-cache")
  let newlyDownloaded = resolved.filterIt(not it.wasCached).len
  echo &"[fasttree] {resolved.len} warstw łącznie, {newlyDownloaded} nowo pobranych " &
       &"({resolved.len - newlyDownloaded} już w cache)"

  # 2. Rozpakuj i scal warstwy (whiteouty OCI) do jednego materialnego drzewa.
  let workDir = getTempDir() / "fasttree-pull" / tag
  removeDir(workDir)  # czyste środowisko robocze na wypadek poprzedniego przerwanego pull
  let mergedDir = workDir / "merged"
  discard materializeLayers(resolved.mapIt(it.path), workDir / "layers", mergedDir)
  echo &"[fasttree] warstwy scalone w {mergedDir}"

  # 3. Zbuduj manifest FastTree: chunkowanie FastCDC + zapis chunków do CAS
  #    (deduplikacja automatyczna — identyczne chunki między wersjami/tagami
  #    trafiają do tego samego obiektu w store).
  let m = buildManifest(mergedDir, s)
  echo &"[fasttree] manifest zbudowany: root={m.root}, {m.entries.len} wpisów"

  let manifestPath = rootDir() / "manifests" / (tag & ".json")
  writeFile(manifestPath, pretty(toJson(m)))
  echo &"[fasttree] zapisano {manifestPath} — gotowe do 'fasttree deploy {tag}'"

  removeDir(workDir)  # posprzątaj drzewo robocze; chunki już bezpiecznie w store

proc loadManifestForTag(tag: string): Manifest =
  let path = rootDir() / "manifests" / (tag & ".json")
  if not fileExists(path):
    raise newException(IOError, &"brak manifestu '{path}' dla tagu '{tag}'")
  manifestFromJson(parseFile(path))

proc cmdStatus*(showDiff: bool) =
  ensureLayout()
  let currentLink = rootDir() / "current"
  if not fileExists(currentLink) and not dirExists(currentLink):
    echo "[fasttree] brak aktywnego wdrożenia"
    return
  echo &"[fasttree] aktywne wdrożenie: {expandSymlink(currentLink)}"
  if showDiff:
    let currentTagFile = rootDir() / "CURRENT_TAG"
    let previousTagFile = rootDir() / "PREVIOUS_TAG"
    if not fileExists(currentTagFile):
      echo "[fasttree] --diff: brak CURRENT_TAG — nie wiem, który manifest jest aktywny"
      return
    if not fileExists(previousTagFile):
      echo "[fasttree] --diff: brak PREVIOUS_TAG — to pierwsze wdrożenie w tym repo, nie ma z czym porównać"
      return
    let currentTag = readFile(currentTagFile).strip()
    let previousTag = readFile(previousTagFile).strip()
    if currentTag == previousTag:
      echo &"[fasttree] --diff: current i previous to ten sam tag ('{currentTag}') — brak zmian"
      return
    try:
      let oldM = loadManifestForTag(previousTag)
      let newM = loadManifestForTag(currentTag)
      let entries = manifest.diff(oldM, newM)
      if entries.len == 0:
        echo &"[fasttree] --diff {previousTag} -> {currentTag}: drzewa identyczne (root-hash bez zmian)"
      else:
        echo &"[fasttree] --diff {previousTag} -> {currentTag}: {entries.len} zmienionych ścieżek"
        for e in entries:
          case e.kind
          of dkAdded:
            echo &"  + {e.path}  ({e.newChunks} chunków)"
          of dkRemoved:
            echo &"  - {e.path}  ({e.oldChunks} chunków)"
          of dkModified:
            echo &"  ~ {e.path}  ({e.oldChunks} -> {e.newChunks} chunków, {e.reusedChunks} reużytych)"
    except IOError as e:
      echo &"[fasttree] --diff: {e.msg}"

proc cmdDeploy*(tag: string, atomic: bool) =
  ensureLayout()
  let s = openStore(rootDir() / "store")
  let manifestPath = rootDir() / "manifests" / (tag & ".json")
  if not fileExists(manifestPath):
    echo &"[fasttree] brak manifestu dla '{tag}' — najpierw 'fasttree pull'"
    return
  let m = manifestFromJson(parseFile(manifestPath))

  let deployDir = rootDir() / "deployments" / tag
  let image = deployDir & ".erofs"
  let digest = buildImage(m, s, materializedDir = deployDir & ".materialized", outputImage = image)
  writeFile(image & ".digest", digest)
  echo &"[fasttree] obraz composefs zbudowany, digest={digest}"

  # dm-verity: hash-tree obliczany od razu przy deploy, nie przy każdym
  # mount — root hash zapisany obok obrazu, gotowy do dmVerityOpen/Verify
  # bez ponownego liczenia całego drzewa przy każdym uruchomieniu.
  let vinfo = dmVerityFormat(image)
  writeFile(image & ".veritysum", vinfo.rootHash)
  echo &"[fasttree] dm-verity hash-tree obliczony, root={vinfo.rootHash}"

  let currentLink = rootDir() / "current"
  if atomic:
    # Atomowy A/B swap: nowy symlink budujemy pod tymczasową nazwą,
    # potem rename(2) na docelową — rename na tym samym FS jest atomowe,
    # więc bootloader/initramfs nigdy nie widzi połowicznego stanu.
    let tmpLink = currentLink & ".tmp"
    if fileExists(tmpLink) or symlinkExists(tmpLink): removeFile(tmpLink)
    createSymlink(image, tmpLink)
    moveFile(tmpLink, currentLink)
    echo &"[fasttree] wdrożono {tag} atomowo, current -> {image}"
  else:
    if fileExists(currentLink) or symlinkExists(currentLink): removeFile(currentLink)
    createSymlink(image, currentLink)
    echo &"[fasttree] wdrożono {tag}, current -> {image}"

  # PREVIOUS_TAG zapamiętuje POPRZEDNI CURRENT_TAG (jeśli inny niż nowy) —
  # wyłącznie do `fasttree status --diff`, żeby wiedzieć z czym porównać
  # nowo wdrożony manifest. Musi być zapisane PRZED nadpisaniem CURRENT_TAG.
  let currentTagFile = rootDir() / "CURRENT_TAG"
  if fileExists(currentTagFile):
    let prevTag = readFile(currentTagFile).strip()
    if prevTag != tag:
      writeFile(rootDir() / "PREVIOUS_TAG", prevTag)

  # CURRENT_TAG wskazuje GC (gc.nim), który manifest reprezentuje aktywny
  # deployment — bez tego GC nie wiedziałby, których chunków nie wolno ruszać.
  writeFile(currentTagFile, tag)

proc cmdGc*(dryRun: bool) =
  ensureLayout()
  echo &"[fasttree] gc{(if dryRun: \" --dry-run\" else: \"\")}: liczę live-set (CURRENT_TAG + pins.json)..."
  let res = runGc(rootDir(), dryRun)
  let verb = if dryRun: "zostałoby usuniętych" else: "usunięto"
  echo &"[fasttree] przeskanowano {res.scannedObjects} obiektów, {res.liveObjects} żywych, " &
       &"{verb} {res.deletedObjects} ({res.freedBytes} B)"

proc cmdPin*(tag: string, note: string) =
  ensureLayout()
  let pinsPath = rootDir() / "pins.json"
  var pins = if fileExists(pinsPath): parseFile(pinsPath) else: newJArray()
  pins.add %*{"tag": tag, "note": note, "pinnedAt": $now()}
  writeFile(pinsPath, pretty(pins))
  echo &"[fasttree] przypięto '{tag}': {note}"

proc cmdOverlayCreate*(name: string, ephemeral: bool) =
  ## Montuje CURRENT_TAG jako read-only lowerdir (composefs), potem dokłada
  ## overlay (trwały domyślnie — jak lokalne zmiany w /etc; --ephemeral dla
  ## efemerycznych/testowych). Dwa mounty na `name`: baza (readonly rootfs)
  ## + sam overlay, żeby lowerdir nie zależał od tego, czy ktoś inny akurat
  ## odmontował "current" pod nogami.
  ensureLayout()
  let currentTagFile = rootDir() / "CURRENT_TAG"
  if not fileExists(currentTagFile):
    echo "[fasttree] brak aktywnego wdrożenia (CURRENT_TAG) — najpierw 'fasttree deploy'"
    return
  let tag = readFile(currentTagFile).strip()
  let image = rootDir() / "deployments" / (tag & ".erofs")
  if not fileExists(image):
    echo &"[fasttree] brak obrazu dla aktywnego wdrożenia '{tag}' ({image})"
    return

  let baseMount = rootDir() / "overlay-base" / tag
  if not dirExists(baseMount) or not fileExists(baseMount / ".mounted"):
    mountImage(image, baseMount)
    writeFile(baseMount / ".mounted", "")
    echo &"[fasttree] zamontowano bazowy obraz '{tag}' (read-only) w {baseMount}"

  let mountpoint = rootDir() / "overlay-mounts" / name
  let ov =
    if ephemeral:
      newTmpOverlay(baseMount, mountpoint)
    else:
      newPersistentOverlay(baseMount, rootDir(), name, mountpoint)
  echo &"[fasttree] overlay '{name}' ({(if ephemeral: \"ulotny\" else: \"trwały\")}) zamontowany w {ov.mountpoint}"

proc cmdOverlayRemove*(name: string) =
  ensureLayout()
  let mountpoint = rootDir() / "overlay-mounts" / name
  let ephemeral = dirExists(mountpoint & ".tmpfs")
  let ov = Overlay(
    lowerdir: "", # nieużywane przez unmountOverlay
    upperdir: (if ephemeral: mountpoint & ".tmpfs" / "upper"
               else: rootDir() / "overlays" / name / "upper"),
    workdir: "",
    mountpoint: mountpoint,
    ephemeral: ephemeral,
    tmpfsMount: (if ephemeral: mountpoint & ".tmpfs" else: ""))
  unmountOverlay(ov)
  echo &"[fasttree] overlay '{name}' odmontowany" &
       (if ephemeral: " (dane ulotne — bezpowrotnie usunięte)" else: " (dane trwałe zachowane)")

proc cmdOverlayList*() =
  ## Roadmap #5: listuje overlaye utworzone przez `overlay create` —
  ## nazwę, typ (ulotny/trwały) i to, czy są FAKTYCZNIE zamontowane teraz
  ## (sprawdzone przez /proc/mounts w `listActiveOverlays`, nie tylko
  ## obecność katalogu, bo katalog przeżywa `overlay remove`... no, nie
  ## przeżywa, ale przeżywa `umount` wykonany ręcznie spoza fasttree).
  ensureLayout()
  let overlays = listActiveOverlays(rootDir())
  if overlays.len == 0:
    echo "[fasttree] brak overlayów"
    return
  echo &"[fasttree] {overlays.len} overlay(ów):"
  for ov in overlays:
    let kind = if ov.ephemeral: "ulotny" else: "trwały"
    let state = if ov.mounted: "zamontowany" else: "NIEZAMONTOWANY (katalog istnieje, ale mount nieaktywny)"
    echo &"  {ov.name}  [{kind}, {state}]  {ov.mountpoint}"

proc cmdOverlayDiff*(name: string) =
  ## Roadmap #5: `fasttree overlay diff <nazwa>` — lista ścieżek zmienionych
  ## w `upperdir` overlaya względem jego lowerdir (obraz composefs), bez
  ## ręcznego przeglądania `upperdir`. Opiera się na `overlay.listChanges`,
  ## który już istniał, ale nie był wcześniej podpięty pod CLI.
  ensureLayout()
  let overlays = listActiveOverlays(rootDir())
  var found = false
  for info in overlays:
    if info.name != name: continue
    found = true
    let ov = Overlay(lowerdir: "", upperdir: info.upperdir, workdir: "",
                      mountpoint: info.mountpoint, ephemeral: info.ephemeral,
                      tmpfsMount: (if info.ephemeral: info.mountpoint & ".tmpfs" else: ""))
    let changes = listChanges(ov)
    if changes.len == 0:
      echo &"[fasttree] overlay '{name}': brak zmian względem bazy"
    else:
      echo &"[fasttree] overlay '{name}': {changes.len} zmienionych ścieżek"
      for path in changes:
        echo &"  {path}"
    break
  if not found:
    echo &"[fasttree] nie znaleziono overlaya '{name}' (patrz 'fasttree overlay list')"