#!/bin/sh
# Helper de "replica ligera" de PBI (root). Sustituto de un PBS remoto para
# NAS sencillos: monta un recurso SMB del NAS (mismo mecanismo de montaje que
# la funcion existente "Anadir almacenamiento" de PBI, pbsStorage.js) y
# sincroniza ahi los chunks/manifiestos YA CERRADOS del datastore local con
# rsync -- sin necesitar PBS corriendo en el remoto.
#
# Igual que pbi-netconf: el servicio web (sin privilegios) NUNCA escala --
# solo escribe una solicitud en $DATA, y este script (disparado por
# pbi-lightsync.path, root, sin sandbox) hace el trabajo real.
#
# Tres solicitudes, cada una consume su fichero al procesarlo:
#   .lightsync-request         monta (o remonta) el SMB de un peer
#                               (8 lineas: peerId, servidor, share, usuario,
#                               password en base64, dominio en base64 o vacio,
#                               datastore local a sincronizar, version SMB)
#   .lightsync-run-request      dispara una pasada de sincronizacion real
#                               (1 linea: peerId) -- SOLO se debe escribir
#                               despues de confirmar por la API de PBS que el
#                               backup que la origino termino bien
#   .lightsync-remove-request   desmonta y borra las credenciales (1 linea:
#                               peerId)
#
# REGLA DE SINCRONIZACION SEGURA (validada con backups reales, ver checkpoint
# del proyecto): los chunks de PBS son inmutables una vez escritos -- se
# sincronizan siempre, sin restriccion. Una carpeta de snapshot SOLO se
# considera cerrada si contiene "index.json.blob" (durante la escritura solo
# hay *.tmp_didx y ese fichero no existe todavia) -- cualquier snapshot sin
# ese fichero se ignora por completo. El fichero ".lock" del datastore nunca
# se sincroniza (es solo de tiempo de ejecucion). Orden: chunks primero,
# manifiestos despues, para que una sincronizacion interrumpida nunca deje en
# el remoto un manifiesto que apunte a un chunk ausente.
set -u

DATA=${PBI_NETCONF_ROOT:-/var/lib/pbi}
REQ_MOUNT="$DATA/.lightsync-request"
REQ_RUN="$DATA/.lightsync-run-request"
REQ_RM="$DATA/.lightsync-remove-request"
STATUS="$DATA/.lightsync-status"
LOG="$DATA/.lightsync.log"
CREDDIR=/etc/pbi-cifs
MOUNTBASE=/mnt/pbi-lightsync

# --- validaciones (mismas expresiones que pbsStorage.js, para ser
# consistentes con la funcion de almacenamiento ya existente) -------------
RE_SERVER='^[A-Za-z0-9][A-Za-z0-9.-]{0,253}$'
RE_SHARE='^[A-Za-z0-9._$%/-]+$'
RE_PEERID='^[a-z0-9][a-z0-9-]{1,30}$'
RE_SMBUSER='^[A-Za-z0-9._@\\ +-]{1,128}$'
RE_SMBDOMAIN='^[A-Za-z0-9._-]{1,128}$'
RE_DSPATH='^/[A-Za-z0-9._/-]*$'
SMB_VERS="3.1.1 3.0 2.1"

validar() { printf '%s' "$2" | grep -Eq "$1"; }

set_status() {
  # "estado|peerId|mensaje|fecha-utc" -- estado: running | ok | error
  printf '%s|%s|%s|%s\n' "$1" "$2" "${3:-}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS" 2>/dev/null || true
  chown pbi:pbi "$STATUS" 2>/dev/null || true
  chmod 640 "$STATUS" 2>/dev/null || true
}
fail() { echo "$2"; set_status error "${3:-}" "$2"; exit "$1"; }
mountpoint_for() { printf '%s/%s' "$MOUNTBASE" "$1"; }
cred_for() { printf '%s/lightsync-%s.cred' "$CREDDIR" "$1"; }

# --------------------------------------------------------- desmontar/limpiar
if [ -f "$REQ_RM" ]; then
  PEER=$(sed -n '1p' "$REQ_RM" 2>/dev/null || true)
  rm -f "$REQ_RM"
  {
    echo "== $(date) == desmontar replica ligera"
    validar "$RE_PEERID" "$PEER" || fail 2 "peerId invalido" "$PEER"
    MP=$(mountpoint_for "$PEER")
    umount -f -l "$MP" 2>/dev/null || true
    rmdir "$MP" 2>/dev/null || true
    rm -f "$(cred_for "$PEER")"
    echo "Replica ligera de '$PEER' desmontada."
    set_status ok "$PEER" "desmontado"
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------------- montar/remontar SMB
if [ -f "$REQ_MOUNT" ]; then
  PEER=$(sed -n '1p' "$REQ_MOUNT" 2>/dev/null || true)
  SRV=$(sed -n '2p' "$REQ_MOUNT" 2>/dev/null || true)
  SHARE=$(sed -n '3p' "$REQ_MOUNT" 2>/dev/null | sed 's#^/*##')
  SMBUSER=$(sed -n '4p' "$REQ_MOUNT" 2>/dev/null || true)
  SMBPASS_B64=$(sed -n '5p' "$REQ_MOUNT" 2>/dev/null || true)
  SMBDOMAIN_B64=$(sed -n '6p' "$REQ_MOUNT" 2>/dev/null || true)
  DSPATH=$(sed -n '7p' "$REQ_MOUNT" 2>/dev/null || true)
  VERS=$(sed -n '8p' "$REQ_MOUNT" 2>/dev/null || true)
  [ -n "$VERS" ] || VERS=3.0
  rm -f "$REQ_MOUNT"   # consumir la solicitud (evita reprocesos)

  {
    echo "== $(date) == montar replica ligera"
    validar "$RE_PEERID" "$PEER"    || fail 2 "peerId invalido" "$PEER"
    validar "$RE_SERVER" "$SRV"     || fail 3 "servidor SMB invalido" "$PEER"
    validar "$RE_SHARE" "$SHARE"    || fail 4 "recurso SMB invalido" "$PEER"
    validar "$RE_SMBUSER" "$SMBUSER" || fail 5 "usuario SMB invalido" "$PEER"
    validar "$RE_DSPATH" "$DSPATH"  || fail 6 "ruta de datastore invalida" "$PEER"
    printf '%s' "$DSPATH" | grep -q '\.\.' && fail 6 "ruta de datastore invalida (..)" "$PEER"
    [ -d "$DSPATH" ] || fail 7 "el datastore local '$DSPATH' no existe" "$PEER"

    set_status running "$PEER" "montando"
    dpkg -l cifs-utils 2>/dev/null | grep -q '^ii' || { apt-get update -qq && apt-get install -y -qq cifs-utils; } \
      || fail 10 "no se pudo instalar cifs-utils" "$PEER"

    MP=$(mountpoint_for "$PEER")
    mkdir -p "$MP" || fail 11 "no se pudo crear $MP" "$PEER"

    mkdir -p "$CREDDIR"; chmod 700 "$CREDDIR"
    CRED=$(cred_for "$PEER")
    umask 177
    {
      printf 'username=%s\n' "$(printf '%s' "$SMBUSER")"
      printf 'password=%s\n' "$(printf '%s' "$SMBPASS_B64" | base64 -d)"
      if [ -n "$SMBDOMAIN_B64" ]; then
        DOMAIN=$(printf '%s' "$SMBDOMAIN_B64" | base64 -d)
        validar "$RE_SMBDOMAIN" "$DOMAIN" || fail 8 "dominio SMB invalido" "$PEER"
        printf 'domain=%s\n' "$DOMAIN"
      fi
    } > "$CRED"
    chmod 600 "$CRED"

    # Si ya hay algo montado ahi, solo seguir si es EXACTAMENTE el mismo
    # recurso (remontar para renovar credenciales); si es otra cosa, abortar.
    if findmnt -n "$MP" >/dev/null 2>&1; then
      CUR=$(findmnt -n -o SOURCE "$MP" 2>/dev/null)
      if [ "$CUR" = "//$SRV/$SHARE" ]; then
        umount -f -l "$MP" 2>/dev/null || true
      else
        fail 12 "el punto de montaje ya esta en uso por otro recurso ($CUR)" "$PEER"
      fi
    fi

    BASE_OPTS="uid=34,gid=34,file_mode=0600,dir_mode=0700,noperm,nobrl,serverino"
    MVERS=""
    for V in $VERS $SMB_VERS; do
      if timeout 30 mount -t cifs -o "credentials=$CRED,vers=$V,$BASE_OPTS" "//$SRV/$SHARE" "$MP" 2>/tmp/pbi_lightsync_mnt; then
        MVERS=$V
        break
      fi
    done
    ERRMSG=$(tail -1 /tmp/pbi_lightsync_mnt 2>/dev/null)
    rm -f /tmp/pbi_lightsync_mnt
    [ -n "$MVERS" ] || fail 13 "no se pudo montar //$SRV/$SHARE ($ERRMSG)" "$PEER"

    MSRC=$(findmnt -n -o SOURCE "$MP" 2>/dev/null)
    case "$MSRC" in
      //*) : ;;
      *) fail 14 "el punto de montaje no quedo como recurso SMB activo" "$PEER" ;;
    esac

    echo "Replica ligera de '$PEER' montada en $MP (SMB vers=$MVERS)."
    set_status ok "$PEER" "montado"
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------------- sincronizar (rsync)
if [ -f "$REQ_RUN" ]; then
  PEER=$(sed -n '1p' "$REQ_RUN" 2>/dev/null || true)
  DSPATH=$(sed -n '2p' "$REQ_RUN" 2>/dev/null || true)
  rm -f "$REQ_RUN"

  {
    echo "== $(date) == sincronizar replica ligera"
    validar "$RE_PEERID" "$PEER"   || fail 2 "peerId invalido" "$PEER"
    validar "$RE_DSPATH" "$DSPATH" || fail 6 "ruta de datastore invalida" "$PEER"
    printf '%s' "$DSPATH" | grep -q '\.\.' && fail 6 "ruta de datastore invalida (..)" "$PEER"
    [ -d "$DSPATH" ] || fail 7 "el datastore local '$DSPATH' no existe" "$PEER"

    MP=$(mountpoint_for "$PEER")
    findmnt -n "$MP" >/dev/null 2>&1 || fail 15 "el recurso SMB no esta montado (¿se emparejo bien?)" "$PEER"

    # Igual que cifs-utils en el paso de montaje -- rsync viene en "Recommends"
    # del .deb, que un "dpkg -i" a mano NO instala solo (a diferencia de "apt
    # install"). Sin este auto-instalado, el montaje SMB funciona pero la
    # sincronizacion falla en silencio con "rsync: not found" hasta que
    # alguien lo instale a mano.
    command -v rsync >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq rsync; } \
      || fail 17 "no se pudo instalar rsync (apt install rsync)" "$PEER"

    set_status running "$PEER" "sincronizando chunks"

    # 1) chunks -- inmutables, siempre seguros, sin restriccion. OJO: PBS
    # pre-crea 65536 subcarpetas vacias (2 bytes de prefijo) al crear el
    # datastore -- un "rsync -a" del arbol entero intentaria recrear las 65536
    # en el remoto, y sobre SMB (latencia por operacion) eso tarda/cuelga para
    # nada. Antes se hacia con "find > lista" + "rsync --files-from", pero eso
    # obliga a DOS barridos completos del origen (find + el stat de rsync): si
    # el origen ademas es un montaje de RED (NAS a NAS), cada barrido de
    # millones de chunks es lentisimo y no se copia nada hasta terminar de
    # enumerar -- parece colgado. Ahora un solo "rsync -a -m": -m
    # (--prune-empty-dirs) evita crear las 65536 vacias, y rsync enumera y
    # copia en la MISMA pasada (recursion incremental), asi empieza a
    # transferir enseguida. SIN --inplace a proposito: los chunks son
    # inmutables; el fichero-temporal+renombrado-atomico por defecto de rsync
    # garantiza que un corte a mitad nunca deja un chunk a medias en su ruta
    # final (con --inplace si lo dejaria). --info=stats1 deja un resumen en el
    # log (cuantos ficheros y bytes) al terminar, para que se vea que avanzo.
    if [ -d "$DSPATH/.chunks" ]; then
      echo "Sincronizando chunks (rsync -a, recursion incremental). Empieza a copiar mientras escanea; en la primera pasada de un datastore grande sobre NAS por red puede tardar en completarse, pero los primeros chunks aparecen enseguida."
      # SIN -m (--prune-empty-dirs) A PROPOSITO: -m obliga a rsync a escanear
      # TODO el arbol antes de transferir nada (para decidir que dir queda
      # vacio) -- sobre un origen en red con millones de chunks eso son horas de
      # fase muda sin escribir nada (parece colgado). Con la recursion
      # incremental por defecto, rsync va enviando la lista por tramos y copia
      # a la vez que escanea, asi los primeros chunks llegan al NAS en minutos.
      # Contrapartida: recrea las subcarpetas de prefijo vacias que PBS pre-crea
      # (65536) -- pero en un datastore con datos casi todas tienen chunks, y en
      # uno pequeno el coste es minimo y una sola vez (las siguientes pasadas ya
      # existen). SIN --inplace: los chunks son inmutables y el temporal+
      # renombrado por defecto evita dejar un chunk a medias en su ruta final.
      #
      # PROGRESO EN VIVO: rsync con --info=progress2 imprime una linea global
      # (bytes, %, velocidad, ETA) que se va actualizando con \r. Se lanza en
      # segundo plano volcando a un temporal, y cada 5 s se extrae la ultima
      # linea de progreso y se escribe al fichero de estado -- asi la interfaz
      # y el dashboard muestran "cuanto le queda" sin esperar al final. (dash no
      # tiene pipefail, por eso el patron background + wait para capturar el
      # codigo de salida real de rsync.) OJO: con recursion incremental el % es
      # aproximado (el total va creciendo segun escanea), pero la velocidad y el
      # ETA ya orientan bien.
      PROG=$(mktemp)
      rsync -a --info=progress2 "$DSPATH/.chunks/" "$MP/.chunks/" >"$PROG" 2>&1 &
      RPID=$!
      while kill -0 "$RPID" 2>/dev/null; do
        LINE=$(tr '\r' '\n' <"$PROG" 2>/dev/null | grep '%' | tail -1 | tr -s ' ' | sed 's/^ *//')
        [ -n "$LINE" ] && set_status running "$PEER" "chunks: $LINE"
        sleep 5
      done
      wait "$RPID"; RC=$?
      cat "$PROG" >>"$LOG" 2>/dev/null || true
      if [ "$RC" -ne 0 ]; then TAIL=$(tail -c 300 "$PROG" 2>/dev/null | tr '\r\n' '  '); rm -f "$PROG"; fail 16 "fallo sincronizando chunks: $TAIL" "$PEER"; fi
      rm -f "$PROG"
    fi

    # 2) manifiestos -- SOLO carpetas de snapshot ya cerradas (con
    # index.json.blob). Las que siguen a medias (*.tmp_didx, sin
    # index.json.blob todavia) se ignoran por completo en esta pasada; se
    # cogeran en la siguiente cuando ya esten cerradas. Busqueda sin fijar la
    # profundidad (host/<id>/<timestamp>/, pero PBS tambien admite vm/<id>/...
    # y namespaces anidados arbitrariamente -- una profundidad fija se rompe
    # con eso) -- se excluye .chunks explicitamente aunque ahi nunca aparezca
    # este nombre de fichero, por claridad.
    set_status running "$PEER" "sincronizando snapshots"
    echo "Sincronizando manifiestos de snapshots cerrados…"
    find "$DSPATH" -path "$DSPATH/.chunks" -prune -o -type f -name 'index.json.blob' -print \
      | while read -r MANIFEST; do
        SNAPDIR=$(dirname "$MANIFEST")
        REL=$(printf '%s' "$SNAPDIR" | sed "s#^$DSPATH/##")
        mkdir -p "$MP/$REL"
        rsync -a "$SNAPDIR/" "$MP/$REL/" \
          || echo "aviso: fallo sincronizando snapshot $REL (se reintenta la proxima vez)"
      done

    echo "Sincronizacion de '$PEER' completada."
    set_status ok "$PEER" "sincronizado"
  } >>"$LOG" 2>&1
  exit 0
fi
