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, options, strutils]
import ./hashing

const StoreFormatVersion* = 1
  ## Wersja layoutu CAS na dysku. ZAMROŻONE od v1:
  ##   <root>/objects/<hex[0:2]>/<hex[2:]>   — jeden plik = jeden obiekt,
  ##   nazwa = pełny hash (hex) po odcięciu pierwszych dwóch znaków użytych
  ##   jako katalog-shard (dokładnie jak .git/objects). Zawartość pliku to
  ##   surowe bajty chunku, BEZ dodatkowego nagłówka/kompresji — kompresja
  ##   (zstd) i szyfrowanie, jeśli kiedyś dojdą, będą osobną, jawnie
  ##   oznaczoną wersją formatu (2+), żeby stare store'y dało się nadal czytać.
  ## Plik <root>/FORMAT zawiera samą liczbę wersji (ASCII, bez nowej linii
  ## wymagane) i jest zapisywany przy pierwszym `openStore` na pustym
  ## katalogu. Kolejne otwarcia weryfikują zgodność.

type
  StoreFormatError* = object of CatchableError

  Store* = object
    root*: string
    formatVersion*: int

proc openStore*(root: string): Store =
  createDir(root / "objects")
  let formatFile = root / "FORMAT"
  if fileExists(formatFile):
    let existing = readFile(formatFile).strip()
    let version = try: parseInt(existing) except ValueError: -1
    if version > StoreFormatVersion:
      raise newException(StoreFormatError,
        "Store pod '" & root & "' ma FORMAT=" & existing &
        ", ta wersja FastTree obsługuje do " & $StoreFormatVersion &
        ". Zaktualizuj fasttree.")
    if version < 1:
      raise newException(StoreFormatError,
        "Store pod '" & root & "' ma nierozpoznany plik FORMAT ('" & existing & "').")
    Store(root: root, formatVersion: version)
  else:
    writeFile(formatFile, $StoreFormatVersion)
    Store(root: root, formatVersion: StoreFormatVersion)

proc shardPath(s: Store, h: Hash): string =
  let hex = $h
  s.root / "objects" / hex[0..1] / hex[2..^1]

proc has*(s: Store, h: Hash): bool =
  fileExists(s.shardPath(h))

proc objectPath*(s: Store, h: Hash): string =
  ## Fizyczna ścieżka obiektu w CAS. Publiczne specjalnie dla `ioengine`
  ## (readBatch/writeBatch operują na ścieżkach plików, nie na abstrakcyjnym
  ## `Hash` — store.nim jest jedynym miejscem, które zna layout `objects/xx/yyyy`,
  ## ale composefs.buildImage potrzebuje surowych ścieżek do wsadowego I/O).
  s.shardPath(h)

proc put*(s: Store, data: openArray[byte]): Hash =
  ## Zapisuje blok, zwraca jego skrót. Zapis idempotentny — jeśli obiekt
  ## już istnieje (deduplikacja), nie jest nadpisywany.
  let h = hashBytes(data)
  let path = s.shardPath(h)
  if not fileExists(path):
    createDir(path.parentDir)
    let tmp = path & ".tmp." & $getCurrentProcessId()
    writeFile(tmp, cast[string](@data))
    moveFile(tmp, path)  # atomowa podmiana w obrębie tego samego FS
  h

proc get*(s: Store, h: Hash): Option[seq[byte]] =
  let path = s.shardPath(h)
  if not fileExists(path):
    return none(seq[byte])
  let content = readFile(path)
  var buf = newSeq[byte](content.len)
  if content.len > 0:
    copyMem(addr buf[0], unsafeAddr content[0], content.len)
  some(buf)

proc missing*(s: Store, hashes: openArray[Hash]): seq[Hash] =
  ## Zwraca listę skrótów, których store jeszcze nie ma lokalnie —
  ## dokładnie to `fasttree pull` wysyła jako zapytanie o brakujące warstwy/chunki.
  result = @[]
  for h in hashes:
    if not s.has(h):
      result.add h

proc listAll*(s: Store): seq[Hash] =
  ## Wylicza wszystkie obiekty aktualnie leżące w CAS — używane przez GC
  ## do policzenia zbioru "wszystko minus live-set = do usunięcia".
  result = @[]
  let objectsDir = s.root / "objects"
  if not dirExists(objectsDir): return
  for shardDir in walkDirs(objectsDir / "*"):
    let shard = shardDir.extractFilename
    if shard.len != 2: continue
    for f in walkFiles(shardDir / "*"):
      let rest = f.extractFilename
      if rest.len == 62 and not rest.contains('.'):  # pomija pliki *.tmp.*
        try:
          result.add hashFromHex(shard & rest)
        except AssertionDefect, ValueError:
          discard  # plik nie pasuje do formatu hasha — ignorujemy (nie nasz obiekt)

proc deleteObject*(s: Store, h: Hash): bool =
  ## Usuwa pojedynczy obiekt z CAS. Zwraca false, jeśli obiektu nie było
  ## (np. już usunięty w poprzednim, przerwanym przebiegu GC — idempotentne).
  let path = s.shardPath(h)
  if fileExists(path):
    removeFile(path)
    true
  else:
    false