Ctrl K
ComunidadGuías…

docker/update-mods.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Updates mods already present in the mods volume directory against the
# Factorio mod portal. Invoked from entrypoint.sh only when UPDATE_MODS=true.
# Does not install new mods, resolve dependencies, or manage mod-list.json —
# it only version-bumps zips that are already there.
set -euo pipefail

FACTORIO_DIR="${FACTORIO_DIR:-/factorio}"
FACTORIO_HOME="${FACTORIO_HOME:-/opt/factorio}"
MODS_DIR="${FACTORIO_DIR}/mods"
BIN="${FACTORIO_HOME}/factorio"

log()  { printf '[update-mods] %s\n' "$*"; }
warn() { printf '[update-mods] WARNING: %s\n' "$*" >&2; }
die()  { printf '[update-mods] ERROR: %s\n' "$*" >&2; exit 1; }

# Returns success if $2 is a newer version than $1. Both are dotted numeric
# versions like "1.2.3" — compared numerically per-component via `sort -V`
# rather than lexically, since lexical comparison would rank "1.10.0" below
# "1.9.0".
mod_version_newer() {
  local current="$1" candidate="$2"
  [ "$current" != "$candidate" ] && \
    [ "$(printf '%s\n%s\n' "$current" "$candidate" | sort -V | tail -n1)" = "$candidate" ]
}

# Splits "name_1.2.3.zip" into "name version". Echoes nothing if the
# filename doesn't match that pattern. Always returns 0 regardless of
# match — callers assign its output via command substitution, and under
# `set -e` a non-zero exit from a bare `var=$(...)` assignment would abort
# the whole script, which is exactly the single-bad-mod-shouldn't-abort-
# boot behavior this script must avoid.
parse_mod_filename() {
  local base="$1"
  base="${base%.zip}"
  if [[ "$base" =~ ^(.+)_([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
    printf '%s %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
  fi
  return 0
}

# Returns success if $1 is present in the comma-separated list $2.
is_ignored() {
  local name="$1" list="$2" item
  [ -z "$list" ] && return 1
  local items=()
  IFS=',' read -ra items <<< "$list"
  for item in "${items[@]:-}"; do
    item="${item#"${item%%[![:space:]]*}"}"
    item="${item%"${item##*[![:space:]]}"}"
    [ "$item" = "$name" ] && return 0
  done
  return 1
}

# Checks one mod against the portal and replaces it in place if a newer
# release exists for the given Factorio major.minor line. Never aborts the
# script — any failure here is logged and the mod is left as-is.
update_one_mod() {
  local zip_path="$1" name="$2" current_version="$3" factorio_line="$4"
  local username="$5" token="$6"

  local name_enc
  name_enc="$(jq -rn --arg n "${name}" '$n|@uri' 2>/dev/null)" || true
  if [ -z "${name_enc}" ]; then
    warn "${name}: could not URL-encode mod name, leaving ${current_version} in place"
    return 0
  fi

  local api_url="https://mods.factorio.com/api/mods/${name_enc}"
  local releases
  if ! releases="$(curl -fsSL --connect-timeout 10 --max-time 30 --retry 2 "${api_url}" 2>/dev/null | jq -c '.releases // []' 2>/dev/null)"; then
    warn "${name}: could not reach mod portal, leaving ${current_version} in place"
    return 0
  fi

  local best
  best="$(printf '%s' "${releases}" | jq -c --arg line "${factorio_line}" '
    [.[] | select(.info_json.factorio_version == $line)]
    | sort_by(.version | split(".") | map(tonumber))
    | last // empty
  ' 2>/dev/null)" || true

  if [ -z "${best}" ] || [ "${best}" = "null" ]; then
    log "${name}: no release for Factorio ${factorio_line}, leaving ${current_version} in place"
    return 0
  fi

  local best_version best_url best_sha1
  if ! { best_version="$(printf '%s' "${best}" | jq -r '.version' 2>/dev/null)" && \
         best_url="$(printf '%s' "${best}" | jq -r '.download_url' 2>/dev/null)" && \
         best_sha1="$(printf '%s' "${best}" | jq -r '.sha1' 2>/dev/null)"; }; then
    warn "${name}: could not parse release metadata, leaving ${current_version} in place"
    return 0
  fi

  if ! mod_version_newer "${current_version}" "${best_version}"; then
    log "${name}: ${current_version} is already current"
    return 0
  fi

  if ! [[ "${best_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    warn "${name}: portal returned an invalid version '${best_version}', leaving ${current_version} in place"
    return 0
  fi
  if [[ "${best_url}" != /* ]]; then
    warn "${name}: portal returned an invalid download URL, leaving ${current_version} in place"
    return 0
  fi

  log "${name}: ${current_version} -> ${best_version}"

  local username_enc token_enc
  username_enc="$(jq -rn --arg u "${username}" '$u|@uri' 2>/dev/null)" || true
  token_enc="$(jq -rn --arg t "${token}" '$t|@uri' 2>/dev/null)" || true
  if [ -z "${username_enc}" ] || [ -z "${token_enc}" ]; then
    warn "${name}: could not URL-encode credentials, leaving ${current_version} in place"
    return 0
  fi

  local tmp_file="${zip_path}.new"
  if ! curl -fsSL --connect-timeout 10 --max-time 300 --retry 2 \
      "https://mods.factorio.com${best_url}?username=${username_enc}&token=${token_enc}" \
      -o "${tmp_file}" 2>/dev/null; then
    warn "${name}: download failed, leaving ${current_version} in place"
    rm -f "${tmp_file}"
    return 0
  fi

  local actual_sha1
  if ! actual_sha1="$(sha1sum "${tmp_file}" | cut -d' ' -f1)"; then
    warn "${name}: could not compute checksum, leaving ${current_version} in place"
    rm -f "${tmp_file}"
    return 0
  fi
  if [ "${actual_sha1}" != "${best_sha1}" ]; then
    warn "${name}: checksum mismatch after download, leaving ${current_version} in place"
    rm -f "${tmp_file}"
    return 0
  fi

  local new_zip_path="${MODS_DIR}/${name}_${best_version}.zip"
  if ! mv "${tmp_file}" "${new_zip_path}"; then
    warn "${name}: could not move new version into place, leaving ${current_version} in place"
    rm -f "${tmp_file}"
    return 0
  fi
  if ! rm -f "${zip_path}"; then
    warn "${name}: could not remove old version ${current_version}; both ${current_version} and ${best_version} are now present on disk"
  fi
}

main() {
  local username="${FACTORIO_USERNAME:-${USERNAME:-}}"
  local token="${FACTORIO_TOKEN:-${TOKEN:-}}"

  if [ -z "${username}" ] || [ -z "${token}" ]; then
    die "UPDATE_MODS=true but FACTORIO_USERNAME/FACTORIO_TOKEN are not set"
  fi

  local version_output factorio_line
  version_output="$("${BIN}" --version)"
  if [[ "${version_output}" =~ Version:\ ([0-9]+)\.([0-9]+)\. ]]; then
    factorio_line="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
  else
    die "could not determine Factorio version from: ${version_output}"
  fi
  log "checking mods against Factorio ${factorio_line}"

  shopt -s nullglob

  # Clean up any leftover temp files from a previous boot that was killed
  # mid-download; nullglob is already set above so a non-matching glob
  # expands to nothing rather than the literal pattern.
  rm -f "${MODS_DIR}"/*.zip.new

  local zip_path
  for zip_path in "${MODS_DIR}"/*.zip; do
    local base parsed name current_version
    base="$(basename "${zip_path}")"
    parsed="$(parse_mod_filename "${base}")"
    if [ -z "${parsed}" ]; then
      warn "${base}: filename doesn't match name_x.y.z.zip, skipping"
      continue
    fi
    name="${parsed% *}"
    current_version="${parsed#* }"

    if is_ignored "${name}" "${MODS_IGNORE:-}"; then
      log "${name}: in MODS_IGNORE, skipping"
      continue
    fi

    update_one_mod "${zip_path}" "${name}" "${current_version}" "${factorio_line}" "${username}" "${token}"
  done
  shopt -u nullglob
}

# Allow sourcing this file (e.g. from scripts/test-update-mods.sh) without
# running main() — only run it when executed directly.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi