#!/bin/sh # Обновляет geosite/geoip-файлы Xray из веток release. Рассчитан на cron. # # Каждый файл скачивается, только если его sha256 отличается от локального, # контрольная сумма проверяется до подмены, а сервис перезапускается один раз # и только если что-то реально изменилось. Молчит, когда обновлять нечего, — # cron не будет слать письма впустую. # # ./update-geodata.sh # RESTART_CMD='x-ui restart' ./update-geodata.sh # # Что качать, задаёт SOURCES — по строке на файл, формат «URL-базы|имя-файла» # (без «|» берётся BASE_URL). По умолчанию это ipcheck.dat из ipcheck-domains # и оба .dat из discord-geodata: # # SOURCES='https://example.org/raw/branch/release|my.dat' ./update-geodata.sh # # Коды возврата: 0 — обновлено или уже актуально, 1 — ошибка. set -eu DEST_DIR="${DEST_DIR:-/usr/local/x-ui/bin}" BASE_URL="${BASE_URL:-https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release}" DISCORD_URL="${DISCORD_URL:-https://gitea.hsrv.site/mrleo1nid/discord-geodata/raw/branch/release}" RESTART_CMD="${RESTART_CMD:-}" TIMEOUT="${TIMEOUT:-30}" # Совместимость со старым вызовом, где DEST задавал единственный файл целиком. if [ -n "${DEST:-}" ]; then DEST_DIR=$(dirname "$DEST") NAME="${NAME:-$(basename "$DEST")}" fi SOURCES="${SOURCES:-\ $BASE_URL|${NAME:-ipcheck.dat} $DISCORD_URL|discord-geosite.dat $DISCORD_URL|discord-geoip.dat}" log() { printf '%s update-geodata: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"; } die() { log "ОШИБКА: $*" >&2; exit 1; } command -v curl >/dev/null 2>&1 || die "нужен curl" if command -v sha256sum >/dev/null 2>&1; then hash_of() { sha256sum "$1" | cut -d' ' -f1; } elif command -v shasum >/dev/null 2>&1; then hash_of() { shasum -a 256 "$1" | cut -d' ' -f1; } else die "нужен sha256sum или shasum" fi fetch() { curl -fsSL --retry 3 --retry-delay 2 --max-time "$TIMEOUT" "$1"; } tmp="" cleanup() { [ -z "$tmp" ] || rm -f "$tmp"; } trap cleanup EXIT INT TERM mkdir -p "$DEST_DIR" changed=0 # Цикл читает SOURCES через here-doc, а не из конвейера: в подоболочке # счётчик changed не пережил бы выход из цикла. while IFS='|' read -r base name; do [ -n "$base" ] || continue if [ -z "$name" ]; then name="$base" base="$BASE_URL" fi dest="$DEST_DIR/$name" # Без промежуточной переменной статус вернул бы cut, а не curl. remote_line=$(fetch "$base/$name.sha256sum") || die "не удалось скачать $base/$name.sha256sum" remote_sum=$(printf '%s' "$remote_line" | cut -d' ' -f1) case "$remote_sum" in [0-9a-f][0-9a-f]*) [ "${#remote_sum}" -eq 64 ] || die "битая контрольная сумма: $remote_sum" ;; *) die "битая контрольная сумма: $remote_sum" ;; esac if [ -f "$dest" ] && [ "$(hash_of "$dest")" = "$remote_sum" ]; then continue # уже актуально — тишина fi tmp=$(mktemp "${TMPDIR:-/tmp}/geodata.XXXXXX") || die "не удалось создать временный файл" fetch "$base/$name" > "$tmp" || die "не удалось скачать $base/$name" got=$(hash_of "$tmp") [ "$got" = "$remote_sum" ] || die "$name: ожидалось $remote_sum, получено $got" [ -s "$tmp" ] || die "$name: скачан пустой файл" [ ! -f "$dest" ] || cp -p "$dest" "$dest.bak" || die "не удалось сохранить резервную копию" # mv в пределах одного каталога атомарен: Xray никогда не увидит половину файла. cat "$tmp" > "$dest.new" && mv -f "$dest.new" "$dest" || die "не удалось записать $dest" rm -f "$tmp" tmp="" log "обновлён $dest (sha256 $remote_sum)" changed=1 done <