#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' MAIN_URL="https://cloud.qortal.org/s/devnet_download/download/qortal-DevNet-MAIN.7z" DB_URL="https://cloud.qortal.org/s/QortalDevNetDatabaseLatest/download/db-DevNet-LATEST.7z" IP_SCRIPT_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/add-public-ip-to-settings-mac.sh" DEFAULT_SETTINGS_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/default-settings.json" START_VALIDATION_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/start-mac.sh" QORT_SCRIPT_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/qort" DEPENDENCIES_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/setup-dependencies-mac.sh" STOP_SCRIPT_URL="https://gitea.qortal.link/crowetic/qortal-DevNet-scripts/raw/branch/main/stop-mac.sh" PUBLISH_SHARE_TOKEN="PublishDevNetIPsHere" PUBLISH_WEBDAV_URL="https://cloud.qortal.org/public.php/webdav" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" HOME_DIR="${HOME}" BASE_DIR="${HOME_DIR}" DEVNET_DIR="" STATE_FILE="" DOWNLOAD_DIR="" MAIN_ARCHIVE="" DB_ARCHIVE="" DEFAULT_SETTINGS_LOCAL="" IP_SCRIPT_LOCAL="" START_VALIDATION_LOCAL="" QORT_SCRIPT_LOCAL="" DEPENDENCIES_LOCAL="" STOP_SCRIPT_LOCAL="" PORT_START_DEFAULT=23391 PORT_START="" LISTEN_PORT="" log() { echo "[$(date +'%F %T')] $*"; } die() { log "ERROR: $*"; exit 1; } need_cmd() { command -v "$1" >/dev/null 2>&1; } ensure_dependencies() { local missing=() if ! need_cmd 7z; then missing+=("7z") fi if ! need_cmd python3; then missing+=("python3") fi if ! need_cmd curl && ! need_cmd wget; then missing+=("curl/wget") fi if ! need_cmd jq; then missing+=("jq") fi if ! need_cmd java; then missing+=("java") fi if [ "${#missing[@]}" -eq 0 ]; then return 0 fi log "Missing dependencies detected: ${missing[*]}" mkdir -p "$DOWNLOAD_DIR" if [ -x "${SCRIPT_DIR}/setup-dependencies-mac.sh" ]; then DEP_SCRIPT="${SCRIPT_DIR}/setup-dependencies-mac.sh" else DEP_SCRIPT="${DEPENDENCIES_LOCAL}" if need_cmd curl; then curl -fL --retry 3 --retry-delay 5 -o "$DEP_SCRIPT" "$DEPENDENCIES_URL" elif need_cmd wget; then wget -qO "$DEP_SCRIPT" "$DEPENDENCIES_URL" else die "Neither curl nor wget found; cannot download setup-dependencies-mac.sh." fi fi chmod +x "$DEP_SCRIPT" "$DEP_SCRIPT" if ! need_cmd 7z || ! need_cmd python3 || ( ! need_cmd curl && ! need_cmd wget ) || ! need_cmd jq || ! need_cmd java; then die "Dependencies are still missing after setup. Please install manually." fi } read -r -t 20 -p "Use default base path ${HOME_DIR}? [Y/n] (auto-continue in 20s): " USE_DEFAULT_BASE || USE_DEFAULT_BASE="" case "$USE_DEFAULT_BASE" in n|N|no|NO) read -r -t 20 -p "Enter the base path to install into (auto-continue in 20s): " BASE_DIR_INPUT || BASE_DIR_INPUT="" if [[ -z "${BASE_DIR_INPUT}" ]]; then die "Base path cannot be empty." fi BASE_DIR="${BASE_DIR_INPUT}" ;; *) BASE_DIR="${HOME_DIR}" ;; esac DEVNET_DIR="${BASE_DIR}/qortal-DevNet" STATE_FILE="${BASE_DIR}/qortal-DevNet.setup.state" DOWNLOAD_DIR="${BASE_DIR}/qortal-DevNet-downloads" MAIN_ARCHIVE="${DOWNLOAD_DIR}/qortal-DevNet-MAIN.7z" DB_ARCHIVE="${DOWNLOAD_DIR}/db-DevNet-LATEST.7z" DEFAULT_SETTINGS_LOCAL="${DOWNLOAD_DIR}/default-settings.json" IP_SCRIPT_LOCAL="${DOWNLOAD_DIR}/add-public-ip-to-settings-mac.sh" START_VALIDATION_LOCAL="${DOWNLOAD_DIR}/start-mac.sh" QORT_SCRIPT_LOCAL="${DOWNLOAD_DIR}/qort" DEPENDENCIES_LOCAL="${DOWNLOAD_DIR}/setup-dependencies-mac.sh" STOP_SCRIPT_LOCAL="${DOWNLOAD_DIR}/stop-mac.sh" ensure_dependencies read -r -t 20 -p "Use default port range ${PORT_START_DEFAULT}-$((${PORT_START_DEFAULT} + 3))? [Y/n] (auto-continue in 20s): " USE_DEFAULT_PORTS || USE_DEFAULT_PORTS="" case "$USE_DEFAULT_PORTS" in n|N|no|NO) read -r -t 20 -p "Enter the starting port for the range (auto-continue in 20s): " PORT_START_INPUT || PORT_START_INPUT="" if [[ -z "${PORT_START_INPUT}" ]]; then die "Port start cannot be empty." fi if ! [[ "$PORT_START_INPUT" =~ ^[0-9]+$ ]]; then die "Port start must be a number." fi PORT_START="$PORT_START_INPUT" ;; *) PORT_START="$PORT_START_DEFAULT" ;; esac if (( PORT_START < 1024 || PORT_START > 65532 )); then die "Port start must be between 1024 and 65532." fi LISTEN_PORT=$((PORT_START + 1)) step_done() { grep -Fxq "$1" "$STATE_FILE" 2>/dev/null } mark_done() { printf '%s\n' "$1" >> "$STATE_FILE" } download_file() { local url="$1" local dest="$2" if need_cmd curl; then curl -fL --retry 3 --retry-delay 5 -o "$dest" "$url" elif need_cmd wget; then wget -qO "$dest" "$url" else die "Neither curl nor wget found. Please install one before running." fi } ensure_file() { local url="$1" local dest="$2" if [[ -s "$dest" ]]; then log "Using existing $(basename "$dest")" else download_file "$url" "$dest" fi } port_in_use() { local port="$1" if need_cmd lsof; then lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 elif need_cmd netstat; then netstat -an | awk '{print $4}' | grep -Eq "[\.:]${port}\$" else return 2 fi } PORT_RANGE_IN_USE=false PORT_CHECK_UNKNOWN=false for p in "$PORT_START" "$((PORT_START + 1))" "$((PORT_START + 2))" "$((PORT_START + 3))"; do if port_in_use "$p"; then PORT_RANGE_IN_USE=true break elif [[ $? -eq 2 ]]; then PORT_CHECK_UNKNOWN=true fi done if [[ "$PORT_RANGE_IN_USE" = true ]]; then log "Port range ${PORT_START}-$((${PORT_START} + 3)) appears in use; shifting by 10000" PORT_START=$((PORT_START + 10000)) if (( PORT_START > 65532 )); then die "Port range exceeds 65535 after shifting." fi LISTEN_PORT=$((PORT_START + 1)) elif [[ "$PORT_CHECK_UNKNOWN" = true ]]; then log "Warning: unable to detect if ports are in use; continuing with selected range." fi get_public_ip() { local ip="" local url="" local urls="https://api.ipify.org https://ipv4.icanhazip.com https://checkip.amazonaws.com https://ifconfig.me https://canhazip.com" local curl_opts="-fsS --max-time 8 -A Mozilla/5.0" for url in $urls; do if need_cmd curl; then ip="$(curl $curl_opts "$url" 2>/dev/null || true)" elif need_cmd wget; then ip="$(wget -qO- "$url" || true)" else return 1 fi ip="$(echo "$ip" | tr -d ' \n\r')" if echo "$ip" | grep -Eq '^[0-9]{1,3}(\.[0-9]{1,3}){3}$'; then echo "$ip" return 0 fi done if need_cmd dig; then ip="$(dig +short myip.opendns.com @resolver1.opendns.com | head -n 1 | tr -d ' \n\r')" if echo "$ip" | grep -Eq '^[0-9]{1,3}(\.[0-9]{1,3}){3}$'; then echo "$ip" return 0 fi fi return 1 } publish_public_ip() { local ip="$1" local port="$2" local host_name="$3" local tmp_file="$4" local remote_name="${host_name}.txt" local url="${PUBLISH_WEBDAV_URL}/${remote_name}" local alt_name="" local alt_url="" local http_code="" printf '%s:%s\n' "$ip" "$port" > "$tmp_file" if need_cmd curl; then http_code="$(curl -sS -u "${PUBLISH_SHARE_TOKEN}:" -T "$tmp_file" -o /dev/null -w "%{http_code}" "$url" || true)" if [[ "$http_code" != "200" && "$http_code" != "201" && "$http_code" != "204" ]]; then alt_name="${host_name}-$(date -u +'%Y%m%d_%H%M%S').txt" alt_url="${PUBLISH_WEBDAV_URL}/${alt_name}" http_code="$(curl -sS -u "${PUBLISH_SHARE_TOKEN}:" -T "$tmp_file" -o /dev/null -w "%{http_code}" "$alt_url" || true)" if [[ "$http_code" != "200" && "$http_code" != "201" && "$http_code" != "204" ]]; then return 1 fi fi elif need_cmd wget; then wget --method=PUT --body-file="$tmp_file" --user="$PUBLISH_SHARE_TOKEN" --password="" -qO- "$url" >/dev/null else return 1 fi } if ! need_cmd 7z; then die "7z not found. Please install p7zip/7z before running." fi log "Step 1/7: Download files..." mkdir -p "$DOWNLOAD_DIR" ensure_file "$MAIN_URL" "$MAIN_ARCHIVE" ensure_file "$DB_URL" "$DB_ARCHIVE" ensure_file "$DEFAULT_SETTINGS_URL" "$DEFAULT_SETTINGS_LOCAL" ensure_file "$IP_SCRIPT_URL" "$IP_SCRIPT_LOCAL" ensure_file "$START_VALIDATION_URL" "$START_VALIDATION_LOCAL" ensure_file "$QORT_SCRIPT_URL" "$QORT_SCRIPT_LOCAL" ensure_file "$STOP_SCRIPT_URL" "$STOP_SCRIPT_LOCAL" chmod +x "$IP_SCRIPT_LOCAL" "$START_VALIDATION_LOCAL" "$STOP_SCRIPT_LOCAL" "$QORT_SCRIPT_LOCAL" if ! step_done "01-download-files"; then mark_done "01-download-files" fi if ! step_done "02-prepare-devnet-dir"; then log "Step 2/7: Prepare devnet directory..." if [[ -d "$DEVNET_DIR" ]]; then BACKUP_DIR="${HOME_DIR}/backup-qortal-DevNet" if [[ -e "$BACKUP_DIR" ]]; then BACKUP_DIR="${BACKUP_DIR}-$(date +%Y%m%d_%H%M%S)" fi log "Existing ${DEVNET_DIR} found; moving to ${BACKUP_DIR}" mv "$DEVNET_DIR" "$BACKUP_DIR" fi log "Creating ${DEVNET_DIR}" mkdir -p "$DEVNET_DIR" mark_done "02-prepare-devnet-dir" fi if ! step_done "03-extract-main"; then log "Step 3/7: Extract devnet main archive..." 7z x "$MAIN_ARCHIVE" -o"$DEVNET_DIR" >/dev/null if [[ -d "${DEVNET_DIR}/temp" ]]; then log "Normalizing devnet main files from temp/ into ${DEVNET_DIR}" rsync -a "${DEVNET_DIR}/temp/" "${DEVNET_DIR}/" rm -rf "${DEVNET_DIR}/temp" fi mark_done "03-extract-main" fi if ! step_done "04-extract-db"; then log "Step 4/7: Extract devnet DB archive..." if [[ -d "${DEVNET_DIR}/db" ]]; then DB_BACKUP="${DEVNET_DIR}/db.bak-$(date +%Y%m%d_%H%M%S)" log "Existing db folder found; moving to ${DB_BACKUP}" mv "${DEVNET_DIR}/db" "$DB_BACKUP" fi 7z x "$DB_ARCHIVE" -o"$DEVNET_DIR" >/dev/null if [[ ! -d "${DEVNET_DIR}/db" ]]; then die "Expected db/ folder in devnet DB archive, but it was not found." fi mark_done "04-extract-db" fi if ! step_done "05-configure-settings"; then log "Step 5/7: Configure settings..." SETTINGS_PATH="${DEVNET_DIR}/settings.json" if [[ -f "$SETTINGS_PATH" ]]; then SETTINGS_BACKUP="${DEVNET_DIR}/backup-settings.json" if [[ -e "$SETTINGS_BACKUP" ]]; then SETTINGS_BACKUP="${SETTINGS_BACKUP}-$(date +%Y%m%d_%H%M%S)" fi log "Existing settings.json found; moving to ${SETTINGS_BACKUP}" mv "$SETTINGS_PATH" "$SETTINGS_BACKUP" fi cp -f "$DEFAULT_SETTINGS_LOCAL" "$SETTINGS_PATH" IP_SCRIPT_PATH="${DEVNET_DIR}/add-public-ip-to-settings.sh" cp -f "$IP_SCRIPT_LOCAL" "$IP_SCRIPT_PATH" chmod +x "$IP_SCRIPT_PATH" log "Running public IP script..." ( cd "$DEVNET_DIR" && QORTAL_PORT_START="$PORT_START" "$IP_SCRIPT_PATH" ) mark_done "05-configure-settings" fi if ! step_done "06-publish-public-ip"; then log "Step 6/7: Publish public IP..." HOST_NAME="$(hostname -s 2>/dev/null || hostname)" PUBLISH_TMP="${DOWNLOAD_DIR}/devnet-public-ip.txt" PUBLIC_IP="$(get_public_ip || true)" if [[ -n "$PUBLIC_IP" ]]; then if publish_public_ip "$PUBLIC_IP" "$LISTEN_PORT" "$HOST_NAME" "$PUBLISH_TMP"; then log "Published ${PUBLIC_IP}:${LISTEN_PORT} for ${HOST_NAME}" else log "Warning: failed to publish public IP to Nextcloud share." fi else log "Warning: could not determine public IP to publish." fi mark_done "06-publish-public-ip" fi if ! step_done "07-install-start-script"; then log "Step 7/7: Install start/stop scripts..." START_VALIDATION_PATH="${DEVNET_DIR}/start-mac.sh" cp -f "$START_VALIDATION_LOCAL" "$START_VALIDATION_PATH" chmod +x "$START_VALIDATION_PATH" log "Replacing start.sh with start-mac.sh" if [[ -f "${DEVNET_DIR}/start.sh" ]]; then START_BACKUP="${DEVNET_DIR}/start.sh.bak-$(date +%Y%m%d_%H%M%S)" mv "${DEVNET_DIR}/start.sh" "$START_BACKUP" log "Backed up existing start.sh to ${START_BACKUP}" fi cp -f "$START_VALIDATION_PATH" "${DEVNET_DIR}/start.sh" if [[ -f "${DEVNET_DIR}/stop-mac.sh" ]]; then STOP_BACKUP="${DEVNET_DIR}/stop-mac.sh.bak-$(date +%Y%m%d_%H%M%S)" mv "${DEVNET_DIR}/stop-mac.sh" "$STOP_BACKUP" log "Backed up existing stop-mac.sh to ${STOP_BACKUP}" fi cp -f "$STOP_SCRIPT_LOCAL" "${DEVNET_DIR}/stop-mac.sh" chmod +x "${DEVNET_DIR}/stop-mac.sh" if [[ -f "${DEVNET_DIR}/stop.sh" ]]; then STOP_ALIAS_BACKUP="${DEVNET_DIR}/stop.sh.bak-$(date +%Y%m%d_%H%M%S)" mv "${DEVNET_DIR}/stop.sh" "$STOP_ALIAS_BACKUP" log "Backed up existing stop.sh to ${STOP_ALIAS_BACKUP}" fi cp -f "$STOP_SCRIPT_LOCAL" "${DEVNET_DIR}/stop.sh" chmod +x "${DEVNET_DIR}/stop.sh" log "Installing qort helper..." cp -f "$QORT_SCRIPT_LOCAL" "${DEVNET_DIR}/qort" chmod +x "${DEVNET_DIR}/qort" mark_done "07-install-start-script" fi log "Done."