218 lines
6.7 KiB
Bash
Executable File
218 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# -------- configurable defaults --------
|
|
REPO_DIR="${REPO_DIR:-/home/hubdeb.qortal.org/public_html}"
|
|
DEB_NAME="${DEB_NAME:-Qortal-Hub-Setup.deb}"
|
|
DEB_SUBDIR="${DEB_SUBDIR:-deb-packages}"
|
|
DEB_URL="${DEB_URL:-https://github.com/Qortal/Qortal-Hub/releases/latest/download/Qortal-Hub-Setup.deb}"
|
|
|
|
# Use your primary key fingerprint (recommended) or specific signing subkey id.
|
|
# From your output: primary fpr = 20C64216BB5C080569F0F6BA2B4015FB935F5F2A
|
|
SIGNING_KEY="${SIGNING_KEY:-20C64216BB5C080569F0F6BA2B4015FB935F5F2A}"
|
|
|
|
# Optionally override auto owner/group after publish. If empty, owner+group are inferred
|
|
# from the directory above REPO_DIR and used as owner:group.
|
|
CHOWN_TO="${CHOWN_TO:-}"
|
|
|
|
# -------- helpers --------
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
|
|
need_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
|
}
|
|
|
|
require_root() {
|
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
|
die "This script must be run as root. Re-run with sudo."
|
|
fi
|
|
}
|
|
|
|
sha256_file() {
|
|
sha256sum "$1" | awk '{print $1}'
|
|
}
|
|
|
|
# -------- usage --------
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
$0
|
|
$0 /path/to/Qortal-Hub-Setup.deb
|
|
|
|
Environment overrides:
|
|
REPO_DIR=... (default: $REPO_DIR)
|
|
DEB_NAME=... (default: $DEB_NAME)
|
|
DEB_SUBDIR=... (default: $DEB_SUBDIR)
|
|
DEB_URL=... (default: $DEB_URL)
|
|
SIGNING_KEY=... (default: $SIGNING_KEY)
|
|
CHOWN_TO=... (optional override; default auto owner:group)
|
|
|
|
Example:
|
|
SIGNING_KEY=20C64216BB5C080569F0F6BA2B4015FB935F5F2A REPO_DIR=/home/hubdeb.qortal.org/public_html $0
|
|
EOF
|
|
}
|
|
|
|
# -------- main --------
|
|
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { usage; exit 0; }
|
|
[[ $# -gt 1 ]] && { usage; exit 2; }
|
|
require_root
|
|
|
|
SRC_DEB="${1:-}"
|
|
TMP_DEB_DIR=""
|
|
STAGE_DIR=""
|
|
BACKUP_DIR=""
|
|
PUBLISH_STARTED=0
|
|
PUBLISH_COMPLETED=0
|
|
|
|
need_cmd dpkg-deb
|
|
need_cmd dpkg-scanpackages
|
|
need_cmd apt-ftparchive
|
|
need_cmd gpg
|
|
need_cmd mktemp
|
|
need_cmd rsync
|
|
need_cmd sha256sum
|
|
need_cmd gzip
|
|
need_cmd curl
|
|
need_cmd stat
|
|
need_cmd install
|
|
need_cmd chown
|
|
need_cmd dirname
|
|
|
|
rollback_publish() {
|
|
if [[ "$PUBLISH_STARTED" -eq 1 && "$PUBLISH_COMPLETED" -eq 0 && -n "${BACKUP_DIR:-}" && -d "$BACKUP_DIR" ]]; then
|
|
echo "Publish failed; attempting rollback..." >&2
|
|
for p in "$DEB_SUBDIR" Packages Packages.gz Release Release.gpg InRelease; do
|
|
rm -rf "$REPO_DIR/$p"
|
|
if [[ -e "$BACKUP_DIR/$p" ]]; then
|
|
mv "$BACKUP_DIR/$p" "$REPO_DIR/$p"
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
cleanup() {
|
|
rollback_publish
|
|
rm -rf "${TMP_DEB_DIR:-}" "${STAGE_DIR:-}"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Validate repo dirs
|
|
[[ -d "$REPO_DIR" ]] || die "REPO_DIR not found: $REPO_DIR"
|
|
[[ -d "$REPO_DIR/$DEB_SUBDIR" ]] || die "Repo deb dir not found: $REPO_DIR/$DEB_SUBDIR"
|
|
|
|
# Determine ownership target from the directory above REPO_DIR unless overridden.
|
|
if [[ -z "$CHOWN_TO" ]]; then
|
|
REPO_PARENT="$(dirname "$REPO_DIR")"
|
|
[[ -d "$REPO_PARENT" ]] || die "Parent directory not found: $REPO_PARENT"
|
|
OWNER_NAME="$(stat -c '%U' "$REPO_PARENT")"
|
|
GROUP_NAME="$(stat -c '%G' "$REPO_PARENT")"
|
|
[[ -n "$OWNER_NAME" && "$OWNER_NAME" != "UNKNOWN" ]] || die "Could not determine owner for: $REPO_PARENT"
|
|
[[ -n "$GROUP_NAME" && "$GROUP_NAME" != "UNKNOWN" ]] || die "Could not determine group for: $REPO_PARENT"
|
|
CHOWN_TO="${OWNER_NAME}:${GROUP_NAME}"
|
|
fi
|
|
|
|
# Ensure signing key exists (public + secret)
|
|
gpg --list-secret-keys --keyid-format LONG "$SIGNING_KEY" >/dev/null 2>&1 \
|
|
|| die "Signing secret key not available in this user's keyring: $SIGNING_KEY"
|
|
|
|
# Resolve source deb:
|
|
# - If arg provided, use it.
|
|
# - Otherwise download latest release artifact into temp file.
|
|
if [[ -n "$SRC_DEB" ]]; then
|
|
[[ -f "$SRC_DEB" ]] || die "Deb not found: $SRC_DEB"
|
|
[[ "$(basename "$SRC_DEB")" == "$DEB_NAME" ]] || die "Deb must be named exactly: $DEB_NAME (got: $(basename "$SRC_DEB"))"
|
|
else
|
|
TMP_DEB_DIR="$(mktemp -d)"
|
|
SRC_DEB="$TMP_DEB_DIR/$DEB_NAME"
|
|
echo "Downloading latest $DEB_NAME from:"
|
|
echo " $DEB_URL"
|
|
curl -fL --retry 3 --connect-timeout 20 -o "$SRC_DEB" "$DEB_URL"
|
|
fi
|
|
|
|
# Validate deb integrity
|
|
dpkg-deb --info "$SRC_DEB" >/dev/null
|
|
|
|
cd "$REPO_DIR"
|
|
|
|
# Create a staging directory on the same filesystem for atomic swaps
|
|
STAGE_DIR="$(mktemp -d "$REPO_DIR/.stage.XXXXXX")"
|
|
|
|
# Copy current repo metadata into stage (not strictly required, but helps for atomic swap approach)
|
|
# We'll write new metadata into STAGE_DIR then move into place.
|
|
mkdir -p "$STAGE_DIR/$DEB_SUBDIR"
|
|
|
|
# Copy all .deb files from current repo into stage, then overwrite with new one
|
|
# This keeps any other debs you may have, while replacing Qortal-Hub-Setup.deb.
|
|
rsync -a --delete "$DEB_SUBDIR/" "$STAGE_DIR/$DEB_SUBDIR/"
|
|
|
|
# Compute old hash if existing, for logging
|
|
OLD_HASH=""
|
|
if [[ -f "$DEB_SUBDIR/$DEB_NAME" ]]; then
|
|
OLD_HASH="$(sha256_file "$DEB_SUBDIR/$DEB_NAME" || true)"
|
|
fi
|
|
|
|
# Put new .deb into stage
|
|
install -m 0644 "$SRC_DEB" "$STAGE_DIR/$DEB_SUBDIR/$DEB_NAME"
|
|
|
|
NEW_HASH="$(sha256_file "$STAGE_DIR/$DEB_SUBDIR/$DEB_NAME")"
|
|
|
|
# If identical content, you can optionally no-op.
|
|
if [[ -n "$OLD_HASH" && "$OLD_HASH" == "$NEW_HASH" ]]; then
|
|
echo "No changes: $DEB_NAME is identical (sha256: $NEW_HASH). Still rebuilding metadata to be safe..."
|
|
fi
|
|
|
|
# Build Packages + Packages.gz inside stage.
|
|
# Run from STAGE_DIR so package filenames stay repo-relative (deb-packages/...).
|
|
(
|
|
cd "$STAGE_DIR"
|
|
dpkg-scanpackages -m "$DEB_SUBDIR" /dev/null > Packages
|
|
gzip -9c Packages > Packages.gz
|
|
)
|
|
|
|
# Build Release inside stage
|
|
(
|
|
cd "$STAGE_DIR"
|
|
apt-ftparchive release . > Release
|
|
|
|
# Sign Release.gpg + InRelease
|
|
gpg --batch --yes --default-key "$SIGNING_KEY" --armor --detach-sign --output Release.gpg Release
|
|
gpg --batch --yes --default-key "$SIGNING_KEY" --clearsign --output InRelease Release
|
|
)
|
|
|
|
# Atomic publish:
|
|
# 1) Move current metadata+deb dir aside (fast)
|
|
# 2) Move staged into place (fast)
|
|
# 3) Remove old backup
|
|
BACKUP_DIR="$REPO_DIR/.backup.$(date -u +%Y%m%dT%H%M%SZ)"
|
|
mkdir -p "$BACKUP_DIR"
|
|
PUBLISH_STARTED=1
|
|
|
|
# Move current repo files that we manage into backup
|
|
for p in "$DEB_SUBDIR" Packages Packages.gz Release Release.gpg InRelease; do
|
|
if [[ -e "$REPO_DIR/$p" ]]; then
|
|
mv "$REPO_DIR/$p" "$BACKUP_DIR/"
|
|
fi
|
|
done
|
|
|
|
# Move stage into place
|
|
mv "$STAGE_DIR/$DEB_SUBDIR" "$REPO_DIR/$DEB_SUBDIR"
|
|
mv "$STAGE_DIR/Packages" "$REPO_DIR/Packages"
|
|
mv "$STAGE_DIR/Packages.gz" "$REPO_DIR/Packages.gz"
|
|
mv "$STAGE_DIR/Release" "$REPO_DIR/Release"
|
|
mv "$STAGE_DIR/Release.gpg" "$REPO_DIR/Release.gpg"
|
|
mv "$STAGE_DIR/InRelease" "$REPO_DIR/InRelease"
|
|
PUBLISH_COMPLETED=1
|
|
|
|
# If you want to keep the backup, comment the next line
|
|
rm -rf "$BACKUP_DIR"
|
|
BACKUP_DIR=""
|
|
|
|
# Ownership fixup on final repo path
|
|
chown -R "$CHOWN_TO" "$REPO_DIR"
|
|
|
|
echo "Published $DEB_NAME"
|
|
echo " sha256: $NEW_HASH"
|
|
echo " repo: $REPO_DIR"
|
|
echo " owner: $CHOWN_TO"
|
|
echo " signed: $(date -u)"
|