This commit is contained in:
+234
-217
@@ -5,8 +5,6 @@ GITEA_BASE_URL="${ERP_GITEA_BASE_URL:-}"
|
||||
REPOSITORY="${ERP_UPDATE_REPOSITORY:-awaioi/ERP}"
|
||||
REQUESTED_VERSION="${ERP_INSTALL_VERSION:-}"
|
||||
INSTALL_ROOT="${ERP_INSTALL_ROOT:-}"
|
||||
DB_MODE="${ERP_DB_MODE:-existing}"
|
||||
NON_INTERACTIVE="${ERP_INSTALL_NON_INTERACTIVE:-0}"
|
||||
ALLOW_INSECURE="${ERP_UPDATE_ALLOW_INSECURE_HTTP:-0}"
|
||||
NO_SERVICE="${ERP_INSTALL_NO_SERVICE:-0}"
|
||||
TOKEN="${ERP_GITEA_TOKEN:-${OA_UPDATE_TOKEN:-}}"
|
||||
@@ -22,13 +20,11 @@ usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install.sh [options]
|
||||
--gitea-url URL Gitea public base URL
|
||||
--repository O/R Release repository
|
||||
--version VERSION Install one exact release
|
||||
--repository O/R Release repository (default: awaioi/ERP)
|
||||
--version VERSION Install one exact stable release
|
||||
--install-root PATH Override installation directory
|
||||
--db-mode MODE existing or local (local is Linux only)
|
||||
--non-interactive Read all database values from ERP_DB_* variables
|
||||
--allow-insecure Development only: allow plain HTTP release URLs
|
||||
--no-service Install files without registering/starting a service
|
||||
--no-service Start without systemd/launchd; online update stays disabled
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -38,8 +34,6 @@ while [[ $# -gt 0 ]]; do
|
||||
--repository) [[ $# -ge 2 ]] || fail '--repository requires a value'; REPOSITORY="$2"; shift 2 ;;
|
||||
--version) [[ $# -ge 2 ]] || fail '--version requires a value'; REQUESTED_VERSION="$2"; shift 2 ;;
|
||||
--install-root) [[ $# -ge 2 ]] || fail '--install-root requires a value'; INSTALL_ROOT="$2"; shift 2 ;;
|
||||
--db-mode) [[ $# -ge 2 ]] || fail '--db-mode requires a value'; DB_MODE="$2"; shift 2 ;;
|
||||
--non-interactive) NON_INTERACTIVE=1; shift ;;
|
||||
--allow-insecure) ALLOW_INSECURE=1; shift ;;
|
||||
--no-service) NO_SERVICE=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
@@ -54,10 +48,6 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
case "$(uname -s)" in
|
||||
Linux) PLATFORM=linux ;;
|
||||
@@ -74,138 +64,106 @@ detect_platform() {
|
||||
fi
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
if [[ "$PLATFORM" == "darwin" ]] && command -v brew >/dev/null 2>&1; then
|
||||
local java_prefix="" libpq_prefix="" openssl_prefix=""
|
||||
java_prefix="$(brew --prefix openjdk@17 2>/dev/null || true)"
|
||||
libpq_prefix="$(brew --prefix libpq 2>/dev/null || true)"
|
||||
openssl_prefix="$(brew --prefix openssl@3 2>/dev/null || true)"
|
||||
[[ -z "$java_prefix" ]] || export PATH="$java_prefix/bin:$PATH"
|
||||
[[ -z "$libpq_prefix" ]] || export PATH="$libpq_prefix/bin:$PATH"
|
||||
[[ -z "$openssl_prefix" ]] || export PATH="$openssl_prefix/bin:$PATH"
|
||||
fi
|
||||
local need=0
|
||||
for command in curl tar python3 psql openssl java; do
|
||||
command -v "$command" >/dev/null 2>&1 || need=1
|
||||
done
|
||||
if [[ "$need" == "1" && "$PLATFORM" == "linux" ]]; then
|
||||
command -v apt-get >/dev/null 2>&1 || fail 'automatic dependency installation currently supports apt-based Linux only'
|
||||
say 'Installing runtime dependencies...'
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl tar python3 openssl openjdk-17-jre-headless postgresql-client
|
||||
elif [[ "$need" == "1" ]]; then
|
||||
command -v brew >/dev/null 2>&1 || fail 'Homebrew is required to install missing macOS dependencies'
|
||||
say 'Installing runtime dependencies...'
|
||||
brew install openjdk@17 libpq openssl@3
|
||||
export PATH="$(brew --prefix openjdk@17)/bin:$(brew --prefix libpq)/bin:$(brew --prefix openssl@3)/bin:$PATH"
|
||||
fi
|
||||
for command in curl tar python3 psql openssl java; do require_command "$command"; done
|
||||
local java_major
|
||||
java_major="$(java -version 2>&1 | awk -F'[".]' '/version/ {print $2; exit}')"
|
||||
[[ "$java_major" =~ ^[0-9]+$ && "$java_major" -ge 17 ]] || fail 'Java 17 or newer is required'
|
||||
local openssl_version algorithms
|
||||
openssl_version="$(openssl version 2>/dev/null || true)"
|
||||
[[ "$openssl_version" =~ ^OpenSSL[[:space:]]3\. ]] \
|
||||
|| fail 'OpenSSL 3 with Ed25519 support is required'
|
||||
algorithms="$(openssl list -public-key-algorithms 2>/dev/null || true)"
|
||||
grep -qi ED25519 <<< "$algorithms" \
|
||||
|| fail 'OpenSSL 3 with Ed25519 support is required'
|
||||
java_major() {
|
||||
local java_bin="${1:-java}"
|
||||
"$java_bin" -version 2>&1 | awk -F'[".]' '/version/ {print $2; exit}'
|
||||
}
|
||||
|
||||
validate_https_url() {
|
||||
has_java_17() {
|
||||
command -v java >/dev/null 2>&1 || return 1
|
||||
local major
|
||||
major="$(java_major "$(command -v java)")"
|
||||
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 17 ]]
|
||||
}
|
||||
|
||||
has_openssl_3_ed25519() {
|
||||
command -v openssl >/dev/null 2>&1 || return 1
|
||||
local version algorithms
|
||||
version="$(openssl version 2>/dev/null || true)"
|
||||
[[ "$version" =~ ^OpenSSL[[:space:]]3\. ]] || return 1
|
||||
algorithms="$(openssl list -public-key-algorithms 2>/dev/null || true)"
|
||||
grep -qi ED25519 <<< "$algorithms"
|
||||
}
|
||||
|
||||
install_linux_dependencies() {
|
||||
local need_java="$1"
|
||||
local packages=(ca-certificates)
|
||||
command -v curl >/dev/null 2>&1 || packages+=(curl)
|
||||
command -v tar >/dev/null 2>&1 || packages+=(tar)
|
||||
command -v python3 >/dev/null 2>&1 || packages+=(python3)
|
||||
has_openssl_3_ed25519 || packages+=(openssl)
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
[[ "$need_java" == "0" ]] || packages+=(openjdk-17-jre-headless)
|
||||
say 'Installing missing runtime packages with apt-get...'
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y "${packages[@]}"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
||||
say 'Installing missing runtime packages with dnf...'
|
||||
dnf install -y "${packages[@]}"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
||||
say 'Installing missing runtime packages with yum...'
|
||||
yum install -y "${packages[@]}"
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
[[ "$need_java" == "0" ]] || packages+=(java-17-openjdk-headless)
|
||||
say 'Installing missing runtime packages with zypper...'
|
||||
zypper --non-interactive install "${packages[@]}"
|
||||
else
|
||||
fail 'no supported package manager found (apt-get, dnf, yum, or zypper is required)'
|
||||
fi
|
||||
}
|
||||
|
||||
install_macos_dependencies() {
|
||||
command -v brew >/dev/null 2>&1 || fail 'Homebrew is required to install missing macOS packages'
|
||||
local packages=()
|
||||
has_java_17 || packages+=(openjdk@17)
|
||||
command -v python3 >/dev/null 2>&1 || packages+=(python@3)
|
||||
has_openssl_3_ed25519 || packages+=(openssl@3)
|
||||
if (( ${#packages[@]} > 0 )); then
|
||||
say 'Installing missing runtime packages with Homebrew...'
|
||||
brew install "${packages[@]}"
|
||||
fi
|
||||
local java_prefix openssl_prefix
|
||||
java_prefix="$(brew --prefix openjdk@17 2>/dev/null || true)"
|
||||
openssl_prefix="$(brew --prefix openssl@3 2>/dev/null || true)"
|
||||
[[ -z "$java_prefix" ]] || export PATH="$java_prefix/bin:$PATH"
|
||||
[[ -z "$openssl_prefix" ]] || export PATH="$openssl_prefix/bin:$PATH"
|
||||
}
|
||||
|
||||
check_and_install_dependencies() {
|
||||
local need_install=0 need_java=0
|
||||
has_java_17 || { need_install=1; need_java=1; }
|
||||
command -v curl >/dev/null 2>&1 || need_install=1
|
||||
command -v tar >/dev/null 2>&1 || need_install=1
|
||||
command -v python3 >/dev/null 2>&1 || need_install=1
|
||||
has_openssl_3_ed25519 || need_install=1
|
||||
|
||||
if [[ "$need_install" == "1" ]]; then
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
install_linux_dependencies "$need_java"
|
||||
else
|
||||
install_macos_dependencies
|
||||
fi
|
||||
fi
|
||||
|
||||
for command in curl tar python3 openssl java; do
|
||||
command -v "$command" >/dev/null 2>&1 || fail "missing command after installation: $command"
|
||||
done
|
||||
has_java_17 || fail 'Java 17 or newer is required and could not be installed'
|
||||
has_openssl_3_ed25519 || fail 'OpenSSL 3 with Ed25519 support is required and could not be installed'
|
||||
JAVA_BIN="$(command -v java)"
|
||||
say "Using Java $(java_major "$JAVA_BIN") at $JAVA_BIN"
|
||||
}
|
||||
|
||||
validate_download_url() {
|
||||
case "$1" in
|
||||
https://*) return 0 ;;
|
||||
http://*) [[ "$ALLOW_INSECURE" == "1" || "$ALLOW_INSECURE" == "true" ]] && return 0 ;;
|
||||
esac
|
||||
fail 'release server must use HTTPS (use --allow-insecure only for local development)'
|
||||
}
|
||||
|
||||
prompt_value() {
|
||||
local variable="$1" label="$2" default="$3" secret="${4:-0}" value="${!variable:-}"
|
||||
if [[ -z "$value" && "$NON_INTERACTIVE" != "1" ]]; then
|
||||
[[ -r /dev/tty && -w /dev/tty ]] \
|
||||
|| fail "cannot prompt for $label; use --non-interactive with ERP_DB_* variables"
|
||||
if [[ "$secret" == "1" ]]; then
|
||||
IFS= read -r -s -p "$label: " value </dev/tty \
|
||||
|| fail "cannot read $label; use --non-interactive with ERP_DB_* variables"
|
||||
printf '\n' >/dev/tty
|
||||
else
|
||||
IFS= read -r -p "$label [$default]: " value </dev/tty \
|
||||
|| fail "cannot read $label; use --non-interactive with ERP_DB_* variables"
|
||||
fi
|
||||
fi
|
||||
value="${value:-$default}"
|
||||
printf -v "$variable" '%s' "$value"
|
||||
}
|
||||
|
||||
run_as_postgres() {
|
||||
if [[ "$(id -un)" == "postgres" ]]; then
|
||||
psql "$@"
|
||||
elif command -v runuser >/dev/null 2>&1; then
|
||||
runuser -u postgres -- psql "$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo -u postgres -- psql "$@"
|
||||
else
|
||||
fail 'cannot switch to the postgres operating-system user (runuser or sudo is required)'
|
||||
fi
|
||||
}
|
||||
|
||||
configure_local_postgres() {
|
||||
[[ "$PLATFORM" == "linux" ]] || fail 'automatic local PostgreSQL provisioning is currently Linux-only'
|
||||
command -v apt-get >/dev/null 2>&1 || fail 'local PostgreSQL provisioning requires apt'
|
||||
require_command systemctl
|
||||
apt-get update
|
||||
apt-get install -y postgresql postgresql-contrib
|
||||
systemctl enable --now postgresql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME="${ERP_DB_NAME:-kaidi_erp}"
|
||||
DB_USER="${ERP_DB_USER:-kaidi_erp}"
|
||||
DB_PASSWORD="${ERP_DB_PASSWORD:-}"
|
||||
if [[ -z "$DB_PASSWORD" ]]; then
|
||||
DB_PASSWORD="$(openssl rand -hex 24)" || fail 'unable to generate a PostgreSQL password'
|
||||
fi
|
||||
DB_SSLMODE=disable
|
||||
[[ "$DB_NAME" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]] || fail 'invalid local database name'
|
||||
[[ "$DB_USER" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]] || fail 'invalid local database user'
|
||||
run_as_postgres -v ON_ERROR_STOP=1 -v role="$DB_USER" -v password="$DB_PASSWORD" <<'SQL'
|
||||
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'role', :'password')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'role') \gexec
|
||||
SELECT format('ALTER ROLE %I LOGIN PASSWORD %L', :'role', :'password') \gexec
|
||||
SQL
|
||||
run_as_postgres -v ON_ERROR_STOP=1 -v db="$DB_NAME" -v role="$DB_USER" <<'SQL'
|
||||
SELECT format('CREATE DATABASE %I OWNER %I', :'db', :'role')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db') \gexec
|
||||
SQL
|
||||
}
|
||||
|
||||
configure_existing_postgres() {
|
||||
prompt_value DB_HOST 'PostgreSQL host' "${ERP_DB_HOST:-127.0.0.1}"
|
||||
prompt_value DB_PORT 'PostgreSQL port' "${ERP_DB_PORT:-5432}"
|
||||
prompt_value DB_NAME 'Database name' "${ERP_DB_NAME:-kaidi_erp}"
|
||||
prompt_value DB_USER 'Database user' "${ERP_DB_USER:-kaidi_erp}"
|
||||
prompt_value DB_PASSWORD 'Database password' "${ERP_DB_PASSWORD:-}" 1
|
||||
prompt_value DB_SSLMODE 'PostgreSQL SSL mode' "${ERP_DB_SSLMODE:-prefer}"
|
||||
[[ -n "$DB_PASSWORD" ]] || fail 'database password is required'
|
||||
}
|
||||
|
||||
validate_postgres() {
|
||||
[[ "$DB_HOST" != *[[:space:]]* && "$DB_PORT" =~ ^[0-9]+$ ]] || fail 'invalid PostgreSQL host or port'
|
||||
[[ "$DB_NAME" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]] || fail 'invalid database name'
|
||||
[[ "$DB_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]*$ ]] || fail 'invalid database user'
|
||||
case "$DB_SSLMODE" in disable|allow|prefer|require|verify-ca|verify-full) ;; *) fail 'invalid PostgreSQL SSL mode' ;; esac
|
||||
local version
|
||||
version="$(PGPASSWORD="$DB_PASSWORD" PGSSLMODE="$DB_SSLMODE" psql \
|
||||
-h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -Atqc \
|
||||
"select current_setting('server_version_num')::int")" \
|
||||
|| fail 'cannot connect to PostgreSQL with the supplied credentials'
|
||||
[[ "$version" =~ ^[0-9]+$ && "$version" -ge 150000 ]] || fail 'PostgreSQL 15 or newer is required'
|
||||
PGPASSWORD="$DB_PASSWORD" PGSSLMODE="$DB_SSLMODE" psql \
|
||||
-h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 \
|
||||
-c 'CREATE EXTENSION IF NOT EXISTS pg_trgm' >/dev/null \
|
||||
|| fail 'database user cannot install the required pg_trgm extension'
|
||||
say "PostgreSQL connection verified (${version:0:2})"
|
||||
fail 'release server must use HTTPS (use --allow-insecure only for an isolated test server)'
|
||||
}
|
||||
|
||||
create_curl_config() {
|
||||
@@ -226,7 +184,7 @@ create_curl_config() {
|
||||
}
|
||||
|
||||
download() {
|
||||
validate_https_url "$1"
|
||||
validate_download_url "$1"
|
||||
curl --config "$CURL_CONFIG" --output "$2" "$1"
|
||||
}
|
||||
|
||||
@@ -235,9 +193,8 @@ download_release() {
|
||||
local owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}"
|
||||
local release_json="$TMP_DIR/release.json"
|
||||
download "${GITEA_BASE_URL%/}/api/v1/repos/$owner/$repo/releases/latest" "$release_json" \
|
||||
|| fail 'unable to read latest Gitea Release'
|
||||
local selection="$TMP_DIR/selection"
|
||||
local selection_error="$TMP_DIR/selection-error"
|
||||
|| fail 'unable to read the latest Gitea Release'
|
||||
local selection="$TMP_DIR/selection" selection_error="$TMP_DIR/selection-error"
|
||||
if ! python3 - "$release_json" "$REQUESTED_VERSION" > "$selection" 2> "$selection_error" <<'PY'
|
||||
import json, re, sys
|
||||
try:
|
||||
@@ -256,7 +213,12 @@ requested = sys.argv[2].removeprefix("v")
|
||||
if requested and requested != version:
|
||||
raise SystemExit("requested version does not match latest release")
|
||||
assets = {str(a.get("name")): str(a.get("browser_download_url") or "") for a in release.get("assets", [])}
|
||||
names = [f"kaidi-erp-{version}.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"]
|
||||
names = [
|
||||
f"kaidi-erp-{version}.tar.gz",
|
||||
f"kaidi-erp-installer-{version}.jar",
|
||||
"SHA256SUMS",
|
||||
"SHA256SUMS.sig",
|
||||
]
|
||||
urls = [assets.get(name, "") for name in names]
|
||||
if any(not url or any(ch.isspace() for ch in url) for url in urls):
|
||||
raise SystemExit("release assets missing")
|
||||
@@ -267,37 +229,53 @@ PY
|
||||
[[ ! -s "$selection_error" ]] || reason="$(<"$selection_error")"
|
||||
fail "$reason"
|
||||
fi
|
||||
local archive_url sums_url signature_url
|
||||
IFS=$'\t' read -r VERSION archive_url sums_url signature_url < "$selection"
|
||||
|
||||
local archive_url installer_url sums_url signature_url
|
||||
IFS=$'\t' read -r VERSION archive_url installer_url sums_url signature_url < "$selection"
|
||||
ARCHIVE_NAME="kaidi-erp-${VERSION}.tar.gz"
|
||||
INSTALLER_NAME="kaidi-erp-installer-${VERSION}.jar"
|
||||
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME"
|
||||
INSTALLER_PATH="$TMP_DIR/$INSTALLER_NAME"
|
||||
download "$archive_url" "$ARCHIVE_PATH"
|
||||
download "$installer_url" "$INSTALLER_PATH"
|
||||
download "$sums_url" "$TMP_DIR/SHA256SUMS"
|
||||
download "$signature_url" "$TMP_DIR/SHA256SUMS.sig"
|
||||
}
|
||||
|
||||
checksum_for() {
|
||||
python3 - "$TMP_DIR/SHA256SUMS" "$1" <<'PY'
|
||||
import re, sys
|
||||
for line in open(sys.argv[1], encoding="utf-8"):
|
||||
match = re.fullmatch(r"([0-9a-fA-F]{64})\s+\*?(.+?)\s*", line)
|
||||
if match and match.group(2) == sys.argv[2]:
|
||||
print(match.group(1).lower())
|
||||
break
|
||||
PY
|
||||
}
|
||||
|
||||
actual_checksum() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
else
|
||||
shasum -a 256 "$1" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
verify_release() {
|
||||
printf '%s\n' "$PUBLIC_KEY" > "$TMP_DIR/release-public-key.pem"
|
||||
openssl pkeyutl -verify -rawin -pubin -inkey "$TMP_DIR/release-public-key.pem" \
|
||||
-sigfile "$TMP_DIR/SHA256SUMS.sig" -in "$TMP_DIR/SHA256SUMS" >/dev/null \
|
||||
|| fail 'Release Ed25519 signature verification failed'
|
||||
local expected actual
|
||||
expected="$(python3 - "$TMP_DIR/SHA256SUMS" "$ARCHIVE_NAME" <<'PY'
|
||||
import re, sys
|
||||
for line in open(sys.argv[1], encoding="utf-8"):
|
||||
m = re.fullmatch(r"([0-9a-fA-F]{64})\s+\*?(.+?)\s*", line)
|
||||
if m and m.group(2) == sys.argv[2]:
|
||||
print(m.group(1).lower())
|
||||
break
|
||||
PY
|
||||
)"
|
||||
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || fail 'release checksum is missing'
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual="$(sha256sum "$ARCHIVE_PATH" | awk '{print $1}')"
|
||||
else
|
||||
actual="$(shasum -a 256 "$ARCHIVE_PATH" | awk '{print $1}')"
|
||||
fi
|
||||
[[ "$actual" == "$expected" ]] || fail 'release checksum verification failed'
|
||||
|
||||
local name path expected actual
|
||||
for name in "$ARCHIVE_NAME" "$INSTALLER_NAME"; do
|
||||
if [[ "$name" == "$ARCHIVE_NAME" ]]; then path="$ARCHIVE_PATH"; else path="$INSTALLER_PATH"; fi
|
||||
expected="$(checksum_for "$name")"
|
||||
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || fail "release checksum is missing for $name"
|
||||
actual="$(actual_checksum "$path")"
|
||||
[[ "$actual" == "$expected" ]] || fail "release checksum verification failed for $name"
|
||||
done
|
||||
|
||||
python3 - "$ARCHIVE_PATH" <<'PY'
|
||||
import pathlib, sys, tarfile
|
||||
with tarfile.open(sys.argv[1], "r:gz") as archive:
|
||||
@@ -317,7 +295,9 @@ prepare_layout() {
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
ERP_USER="${ERP_SERVICE_USER:-kaidi-erp}"
|
||||
ERP_GROUP="$ERP_USER"
|
||||
id "$ERP_USER" >/dev/null 2>&1 || useradd --system --home-dir "$INSTALL_ROOT" --shell /usr/sbin/nologin "$ERP_USER"
|
||||
[[ "$ERP_USER" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] || fail 'invalid Linux service user name'
|
||||
id "$ERP_USER" >/dev/null 2>&1 \
|
||||
|| useradd --system --home-dir "$INSTALL_ROOT" --shell /usr/sbin/nologin "$ERP_USER"
|
||||
CONFIG_ROOT="${ERP_CONFIG_ROOT:-/etc/kaidi-erp}"
|
||||
STATE_ROOT="${ERP_STATE_ROOT:-/var/lib/kaidi-erp}"
|
||||
LOG_ROOT="${ERP_LOG_ROOT:-/var/log/kaidi-erp}"
|
||||
@@ -326,7 +306,6 @@ prepare_layout() {
|
||||
[[ "$path" =~ ^/[A-Za-z0-9._/@:+-]+$ ]] \
|
||||
|| fail "Linux installation paths must be absolute and contain only safe characters: $path"
|
||||
done
|
||||
[[ "$ERP_USER" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] || fail 'invalid Linux service user name'
|
||||
else
|
||||
ERP_USER="$(id -un)"
|
||||
ERP_GROUP="$(id -gn)"
|
||||
@@ -334,11 +313,15 @@ prepare_layout() {
|
||||
STATE_ROOT="${ERP_STATE_ROOT:-$INSTALL_ROOT/state}"
|
||||
LOG_ROOT="${ERP_LOG_ROOT:-$HOME/Library/Logs/KaidiERP}"
|
||||
fi
|
||||
mkdir -p "$INSTALL_ROOT/releases" "$INSTALL_ROOT/run" "$INSTALL_ROOT/backups" \
|
||||
"$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
||||
|
||||
mkdir -p "$INSTALL_ROOT/releases" "$INSTALL_ROOT/installer" "$INSTALL_ROOT/run" \
|
||||
"$INSTALL_ROOT/backups" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
chown -R "$ERP_USER:$ERP_GROUP" "$INSTALL_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
||||
chown -R "$ERP_USER:$ERP_GROUP" "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
||||
chmod 750 "$CONFIG_ROOT" "$STATE_ROOT"
|
||||
fi
|
||||
[[ ! -e "$STATE_ROOT/install.lock" ]] || fail 'Kaidi ERP is already installed; use the system update page'
|
||||
[[ ! -e "$STATE_ROOT/install.pending" ]] || fail 'a previous installation is still pending; inspect the service before retrying'
|
||||
}
|
||||
|
||||
install_release_files() {
|
||||
@@ -348,9 +331,10 @@ install_release_files() {
|
||||
local source="$unpack/kaidi-erp-${VERSION}"
|
||||
[[ -r "$source/app/kaidi-erp.jar" && -x "$source/bin/erp-run" && -x "$source/bin/erp-update" ]] \
|
||||
|| fail 'release archive is incomplete'
|
||||
[[ "$(<"$source/VERSION")" == "$VERSION" ]] || fail 'release archive version mismatch'
|
||||
local destination="$INSTALL_ROOT/releases/$VERSION"
|
||||
local replaced=""
|
||||
[[ -r "$source/VERSION" && "$(<"$source/VERSION")" == "$VERSION" ]] \
|
||||
|| fail 'release archive version mismatch'
|
||||
|
||||
local destination="$INSTALL_ROOT/releases/$VERSION" replaced=""
|
||||
if [[ -e "$destination" ]]; then
|
||||
replaced="${destination}.replaced-$$"
|
||||
mv "$destination" "$replaced"
|
||||
@@ -360,47 +344,59 @@ install_release_files() {
|
||||
fail 'unable to write the release directory'
|
||||
fi
|
||||
[[ -z "$replaced" ]] || rm -rf "$replaced"
|
||||
|
||||
local installer_tmp="$INSTALL_ROOT/installer/.kaidi-erp-installer.jar.tmp-$$"
|
||||
cp "$INSTALLER_PATH" "$installer_tmp"
|
||||
chmod 600 "$installer_tmp"
|
||||
mv "$installer_tmp" "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
||||
|
||||
python3 - "$INSTALL_ROOT/current" "releases/$VERSION" <<'PY'
|
||||
import os, sys
|
||||
link, target = sys.argv[1:]
|
||||
tmp = f"{link}.new-{os.getpid()}"
|
||||
temporary = f"{link}.new-{os.getpid()}"
|
||||
try:
|
||||
os.symlink(target, tmp)
|
||||
os.replace(tmp, link)
|
||||
os.symlink(target, temporary)
|
||||
os.replace(temporary, link)
|
||||
finally:
|
||||
if os.path.lexists(tmp): os.unlink(tmp)
|
||||
if os.path.lexists(temporary):
|
||||
os.unlink(temporary)
|
||||
PY
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
chown -R "$ERP_USER:$ERP_GROUP" "$destination" "$INSTALL_ROOT/current"
|
||||
chown -R "$ERP_USER:$ERP_GROUP" "$destination" "$INSTALL_ROOT/installer" "$INSTALL_ROOT/current"
|
||||
fi
|
||||
}
|
||||
|
||||
write_configuration() {
|
||||
write_bootstrap_configuration() {
|
||||
CONFIG_FILE="$CONFIG_ROOT/erp.env"
|
||||
PUBLIC_KEY_FILE="$CONFIG_ROOT/release-public-key.pem"
|
||||
local update_enabled=true
|
||||
PENDING_FILE="$STATE_ROOT/install.pending"
|
||||
INSTALL_LOCK_FILE="$STATE_ROOT/install.lock"
|
||||
OPERATION_LOCK_FILE="$STATE_ROOT/install-operation.lock"
|
||||
SETUP_TOKEN="$(openssl rand -hex 32)" || fail 'unable to generate the one-time setup token'
|
||||
local update_enabled=true port="${ERP_SERVER_PORT:-8091}"
|
||||
[[ "$NO_SERVICE" != "1" ]] || update_enabled=false
|
||||
|
||||
printf '%s\n' "$PUBLIC_KEY" > "$PUBLIC_KEY_FILE"
|
||||
{
|
||||
shell_setting ERP_INSTALL_ROOT "$INSTALL_ROOT"
|
||||
shell_setting ERP_CONFIG_FILE "$CONFIG_FILE"
|
||||
shell_setting ERP_RUN_DIR "$INSTALL_ROOT/run"
|
||||
shell_setting ERP_STATE_FILE "$STATE_ROOT/update-state.json"
|
||||
shell_setting ERP_HEALTH_URL "http://127.0.0.1:${ERP_SERVER_PORT:-8091}/api/oa/health"
|
||||
shell_setting ERP_INSTALL_PENDING_FILE "$PENDING_FILE"
|
||||
shell_setting ERP_INSTALL_LOCK_FILE "$INSTALL_LOCK_FILE"
|
||||
shell_setting ERP_INSTALL_OPERATION_LOCK_FILE "$OPERATION_LOCK_FILE"
|
||||
shell_setting ERP_SETUP_TOKEN "$SETUP_TOKEN"
|
||||
shell_setting ERP_INSTALLER_JAR "$INSTALL_ROOT/installer/kaidi-erp-installer.jar"
|
||||
shell_setting ERP_JAR_PATH "$INSTALL_ROOT/current/app/kaidi-erp.jar"
|
||||
shell_setting ERP_JAVA_BIN "$JAVA_BIN"
|
||||
shell_setting ERP_RELEASE_VERSION "$VERSION"
|
||||
shell_setting ERP_HEALTH_URL "http://127.0.0.1:${port}/api/oa/health"
|
||||
shell_setting ERP_UPDATE_PUBLIC_KEY_FILE "$PUBLIC_KEY_FILE"
|
||||
shell_setting ERP_UPDATE_REQUIRE_SIGNATURE true
|
||||
shell_setting ERP_UPDATE_BACKUP_MODE "${ERP_UPDATE_BACKUP_MODE:-none}"
|
||||
shell_setting ERP_UPDATE_HEALTH_TIMEOUT_SECONDS "${ERP_UPDATE_HEALTH_TIMEOUT_SECONDS:-120}"
|
||||
shell_setting ERP_UPDATE_HEALTH_POLL_SECONDS "${ERP_UPDATE_HEALTH_POLL_SECONDS:-2}"
|
||||
shell_setting ERP_PGHOST "$DB_HOST"
|
||||
shell_setting ERP_PGPORT "$DB_PORT"
|
||||
shell_setting ERP_PGDATABASE "$DB_NAME"
|
||||
shell_setting ERP_PGSSLMODE "$DB_SSLMODE"
|
||||
shell_setting SPRING_PROFILES_ACTIVE postgres
|
||||
shell_setting SERVER_PORT "${ERP_SERVER_PORT:-8091}"
|
||||
shell_setting OA_DB_URL "jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
|
||||
shell_setting OA_DB_USERNAME "$DB_USER"
|
||||
shell_setting OA_DB_PASSWORD "$DB_PASSWORD"
|
||||
shell_setting SERVER_PORT "$port"
|
||||
shell_setting OA_UPDATE_ENABLED "$update_enabled"
|
||||
shell_setting OA_UPDATE_GITEA_BASE_URL "$GITEA_BASE_URL"
|
||||
shell_setting OA_UPDATE_REPOSITORY "$REPOSITORY"
|
||||
@@ -411,15 +407,19 @@ write_configuration() {
|
||||
shell_setting OA_UPDATE_ALLOW_INSECURE_HTTP "$ALLOW_INSECURE"
|
||||
} > "$CONFIG_FILE"
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
chown root:"$ERP_GROUP" "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
||||
chmod 640 "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
||||
else
|
||||
chmod 600 "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
||||
chown "$ERP_USER:$ERP_GROUP" "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
||||
fi
|
||||
chmod 600 "$CONFIG_FILE" "$PUBLIC_KEY_FILE"
|
||||
}
|
||||
|
||||
install_service() {
|
||||
[[ "$NO_SERVICE" == "1" ]] && return 0
|
||||
start_service() {
|
||||
if [[ "$NO_SERVICE" == "1" ]]; then
|
||||
nohup env ERP_INSTALL_ROOT="$INSTALL_ROOT" ERP_CONFIG_FILE="$CONFIG_FILE" \
|
||||
"$INSTALL_ROOT/current/bin/erp-run" >> "$LOG_ROOT/erp.log" 2>> "$LOG_ROOT/erp-error.log" &
|
||||
printf '%s\n' "$!" > "$INSTALL_ROOT/run/launcher.pid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
cat > /etc/systemd/system/kaidi-erp.service <<EOF
|
||||
[Unit]
|
||||
@@ -439,20 +439,21 @@ Restart=always
|
||||
RestartSec=3
|
||||
TimeoutStopSec=90
|
||||
SuccessExitStatus=143
|
||||
UMask=0077
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths="$INSTALL_ROOT" "$STATE_ROOT"
|
||||
ReadWritePaths="$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now kaidi-erp.service
|
||||
systemctl enable kaidi-erp.service
|
||||
systemctl restart kaidi-erp.service
|
||||
else
|
||||
local agents="$HOME/Library/LaunchAgents"
|
||||
local plist="$agents/com.kaidi.erp.plist"
|
||||
local agents="$HOME/Library/LaunchAgents" plist="$HOME/Library/LaunchAgents/com.kaidi.erp.plist"
|
||||
mkdir -p "$agents"
|
||||
python3 - "$plist" "$INSTALL_ROOT" "$CONFIG_FILE" "$LOG_ROOT" <<'PY'
|
||||
import plistlib, sys
|
||||
@@ -476,16 +477,31 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
[[ "$NO_SERVICE" == "1" ]] && return 0
|
||||
local url="http://127.0.0.1:${ERP_SERVER_PORT:-8091}/api/oa/health" deadline=$((SECONDS + 120))
|
||||
wait_for_installer() {
|
||||
local url="http://127.0.0.1:${ERP_SERVER_PORT:-8091}/api/install/status"
|
||||
local deadline=$((SECONDS + 120))
|
||||
while (( SECONDS < deadline )); do
|
||||
if curl -fsS --connect-timeout 2 --max-time 5 "$url" >/dev/null 2>&1; then
|
||||
if curl -fsS --connect-timeout 2 --max-time 5 \
|
||||
-H "X-Setup-Token: $SETUP_TOKEN" "$url" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fail 'ERP service did not become healthy within 120 seconds'
|
||||
fail 'the web installer did not start within 120 seconds; inspect the kaidi-erp service log'
|
||||
}
|
||||
|
||||
detect_lan_address() {
|
||||
local address=""
|
||||
if command -v hostname >/dev/null 2>&1; then
|
||||
address="$(hostname -I 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i ~ /^[0-9]+\./) {print $i; exit}}' || true)"
|
||||
fi
|
||||
if [[ -z "$address" && "$PLATFORM" == "linux" ]] && command -v ip >/dev/null 2>&1; then
|
||||
address="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}' || true)"
|
||||
fi
|
||||
if [[ -z "$address" && "$PLATFORM" == "darwin" ]] && command -v ipconfig >/dev/null 2>&1; then
|
||||
address="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)"
|
||||
fi
|
||||
printf '%s' "${address:-127.0.0.1}"
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -498,26 +514,27 @@ main() {
|
||||
fi
|
||||
fi
|
||||
[[ -n "$GITEA_BASE_URL" ]] || fail 'Gitea URL is required; use --gitea-url or ERP_GITEA_BASE_URL'
|
||||
validate_https_url "$GITEA_BASE_URL"
|
||||
install_dependencies
|
||||
case "$DB_MODE" in
|
||||
existing) configure_existing_postgres ;;
|
||||
local) configure_local_postgres ;;
|
||||
*) fail 'db mode must be existing or local' ;;
|
||||
esac
|
||||
validate_postgres
|
||||
validate_download_url "$GITEA_BASE_URL"
|
||||
|
||||
say "Detected $PLATFORM/$ARCH"
|
||||
check_and_install_dependencies
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/kaidi-erp-install.XXXXXX")"
|
||||
create_curl_config
|
||||
say 'Downloading signed release...'
|
||||
say 'Downloading and verifying the signed release...'
|
||||
download_release
|
||||
verify_release
|
||||
prepare_layout
|
||||
install_release_files
|
||||
write_configuration
|
||||
install_service
|
||||
wait_for_health
|
||||
say "Installed Kaidi ERP $VERSION"
|
||||
say "Local URL: http://127.0.0.1:${ERP_SERVER_PORT:-8091}"
|
||||
write_bootstrap_configuration
|
||||
start_service
|
||||
wait_for_installer
|
||||
|
||||
local port="${ERP_SERVER_PORT:-8091}" address
|
||||
address="$(detect_lan_address)"
|
||||
say "Kaidi ERP $VERSION installer is running"
|
||||
say "Setup URL: http://${address}:${port}/?token=${SETUP_TOKEN}"
|
||||
say "Local URL: http://127.0.0.1:${port}/?token=${SETUP_TOKEN}"
|
||||
say 'Complete PostgreSQL and administrator setup in the browser. The installer will remove itself after the formal service is healthy.'
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user