#!/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"

    # 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" normal del arbol entero intenta recrear las
    # 65536 tambien en el remoto, y sobre SMB (con su latencia por operacion)
    # eso puede tardar minutos/colgarse para nada, ya que estan vacias. Se
    # listan solo los FICHEROS reales y se pasan con --files-from, que solo
    # crea los directorios que de verdad hacen falta.
    if [ -d "$DSPATH/.chunks" ]; then
      CHUNKLIST=$(mktemp)
      find "$DSPATH/.chunks" -type f -printf '%P\n' > "$CHUNKLIST"
      if [ -s "$CHUNKLIST" ]; then
        # SIN --inplace a proposito: los chunks son inmutables y se copian una
      # sola vez -- el comportamiento por defecto de rsync (fichero temporal +
      # renombrado atomico al terminar) garantiza que un corte a mitad nunca
      # deja un chunk a medias en su ruta final (con --inplace si lo dejaria).
      rsync -a --files-from="$CHUNKLIST" "$DSPATH/.chunks/" "$MP/.chunks/" \
          || { rm -f "$CHUNKLIST"; fail 16 "fallo sincronizando chunks" "$PEER"; }
      fi
      rm -f "$CHUNKLIST"
    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.
    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
