#!/usr/bin/env bash # Instalador Linux de CloudRestoreAS. # # NO instala ni descarga NADA del sistema: el binario es 100% autocontenido (Qt/xcb, # driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo coloca el # binario, marca permisos, hace el bootstrap de config/, opcionalmente siembra las # credenciales del PANEL, y registra el arranque automático (servicio systemd headless # o autostart de escritorio). # # Uso: # sudo ./install.sh --service # 24/7 headless vía systemd (recomendado en servidor) # ./install.sh --user-service # 24/7 headless SIN root: unit de systemd de usuario # ./install.sh --desktop # autostart de escritorio (requiere sesión gráfica) # ./install.sh # solo instala; sin arranque automático # # --user-service no necesita privilegios: instala donde apunte PREFIX (que debe ser escribible # por el usuario, típicamente bajo su home), registra el unit en ~/.config/systemd/user/ y lo # arranca con `systemctl --user`. Para que sobreviva al cierre de sesión intenta habilitar # lingering; si el destino no lo permite, cae a una entrada @reboot en el crontab del usuario # más un vigilante que lo rearranca. Es la vía cuando la cuenta SSH no es root ni tiene sudo # sin contraseña: al panel le bastan entonces el usuario y la contraseña que ya tiene guardados. # # Opciones: # --panel-env-file # archivo KEY=valor con CLOUDRESTORE_PANEL_* que se # # fusiona en config/.env y se borra al terminar. # # Lo usa el instalador remoto del PANEL para dejar # # el servidor configurado sin intervención. # # Variables: # PREFIX=/opt/cloudrestoreas # carpeta destino (default) # SERVICE_USER= # usuario del servicio systemd (default: quien invoca) set -euo pipefail MODE="none" PANEL_ENV_FILE="" while [[ $# -gt 0 ]]; do case "$1" in --service) MODE="service"; shift ;; --user-service) MODE="user-service"; shift ;; --desktop) MODE="desktop"; shift ;; --panel-env-file) [[ $# -ge 2 ]] || { echo "ERROR: --panel-env-file requiere una ruta" >&2; exit 2; } PANEL_ENV_FILE="$2"; shift 2 ;; --panel-env-file=*) PANEL_ENV_FILE="${1#*=}"; shift ;; -h|--help) grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Opción desconocida: $1" >&2; exit 2 ;; esac done SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PREFIX="${PREFIX:-/opt/cloudrestoreas}" BIN_NAME="CloudRestoreAS" UNIT_NAME="cloudrestoreas.service" # Patrón para localizar el agente vivo. El ancla ^ NO es opcional: sin ella, la propia # línea de cron del vigilante contiene esta ruta, así que el `sh -c` que la ejecuta hace # match consigo mismo, el vigilante cree que el agente corre y no lo rearranca nunca. PGREP_PAT="^$PREFIX/$BIN_NAME" # Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script. SRC="" for cand in \ "$SCRIPT_DIR/dist/$BIN_NAME" \ "$SCRIPT_DIR/dist/${BIN_NAME}-linux" \ "$SCRIPT_DIR/$BIN_NAME" \ "$SCRIPT_DIR/${BIN_NAME}-linux"; do if [[ -f "$cand" ]]; then SRC="$cand"; break; fi done if [[ -z "$SRC" ]]; then echo "ERROR: no se encontró el binario ($BIN_NAME). Ejecuta ./build.sh primero" >&2 exit 1 fi echo "===============================================" echo "CloudRestoreAS - Instalación Linux" echo " Binario : $SRC" echo " Destino : $PREFIX" echo " Modo : $MODE" echo "===============================================" # --- Detener el servicio si está corriendo ------------------------------------ # Un ELF en ejecución no se puede sobrescribir (ETXTBSY), así que una actualización # tiene que detenerlo antes de reemplazar el binario. Se recuerda si estaba activo # para volver a levantarlo al final incluso en los modos que no tocan systemd. WAS_ACTIVE=0 # En modo usuario el unit vive en la instancia de systemd del propio usuario, que necesita # XDG_RUNTIME_DIR: un `exec` de SSH no es una sesión de login y no siempre lo trae. export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" SYSTEMCTL_USER=(systemctl --user) if command -v systemctl >/dev/null 2>&1; then if [[ "$MODE" == "user-service" ]]; then if "${SYSTEMCTL_USER[@]}" is-active --quiet "$UNIT_NAME" 2>/dev/null; then WAS_ACTIVE=1 echo "Deteniendo $UNIT_NAME (usuario) para reemplazar el binario..." "${SYSTEMCTL_USER[@]}" stop "$UNIT_NAME" >/dev/null 2>&1 || \ echo "AVISO: no se pudo detener $UNIT_NAME; la copia puede fallar." >&2 fi # Sin systemd de usuario disponible el arranque pudo haber quedado por cron: el binario # sigue vivo y sobrescribirlo daría ETXTBSY, así que se termina igual. if [[ "$WAS_ACTIVE" -eq 0 ]] && pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then WAS_ACTIVE=1 echo "Terminando el proceso en curso para reemplazar el binario..." pkill -f "$PGREP_PAT" >/dev/null 2>&1 || true sleep 2 fi elif systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then WAS_ACTIVE=1 echo "Deteniendo $UNIT_NAME para reemplazar el binario..." systemctl stop "$UNIT_NAME" >/dev/null 2>&1 || \ echo "AVISO: no se pudo detener $UNIT_NAME (¿falta sudo?); la copia puede fallar." >&2 fi fi mkdir -p "$PREFIX" if ! install -m 0755 "$SRC" "$PREFIX/$BIN_NAME"; then echo "ERROR: no se pudo instalar el binario en $PREFIX/$BIN_NAME" >&2 echo " Si el archivo está en uso, detén el proceso y reintenta:" >&2 echo " sudo systemctl stop $UNIT_NAME # o mata el proceso de escritorio" >&2 exit 1 fi echo "Binario instalado en $PREFIX/$BIN_NAME" # Bootstrap de config/: el propio binario crea config/, .env y carpetas de trabajo # al inicio de su arranque. Se corre una vez en modo offscreen (sin display) y se # corta con timeout; así el usuario ya puede editar config/.env antes de habilitar # el servicio. echo "Inicializando config/ (bootstrap)..." # `/dev/null 2>&1 || true ) if [[ -f "$PREFIX/config/.env" ]]; then echo "config/.env creado." else echo "NOTA: config/.env se creará en la primera ejecución." fi # --- Siembra de credenciales del PANEL ---------------------------------------- # Fusión replace-or-append: respeta el resto de config/.env y es idempotente, así que # reinstalar no duplica claves ni pierde ajustes locales. Se escribe vía archivo y no # por argumentos para que el token no quede visible en `ps` ni en el historial. merge_env_keys() { local target="$1"; shift local -a pairs=("$@") local tmp line key pair matched tmp="$(mktemp)" if [[ -f "$target" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do matched="" for pair in "${pairs[@]}"; do key="${pair%%=*}" if [[ "$line" =~ ^[[:space:]]*"$key"[[:space:]]*= ]]; then matched="$pair" break fi done if [[ -n "$matched" ]]; then printf '%s\n' "$matched" >> "$tmp" else printf '%s\n' "$line" >> "$tmp" fi done < "$target" fi for pair in "${pairs[@]}"; do key="${pair%%=*}" if ! grep -qE "^[[:space:]]*${key}[[:space:]]*=" "$tmp" 2>/dev/null; then printf '%s\n' "$pair" >> "$tmp" fi done # cat > preserva el inodo y los permisos 0600 del .env; mv los reemplazaría. cat "$tmp" > "$target" rm -f "$tmp" } if [[ -n "$PANEL_ENV_FILE" ]]; then if [[ ! -f "$PANEL_ENV_FILE" ]]; then echo "ERROR: no existe el archivo indicado en --panel-env-file: $PANEL_ENV_FILE" >&2 exit 1 fi ENV_PATH="$PREFIX/config/.env" touch "$ENV_PATH" chmod 0600 "$ENV_PATH" 2>/dev/null || true # Lista blanca: el archivo llega por la red y no debe poder inyectar otras claves. ALLOWED_KEYS="CLOUDRESTORE_PANEL_API_URL CLOUDRESTORE_PANEL_API_TOKEN" ALLOWED_KEYS="$ALLOWED_KEYS CLOUDRESTORE_PANEL_INSTANCE_KEY CLOUDRESTORE_PANEL_VERIFY_SSL" PAIRS=() while IFS= read -r raw || [[ -n "$raw" ]]; do raw="${raw%$'\r'}" # tolera CRLF si vino de Windows raw="${raw#"${raw%%[![:space:]]*}"}" # recorta espacios a la izquierda if [[ -z "$raw" || "${raw:0:1}" == "#" || "$raw" != *=* ]]; then continue fi key="${raw%%=*}" key="${key//[[:space:]]/}" value="${raw#*=}" for allowed in $ALLOWED_KEYS; do if [[ "$key" == "$allowed" ]]; then PAIRS+=("${key}=${value}") break fi done done < "$PANEL_ENV_FILE" if [[ "${#PAIRS[@]}" -gt 0 ]]; then merge_env_keys "$ENV_PATH" "${PAIRS[@]}" echo "${#PAIRS[@]} clave(s) del PANEL escritas en config/.env" else echo "AVISO: --panel-env-file no traía claves CLOUDRESTORE_PANEL_* válidas." >&2 fi # El archivo trae el token en claro: se borra en cuanto se consumió. rm -f "$PANEL_ENV_FILE" fi case "$MODE" in service) SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}" UNIT_SRC="$SCRIPT_DIR/packaging/linux/$UNIT_NAME" UNIT_DST="/etc/systemd/system/$UNIT_NAME" if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then echo "ERROR: se requiere sudo para instalar el servicio systemd" >&2 exit 1 fi sed -e "s|__USER__|$SERVICE_USER|g" \ -e "s|__WORKDIR__|$PREFIX|g" \ -e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \ "$UNIT_SRC" > "$UNIT_DST" systemctl daemon-reload systemctl enable --now "$UNIT_NAME" echo "Servicio systemd instalado y arrancado (usuario: $SERVICE_USER)." echo " Estado : systemctl status cloudrestoreas" echo " Logs : journalctl -u cloudrestoreas -f" ;; user-service) # Sin root: unit en el home y arranque con la instancia de systemd del propio usuario. # Requisito previo: PREFIX escribible por este usuario (el panel lo apunta a su home). UNIT_SRC="$SCRIPT_DIR/packaging/linux/cloudrestoreas-user.service" UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" UNIT_DST="$UNIT_DIR/$UNIT_NAME" if [[ ! -f "$UNIT_SRC" ]]; then echo "ERROR: falta la plantilla del unit de usuario ($UNIT_SRC)" >&2 exit 1 fi mkdir -p "$UNIT_DIR" # Sin __USER__: un unit de usuario no admite User=, ya corre como su dueño. sed -e "s|__WORKDIR__|$PREFIX|g" \ -e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \ "$UNIT_SRC" > "$UNIT_DST" STARTED="" if "${SYSTEMCTL_USER[@]}" daemon-reload >/dev/null 2>&1 && \ "${SYSTEMCTL_USER[@]}" enable --now "$UNIT_NAME" >/dev/null 2>&1; then STARTED="systemd-usuario" # Sin lingering, la instancia de usuario muere al cerrar la última sesión y con ella el # restaurador. Muchos polkit permiten habilitarlo para uno mismo; si no, no es fatal: # se complementa con cron más abajo. if loginctl enable-linger "$(id -un)" >/dev/null 2>&1; then echo "Lingering habilitado: el servicio sobrevive al cierre de sesión." else echo "AVISO: no se pudo habilitar lingering; se agrega respaldo por cron." >&2 STARTED="systemd-usuario+cron" fi else echo "AVISO: systemd de usuario no disponible; se usará cron." >&2 fi # Respaldo (o sustituto) por cron: @reboot para el arranque y un vigilante cada 5 min que # cubre lo que en systemd hace Restart=always. El crontab del usuario no requiere root. if [[ "$STARTED" != "systemd-usuario" ]]; then if command -v crontab >/dev/null 2>&1; then CRON_CMD="cd $PREFIX && QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless >/dev/null 2>&1 &" CRON_TAG="# cloudrestoreas-autostart" # Se reescribe el crontab completo filtrando las entradas propias anteriores, para que # reinstalar no acumule duplicados. { crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true echo "@reboot $CRON_CMD $CRON_TAG" echo "*/5 * * * * pgrep -f '$PGREP_PAT' >/dev/null || ($CRON_CMD) $CRON_TAG" } | crontab - STARTED="${STARTED:+$STARTED+}cron" # El vigilante tarda hasta 5 min en actuar; se arranca ya para no dejar el hueco. if ! pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then ( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen "$PREFIX/$BIN_NAME" --start-engine --headless /dev/null 2>&1 & ) fi else echo "ERROR: sin systemd de usuario y sin crontab no hay forma de dejarlo arrancado." >&2 echo " Instala cron, habilita lingering, o usa --service con privilegios." >&2 exit 1 fi fi echo "Servicio de usuario instalado y arrancado (mecanismo: $STARTED)." echo " Estado : systemctl --user status cloudrestoreas" echo " Logs : journalctl --user -u cloudrestoreas -f" ;; desktop) echo "Modo escritorio: la app registra su autostart .desktop al iniciarse." echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica." ;; none) echo "Instalación sin arranque automático." echo "Ejecuta: QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless" ;; esac # Si se detuvo un servicio que estaba activo y el modo elegido no lo relevanta, se # restaura: una actualización no debe dejar el restaurador apagado en silencio. if [[ "$WAS_ACTIVE" -eq 1 && "$MODE" != "service" && "$MODE" != "user-service" ]]; then echo "Reiniciando $UNIT_NAME (estaba activo antes de la actualización)..." systemctl start "$UNIT_NAME" >/dev/null 2>&1 || \ echo "AVISO: no se pudo reiniciar $UNIT_NAME; hazlo a mano." >&2 fi echo "" echo "Siguiente paso: edita $PREFIX/config/.env (CLOUDRESTORE_PANEL_*) y reinicia." [[ "$MODE" == "service" ]] && echo " Tras editar: sudo systemctl restart cloudrestoreas" [[ "$MODE" == "user-service" ]] && echo " Tras editar: systemctl --user restart cloudrestoreas" echo "Listo."