#!/bin/sh # torrcast: curl -fsSL https://torrcast.anysda.space | sh (или .../install - тот же файл) # # Бутстрап, а не установка: узнаёт у GitHub последнюю версию, тащит тарбол этой # версии, сверяет sha256, распаковывает в mktemp -d и зовёт install.sh оттуда. # Всё тело - внутри функций: оборванная закачка (curl режет на середине) ничего # не выполнит, потому что не доедет до вызова main в конце файла. # # 🔴 TC-887. Загрузчик тут ОДИН, а входа в него два, и это не совпадение, а решение: # `cast --upgrade` зовёт этот же файл (install.sh кладёт его в $PREFIX/install), а # не второй такой же на питоне. Отличается только преддверие - названная версия, от # которой идём, - а узнавание тега, закачка, сверка sha256, распаковка и передача # install.sh общие обоим входам. Заведи обновление своей закачкой, и два тела разъедутся # ровно там, где чинить будут только одно из них. set -eu BOOTSTRAP_URL="${TORRCAST_BOOTSTRAP_URL:-https://torrcast.anysda.space}" GITHUB_WEB="${TORRCAST_GITHUB_WEB:-https://github.com}" PROJECT_PATH="${TORRCAST_PROJECT_PATH:-anysda/torrcast}" LANGUAGE="${TORRCAST_LANGUAGE:-en}" #: Установленная версия, от которой идёт обновление. Непустое значение - это и есть #: второй вход: работа начинается со сверки версий, а не со скачивания. UPGRADE_FROM="${TORRCAST_UPGRADE_FROM:-}" case "$LANGUAGE" in en|ru) ;; # 🔴 TC-887. Код 1, а НЕ 2. Двойка занята штатным исходом установщика - урезанным # каталогом индексеров (EXIT_CATALOG_CUT в install.sh), - и `cast --upgrade` читает # её как «поставилось, но беднее полного». Отдай отсюда двойку, и отказ разбора # доехал бы до человека словом «обновлено», хотя не тронуто ничего. *) printf 'error: TORRCAST_LANGUAGE must be en or ru\n' >&2; exit 1 ;; esac say() { if [ "$LANGUAGE" = ru ]; then printf '%s\n' "$2" >&2; else printf '%s\n' "$1" >&2; fi; } fail() { say "error: $1" "ошибка: $2"; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || fail "$1 is required but is not in PATH" "нужен $1, его нет в PATH"; } # Не root - не падаем молча: под sudo перезапускаемся тем же одностроком, без него # просим человека сделать это самому. TORRCAST_NO_ROOT - лазейка для песочницы, как # в install.sh. # `sudo -E` тут не годится: на части машин sudo собран без сохранения окружения, и # `-E` тихо игнорируется (sudo молча предупреждает в stderr и правит по-своему), из-за # чего TORRCAST_GITHUB_WEB/TORRCAST_PROJECT_PATH/TORRCAST_BOOTSTRAP_URL пропадали бы у # перезапущенного процесса без единого слова. Поэтому переопределения тащим не через # окружение, а явными аргументами позиционника, и экспортируем их уже внутри новой # оболочки за трубой - так их видит и curl, и запущенный им sh. become_root() { [ -n "${TORRCAST_NO_ROOT:-}" ] && return 0 [ "$(id -u)" -eq 0 ] && return 0 # 🔴 Обновление под sudo НЕ перезапускается одностроком. Односторок - это установка # с нуля: он потерял бы и версию, от которой идём, и заставку обновления, и человек # получил бы вместо `cast --upgrade` полную переустановку, о которой не просил. # Поэтому тут именно строка «повтори вот так», а не тихая подмена работы. have_sudo="" command -v sudo >/dev/null 2>&1 && have_sudo=1 if [ -n "$UPGRADE_FROM" ]; then [ -n "$have_sudo" ] \ && fail "root is required: sudo cast --upgrade" "нужен root: sudo cast --upgrade" fail "root is required: run cast --upgrade as root" \ "нужен root: запусти cast --upgrade от root" fi if [ -n "$have_sudo" ]; then say "not root - restarting through sudo" "не root - перезапуск через sudo" # Язык через sudo сносим, только если он НАЗВАН в окружении однострока (TC-955): # экспорт дефолта отсюда сбрасывал бы язык из живого конфига при каждом # обновлении не-root'ом. Молчание обязано доехать до install.sh молчанием. exec sudo sh -c ' export TORRCAST_BOOTSTRAP_URL="$1" TORRCAST_GITHUB_WEB="$2" TORRCAST_PROJECT_PATH="$3" [ -z "$4" ] || export TORRCAST_LANGUAGE="$4" curl -fsSL "$1" | sh ' _ "$BOOTSTRAP_URL" "$GITHUB_WEB" "$PROJECT_PATH" "${TORRCAST_LANGUAGE:-}" fi # Сюда доходят только там, где sudo нет вовсе: советовать его тут значило бы # послать человека за командой, которой у него на машине не существует. fail "root is required: run it as root: sh -c \"curl -fsSL $BOOTSTRAP_URL | sh\"" \ "нужен root: запусти от root: sh -c \"curl -fsSL $BOOTSTRAP_URL | sh\"" } # Печатает тег последней версии в stdout. Спрашивается не API, а сам `/releases/latest`: # он отвечает 302, и в Location стоит `/releases/tag/vX.Y.Z`. Так узнаётся версия без # api.github.com, у которого анониму отведено 60 запросов в час на адрес - потолок, # делящийся на всех за одним NAT, и упереться в него значило бы получить отказ установки # на ровном месте. До первого релиза тот же адрес уводит на `/releases` без `/tag/`: # релизов ещё нет - сказать об этом словами, а не сырым перенаправлением. latest_version() { loc="$(curl -fsS --connect-timeout 10 -o /dev/null -w '%{redirect_url}' \ "$GITHUB_WEB/$PROJECT_PATH/releases/latest" 2>/dev/null)" \ || fail "could not reach $GITHUB_WEB/$PROJECT_PATH/releases/latest" \ "не достучаться до $GITHUB_WEB/$PROJECT_PATH/releases/latest" case "$loc" in */releases/tag/*) ;; "") fail "no redirect from $GITHUB_WEB/$PROJECT_PATH/releases/latest" \ "$GITHUB_WEB/$PROJECT_PATH/releases/latest не перенаправил никуда" ;; *) fail "no releases yet (releases/latest leads to the release list) - come back after the first release" \ "релизов ещё нет (releases/latest ведёт на список релизов) - вернись, когда выйдет первый релиз" ;; esac ver="${loc##*/releases/tag/}" [ -n "$ver" ] || fail "the redirect names no tag" "в перенаправлении нет тега" printf '%s\n' "$ver" } main() { need curl need tar need sha256sum need bash become_root # Тег из GitHub - с ведущей v (v1.0.0): так он стоит в Location у releases/latest, # и так же он стоит в пути к ассету. Имена самих файлов - БЕЗ v (release.sh срезает # её через ${tag#v}), а имя внутри .sha256 обязано побайтово совпасть с локальным # именем файла. tag="$(latest_version)" ver="${tag#v}" # Имя продукта не переводится и не транслитерируется: torrcast на обоих языках. if [ -n "$UPGRADE_FROM" ]; then # 🔴 Совпало - работы нет ВОВСЕ: ни временного каталога, ни закачки, ни единого # тронутого файла. Ровно этим `cast --upgrade` отличается от «переустановить и # посмотреть, что выйдет»: у переустановки тот же исход стоит полного прогона # установщика, перезаписи юнитов и перезапуска служб под идущим показом. # Выход отсюда стоит ДО mktemp нарочно: так «не тронул ничего» держится кодом, # а не обещанием прибрать за собой. if [ "$ver" = "$UPGRADE_FROM" ]; then say "torrcast $ver is already the latest version" \ "torrcast $ver - уже последняя версия" return 0 fi say "torrcast $UPGRADE_FROM → $ver" "torrcast $UPGRADE_FROM → $ver" # Дальше по цепочке это читает install.sh из тарбола: он и рисует заставку # обновления вместо заставки установки. export TORRCAST_UPGRADE_FROM="$UPGRADE_FROM" else say "torrcast $ver" "torrcast $ver" fi tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT pkg="$GITHUB_WEB/$PROJECT_PATH/releases/download/$tag" curl -fsSL -o "$tmp/torrcast-$ver.tar.gz" "$pkg/torrcast-$ver.tar.gz" \ || fail "could not download torrcast-$ver.tar.gz" "не скачался тарбол torrcast-$ver.tar.gz" curl -fsSL -o "$tmp/torrcast-$ver.tar.gz.sha256" "$pkg/torrcast-$ver.tar.gz.sha256" \ || fail "could not download the sha256 checksum" "не скачалась сверка sha256" (cd "$tmp" && sha256sum -c "torrcast-$ver.tar.gz.sha256") >/dev/null \ || fail "sha256 mismatch - the archive is damaged or was replaced" "sha256 не сошёлся - тарбол повреждён или подменён" mkdir "$tmp/src" tar -xzf "$tmp/torrcast-$ver.tar.gz" -C "$tmp/src" [ -f "$tmp/src/install.sh" ] || fail "the archive has no install.sh" "в тарболе нет install.sh" # install.sh - bash-скрипт со своим шебангом (#!/usr/bin/env bash), не sh: на # Debian /bin/sh - dash, и на башизмах install.sh он падает сразу.