#!/usr/bin/env bash # QMAdmin — static SPA behind nginx: native (system nginx) or Docker. # Releases: https://github.com/mindevis/QMAdmin/releases # # Usage: # curl -fsSL https://raw.githubusercontent.com/mindevis/QMAdmin/main/install.sh | sudo bash -s -- --native # curl -fsSL https://raw.githubusercontent.com/mindevis/QMAdmin/main/install.sh | bash -s -- --docker # # Optional env: # QMADMIN_RELEASE_TAG — exact tag (e.g. v1.0.0-abc1234); default: newest release with qmadmin-*-dist.tar.gz # QMADMIN_GITHUB_REPO — owner/repo (default: mindevis/QMAdmin) # QMADMIN_GHCR_IMAGE — image for --docker (default: ghcr.io/mindevis/qmadmin) # GITHUB_TOKEN / GH_TOKEN — private repos # DOCKER_INSTALL_DIR — compose directory for --docker (default: $HOME/qmadmin) set -euo pipefail GITHUB_REPO_DEFAULT="mindevis/QMAdmin" GHCR_IMAGE_DEFAULT="ghcr.io/mindevis/qmadmin" COMPOSE=() die() { printf '%s\n' "install.sh: $*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "command not found: $1 (install it and retry)" } usage() { cat <tagsha256_asset_url (last may be empty). resolve_release_dist() { local repo="${QMADMIN_GITHUB_REPO:-$GITHUB_REPO_DEFAULT}" local want_tag="${QMADMIN_RELEASE_TAG:-}" need_cmd python3 local json="" local err_out if [[ -n "$want_tag" ]]; then err_out="$(mktemp)" if ! json="$(curl_github_json "https://api.github.com/repos/${repo}/releases/tags/${want_tag}" 2>"$err_out")"; then rm -f "$err_out" json="" else rm -f "$err_out" fi fi if [[ -z "$json" ]] || echo "$json" | grep -q '"message"[[:space:]]*:[[:space:]]*"Not Found"'; then err_out="$(mktemp)" if ! json="$(curl_github_json "https://api.github.com/repos/${repo}/releases/latest" 2>"$err_out")"; then rm -f "$err_out" json="" else rm -f "$err_out" fi fi if [[ -z "$json" ]] || echo "$json" | grep -q '"message"[[:space:]]*:[[:space:]]*"Not Found"'; then json="$(curl_github_json "https://api.github.com/repos/${repo}/releases?per_page=25")" \ || die "failed to list releases (private repo? set GITHUB_TOKEN)" fi echo "$json" | python3 -c " import json, sys raw = sys.stdin.read() try: data = json.loads(raw) except json.JSONDecodeError: print('invalid JSON from GitHub API', file=sys.stderr) sys.exit(1) releases = data if isinstance(data, list) else [data] for rel in releases: if not isinstance(rel, dict) or rel.get('draft'): continue tag_name = rel.get('tag_name') or '' assets = rel.get('assets') or [] pick = None for a in assets: name = a.get('name') or '' if name.startswith('qmadmin-') and name.endswith('-dist.tar.gz'): pick = a break if not pick: continue url = pick.get('browser_download_url') or '' if not url: continue tgz_name = pick['name'] sha_url = '' for a in assets: n = a.get('name') or '' if n == tgz_name + '.sha256' or (n.endswith('.sha256') and n.replace('.sha256', '') == tgz_name): sha_url = a.get('browser_download_url') or '' break print(url + '\t' + tag_name + '\t' + sha_url) raise SystemExit(0) print('no qmadmin-*-dist.tar.gz found in releases', file=sys.stderr) sys.exit(1) " } verify_sha256_asset() { local tarball=$1 local sha_url=$2 [[ -n "$sha_url" ]] || return 0 local dest_dir base dest_dir="$(dirname "$tarball")" base="$(basename "$tarball")" curl -fsSL -o "${dest_dir}/${base}.sha256" "$sha_url" (cd "$dest_dir" && sha256sum -c "${base}.sha256") || die "SHA256 check failed for ${base}" } check_docker_tooling() { need_cmd curl need_cmd docker if docker compose version >/dev/null 2>&1; then COMPOSE=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then COMPOSE=(docker-compose) else die "install docker compose (plugin: 'docker compose' or standalone 'docker-compose')" fi docker info >/dev/null 2>&1 || die "Docker daemon not reachable; start Docker and retry" } cmd_native() { [[ "$(id -u)" -eq 0 ]] || die "--native must run as root (e.g. curl ... | sudo bash -s -- --native)" check_platform need_cmd curl need_cmd tar command -v nginx >/dev/null 2>&1 || die "nginx not found; install nginx first (e.g. apt install nginx)" local repo="${QMADMIN_GITHUB_REPO:-$GITHUB_REPO_DEFAULT}" local resolved url tag sha_asset rest resolved="$(resolve_release_dist)" || die "could not resolve a release dist tarball" url="${resolved%%$'\t'*}" rest="${resolved#*$'\t'}" tag="${rest%%$'\t'*}" sha_asset="${rest#*$'\t'}" [[ -n "$url" && -n "$tag" ]] || die "empty URL or tag from GitHub API" local tmp tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT printf '%s\n' "Downloading ${tag} ..." curl -fsSL -o "${tmp}/qmadmin-dist.tgz" "$url" verify_sha256_asset "${tmp}/qmadmin-dist.tgz" "$sha_asset" install -d /opt/qmadmin/html tar -xzf "${tmp}/qmadmin-dist.tgz" -C /opt/qmadmin/html local raw_base="https://raw.githubusercontent.com/${repo}/${tag}" if ! curl -fsSL -o "${tmp}/qmadmin-site.conf" "${raw_base}/deploy/nginx/qmadmin-site.conf"; then printf '%s\n' "warning: nginx snippet not at tag ${tag}; using main branch" >&2 curl -fsSL -o "${tmp}/qmadmin-site.conf" "https://raw.githubusercontent.com/${repo}/main/deploy/nginx/qmadmin-site.conf" fi if [[ -d /etc/nginx/sites-available ]]; then install -m 0644 "${tmp}/qmadmin-site.conf" /etc/nginx/sites-available/qmadmin ln -sf ../sites-available/qmadmin /etc/nginx/sites-enabled/qmadmin 2>/dev/null || true elif [[ -d /etc/nginx/conf.d ]]; then install -m 0644 "${tmp}/qmadmin-site.conf" /etc/nginx/conf.d/qmadmin.conf else die "unsupported nginx layout (expected sites-available or conf.d)" fi nginx -t systemctl reload nginx 2>/dev/null || nginx -s reload cat <<'EOF' Native install done. Static files: /opt/qmadmin/html Adjust server_name / TLS in the nginx site file as needed. nginx -t && systemctl reload nginx EOF } cmd_docker() { check_platform check_docker_tooling local dir="${DOCKER_INSTALL_DIR:-$HOME/qmadmin}" local ghcr="${QMADMIN_GHCR_IMAGE:-$GHCR_IMAGE_DEFAULT}" install -d "$dir" cd "$dir" cat > docker-compose.yml <