417 lines
9.1 KiB
Bash
Executable File
417 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_URL="https://dock-it.dev/Idea-Studios/Maintainarr.git"
|
|
REPO_BRANCH="${MAINTAINARR_BRANCH:-main}"
|
|
APP_USER="${MAINTAINARR_USER:-maintainarr}"
|
|
INSTALL_ROOT="${MAINTAINARR_INSTALL_ROOT:-/opt/maintainarr}"
|
|
SRC_DIR="$INSTALL_ROOT/src"
|
|
BIN_DIR="$INSTALL_ROOT/bin"
|
|
BIN_PATH="$BIN_DIR/maintainarr"
|
|
DATA_DIR="${MAINTAINARR_DATA_DIR:-/var/lib/maintainarr}"
|
|
CONFIG_DIR="${MAINTAINARR_CONFIG_DIR:-/etc/maintainarr}"
|
|
ENV_FILE="$CONFIG_DIR/maintainarr.env"
|
|
SYSTEMD_UNIT="/etc/systemd/system/maintainarr.service"
|
|
UPDATE_SCRIPT="/usr/local/bin/maintainarr-update"
|
|
CRON_FILE="/etc/cron.d/maintainarr-update"
|
|
GO_VERSION="${MAINTAINARR_GO_VERSION:-1.26.4}"
|
|
|
|
log() {
|
|
printf '[maintainarr] %s\n' "$*"
|
|
}
|
|
|
|
fail() {
|
|
printf '[maintainarr] ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_root() {
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
fail "Run this installer as root, for example: curl -fsSL ... | sudo bash"
|
|
fi
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
apt_package_installed() {
|
|
dpkg -s "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
version_ge() {
|
|
[[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]
|
|
}
|
|
|
|
detect_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64)
|
|
echo "amd64"
|
|
;;
|
|
aarch64|arm64)
|
|
echo "arm64"
|
|
;;
|
|
*)
|
|
fail "Unsupported architecture: $(uname -m)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
pkg_install() {
|
|
if command_exists apt-get; then
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get install -y "$@"
|
|
return
|
|
fi
|
|
|
|
if command_exists dnf; then
|
|
dnf install -y "$@"
|
|
return
|
|
fi
|
|
|
|
if command_exists yum; then
|
|
yum install -y "$@"
|
|
return
|
|
fi
|
|
|
|
if command_exists pacman; then
|
|
pacman -Sy --noconfirm "$@"
|
|
return
|
|
fi
|
|
|
|
if command_exists zypper; then
|
|
zypper --non-interactive install "$@"
|
|
return
|
|
fi
|
|
|
|
if command_exists apk; then
|
|
apk add --no-cache "$@"
|
|
return
|
|
fi
|
|
|
|
fail "No supported package manager found"
|
|
}
|
|
|
|
install_base_packages() {
|
|
if command_exists apt-get; then
|
|
local packages=()
|
|
local package
|
|
|
|
for package in ca-certificates curl git tar cron; do
|
|
if ! apt_package_installed "$package"; then
|
|
packages+=("$package")
|
|
fi
|
|
done
|
|
|
|
if [[ "${#packages[@]}" -eq 0 ]]; then
|
|
log "System packages already present: ca-certificates curl git tar cron"
|
|
return
|
|
fi
|
|
|
|
log "Installing missing system packages: ${packages[*]}"
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update
|
|
apt-get install -y "${packages[@]}"
|
|
return
|
|
fi
|
|
|
|
if command_exists dnf; then
|
|
pkg_install ca-certificates curl git tar cronie
|
|
return
|
|
fi
|
|
|
|
if command_exists yum; then
|
|
pkg_install ca-certificates curl git tar cronie
|
|
return
|
|
fi
|
|
|
|
if command_exists pacman; then
|
|
pkg_install ca-certificates curl git tar cronie
|
|
return
|
|
fi
|
|
|
|
if command_exists zypper; then
|
|
pkg_install ca-certificates curl git tar cron
|
|
return
|
|
fi
|
|
|
|
if command_exists apk; then
|
|
pkg_install ca-certificates curl git tar dcron
|
|
return
|
|
fi
|
|
|
|
fail "Unable to install prerequisites for this distribution"
|
|
}
|
|
|
|
install_go() {
|
|
local current_version=""
|
|
|
|
if command_exists go; then
|
|
current_version="$(go version | awk '{print $3}' | sed 's/^go//')"
|
|
elif [[ -x /usr/local/go/bin/go ]]; then
|
|
current_version="$(/usr/local/go/bin/go version | awk '{print $3}' | sed 's/^go//')"
|
|
fi
|
|
|
|
if [[ -n "$current_version" ]] && version_ge "$current_version" "$GO_VERSION"; then
|
|
log "Go $current_version already satisfies the requirement"
|
|
return
|
|
fi
|
|
|
|
local arch
|
|
arch="$(detect_arch)"
|
|
local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz"
|
|
|
|
log "Installing Go $GO_VERSION"
|
|
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" -o "$archive"
|
|
rm -rf /usr/local/go
|
|
tar -C /usr/local -xzf "$archive"
|
|
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
|
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
|
}
|
|
|
|
tty_available() {
|
|
[[ -r /dev/tty && -w /dev/tty ]]
|
|
}
|
|
|
|
prompt_yes_no() {
|
|
local prompt="$1"
|
|
local default_answer="$2"
|
|
local reply=""
|
|
local suffix="[y/N]"
|
|
|
|
if [[ "$default_answer" == "y" ]]; then
|
|
suffix="[Y/n]"
|
|
fi
|
|
|
|
if ! tty_available; then
|
|
[[ "$default_answer" == "y" ]]
|
|
return
|
|
fi
|
|
|
|
while true; do
|
|
printf '%s %s ' "$prompt" "$suffix" > /dev/tty
|
|
IFS= read -r reply < /dev/tty || true
|
|
reply="${reply:-$default_answer}"
|
|
|
|
case "$reply" in
|
|
y|Y|yes|YES)
|
|
return 0
|
|
;;
|
|
n|N|no|NO)
|
|
return 1
|
|
;;
|
|
*)
|
|
printf 'Please answer yes or no.\n' > /dev/tty
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
ensure_user() {
|
|
if id -u "$APP_USER" >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
local nologin_shell
|
|
nologin_shell="$(command -v nologin || true)"
|
|
nologin_shell="${nologin_shell:-/usr/sbin/nologin}"
|
|
useradd --system --home "$INSTALL_ROOT" --create-home --shell "$nologin_shell" "$APP_USER"
|
|
}
|
|
|
|
clone_or_update_repo() {
|
|
mkdir -p "$INSTALL_ROOT" "$BIN_DIR"
|
|
|
|
if [[ -d "$SRC_DIR/.git" ]]; then
|
|
log "Updating source in $SRC_DIR"
|
|
git -C "$SRC_DIR" fetch --tags origin
|
|
git -C "$SRC_DIR" checkout "$REPO_BRANCH"
|
|
git -C "$SRC_DIR" pull --ff-only origin "$REPO_BRANCH"
|
|
return
|
|
fi
|
|
|
|
if [[ -f "$SRC_DIR/go.mod" && -d "$SRC_DIR/cmd/maintainarr" ]]; then
|
|
log "Existing Maintainarr source found in $SRC_DIR; skipping download"
|
|
return
|
|
fi
|
|
|
|
log "Cloning source into $SRC_DIR"
|
|
git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$SRC_DIR"
|
|
}
|
|
|
|
build_binary() {
|
|
log "Building Maintainarr"
|
|
mkdir -p "$BIN_DIR"
|
|
(
|
|
cd "$SRC_DIR"
|
|
export PATH="/usr/local/go/bin:/usr/local/bin:$PATH"
|
|
export CGO_ENABLED=0
|
|
go build -o "$BIN_PATH" ./cmd/maintainarr
|
|
)
|
|
chmod 755 "$BIN_PATH"
|
|
}
|
|
|
|
random_hex() {
|
|
local bytes="$1"
|
|
od -An -N"$bytes" -tx1 /dev/urandom | tr -d ' \n'
|
|
}
|
|
|
|
write_env_file() {
|
|
mkdir -p "$CONFIG_DIR" "$DATA_DIR/log-archives"
|
|
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
log "Keeping existing environment file at $ENV_FILE"
|
|
return
|
|
fi
|
|
|
|
local session_key
|
|
local encryption_key
|
|
session_key="$(random_hex 32)"
|
|
encryption_key="$(random_hex 16)"
|
|
|
|
cat > "$ENV_FILE" <<EOF
|
|
MAINTAINARR_ADDR=:8080
|
|
MAINTAINARR_DB_PATH=$DATA_DIR/maintainarr.db
|
|
MAINTAINARR_LOG_ARCHIVE_DIR=$DATA_DIR/log-archives
|
|
MAINTAINARR_SESSION_KEY=$session_key
|
|
MAINTAINARR_ENCRYPTION_KEY=$encryption_key
|
|
MAINTAINARR_ORG_NAME=Maintainarr
|
|
MAINTAINARR_BASE_URL=http://localhost:8080
|
|
MAINTAINARR_THEME=blue
|
|
MAINTAINARR_THEME_MODE=dark
|
|
MAINTAINARR_REFRESH_CRON=@every 5s
|
|
EOF
|
|
|
|
chmod 640 "$ENV_FILE"
|
|
}
|
|
|
|
write_update_script() {
|
|
cat > "$UPDATE_SCRIPT" <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export PATH="/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
|
APP_USER="$APP_USER"
|
|
SRC_DIR="$SRC_DIR"
|
|
BIN_PATH="$BIN_PATH"
|
|
REPO_BRANCH="$REPO_BRANCH"
|
|
|
|
if [[ ! -d "\$SRC_DIR/.git" ]]; then
|
|
echo "Maintainarr source directory not found: \$SRC_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "\$SRC_DIR" fetch --tags origin
|
|
git -C "\$SRC_DIR" checkout "\$REPO_BRANCH"
|
|
git -C "\$SRC_DIR" pull --ff-only origin "\$REPO_BRANCH"
|
|
|
|
(
|
|
cd "\$SRC_DIR"
|
|
export CGO_ENABLED=0
|
|
go build -o "\$BIN_PATH" ./cmd/maintainarr
|
|
)
|
|
|
|
chown "\$APP_USER:\$APP_USER" "\$BIN_PATH"
|
|
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet maintainarr 2>/dev/null; then
|
|
systemctl restart maintainarr
|
|
fi
|
|
EOF
|
|
|
|
chmod 755 "$UPDATE_SCRIPT"
|
|
}
|
|
|
|
write_systemd_service() {
|
|
cat > "$SYSTEMD_UNIT" <<EOF
|
|
[Unit]
|
|
Description=Maintainarr service
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$APP_USER
|
|
Group=$APP_USER
|
|
WorkingDirectory=$SRC_DIR
|
|
EnvironmentFile=$ENV_FILE
|
|
ExecStart=$BIN_PATH
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
}
|
|
|
|
enable_start_on_boot() {
|
|
if ! command_exists systemctl; then
|
|
log "systemd is not available; skipping boot-start setup"
|
|
return
|
|
fi
|
|
|
|
write_systemd_service
|
|
systemctl daemon-reload
|
|
systemctl enable --now maintainarr
|
|
log "Enabled and started maintainarr.service"
|
|
}
|
|
|
|
configure_auto_updates() {
|
|
write_update_script
|
|
|
|
cat > "$CRON_FILE" <<EOF
|
|
SHELL=/bin/bash
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
17 3 * * * root $UPDATE_SCRIPT >> /var/log/maintainarr-update.log 2>&1
|
|
EOF
|
|
|
|
chmod 644 "$CRON_FILE"
|
|
|
|
if command_exists systemctl; then
|
|
if systemctl list-unit-files | grep -q '^cron\.service'; then
|
|
systemctl enable --now cron >/dev/null 2>&1 || true
|
|
elif systemctl list-unit-files | grep -q '^crond\.service'; then
|
|
systemctl enable --now crond >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
log "Installed daily update job in $CRON_FILE"
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
install_base_packages
|
|
install_go
|
|
ensure_user
|
|
clone_or_update_repo
|
|
write_env_file
|
|
build_binary
|
|
write_update_script
|
|
|
|
mkdir -p "$DATA_DIR"
|
|
chown -R "$APP_USER:$APP_USER" "$INSTALL_ROOT" "$DATA_DIR"
|
|
chown root:"$APP_USER" "$ENV_FILE"
|
|
|
|
if prompt_yes_no "Enable automatic daily updates with cron?" "n"; then
|
|
configure_auto_updates
|
|
else
|
|
rm -f "$CRON_FILE"
|
|
log "Skipped cron auto-update setup"
|
|
fi
|
|
|
|
if prompt_yes_no "Start Maintainarr at boot with systemd?" "y"; then
|
|
enable_start_on_boot
|
|
else
|
|
log "Skipped boot-start setup"
|
|
log "Run it manually with: $BIN_PATH"
|
|
fi
|
|
|
|
log "Install complete"
|
|
log "Config: $ENV_FILE"
|
|
log "Data: $DATA_DIR"
|
|
log "Source: $SRC_DIR"
|
|
log "Binary: $BIN_PATH"
|
|
}
|
|
|
|
main "$@"
|