#!/usr/bin/env bash # Debian/Ubuntu installer. Review https://sh.cx.edu.kg/install.sh before execution. set -Eeuo pipefail umask 077 BASE='https://sh.cx.edu.kg' MODE=install case "${1:-}" in '') ;; --activate-only) MODE=activate ;; --download-only) MODE=download ;; --help|-h) printf '%s\n' '用法:bash install.sh [--activate-only | --download-only]' '默认:下载并验证全部 deb,确认后通过 apt 安装,再领取激活码。' '需要 bash、curl、python3;安装需要 Debian/Ubuntu、apt-get 和 dpkg-deb。'; exit 0 ;; *) echo '未知参数,请使用 --help' >&2; exit 2 ;; esac fail() { printf '错误:%s\n' "$*" >&2; exit 1; } for cmd in curl python3; do command -v "$cmd" >/dev/null || fail "请先安装 $cmd"; done [[ -r /dev/tty && -w /dev/tty ]] || fail '需要交互终端,请在终端中运行(不要使用无 TTY 的任务)。' exec 3<>/dev/tty ask() { printf '%s' "$1" >&3; IFS= read -r REPLY <&3 || fail '未读取到终端输入'; } TMP=$(mktemp -d) KEEP=0 cleanup() { if [[ "$KEEP" = 0 ]]; then rm -rf -- "$TMP"; else printf '下载文件保留在:%s\n' "$TMP"; fi; } trap cleanup EXIT trap 'printf "操作已中止。可用 --activate-only 单独领取激活码。\n" >&2' INT TERM request() { local path=$1 input=$2 output=$3 if ! curl --proto '=https' --proto-redir '=https' -fsS --connect-timeout 15 --max-time 60 -H 'Content-Type: application/json' --data-binary "@$input" "$BASE$path" -o "$output"; then [[ ! -s "$output" ]] || python3 - "$output" <<'PY' import json,sys try: print(json.load(open(sys.argv[1])).get('error','请求失败。'),file=sys.stderr) except Exception: pass PY fail '服务请求失败;库存为空时请先联系管理员导入激活码。' fi } activate() { printf '\n—— 激活码领取 ——\n序列号会发送到 sh.cx.edu.kg 并保存,用于重复查询相同激活码。\n' printf '请勿输入其他人的序列号;知道序列号的人也能查询对应激活码。\n' ask '请输入机器序列号(区分大小写,直接回车跳过):' [[ -n "$REPLY" ]] || return 0 # Read serial via stdin: never put private values in process arguments. printf '%s' "$REPLY" | python3 -c 'import json,sys;json.dump({"serial":sys.stdin.read().strip()},sys.stdout)' > "$TMP/claim.json" request /api/claim "$TMP/claim.json" "$TMP/result.json" python3 - "$TMP/result.json" <<'PY' import json,sys r=json.load(open(sys.argv[1]));print('\n机器序列号:'+r['serial']);print('激活码:'+r['code']);print('状态:'+('已确认激活' if r['status']=='confirmed' else '已预留,等待您激活确认')) PY ask '请自行完成激活。确认已经成功激活?[y/N]:' if [[ "$REPLY" =~ ^[yY]([eE][sS])?$ ]]; then python3 - "$TMP/result.json" "$TMP/confirm.json" <<'PY' import json,sys r=json.load(open(sys.argv[1]));json.dump({'serial':r['serial'],'receipt':r['receipt']},open(sys.argv[2],'w')) PY request /api/confirm "$TMP/confirm.json" "$TMP/confirmed.json" printf '已记录您的激活确认。今后相同序列号会返回同一个激活码。\n' else printf '激活码已保留,不会分配给其他序列号。可稍后用 --activate-only 继续。\n' fi } if [[ "$MODE" = activate ]]; then activate; exit 0; fi for cmd in dpkg dpkg-deb; do command -v "$cmd" >/dev/null || fail "需要 Debian/Ubuntu 环境和 $cmd;可用 --activate-only 单独领取激活码。"; done ARCH=$(dpkg --print-architecture) printf '本机架构:%s\n正在读取最新安装包清单……\n' "$ARCH" curl --proto '=https' --proto-redir '=https' -fLsS --retry 3 --connect-timeout 15 --max-time 90 "$BASE/api/manifest" -o "$TMP/manifest.json" python3 - "$TMP/manifest.json" "$TMP/list" <<'PY' import json,sys,urllib.parse p=json.load(open(sys.argv[1]))['packages'] if not p: raise SystemExit('目录中没有 deb 安装包。') print('共 %s 个安装包,总大小约 %.2f GiB。'%(len(p),sum(x['size'] for x in p)/1024**3)) with open(sys.argv[2],'wb') as f: for x in p: name=x['name'];u=x['url'] if '/' in name or '\\' in name or not name.lower().endswith('.deb') or any(ord(c)<32 for c in name): raise SystemExit('不安全的文件名。') if urllib.parse.urlsplit(u).scheme!='https' or urllib.parse.urlsplit(u).netloc!='sh.cx.edu.kg': raise SystemExit('下载地址无效。') print(' '+name) f.write(name.encode()+b'\0'+u.encode()+b'\0'+str(x['size']).encode()+b'\0') PY printf '当前清单的文件名标注 arm64;下载后还会检查包内真实架构。\n' ask '继续下载?这可能消耗约 1.75 GiB 流量。[y/N]:' [[ "$REPLY" =~ ^[yY]([eE][sS])?$ ]] || { printf '已取消下载。\n'; activate; exit 0; } mkdir "$TMP/packages" packages=() while IFS= read -r -d '' name && IFS= read -r -d '' url && IFS= read -r -d '' size; do printf '\n下载:%s\n' "$name" target="$TMP/packages/$name" curl --proto '=https' --proto-redir '=https' -fL --retry 3 --retry-delay 3 --connect-timeout 20 --speed-limit 1024 --speed-time 120 "$url" -o "$target" actual=$(stat -c %s "$target") [[ "$actual" = "$size" ]] || fail '文件大小不符,已停止安装。' dpkg-deb --info "$target" >/dev/null || fail 'deb 元数据检查失败,已停止安装。' pkgarch=$(dpkg-deb --field "$target" Architecture) if [[ "$pkgarch" != all && "$pkgarch" != "$ARCH" ]]; then printf '包架构 %s 与本机 %s 不匹配,停止安装。可用 --activate-only 领取激活码。\n' "$pkgarch" "$ARCH" >&2 exit 1 fi packages+=("$target") done < "$TMP/list" if [[ "$MODE" = download ]]; then KEEP=1; printf '\n所有安装包已下载并检查;没有进行安装。\n'; exit 0; fi command -v apt-get >/dev/null || fail '找不到 apt-get,请使用 Debian/Ubuntu。' printf '\n校验通过。安装需要 root/sudo,apt 会执行包内脚本并可能从系统软件源下载依赖。\n' printf '本工具只核对大小、deb 元数据和架构,不代表第三方包可信或具有数字签名。\n' ask '确认使用 apt 一次安装以上全部包?[y/N]:' if [[ "$REPLY" =~ ^[yY]([eE][sS])?$ ]]; then root=() if [[ "$EUID" != 0 ]]; then command -v sudo >/dev/null || fail '需要 root 或 sudo'; root=(sudo); fi # apt sandbox must be able to traverse temporary directory and read local packages. chmod 755 "$TMP" "$TMP/packages"; chmod 644 "${packages[@]}" if ! "${root[@]}" apt-get install -- "${packages[@]}" <&3; then printf 'apt 安装失败,没有忽略错误。请检查依赖、发行版兼容性或用 --activate-only 单独领取激活码。\n' >&2 ask '仍然单独领取激活码?[y/N]:' [[ "$REPLY" =~ ^[yY]([eE][sS])?$ ]] && activate exit 1 fi else printf '已跳过安装。\n'; fi activate