diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..49317b8 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,141 @@ +name: build + +on: + push: + branches: [main] + paths: + - "data/**" + - "tools/**" + - ".gitea/workflows/build.yml" + pull_request: + paths: + - "data/**" + - "tools/**" + - ".gitea/workflows/build.yml" + schedule: + - cron: "17 3 * * *" + workflow_dispatch: + +env: + GITEA_URL: ${{ github.server_url }} + GITEA_REPO: ${{ github.repository }} + # Автоматического токена обычно хватает. Если у раннера нет прав на push + # или на релизы — заведите секрет RELEASE_TOKEN с правами repo:write. + AUTO_TOKEN: ${{ github.token }} + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Окружение + run: | + set -eu + missing="" + for cmd in python3 curl unzip tar git; do + command -v "$cmd" >/dev/null 2>&1 || missing="$missing $cmd" + done + if [ -n "$missing" ]; then + echo "не хватает:$missing — доустанавливаем" + sudo=""; [ "$(id -u)" -eq 0 ] || sudo=sudo + $sudo apt-get update -qq + $sudo apt-get install -y -qq python3 curl unzip tar git ca-certificates + fi + python3 --version + + - name: Проверка исходников + run: python3 tools/build.py --check + + - name: Тесты + run: python3 -m unittest discover -s tools -t tools + + - name: Внешние инструменты + run: | + sh tools/fetch-tools.sh "$GITHUB_WORKSPACE/bin" + echo "$GITHUB_WORKSPACE/bin" >> "$GITHUB_PATH" + + - name: Сборка + run: | + SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) python3 tools/build.py + + - name: Проверка .dat настоящим Xray + run: | + # Роутинг-правило заставляет Xray реально распарсить файл: + # битый .dat или отсутствующий тег дают ненулевой код возврата. + cat > /tmp/xray-test.json <<'EOF' + { + "inbounds": [{"port": 10800, "protocol": "socks"}], + "outbounds": [{"protocol": "freedom", "tag": "direct"}], + "routing": { + "rules": [ + {"type": "field", "domain": ["ext:ipcheck.dat:ipcheck"], "outboundTag": "direct"}, + {"type": "field", "domain": ["ext:geosite.dat:leak-test"], "outboundTag": "direct"} + ] + } + } + EOF + XRAY_LOCATION_ASSET="$GITHUB_WORKSPACE/dist" xray run -test -c /tmp/xray-test.json + + - name: Проверка rule-set для sing-box + run: | + sing-box rule-set decompile --output /tmp/ipcheck.json dist/sing-box/ipcheck.srs + grep -q '"2ip.ru"' /tmp/ipcheck.json + + - name: Описание релиза + id: notes + run: | + set -eu + tag=$(date -u '+%Y.%m.%d-%H%M') + echo "tag=$tag" >> "$GITHUB_OUTPUT" + { + echo "Собрано из \`$(git rev-parse --short HEAD)\`." + echo + echo '| Категория | Правил |' + echo '| --- | ---: |' + python3 -c "import json; [print(f'| {k} | {v} |') for k, v in json.load(open('dist/metadata.json'))['categories'].items()]" + echo + echo "sha256(ipcheck.dat) = \`$(cut -d' ' -f1 dist/ipcheck.dat.sha256sum)\`" + } > /tmp/notes.md + cat /tmp/notes.md + + - name: Публикация в ветку release + if: github.event_name != 'pull_request' + run: | + set -eu + token="${RELEASE_TOKEN:-$AUTO_TOKEN}" + [ -n "$token" ] || { echo "нет токена для push" >&2; exit 1; } + host="${GITEA_URL#*://}" + scheme="${GITEA_URL%%://*}" + + cp README.md LICENSE dist/ + cd dist + git init -q -b release + git config user.name "gitea-actions" + git config user.email "gitea-actions@noreply.local" + git add . + git commit -q -m "build $(date -u '+%Y-%m-%d %H:%M UTC') ($GITHUB_SHA)" + git push -qf "$scheme://gitea-actions:$token@$host/$GITEA_REPO.git" release + cd .. + rm -f dist/README.md dist/LICENSE + echo "ветка release обновлена" + + - name: Релиз в Gitea + if: github.event_name != 'pull_request' + run: | + set -eu + export GITEA_TOKEN="${RELEASE_TOKEN:-$AUTO_TOKEN}" + # Собираем архив вне dist/, иначе tar пакует сам себя на лету. + tar -czf /tmp/ipcheck-domains-all.tar.gz -C dist . + mv /tmp/ipcheck-domains-all.tar.gz dist/ + sh tools/gitea-release.sh "${{ steps.notes.outputs.tag }}" /tmp/notes.md \ + dist/ipcheck.dat \ + dist/ipcheck.dat.sha256sum \ + dist/geosite.dat \ + dist/geosite.dat.sha256sum \ + dist/sing-box/ipcheck.srs \ + dist/mihomo/ipcheck.mrs \ + dist/metadata.json \ + dist/SHA256SUMS \ + dist/ipcheck-domains-all.tar.gz diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 440b93c..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: build - -on: - push: - branches: [main] - paths: - - "data/**" - - "tools/**" - - ".github/workflows/build.yml" - pull_request: - paths: - - "data/**" - - "tools/**" - - ".github/workflows/build.yml" - schedule: - - cron: "17 3 * * *" - workflow_dispatch: - -permissions: - contents: write - -env: - GH_TOKEN: ${{ github.token }} - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Lint sources - run: python tools/build.py --check - - - name: Unit tests - run: python -m unittest discover -s tools -t tools - - - name: Install sing-box - run: | - mkdir -p "$GITHUB_WORKSPACE/bin" - url=$(gh api repos/SagerNet/sing-box/releases/latest \ - --jq '.assets[] | select(.name | test("-linux-amd64\\.tar\\.gz$")) | .browser_download_url') - echo "sing-box: $url" - curl -fsSL "$url" | tar -xz --strip-components=1 -C "$GITHUB_WORKSPACE/bin" --wildcards '*/sing-box' - echo "$GITHUB_WORKSPACE/bin" >> "$GITHUB_PATH" - - - name: Install mihomo - run: | - url=$(gh api repos/MetaCubeX/mihomo/releases/latest \ - --jq '.assets[] | select(.name | test("^mihomo-linux-amd64-v[0-9.]+\\.gz$")) | .browser_download_url') - echo "mihomo: $url" - curl -fsSL "$url" | gunzip > "$GITHUB_WORKSPACE/bin/mihomo" - chmod +x "$GITHUB_WORKSPACE/bin/mihomo" - - - name: Build - run: | - export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) - python tools/build.py - - - name: Validate geosite.dat with Xray - run: | - url=$(gh api repos/XTLS/Xray-core/releases/latest \ - --jq '.assets[] | select(.name == "Xray-linux-64.zip") | .browser_download_url') - curl -fsSL -o xray.zip "$url" - unzip -o -j xray.zip xray -d "$GITHUB_WORKSPACE/bin" - chmod +x "$GITHUB_WORKSPACE/bin/xray" - - # Роутинг-правило заставляет Xray реально распарсить geosite.dat. - # Битый файл или отсутствующий тег -> ненулевой exit code. - cat > /tmp/xray-test.json <<'EOF' - { - "inbounds": [{"port": 10800, "protocol": "socks"}], - "outbounds": [{"protocol": "freedom", "tag": "direct"}], - "routing": { - "rules": [ - {"type": "field", "domain": ["ext:geosite.dat:ipcheck"], "outboundTag": "direct"}, - {"type": "field", "domain": ["ext:geosite.dat:leak-test"], "outboundTag": "direct"} - ] - } - } - EOF - XRAY_LOCATION_ASSET="$GITHUB_WORKSPACE/dist" xray run -test -c /tmp/xray-test.json - - - name: Validate sing-box rule-set - run: | - sing-box rule-set decompile --output /tmp/ipcheck.json dist/sing-box/ipcheck.srs - grep -q '"2ip.ru"' /tmp/ipcheck.json - - - name: Release notes - id: notes - run: | - echo "tag=$(date -u '+%Y%m%d%H%M')" >> "$GITHUB_OUTPUT" - { - echo "Собрано из \`${GITHUB_SHA::7}\`." - echo - echo '| Категория | Правил |' - echo '| --- | ---: |' - python -c "import json;[print(f'| {k} | {v} |') for k,v in json.load(open('dist/metadata.json'))['categories'].items()]" - echo - echo "sha256(geosite.dat) = \`$(cut -d' ' -f1 dist/geosite.dat.sha256sum)\`" - } > /tmp/notes.md - cat /tmp/notes.md >> "$GITHUB_STEP_SUMMARY" - - - uses: actions/upload-artifact@v4 - with: - name: ipcheck-domains - path: dist/ - if-no-files-found: error - - - name: Publish to release branch - if: github.event_name != 'pull_request' - run: | - cp README.md dist/README.md - cp LICENSE dist/LICENSE - cd dist - git init -q -b release - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add . - git commit -q -m "build: $(date -u '+%Y-%m-%d %H:%M UTC') ($GITHUB_SHA)" - git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" release - cd .. - rm -f dist/README.md dist/LICENSE - - - name: Create release - if: github.event_name != 'pull_request' - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.notes.outputs.tag }} - name: ${{ steps.notes.outputs.tag }} - body_path: /tmp/notes.md - files: | - dist/geosite.dat - dist/geosite.dat.sha256sum - dist/metadata.json - dist/sing-box/*.srs - dist/mihomo/*.mrs - dist/clash/* - dist/txt/* - dist/json/* - dist/adguard/* diff --git a/README.md b/README.md index 7a5d30e..927150b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ тесты утечек DNS/WebRTC, GeoIP-базы и скоринг прокси/VPN. Собирается в несколько форматов. Основной — `geosite.dat` (protobuf v2ray/Xray) для 3x-ui. -Сборка идёт автоматически при каждом изменении `data/` и раз в сутки по расписанию. +CI на Gitea Actions пересобирает всё при каждом изменении `data/` и раз в сутки по расписанию. + +> Во всех примерах ниже `gitea.example.com/mrleo1nid/ipcheck-domains` — замените на адрес +> своего инстанса и репозитория. ## Категории @@ -18,16 +21,18 @@ ## Скачать -Ветка `release` обновляется при каждой сборке — ссылки стабильны: +Ветка `release` перезаписывается при каждой сборке, ссылки стабильные: ``` -https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/<путь> +https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/<путь> ``` | Файл | Для чего | | --- | --- | | `ipcheck.dat` | Xray / v2ray / 3x-ui — кладётся рядом со штатным `geosite.dat` | | `geosite.dat` | то же самое, но именем по умолчанию — если хотите заменить штатный файл | +| `ipcheck.dat.sha256sum` | контрольная сумма, по ней работает автообновление | +| `update-geosite.sh` | скрипт обновления для cron (см. ниже) | | `sing-box/<категория>.srs` | sing-box, скомпилированный rule-set | | `sing-box/<категория>.json` | sing-box, исходный rule-set | | `mihomo/<категория>.mrs` | mihomo, бинарный rule-provider | @@ -36,17 +41,18 @@ https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/<путь> | `txt/<категория>.txt` | просто домены, по одному на строку | | `json/<категория>.json` | структурированный список с типами правил | | `adguard/<категория>.txt` | AdGuard Home / uBlock — если нужно блокировать, а не роутить | -| `metadata.json` | время сборки, счётчики, sha256 | +| `metadata.json`, `SHA256SUMS` | время сборки, счётчики, суммы по всем файлам | -Плюс каждая сборка публикуется как GitHub Release с датой в теге. +Плюс каждая сборка публикуется как релиз Gitea с датой в теге — там же лежит +`ipcheck-domains-all.tar.gz` со всем содержимым `dist/` разом. ## 3x-ui -Файл кладётся туда же, где лежат `geoip.dat` и `geosite.dat` панели. Обычно это -`/usr/local/x-ui/bin` (панель, установленная скриптом) или `/usr/local/share/xray`: +Файл кладётся туда же, где у панели лежат `geoip.dat` и `geosite.dat`. Обычно это +`/usr/local/x-ui/bin` (установка скриптом) или `/usr/local/share/xray`: ```bash -curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/ipcheck.dat +curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/ipcheck.dat ``` Дальше в панели: **Xray Settings → Routing Rules → Add Rule**, поле `domain`: @@ -70,6 +76,123 @@ ext:ipcheck.dat:ipcheck После замены файла Xray нужно перезапустить (в 3x-ui — **Restart Xray**). +## Автообновление на сервере + +Xray читает `.dat` только при старте, поэтому обновление — это «скачать файл и +перезапустить сервис». Этим занимается [`update-geosite.sh`](tools/update-geosite.sh): + +- сначала тянет `.sha256sum` и, если сумма совпала с локальным файлом, **молча выходит** — + ни лишней закачки, ни лишнего перезапуска Xray; +- проверяет контрольную сумму скачанного файла **до** подмены; +- подменяет файл атомарно (`mv` в пределах каталога), старый сохраняет в `.bak`; +- перезапускает сервис только если файл действительно изменился; +- пишет в stdout только когда что-то произошло, так что cron не будет слать письма впустую. + +### Установка + +```bash +curl -fsSL -o /usr/local/bin/update-geosite.sh https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/update-geosite.sh +chmod +x /usr/local/bin/update-geosite.sh +``` + +Проверьте вручную — первый запуск скачает файл и перезапустит панель: + +```bash +BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release \ +DEST=/usr/local/x-ui/bin/ipcheck.dat \ +RESTART_CMD='x-ui restart' \ +/usr/local/bin/update-geosite.sh +``` + +Второй запуск подряд должен отработать молча и мгновенно — значит сравнение сумм работает. + +### Вариант 1: cron + +`crontab -e` от root, одна строка (04:17 ночи, чтобы не совпадать с чужими крон-джобами): + +```cron +17 4 * * * BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release DEST=/usr/local/x-ui/bin/ipcheck.dat RESTART_CMD='x-ui restart' /usr/local/bin/update-geosite.sh >> /var/log/ipcheck-update.log 2>&1 +``` + +Или, если не хочется длинной строки, положите настройки в `/etc/default/ipcheck`: + +```bash +BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release +DEST=/usr/local/x-ui/bin/ipcheck.dat +RESTART_CMD=x-ui restart +``` + +```cron +17 4 * * * . /etc/default/ipcheck && /usr/local/bin/update-geosite.sh >> /var/log/ipcheck-update.log 2>&1 +``` + +Скрипт не экспортирует переменные сам — при таком варианте добавьте в файл +`export BASE_URL DEST RESTART_CMD` последней строкой. + +### Вариант 2: systemd timer + +Точнее cron: `Persistent=true` догоняет пропущенный запуск после перезагрузки, +`RandomizedDelaySec` размазывает нагрузку, а логи идут в journald. + +`/etc/systemd/system/ipcheck-geosite.service`: + +```ini +[Unit] +Description=Обновление ipcheck.dat для Xray +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +Environment=BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release +Environment=DEST=/usr/local/x-ui/bin/ipcheck.dat +Environment=RESTART_CMD=x-ui restart +ExecStart=/usr/local/bin/update-geosite.sh +``` + +`/etc/systemd/system/ipcheck-geosite.timer`: + +```ini +[Unit] +Description=Ежедневное обновление ipcheck.dat + +[Timer] +OnCalendar=daily +RandomizedDelaySec=1h +Persistent=true + +[Install] +WantedBy=timers.target +``` + +```bash +systemctl daemon-reload +systemctl enable --now ipcheck-geosite.timer +systemctl start ipcheck-geosite.service # разовая проверка +journalctl -u ipcheck-geosite.service -n 20 +``` + +### Если 3x-ui в Docker + +Путь указывайте внутрь примонтированного тома, а перезапуск делайте через docker: + +```bash +DEST=/var/lib/x-ui-bin/ipcheck.dat RESTART_CMD='docker restart 3x-ui' /usr/local/bin/update-geosite.sh +``` + +### Переменные скрипта + +| Переменная | По умолчанию | Значение | +| --- | --- | --- | +| `BASE_URL` | ссылка на ветку `release` | база, откуда качать | +| `DEST` | `/usr/local/x-ui/bin/ipcheck.dat` | куда положить файл | +| `NAME` | имя файла из `DEST` | имя файла в репозитории | +| `RESTART_CMD` | пусто | что выполнить после успешного обновления | +| `TIMEOUT` | `30` | таймаут curl, секунды | + +Коды возврата: `0` — обновлено или уже актуально, `1` — ошибка (не скачалось, +не совпала сумма, не удался перезапуск). + ## sing-box ```json @@ -81,14 +204,17 @@ ext:ipcheck.dat:ipcheck "type": "remote", "tag": "ipcheck", "format": "binary", - "url": "https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/sing-box/ipcheck.srs", - "download_detour": "direct" + "url": "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/sing-box/ipcheck.srs", + "download_detour": "direct", + "update_interval": "1d" } ] } } ``` +Здесь cron не нужен — sing-box обновляет rule-set сам. + ## Clash / mihomo ```yaml @@ -97,7 +223,7 @@ rule-providers: type: http behavior: domain format: yaml - url: "https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/clash/ipcheck.yaml" + url: "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/clash/ipcheck.yaml" path: ./ruleset/ipcheck.yaml interval: 86400 @@ -105,7 +231,7 @@ rules: - RULE-SET,ipcheck,PROXY ``` -Для `.mrs` укажите `behavior: domain` и `format: mrs`. +Для `.mrs` укажите `behavior: domain` и `format: mrs`. Обновление тоже своё, по `interval`. ## Как добавить домен @@ -131,26 +257,45 @@ example.com @ads # атрибут — попадёт в .dat как geosit Нужен только Python 3.10+, зависимостей нет: ```bash -python tools/build.py +python3 tools/build.py ``` Результат — в `dist/`. Проверка исходников без сборки (синтаксис, дубликаты, домены, уже покрытые родительским суффиксом, циклические `include`): ```bash -python tools/build.py --check +python3 tools/build.py --check +``` + +Тесты сборщика: + +```bash +python3 -m unittest discover -s tools -t tools ``` `.srs` и `.mrs` собираются, только если в `PATH` есть `sing-box` и `mihomo` — -без них остальные форматы всё равно соберутся. Пути можно указать явно: +без них остальные форматы всё равно соберутся. Скачать их вместе с `xray`: ```bash -python tools/build.py --sing-box ./bin/sing-box --mihomo ./bin/mihomo +sh tools/fetch-tools.sh ./bin ``` Сборщик проверяет сам себя: после записи `geosite.dat` он разбирает файл обратно и сверяет с исходником. В CI дополнительно поднимается настоящий Xray с правилом -`ext:geosite.dat:ipcheck` — если файл не читается, сборка падает. +`ext:ipcheck.dat:ipcheck` — если файл не читается, сборка падает. + +## CI + +[`.gitea/workflows/build.yml`](.gitea/workflows/build.yml): проверка → тесты → +сборка → валидация настоящими Xray и sing-box → публикация в ветку `release` → +релиз в Gitea. + +Раннеру нужен образ с `python3`, `curl`, `unzip`, `tar`, `git` (например +`catthehacker/ubuntu:act-latest`); если чего-то нет, первый шаг доставит пакеты сам. + +Публикация идёт под автоматическим токеном `${{ github.token }}`. Если у раннера +нет прав на push в репозиторий или на создание релизов — заведите секрет +`RELEASE_TOKEN` с правами `repo:write`, workflow предпочтёт его. ## Лицензия diff --git a/tools/build.py b/tools/build.py index 0f157cf..5b52d5a 100644 --- a/tools/build.py +++ b/tools/build.py @@ -443,6 +443,19 @@ def build(out_dir: Path, singbox: str | None, mihomo: str | None) -> int: + "\n", ) + # Скрипт обновления едет вместе с артефактами: на сервере тогда нужен + # ровно один базовый URL — и для .dat, и для самого апдейтера. + updater = Path(__file__).with_name("update-geosite.sh") + if updater.is_file(): + write(out_dir / updater.name, updater.read_text(encoding="utf-8")) + + checksums = "".join( + f"{hashlib.sha256(path.read_bytes()).hexdigest()} {path.relative_to(out_dir).as_posix()}\n" + for path in sorted(out_dir.rglob("*")) + if path.is_file() + ) + write(out_dir / "SHA256SUMS", checksums) + print(f"geosite.dat: {len(dat)} байт, sha256 {digest[:16]}…") for name in categories: print(f" {name:<12} {len(resolved[name]):>4} правил") diff --git a/tools/fetch-tools.sh b/tools/fetch-tools.sh new file mode 100755 index 0000000..b3e95df --- /dev/null +++ b/tools/fetch-tools.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Кладёт sing-box, mihomo и xray в каталог (по умолчанию ./bin). +# Нужен только для сборки .srs/.mrs и для проверки .dat настоящим Xray. +# +# ./tools/fetch-tools.sh [каталог] + +set -eu + +BIN="${1:-./bin}" +mkdir -p "$BIN" +BIN=$(cd "$BIN" && pwd) + +case "$(uname -m)" in + x86_64 | amd64) sb_arch=amd64; mh_arch=amd64; xr_arch=64 ;; + aarch64 | arm64) sb_arch=arm64; mh_arch=arm64; xr_arch=arm64-v8a ;; + *) echo "неподдерживаемая архитектура: $(uname -m)" >&2; exit 1 ;; +esac + +# Печатает ссылку на ассет последнего релиза, имя которого совпало с regex. +asset_url() { + curl -fsSL "https://api.github.com/repos/$1/releases/latest" | python3 -c ' +import json, re, sys +assets = json.load(sys.stdin)["assets"] +match = next((a for a in assets if re.search(sys.argv[1], a["name"])), None) +if match is None: + sys.exit(f"в релизе нет ассета по шаблону {sys.argv[1]}") +print(match["browser_download_url"]) +' "$2" +} + +echo "sing-box…" +curl -fsSL "$(asset_url SagerNet/sing-box "-linux-$sb_arch\\.tar\\.gz\$")" | + tar -xz --strip-components=1 -C "$BIN" --wildcards '*/sing-box' + +echo "mihomo…" +curl -fsSL "$(asset_url MetaCubeX/mihomo "^mihomo-linux-$mh_arch-v[0-9.]+\\.gz\$")" | + gunzip > "$BIN/mihomo" + +echo "xray…" +curl -fsSL -o "$BIN/xray.zip" "$(asset_url XTLS/Xray-core "^Xray-linux-$xr_arch\\.zip\$")" +unzip -oqj "$BIN/xray.zip" xray -d "$BIN" +rm -f "$BIN/xray.zip" + +chmod +x "$BIN/sing-box" "$BIN/mihomo" "$BIN/xray" +echo "готово: $BIN" +"$BIN/sing-box" version | head -1 +"$BIN/mihomo" -v | head -1 +"$BIN/xray" version | head -1 diff --git a/tools/gitea-release.sh b/tools/gitea-release.sh new file mode 100755 index 0000000..c2e2501 --- /dev/null +++ b/tools/gitea-release.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Создаёт (или обновляет) релиз в Gitea и заливает в него файлы. +# +# GITEA_URL=https://gitea.example.com GITEA_REPO=owner/repo GITEA_TOKEN=… \ +# ./tools/gitea-release.sh <тег> <файл-с-описанием> <файл> [файл …] +# +# Имя ассета — путь относительно dist/ с заменой "/" на "-", чтобы +# sing-box/ipcheck.json и json/ipcheck.json не конфликтовали. + +set -eu + +[ $# -ge 3 ] || { echo "использование: $0 <тег> <файл...>" >&2; exit 1; } + +TAG="$1"; NOTES="$2"; shift 2 +: "${GITEA_URL:?не задан GITEA_URL}" +: "${GITEA_REPO:?не задан GITEA_REPO}" +: "${GITEA_TOKEN:?не задан GITEA_TOKEN}" + +API="$GITEA_URL/api/v1/repos/$GITEA_REPO/releases" +auth="Authorization: token $GITEA_TOKEN" + +json_field() { python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "$1"; } + +payload=$(NOTES_FILE="$NOTES" TAG="$TAG" python3 -c ' +import json, os +print(json.dumps({ + "tag_name": os.environ["TAG"], + "name": os.environ["TAG"], + "body": open(os.environ["NOTES_FILE"], encoding="utf-8").read(), + "draft": False, + "prerelease": False, +}))') + +# Релиз с таким тегом мог остаться от прошлого прогона — тогда переиспользуем его. +id=$(curl -fsS -X POST "$API" -H "$auth" -H 'Content-Type: application/json' \ + -d "$payload" 2>/dev/null | json_field id || true) +if [ -z "$id" ]; then + id=$(curl -fsS "$API/tags/$TAG" -H "$auth" | json_field id) || + { echo "не удалось ни создать, ни найти релиз $TAG" >&2; exit 1; } + echo "релиз $TAG уже существует (id $id), дозаливаем ассеты" +fi +echo "релиз $TAG: id $id" + +for file in "$@"; do + [ -f "$file" ] || { echo "нет файла: $file" >&2; exit 1; } + name=$(printf '%s' "${file#dist/}" | tr '/' '-') + curl -fsS -X POST "$API/$id/assets?name=$name" -H "$auth" \ + -F "attachment=@$file" >/dev/null + echo " + $name" +done diff --git a/tools/update-geosite.sh b/tools/update-geosite.sh new file mode 100755 index 0000000..cc201fc --- /dev/null +++ b/tools/update-geosite.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# Обновляет локальный ipcheck.dat из ветки release. Рассчитан на запуск по cron. +# +# Скачивает файл, только если его sha256 отличается от локального, проверяет +# контрольную сумму до подмены и перезапускает сервис только когда файл реально +# изменился. Молчит, если обновлять нечего, — cron не будет слать письма впустую. +# +# ./update-geosite.sh +# BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release \ +# DEST=/usr/local/x-ui/bin/ipcheck.dat RESTART_CMD='x-ui restart' ./update-geosite.sh +# +# Коды возврата: 0 — обновлено или уже актуально, 1 — ошибка. + +set -eu + +BASE_URL="${BASE_URL:-https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release}" +DEST="${DEST:-/usr/local/x-ui/bin/ipcheck.dat}" +NAME="${NAME:-$(basename "$DEST")}" +RESTART_CMD="${RESTART_CMD:-}" +TIMEOUT="${TIMEOUT:-30}" + +log() { printf '%s update-geosite: %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"; } + +# Без промежуточной переменной статус вернул бы cut, а не curl. +remote_line=$(fetch "$BASE_URL/$NAME.sha256sum") || + die "не удалось скачать $BASE_URL/$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 + exit 0 # уже актуально — тишина +fi + +tmp=$(mktemp "${TMPDIR:-/tmp}/ipcheck.XXXXXX") || die "не удалось создать временный файл" +trap 'rm -f "$tmp"' EXIT INT TERM + +fetch "$BASE_URL/$NAME" > "$tmp" || die "не удалось скачать $BASE_URL/$NAME" + +got=$(hash_of "$tmp") +[ "$got" = "$remote_sum" ] || die "контрольная сумма не совпала: ожидалось $remote_sum, получено $got" +[ -s "$tmp" ] || die "скачан пустой файл" + +mkdir -p "$(dirname "$DEST")" +if [ -f "$DEST" ]; then + cp -p "$DEST" "$DEST.bak" || die "не удалось сохранить резервную копию" +fi +# mv в пределах одного каталога атомарен: Xray никогда не увидит половину файла. +cat "$tmp" > "$DEST.new" && mv -f "$DEST.new" "$DEST" || die "не удалось записать $DEST" + +log "обновлён $DEST (sha256 $remote_sum)" + +if [ -n "$RESTART_CMD" ]; then + if sh -c "$RESTART_CMD"; then + log "выполнено: $RESTART_CMD" + else + die "перезапуск не удался: $RESTART_CMD" + fi +fi