카테고리 없음

apt mirror fix

moonee 2025. 11. 5. 22:45
#!/usr/bin/env bash
# js-sec-byhash-snapshot-v3.sh
# Jammy-security: Release의 SHA256 기준 by-hash 정확 동기화
# - CNF(Commands-*), binary-{amd64,i386}/Packages.*, i18n(Translation-*), dep11/Components-*.yml(.gz) 포함
# - 평문 Packages / Translation-* / Components-*.yml 자동 생성
# - 원자적 교체로 배포

set -euo pipefail
umask 022

# ====== CONFIG ======
BASE_DIR="${BASE_DIR:-/var/www/html/ubuntu/mirror/archive.ubuntu.com/ubuntu}"
DIST="jammy-security"
COMPS=("main" "universe")       # 필요 시 restricted multiverse 추가
ARCHES=("amd64" "i386")
HOST="security.ubuntu.com"
STAGE="$(mktemp -d /var/tmp/js-sec.XXXXXX)"
# ====================

need(){ command -v "$1" >/dev/null || { echo "필요 명령 누락: $1"; exit 1; }; }
need wget; need awk; need sha256sum; need stat; need xz; need gzip; need grep; need sed; need cp; need mkdir

echo "[INFO] BASE_DIR=$BASE_DIR"
mkdir -p "$BASE_DIR/dists" "$STAGE/dists/$DIST"

die(){ echo "ERROR: $*" >&2; exit 1; }

rel_lookup_sha256() {
  # $1: relpath (예: main/dep11/Components-amd64.yml.gz)
  local relpath="$1" line
  line=$(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" | grep -F " $relpath" || true)
  [[ -n "$line" ]] || return 1
  # <hash> <size> <path>
  local h s p
  h="$(awk '{print $1}' <<<"$line")"
  s="$(awk '{print $2}' <<<"$line")"
  p="$(awk '{print $3}' <<<"$line")"
  [[ "$p" == "$relpath" ]] || return 1
  printf "%s %s" "$h" "$s"
}

fetch_byhash_to() {
  # $1: relpath, $2: hash
  local relpath="$1" hash="$2"
  local url="http://$HOST/ubuntu/dists/$DIST/$(dirname "$relpath")/by-hash/SHA256/$hash"
  local out="$STAGE/dists/$DIST/$relpath"
  mkdir -p "$(dirname "$out")"
  wget -q -O "$out" "$url" || die "다운로드 실패: $url"
  # by-hash 경로에도 동일 파일 저장
  local bh="$STAGE/dists/$DIST/$(dirname "$relpath")/by-hash/SHA256/$hash"
  mkdir -p "$(dirname "$bh")"
  cp -a "$out" "$bh"
}

verify_exact() {
  # $1: relpath, $2: expected hash, $3: expected size
  local relpath="$1" exp_h="$2" exp_s="$3"
  local full="$STAGE/dists/$DIST/$relpath"
  [[ -s "$full" ]] || die "누락: $relpath"
  local act_s act_h
  act_s="$(stat -c %s "$full")"
  act_h="$(sha256sum "$full" | awk '{print $1}')"
  [[ "$act_s" == "$exp_s" && "$act_h" == "$exp_h" ]] || {
    echo "불일치: $relpath"
    echo "  exp size/hash: $exp_s / $exp_h"
    echo "  act size/hash: $act_s / $act_h"
    return 1
  }
  echo "OK  $relpath"
}

# 1) Release/InRelease
echo "[STEP1] fetch Release/InRelease"
wget -q -O "$STAGE/dists/$DIST/InRelease"   "http://$HOST/ubuntu/dists/$DIST/InRelease"   || true
wget -q -O "$STAGE/dists/$DIST/Release"     "http://$HOST/ubuntu/dists/$DIST/Release"     || die "Release 없음"
wget -q -O "$STAGE/dists/$DIST/Release.gpg" "http://$HOST/ubuntu/dists/$DIST/Release.gpg" || true
REL="$STAGE/dists/$DIST/Release"

# 2) 대상 목록 구성 (CNF/Packages/i18n/dep11 Components)
echo "[STEP2] build target list"
declare -a TARGETS=()

# CNF + Packages
for comp in "${COMPS[@]}"; do
  TARGETS+=("$comp/cnf/Commands-amd64.xz")
  TARGETS+=("$comp/cnf/Commands-i386.xz")
  for arch in "${ARCHES[@]}"; do
    TARGETS+=("$comp/binary-$arch/Packages.xz")
    TARGETS+=("$comp/binary-$arch/Packages.gz")
  done
done

# i18n: Release에 있는 것만
mapfile -t I18N_PATHS < <(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" \
  | awk '{print $3}' | grep -E '^(main|universe)/i18n/Translation-')
TARGETS+=("${I18N_PATHS[@]}")

# dep11 Components: .yml 또는 .yml.gz (Release에 있는 항목만)
mapfile -t DEP11_COMPONENTS < <(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" \
  | awk '{print $3}' | grep -E '^(main|universe)/dep11/Components-.*\.yml(\.gz)?$')
TARGETS+=("${DEP11_COMPONENTS[@]}")

# 3) by-hash로 받고 검증 + 평문 생성
echo "[STEP3] fetch via by-hash & verify"
for relpath in "${TARGETS[@]}"; do
  info="$(rel_lookup_sha256 "$relpath" || true)"
  if [[ -z "${info:-}" ]]; then
    echo "SKIP (Release에 없음): $relpath"
    continue
  fi
  exp_h="$(awk '{print $1}' <<<"$info")"
  exp_s="$(awk '{print $2}' <<<"$info")"

  fetch_byhash_to "$relpath" "$exp_h"
  verify_exact "$relpath" "$exp_h" "$exp_s" || die "검증 실패: $relpath"

  # 평문 생성: Packages / Translation-* / Components-*.yml
  case "$relpath" in
    */Packages.xz) plain="$STAGE/dists/$DIST/$(dirname "$relpath")/Packages"; [[ -s "$plain" ]] || xz   -dc "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
    */Packages.gz) plain="$STAGE/dists/$DIST/$(dirname "$relpath")/Packages"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
    */i18n/Translation-*.xz) plain="${STAGE}/dists/${DIST}/${relpath%.xz}"; [[ -s "$plain" ]] || xz   -dc "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
    */i18n/Translation-*.gz) plain="${STAGE}/dists/${DIST}/${relpath%.gz}"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
    */dep11/Components-*.yml.gz) plain="${STAGE}/dists/${DIST}/${relpath%.gz}"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
  esac
done

# 4) (옵션) dep11 아이콘
echo "[STEP4] dep11 icons (optional)"
for comp in "${COMPS[@]}"; do
  base="http://$HOST/ubuntu/dists/$DIST/$comp/dep11"
  dst="$STAGE/dists/$DIST/$comp/dep11"
  mkdir -p "$dst"
  for s in 48 64 128; do
    for suf in "" "@2"; do
      wget -q -c -O "$dst/icons-${s}x${s}${suf}.tar.gz" "$base/icons-${s}x${s}${suf}.tar.gz" || true
    done
  done
done

# 5) 원자적 교체
echo "[STEP5] publish atomically"
PUB="$BASE_DIR/dists/$DIST"
TS="$(date +%Y%m%d%H%M%S)"
sudo mkdir -p "$(dirname "$PUB")"
[[ -e "$PUB" && ! -L "$PUB" ]] && sudo mv "$PUB" "${PUB}.bak.${TS}" || true
sudo rm -rf "$PUB"
sudo mv "$STAGE/dists/$DIST" "$PUB"

echo "[DONE] published to $PUB"
echo "클라이언트에서 갱신:"
echo "  sudo rm -rf /var/lib/apt/lists/*"
echo "  sudo apt-get clean"
echo "  sudo apt-get update -o Acquire::By-Hash=true"