#!/usr/bin/env bash # QMServer — native (systemd) or Docker install. # Releases: https://github.com/mindevis/QMServer/releases # # Usage: # curl -fsSL https://raw.githubusercontent.com/mindevis/QMServer/main/install.sh | sudo bash -s -- --native # curl -fsSL https://raw.githubusercontent.com/mindevis/QMServer/main/install.sh | bash -s -- --docker # # Optional env: # QMSERVER_RELEASE_TAG — exact tag (e.g. v1.0.0); default: newest release with qmserver-*-linux-amd64*.tar.gz # QMSERVER_GITHUB_REPO — owner/repo (default: mindevis/QMServer) # QMSERVER_GHCR_IMAGE — image for --docker (default: ghcr.io/mindevis/qmserver) # GITHUB_TOKEN / GH_TOKEN — private repos (needs read access to releases / packages) # DOCKER_INSTALL_DIR — compose directory for --docker (default: $HOME/qmserver-cloud) set -euo pipefail GITHUB_REPO_DEFAULT="mindevis/QMServer" GHCR_IMAGE_DEFAULT="ghcr.io/mindevis/qmserver" 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). Requires python3. resolve_release_tarball() { local repo="${QMSERVER_GITHUB_REPO:-$GITHUB_REPO_DEFAULT}" local want_tag="${QMSERVER_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.endswith('.tar.gz') and 'linux-amd64' in name: 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 qmserver-*-linux-amd64*.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 local repo="${QMSERVER_GITHUB_REPO:-$GITHUB_REPO_DEFAULT}" local resolved url tag sha_asset rest resolved="$(resolve_release_tarball)" || die "could not resolve a release 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}/qmserver.tgz" "$url" verify_sha256_asset "${tmp}/qmserver.tgz" "$sha_asset" tar -xzf "${tmp}/qmserver.tgz" -C "$tmp" [[ -f "${tmp}/qmserver" ]] || die "archive must contain top-level file: qmserver" install -d /opt/qmserver /opt/qmserver/data install -m 0755 "${tmp}/qmserver" /opt/qmserver/qmserver local raw_base="https://raw.githubusercontent.com/${repo}/${tag}" if ! curl -fsSL -o "${tmp}/qmserver.service" "${raw_base}/deploy/systemd/qmserver.service"; then printf '%s\n' "warning: unit not at tag ${tag}; using main branch" >&2 curl -fsSL -o "${tmp}/qmserver.service" "https://raw.githubusercontent.com/${repo}/main/deploy/systemd/qmserver.service" fi install -m 0644 "${tmp}/qmserver.service" /etc/systemd/system/qmserver.service if [[ ! -f /opt/qmserver/qmserver.conf ]]; then if ! curl -fsSL -o /opt/qmserver/qmserver.conf "${raw_base}/deploy/systemd/qmserver.conf.example"; then curl -fsSL -o /opt/qmserver/qmserver.conf "https://raw.githubusercontent.com/${repo}/main/deploy/systemd/qmserver.conf.example" fi chmod 0600 /opt/qmserver/qmserver.conf printf '%s\n' "Edit /opt/qmserver/qmserver.conf (PORT, DB_DSN, APP_NAME, secrets), then: systemctl restart qmserver" else printf '%s\n' "Left existing /opt/qmserver/qmserver.conf unchanged" fi ln -sf /opt/qmserver/qmserver /usr/local/bin/qmserver systemctl daemon-reload systemctl enable qmserver.service systemctl restart qmserver.service || true cat <<'EOF' Native install done. journalctl -u qmserver -f systemctl status qmserver EOF } cmd_docker() { check_platform check_docker_tooling local dir="${DOCKER_INSTALL_DIR:-$HOME/qmserver-cloud}" local repo="${QMSERVER_GITHUB_REPO:-$GITHUB_REPO_DEFAULT}" local ghcr="${QMSERVER_GHCR_IMAGE:-$GHCR_IMAGE_DEFAULT}" install -d "$dir" cd "$dir" if [[ ! -f .env ]]; then curl -fsSL -o .env "https://raw.githubusercontent.com/${repo}/main/deploy/systemd/qmserver.conf.example" chmod 0600 .env fi if [[ -f .env ]] && ! grep -q '^QMSERVER_DATA_PATH=' .env; then printf '\n# Added by install.sh (Compose volume)\nQMSERVER_DATA_PATH=/app/data\n' >> .env fi cat > docker-compose.yml <