Skip to content

Instantly share code, notes, and snippets.

@PedroCavaleiro
Last active June 3, 2026 09:21
Show Gist options
  • Select an option

  • Save PedroCavaleiro/e1d5e2b01da159d95b10e5c2356afeaf to your computer and use it in GitHub Desktop.

Select an option

Save PedroCavaleiro/e1d5e2b01da159d95b10e5c2356afeaf to your computer and use it in GitHub Desktop.
Upload Assistant Installer
#!/usr/bin/env bash
# Installs Audionut's fork of Upload Assistant https://github.com/Audionut/Upload-Assistant
# install-upload-assistant.sh
# Must be run as root (sudo or direct root — works in both)
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'
BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}${BOLD}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}${BOLD}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}[WARN]${RESET} $*"; }
die() { echo -e "${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; }
[[ "$EUID" -eq 0 ]] || die "This script must be run as root."
if [[ -n "${SUDO_USER:-}" ]]; then
REAL_USER="$SUDO_USER"
REAL_HOME="$(getent passwd "$REAL_USER" | cut -d: -f6)"
else
REAL_USER="root"
REAL_HOME="$HOME"
fi
NVM_DIR="${REAL_HOME}/.nvm"
INSTALL_DIR="/opt/Upload-Assistant"
CONFIG_JSON="${INSTALL_DIR}/data/config.json"
UA_BIN="/usr/bin/upload-assistant"
info "Cloning Upload-Assistant into ${INSTALL_DIR} ..."
if [[ -d "$INSTALL_DIR/.git" ]]; then
warn "Directory already exists — fetching latest instead of cloning."
git -C "$INSTALL_DIR" fetch --all --tags --quiet
else
git clone https://github.com/Audionut/Upload-Assistant.git "$INSTALL_DIR"
fi
cd "$INSTALL_DIR"
info "Fetching all tags ..."
git fetch --all --tags --quiet
LATEST_TAG="$(git tag --sort=-version:refname | head -n 1)"
if [[ -z "$LATEST_TAG" ]]; then
warn "No tags found; staying on default branch."
else
info "Checking out latest tag: ${BOLD}${LATEST_TAG}${RESET}"
git checkout "tags/${LATEST_TAG}" --quiet
success "Checked out ${LATEST_TAG}"
fi
info "Installing system packages (python3-venv, pip, ffmpeg, curl) ..."
apt-get update -qq
apt-get install -y python3-venv python3-pip ffmpeg curl -qq
success "System packages installed."
info "Installing nvm for user: ${REAL_USER} (home: ${REAL_HOME}) ..."
_run_as_real_user() {
if [[ "$REAL_USER" == "root" ]] || [[ "$(whoami)" == "$REAL_USER" ]]; then
HOME="$REAL_HOME" bash -c "$1"
elif command -v sudo &>/dev/null; then
sudo -u "$REAL_USER" HOME="$REAL_HOME" bash -c "$1"
else
die "Cannot run commands as '${REAL_USER}': sudo is not available."
fi
}
_run_as_real_user "
export HOME='${REAL_HOME}'
export NVM_DIR='${NVM_DIR}'
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
. \"\${NVM_DIR}/nvm.sh\"
nvm install 24
nvm use 24
nvm alias default 24
"
# Load nvm into the current shell session
export NVM_DIR="${NVM_DIR}"
# shellcheck disable=SC1091
[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
# Symlink node/npm/npx into /usr/local/bin so they are on PATH for all users
# without needing to source nvm in every shell
NODE_BIN_DIR="$(dirname "$(. "${NVM_DIR}/nvm.sh" && nvm which current)")"
info "Symlinking node, npm, npx from ${NODE_BIN_DIR} into /usr/local/bin ..."
ln -sf "${NODE_BIN_DIR}/node" /usr/local/bin/node
ln -sf "${NODE_BIN_DIR}/npm" /usr/local/bin/npm
ln -sf "${NODE_BIN_DIR}/npx" /usr/local/bin/npx
success "Node.js $(node --version) installed via nvm."
# Install pm2 globally
if ! command -v pm2 &>/dev/null; then
info "Installing pm2 via npm ..."
npm install -g pm2 --quiet
fi
# Symlink pm2 into /usr/local/bin as well
PM2_PATH="$(which pm2 2>/dev/null || true)"
if [[ -n "$PM2_PATH" ]] && [[ "$PM2_PATH" != "/usr/local/bin/pm2" ]]; then
ln -sf "$PM2_PATH" /usr/local/bin/pm2
fi
success "pm2 $(pm2 --version) installed and symlinked."
info "Creating Python virtual environment ..."
python3 -m venv "${INSTALL_DIR}/venv"
# shellcheck disable=SC1091
source "${INSTALL_DIR}/venv/bin/activate"
success "Virtual environment activated."
REQ_FILE=""
for f in requirements.txt requirements; do
[[ -f "${INSTALL_DIR}/${f}" ]] && REQ_FILE="${INSTALL_DIR}/${f}" && break
done
if [[ -n "$REQ_FILE" ]]; then
info "Installing Python requirements from ${REQ_FILE} ..."
pip install --quiet -r "$REQ_FILE"
success "Python requirements installed."
else
warn "No requirements file found — skipping pip install."
fi
echo ""
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} Configuration Setup${RESET}"
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e " ${CYAN}a${RESET}) Automatically generate config (runs config-generator.py)"
echo -e " ${CYAN}m${RESET}) Manually edit config (copies example and opens nano)"
echo ""
read -rp "$(echo -e "${BOLD}Your choice [a/m]:${RESET} ")" CONFIG_CHOICE
case "${CONFIG_CHOICE,,}" in
a|auto)
info "Running automatic config generator ..."
python3 "${INSTALL_DIR}/config-generator.py" || die "config-generator.py failed."
success "Config generated automatically."
;;
m|manual|*)
info "Copying example config and opening nano ..."
cp "${INSTALL_DIR}/data/example-config.py" "${INSTALL_DIR}/config.py"
nano "${INSTALL_DIR}/config.py"
success "Manual config saved."
;;
esac
echo ""
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} Storage Paths${RESET}"
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "Enter one or more paths where media files are stored."
echo -e "Wrap paths that contain spaces in ${BOLD}double quotes${RESET}."
echo -e "Example: ${CYAN}/mnt/sda1/downloads \"/mnt/sda1/my downloads\" /mnt/sdb1/media${RESET}"
echo ""
read -rp "$(echo -e "${BOLD}Paths:${RESET} ")" RAW_PATHS
MEDIA_PATHS=()
eval "MEDIA_PATHS=($RAW_PATHS)"
if [[ ${#MEDIA_PATHS[@]} -eq 0 ]]; then
die "No paths provided."
fi
echo ""
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} Web UI Bind Address${RESET}"
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
read -rp "$(echo -e "${BOLD}Bind address [default: 0.0.0.0:8080]:${RESET} ")" BIND_INPUT
BIND="${BIND_INPUT:-0.0.0.0:8080}"
mkdir -p "${INSTALL_DIR}/data"
PATHS_JSON="["
for i in "${!MEDIA_PATHS[@]}"; do
path="${MEDIA_PATHS[$i]}"
path_escaped="${path//\\/\\\\}"
path_escaped="${path_escaped//\"/\\\"}"
[[ $i -gt 0 ]] && PATHS_JSON+=","
PATHS_JSON+="\"${path_escaped}\""
done
PATHS_JSON+="]"
cat > "$CONFIG_JSON" <<EOF
{
"paths": ${PATHS_JSON},
"bind": "${BIND}"
}
EOF
success "Saved configuration to ${CONFIG_JSON}"
info "Generating ${UA_BIN} control script ..."
cat > "$UA_BIN" << 'UASCRIPT_EOF'
#!/usr/bin/env bash
# upload-assistant — control script for Upload-Assistant
# Auto-generated by install-upload-assistant.sh
set -euo pipefail
INSTALL_DIR="/opt/Upload-Assistant"
CONFIG_JSON="${INSTALL_DIR}/data/config.json"
VENV_PYTHON="${INSTALL_DIR}/venv/bin/python3"
PM2_APP_NAME="upload-assistant"
# node/npm/pm2 are symlinked into /usr/local/bin at install time —
# no nvm sourcing needed at runtime.
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'
BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}${BOLD}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}${BOLD}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}[WARN]${RESET} $*"; }
die() { echo -e "${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; }
# ── Sanity checks ─────────────────────────────────────────────────────────────
command -v pm2 &>/dev/null || die "pm2 not found on PATH. Re-run the installer."
command -v python3 &>/dev/null || die "python3 not found on PATH."
# ── Parse config.json helpers ─────────────────────────────────────────────────
_read_bind() {
python3 -c "import json; d=json.load(open('${CONFIG_JSON}')); print(d.get('bind','0.0.0.0:8080'))"
}
_read_paths_args() {
python3 - <<'PYEOF'
import json, shlex
with open("/opt/Upload-Assistant/data/config.json") as f:
d = json.load(f)
print(" ".join(shlex.quote(p) for p in d.get("paths", [])))
PYEOF
}
# ── Commands ──────────────────────────────────────────────────────────────────
cmd_start() {
info "Starting ${PM2_APP_NAME} ..."
pm2 start "${INSTALL_DIR}/upload.py" \
--name "${PM2_APP_NAME}" \
--interpreter "${VENV_PYTHON}" \
-- $(_read_paths_args) --webui "$(_read_bind)"
success "Upload-Assistant started."
}
cmd_stop() {
info "Stopping ${PM2_APP_NAME} ..."
pm2 stop "${PM2_APP_NAME}"
success "Upload-Assistant stopped."
}
cmd_restart() {
info "Restarting ${PM2_APP_NAME} ..."
pm2 restart "${PM2_APP_NAME}"
success "Upload-Assistant restarted."
}
cmd_enable() {
info "Enabling startup on boot ..."
pm2 startup
pm2 save
success "Upload-Assistant will start on boot."
}
cmd_disable() {
info "Disabling startup on boot ..."
pm2 unstartup
success "Boot startup disabled."
}
cmd_edit_config() {
nano "${INSTALL_DIR}/config.py"
info "Config saved. Restarting Upload-Assistant ..."
pm2 restart "${PM2_APP_NAME}"
success "Upload-Assistant restarted with new config."
}
cmd_set_paths() {
echo ""
echo -e "${BOLD}Current paths:${RESET}"
python3 - <<'PYEOF'
import json
with open("/opt/Upload-Assistant/data/config.json") as f:
d = json.load(f)
for i, p in enumerate(d.get("paths", []), 1):
print(f" {i}. {p}")
PYEOF
echo ""
read -rp "$(echo -e "${BOLD}Enter numbers of paths to REMOVE (space-separated, or blank to keep all):${RESET} ")" REMOVE_INPUT
echo ""
echo -e "Enter new paths to ADD (quoted if they contain spaces, blank to skip):"
read -rp "$(echo -e "${BOLD}New paths:${RESET} ")" RAW_NEW
python3 - "${REMOVE_INPUT}" "${RAW_NEW}" <<'PYEOF'
import json, sys, shlex
config_path = "/opt/Upload-Assistant/data/config.json"
with open(config_path) as f:
d = json.load(f)
paths = d.get("paths", [])
remove_input = sys.argv[1].strip()
raw_new = sys.argv[2].strip()
if remove_input:
to_remove = set()
for token in remove_input.split():
try:
to_remove.add(int(token) - 1)
except ValueError:
pass
paths = [p for i, p in enumerate(paths) if i not in to_remove]
if raw_new:
paths.extend(shlex.split(raw_new))
d["paths"] = paths
with open(config_path, "w") as f:
json.dump(d, f, indent=2)
print("Updated paths:")
for p in paths:
print(f" • {p}")
PYEOF
info "Restarting Upload-Assistant with new paths ..."
pm2 restart "${PM2_APP_NAME}"
success "Paths updated and service restarted."
}
cmd_set_bind() {
local current new_bind
current="$(_read_bind)"
echo -e "${BOLD}Current bind address:${RESET} ${current}"
read -rp "$(echo -e "${BOLD}New bind address [ip:port]:${RESET} ")" new_bind
[[ -z "$new_bind" ]] && { warn "No input — keeping current bind address."; return; }
python3 - "${new_bind}" <<'PYEOF'
import json, sys
config_path = "/opt/Upload-Assistant/data/config.json"
with open(config_path) as f:
d = json.load(f)
d["bind"] = sys.argv[1]
with open(config_path, "w") as f:
json.dump(d, f, indent=2)
print(f"Bind address updated to: {sys.argv[1]}")
PYEOF
info "Restarting Upload-Assistant with new bind address ..."
pm2 restart "${PM2_APP_NAME}"
success "Bind address updated and service restarted."
}
cmd_up() {
if [[ "${VIRTUAL_ENV:-}" == "${INSTALL_DIR}/venv" ]]; then
python3 "${INSTALL_DIR}/upload.py" "$@"
else
"${VENV_PYTHON}" "${INSTALL_DIR}/upload.py" "$@"
fi
}
cmd_help() {
echo ""
echo -e "${BOLD}upload-assistant${RESET} — Upload-Assistant control utility"
echo ""
echo -e " ${CYAN}start${RESET} Start the service via pm2"
echo -e " ${CYAN}stop${RESET} Stop the service via pm2"
echo -e " ${CYAN}restart${RESET} Restart the service via pm2"
echo -e " ${CYAN}enable${RESET} Enable auto-start on boot (pm2 startup + save)"
echo -e " ${CYAN}disable${RESET} Disable auto-start on boot"
echo -e " ${CYAN}edit-config${RESET} Edit config.py in nano, then auto-restart"
echo -e " ${CYAN}set-paths${RESET} Interactively update media paths, then restart"
echo -e " ${CYAN}set-bind${RESET} Change the WebUI bind address, then restart"
echo -e " ${CYAN}up [args]${RESET} Run upload.py directly (always inside venv)"
echo ""
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
COMMAND="${1:-help}"
shift || true
case "$COMMAND" in
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
edit-config) cmd_edit_config ;;
set-paths) cmd_set_paths ;;
set-bind) cmd_set_bind ;;
up) cmd_up "$@" ;;
help|--help|-h) cmd_help ;;
*)
die "Unknown command: '${COMMAND}'. Run 'upload-assistant help' for usage."
;;
esac
UASCRIPT_EOF
chmod +x "$UA_BIN"
success "Control script installed at ${UA_BIN}"
command -v upload-assistant &>/dev/null \
&& success "upload-assistant is available on PATH (${UA_BIN})" \
|| warn "upload-assistant not found on PATH — you may need to open a new shell."
echo ""
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} pm2 Startup${RESET}"
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
read -rp "$(echo -e "${BOLD}Start Upload-Assistant now and enable boot startup? [Y/n]:${RESET} ")" START_NOW
START_NOW="${START_NOW:-Y}"
if [[ "${START_NOW,,}" == "y" ]]; then
info "Starting Upload-Assistant via pm2 ..."
PATHS_ARGS_ARR=()
for p in "${MEDIA_PATHS[@]}"; do
PATHS_ARGS_ARR+=("$p")
done
pm2 start "${INSTALL_DIR}/upload.py" \
--name "upload-assistant" \
--interpreter "${INSTALL_DIR}/venv/bin/python3" \
-- "${PATHS_ARGS_ARR[@]}" --webui "${BIND}"
pm2 startup
pm2 save
success "Upload-Assistant is running and configured to start on boot."
else
info "Skipping startup. Run 'upload-assistant start' when ready."
info "Run 'upload-assistant enable' to enable boot startup later."
fi
echo ""
echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${GREEN}${BOLD} Installation complete!${RESET}"
echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo ""
echo -e " ${BOLD}Install directory:${RESET} ${INSTALL_DIR}"
echo -e " ${BOLD}Config file:${RESET} ${INSTALL_DIR}/config.py"
echo -e " ${BOLD}Data config:${RESET} ${CONFIG_JSON}"
echo -e " ${BOLD}Control script:${RESET} ${UA_BIN}"
echo -e " ${BOLD}WebUI bind:${RESET} ${BIND}"
echo ""
echo -e " Run ${CYAN}${BOLD}upload-assistant help${RESET} to see all available commands."
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment