feat: 一条 curl 自动装/升(未装安装、已装升级)

This commit is contained in:
Hermes
2026-07-21 14:23:31 +00:00
commit b2a7332f86
20 changed files with 3459 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# 将 sing-box 官方 release 预下载并上传到本仓库 Gitea Release
# 用法(维护机):
# set -a; source ~/.hermes/env/gitea.env; set +a
# ./scripts/publish-binaries-to-gitea.sh
# SING_BOX_VERSION=1.11.7 ARCHS='amd64 arm64 armv7' ./scripts/publish-binaries-to-gitea.sh
#
# GitHub 下载默认经 https://git.86482425.xyz
# 上传走 multipart>约 8MB 可能被反向代理 504,自动改 1MB 分片上传
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
: "${GITEA_URL:?请先 source ~/.hermes/env/gitea.env}"
: "${GITEA_TOKEN:?请先 source ~/.hermes/env/gitea.env}"
OWNER="${GITEA_OWNER:-Hermes}"
REPO="${GITEA_REPO:-cellular-proxy}"
VER="${SING_BOX_VERSION:-1.11.7}"
TAG="${SING_BOX_RELEASE_TAG:-bin-v${VER}}"
ARCHS="${ARCHS:-amd64 arm64 armv7}"
GITHUB_PROXY="${GITHUB_PROXY:-https://git.86482425.xyz}"
PART_BYTES="${PART_BYTES:-1048576}"
# 经验阈值:整包上传超时则分片(nginx 常见 ~80s)
WHOLE_MAX_BYTES="${WHOLE_MAX_BYTES:-9000000}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
download() {
local url="$1" dest="$2"
log "下载 $url"
curl -fL --retry 3 --connect-timeout 20 --max-time 300 -o "$dest" "$url"
}
github_fetch() {
local raw="$1" dest="$2"
local proxied="${GITHUB_PROXY%/}/${raw}"
if [[ -n "${GITHUB_PROXY:-}" ]]; then
if download "$proxied" "$dest"; then
return 0
fi
log "代理失败,直连"
fi
download "$raw" "$dest"
}
API="$GITEA_URL/api/v1/repos/$OWNER/$REPO"
auth=(-H "Authorization: token $GITEA_TOKEN")
delete_asset() {
local id="$1"
# Gitea 正确路径:/releases/{id}/assets/{asset_id}
curl -sS -X DELETE "${auth[@]}" -o /dev/null -w "DEL %{http_code}\n" \
"$API/releases/$REL_ID/assets/$id" || true
}
upload_file() {
local fpath="$1" fname="$2"
local code
code=$(curl -sS --http1.1 --max-time 300 \
"${auth[@]}" \
-F "attachment=@${fpath};filename=${fname}" \
-o "$TMP/up_${fname}.json" \
-w '%{http_code}' \
"$API/releases/$REL_ID/assets?name=${fname}" || echo 000)
if [[ "$code" != "201" && "$code" != "200" ]]; then
log "上传失败 $fname HTTP $code"
head -c 300 "$TMP/up_${fname}.json" 2>/dev/null || true
return 1
fi
python3 - <<PY
import json
d=json.load(open("$TMP/up_${fname}.json"))
assert d.get("name")=="$fname", d
print("asset ok:", d.get("name"), "size=", d.get("size"))
PY
}
ensure_release() {
local code
code=$(curl -sS "${auth[@]}" -o "$TMP/rel.json" -w '%{http_code}' "$API/releases/tags/$TAG")
if [[ "$code" == "200" ]]; then
REL_ID=$(python3 -c 'import json; print(json.load(open("'"$TMP"'/rel.json"))["id"])')
log "已有 Release tag=$TAG id=$REL_ID"
else
log "创建 Release tag=$TAG"
curl -sS "${auth[@]}" -H 'Content-Type: application/json' \
-d "$(python3 - <<PY
import json
print(json.dumps({
"tag_name": "$TAG",
"name": "sing-box $VER prebundled binaries",
"body": "预置 sing-box v$VER。安装端优先从此 Release 下载,无需访问 GitHub。\\n架构: $ARCHS\\n大文件可能以 .part-XX 分片提供。",
"draft": False,
"prerelease": False,
"target_commitish": "main",
}))
PY
)" \
-o "$TMP/rel_create.json" \
"$API/releases"
REL_ID=$(python3 -c 'import json,sys; d=json.load(open("'"$TMP"'/rel_create.json"));
print(d.get("id") or "");
assert d.get("id"), d')
log "Release id=$REL_ID"
fi
}
delete_named_assets() {
local prefix="$1"
curl -sS "${auth[@]}" "$API/releases/$REL_ID" -o "$TMP/rel_full.json"
python3 - <<PY
import json,subprocess,os
d=json.load(open("$TMP/rel_full.json"))
prefix="$prefix"
for a in d.get("assets") or []:
n=a.get("name") or ""
if n==prefix or n.startswith(prefix+".") or n.startswith(prefix+".part-") or n==prefix+".SHA256SUMS" or (prefix.endswith(".tar.gz") and n=="SHA256SUMS" and "armv7" in prefix):
print("delete", a["id"], n)
subprocess.check_call(["bash","-lc", f'curl -sS -X DELETE -H "Authorization: token $GITEA_TOKEN" -o /dev/null -w "DEL %{{http_code}}\\n" "$API/releases/$REL_ID/assets/{a["id"]}"'])
PY
}
upload_whole_or_parts() {
local fpath="$1" fname="$2"
local size
size=$(stat -c%s "$fpath")
delete_named_assets "$fname"
if [[ "$size" -le "$WHOLE_MAX_BYTES" ]]; then
log "整包上传 $fname ($size bytes)"
if upload_file "$fpath" "$fname"; then
return 0
fi
log "整包失败,改分片"
else
log "文件 $size > $WHOLE_MAX_BYTES,直接分片上传 $fname"
fi
local pdir="$TMP/parts_${fname}"
rm -rf "$pdir"
mkdir -p "$pdir"
split -b "$PART_BYTES" -d -a 2 "$fpath" "$pdir/${fname}.part-"
(
cd "$pdir"
# 清单使用纯文件名,便于客户端校验
sha256sum "${fname}.part-"* > "${fname}.SHA256SUMS"
)
local p
for p in "$pdir/${fname}.part-"* "$pdir/${fname}.SHA256SUMS"; do
bn=$(basename "$p")
log "分片上传 $bn ($(stat -c%s "$p") bytes)"
upload_file "$p" "$bn"
done
}
# 1) 下载
for arch in $ARCHS; do
name="sing-box-${VER}-linux-${arch}"
raw="https://github.com/SagerNet/sing-box/releases/download/v${VER}/${name}.tar.gz"
dest="$TMP/${name}.tar.gz"
github_fetch "$raw" "$dest"
tar -tzf "$dest" | head -3 >/dev/null
ls -lh "$dest"
done
# 2) release
ensure_release
# 3) 上传
for arch in $ARCHS; do
fname="sing-box-${VER}-linux-${arch}.tar.gz"
upload_whole_or_parts "$TMP/$fname" "$fname"
done
# 4) 公开下载自检
for arch in $ARCHS; do
fname="sing-box-${VER}-linux-${arch}.tar.gz"
url="$GITEA_URL/$OWNER/$REPO/releases/download/$TAG/$fname"
code=$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 30 "$url" || true)
if [[ "$code" == "200" || "$code" == "206" ]]; then
log "公开整包 OK $url -> $code"
else
# 分片?
p0="$GITEA_URL/$OWNER/$REPO/releases/download/$TAG/${fname}.part-00"
code2=$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 30 "$p0" || true)
log "整包 $code,分片 part-00 -> $code2 ($p0)"
fi
done
log "完成。安装端优先:"
log " $GITEA_URL/$OWNER/$REPO/releases/download/$TAG/sing-box-${VER}-linux-<arch>.tar.gz"
log " 或分片: .../${fname}.part-XX + .../${fname}.SHA256SUMS"