Files
gitlab-instance-0a899031_do…/scripts/linux/lib/common.sh

736 lines
21 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "$LIB_DIR/../../.." && pwd)}"
CONFIG_DIR="$ROOT_DIR/config"
STACK_ENV="$CONFIG_DIR/system-stack.env"
STACK_ENV_EXAMPLE="$CONFIG_DIR/system-stack.env.example"
SERVICE_CTL="$ROOT_DIR/scripts/linux/service_ctl.sh"
WEBTTY_SHELL="$ROOT_DIR/scripts/linux/webtty_shell.sh"
APT_STAMP="/tmp/live-stack-apt-updated.stamp"
INSTALL_TAG="${INSTALL_TAG:-stack}"
log() {
printf '[%s] %s\n' "$INSTALL_TAG" "$*"
}
ensure_root() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
exec sudo -E bash "$0" "$@"
fi
}
apt_update_once() {
local now age
now="$(date +%s)"
if [ -f "$APT_STAMP" ]; then
age=$((now - $(stat -c %Y "$APT_STAMP")))
if [ "$age" -lt 21600 ]; then
return 0
fi
fi
log "Updating apt metadata"
apt-get update
touch "$APT_STAMP"
}
apt_install() {
apt_update_once
apt-get install -y --no-install-recommends "$@"
}
have() {
command -v "$1" >/dev/null 2>&1
}
need_docker() {
[ "${ENABLE_SRS:-1}" = "1" ] || [ "${ENABLE_HOMEPAGE:-1}" = "1" ] || [ "${ENABLE_FILEBROWSER:-1}" = "1" ] || [ "${ENABLE_NEKO:-0}" = "1" ]
}
compose_run() {
local compose_file="$1"
shift
if docker compose version >/dev/null 2>&1; then
docker compose -f "$compose_file" "$@"
else
docker-compose -f "$compose_file" "$@"
fi
}
as_live() {
runuser -u live -- bash -lc "cd '$ROOT_DIR' && $*"
}
load_env() {
mkdir -p "$CONFIG_DIR"
if [ ! -f "$STACK_ENV" ] && [ -f "$STACK_ENV_EXAMPLE" ]; then
cp "$STACK_ENV_EXAMPLE" "$STACK_ENV"
fi
if [ -f "$STACK_ENV" ]; then
set -a
. "$STACK_ENV"
set +a
fi
HOSTNAME_ALIAS="${HOSTNAME_ALIAS:-live}"
PORT="${PORT:-8001}"
WEBTTY_PORT="${WEBTTY_PORT:-7681}"
HOMEPAGE_PORT="${HOMEPAGE_PORT:-80}"
FILEBROWSER_PORT="${FILEBROWSER_PORT:-8082}"
NETDATA_PORT="${NETDATA_PORT:-19999}"
SRS_HTTP_PORT="${SRS_HTTP_PORT:-8080}"
SRS_API_PORT="${SRS_API_PORT:-1985}"
SRS_RTMP_PORT="${SRS_RTMP_PORT:-1935}"
SRS_SRT_PORT="${SRS_SRT_PORT:-10080}"
SRS_WEBRTC_PORT="${SRS_WEBRTC_PORT:-8088}"
SHELLCRASH_DIR="${SHELLCRASH_DIR:-/etc/ShellCrash}"
WEBTTY_USER="${WEBTTY_USER:-live}"
WEBTTY_PASS="${WEBTTY_PASS:-12345678}"
FILEBROWSER_USER="${FILEBROWSER_USER:-live}"
FILEBROWSER_PASS="${FILEBROWSER_PASS:-12345678}"
WIFI_CONNECTION_NAME="${WIFI_CONNECTION_NAME:-live-wifi}"
WIFI_SSID="${WIFI_SSID:-live}"
WIFI_PSK="${WIFI_PSK:-12345678}"
BROWSER_PROFILE_DIR="${BROWSER_PROFILE_DIR:-/var/lib/live-browser/profile}"
DISABLE_IPV6="${DISABLE_IPV6:-1}"
ANDROID_PANEL_PORT="${ANDROID_PANEL_PORT:-5000}"
ANDROID_PANEL_DIR="${ANDROID_PANEL_DIR:-/opt/live-modules/web-scrcpy}"
ANDROID_PANEL_VIDEO_BIT_RATE="${ANDROID_PANEL_VIDEO_BIT_RATE:-1024000}"
}
ensure_live_user() {
if ! id -u live >/dev/null 2>&1; then
log "Creating live user"
useradd -m -s /bin/bash live
fi
echo "live:12345678" | chpasswd
usermod -aG sudo,audio,video,plugdev,render,gpio,spidev,pwm,i2c,docker live 2>/dev/null || true
}
install_base_packages() {
log "Installing base packages"
export DEBIAN_FRONTEND=noninteractive
apt_install \
git curl wget jq unzip xz-utils ca-certificates sudo \
python3 python3-venv python3-pip build-essential \
avahi-daemon avahi-utils libnss-mdns \
network-manager dnsmasq net-tools \
bluez
systemctl enable --now NetworkManager 2>/dev/null || true
}
install_media_packages() {
log "Installing media and Android tools"
export DEBIAN_FRONTEND=noninteractive
apt_install ffmpeg mpv adb xinit xserver-xorg openbox dbus-x11
apt_install scrcpy || true
}
install_nodejs() {
if have node; then
local major
major="$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)"
if [ "$major" -ge 20 ]; then
return 0
fi
fi
log "Installing Node.js 20"
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt_install nodejs
}
install_docker_stack() {
if ! need_docker; then
return 0
fi
if have docker && (docker compose version >/dev/null 2>&1 || have docker-compose); then
return 0
fi
log "Installing Docker"
apt_install docker.io docker-compose
systemctl enable --now docker
usermod -aG docker live || true
}
prepare_python_runtime() {
log "Preparing Python virtual environment"
chown -R live:live "$ROOT_DIR"
as_live "python3 -m venv .venv"
as_live ". .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt"
}
prepare_web_console() {
log "Building web console"
as_live "cd web-console && if [ -f package-lock.json ]; then npm ci; else npm install; fi && npm run build"
}
install_live_console_service() {
log "Installing live console service"
chmod +x "$ROOT_DIR/start.sh" "$ROOT_DIR/web.sh" "$ROOT_DIR/dev.sh" "$SERVICE_CTL" "$WEBTTY_SHELL"
cat >/etc/systemd/system/live-console.service <<UNIT
[Unit]
Description=Live Super Hub Control Plane
After=network-online.target docker.service
Wants=network-online.target
[Service]
User=live
WorkingDirectory=$ROOT_DIR
Environment=HOST=0.0.0.0
Environment=PORT=$PORT
ExecStart=$ROOT_DIR/start.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now live-console.service
}
configure_sudoers() {
log "Configuring sudoers for live control plane"
cat >/etc/sudoers.d/live-control-plane <<SUDOERS
live ALL=(root) NOPASSWD: /bin/bash $SERVICE_CTL *
SUDOERS
chmod 440 /etc/sudoers.d/live-control-plane
}
detect_mdns_interfaces() {
local primary iface path found=0
local -a ifaces=()
primary="$(ip route show default 2>/dev/null | awk 'NR==1 {print $5}')"
if [ -n "$primary" ] && [ -d "/sys/class/net/$primary" ]; then
ifaces+=("$primary")
found=1
fi
for path in /sys/class/net/*; do
[ -e "$path" ] || continue
iface="${path##*/}"
[ "$iface" = "lo" ] && continue
if [ -d "$path/wireless" ]; then
case " ${ifaces[*]} " in
*" $iface "*) ;;
*)
ifaces+=("$iface")
found=1
;;
esac
fi
done
if [ "$found" -eq 0 ]; then
for path in /sys/class/net/*; do
[ -e "$path" ] || continue
iface="${path##*/}"
case "$iface" in
lo|docker*|br-*|veth*|virbr*|tun*|tap*|zt*|tailscale*|wg*|sit*|dummy*)
continue
;;
esac
ifaces+=("$iface")
found=1
break
done
fi
if [ "${#ifaces[@]}" -gt 0 ]; then
local joined
joined="$(IFS=,; echo "${ifaces[*]}")"
printf '%s\n' "$joined"
fi
}
configure_mdns() {
if [ "${ENABLE_MDNS:-1}" != "1" ]; then
return 0
fi
log "Configuring hostname and mDNS"
hostnamectl set-hostname "$HOSTNAME_ALIAS"
local mdns_interfaces
mdns_interfaces="$(detect_mdns_interfaces)"
python3 - <<PY
from pathlib import Path
import re
hosts = Path("/etc/hosts")
hosts_text = hosts.read_text(encoding="utf-8")
if f"127.0.1.1\t${HOSTNAME_ALIAS}" not in hosts_text and f"127.0.1.1 ${HOSTNAME_ALIAS}" not in hosts_text:
lines = []
replaced = False
for raw in hosts_text.splitlines():
if raw.startswith("127.0.1.1"):
lines.append(f"127.0.1.1\t${HOSTNAME_ALIAS}")
replaced = True
else:
lines.append(raw)
if not replaced:
lines.append(f"127.0.1.1\t${HOSTNAME_ALIAS}")
hosts.write_text("\n".join(lines) + "\n", encoding="utf-8")
path = Path("/etc/avahi/avahi-daemon.conf")
raw_lines = path.read_text(encoding="utf-8").splitlines()
server_keys = {
"use-ipv4": "yes",
"use-ipv6": "no",
}
mdns_interfaces = "${mdns_interfaces}".strip()
if mdns_interfaces:
server_keys["allow-interfaces"] = mdns_interfaces
key_pattern = re.compile(r"^\s*[#;]?\s*([A-Za-z0-9-]+)\s*=.*$")
section_pattern = re.compile(r"^\s*\[([^\]]+)\]\s*$")
lines = []
current_section = None
server_seen = set()
server_found = False
def flush_server_defaults():
missing = [f"{key}={value}" for key, value in server_keys.items() if key not in server_seen]
if missing:
lines.extend(missing)
for raw in raw_lines:
section_match = section_pattern.match(raw)
if section_match:
if current_section == "server":
flush_server_defaults()
current_section = section_match.group(1).strip().lower()
server_found = server_found or current_section == "server"
lines.append(raw)
continue
key_match = key_pattern.match(raw)
if key_match and key_match.group(1) in server_keys:
key = key_match.group(1)
if current_section == "server":
if key not in server_seen:
lines.append(f"{key}={server_keys[key]}")
server_seen.add(key)
continue
lines.append(raw)
if server_found:
if current_section == "server":
flush_server_defaults()
else:
if lines and lines[-1] != "":
lines.append("")
lines.append("[server]")
flush_server_defaults()
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
if [ "$DISABLE_IPV6" = "1" ]; then
cat >/etc/sysctl.d/99-live-disable-ipv6.conf <<CONF
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
CONF
sysctl --system >/dev/null 2>&1 || true
fi
systemctl enable --now avahi-daemon
systemctl restart avahi-daemon
}
configure_wifi_profile() {
if [ "${ENABLE_WIFI_PROFILE:-1}" != "1" ]; then
return 0
fi
if ! have nmcli || [ ! -d /sys/class/net/wlan0 ]; then
log "Skipping Wi-Fi profile setup: wlan0 or nmcli is missing"
return 0
fi
log "Ensuring Wi-Fi profile ${WIFI_CONNECTION_NAME}"
if ! nmcli connection show "$WIFI_CONNECTION_NAME" >/dev/null 2>&1; then
nmcli connection add type wifi ifname wlan0 con-name "$WIFI_CONNECTION_NAME" ssid "$WIFI_SSID" \
wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$WIFI_PSK" ipv4.method auto connection.autoconnect yes
fi
}
install_shellcrash() {
if [ "${ENABLE_SHELLCRASH:-1}" != "1" ]; then
return 0
fi
if [ -x "$SHELLCRASH_DIR/start.sh" ]; then
log "ShellCrash already installed"
return 0
fi
log "Installing ShellCrash"
local url
for url in \
'https://testingcf.jsdelivr.net/gh/juewuy/ShellCrash@dev' \
'https://raw.githubusercontent.com/juewuy/ShellCrash/dev'
do
if wget -q --no-check-certificate -O /tmp/install_shellcrash.sh "$url/install_en.sh" && [ -s /tmp/install_shellcrash.sh ]; then
break
fi
done
if [ ! -s /tmp/install_shellcrash.sh ]; then
log "ShellCrash installer download failed"
return 1
fi
printf '1\n1\n1\n1\n' | bash /tmp/install_shellcrash.sh
}
install_browser_stack() {
if [ "${ENABLE_BROWSER:-1}" != "1" ]; then
return 0
fi
log "Installing browser stack"
export DEBIAN_FRONTEND=noninteractive
apt_install chromium || apt_install chromium-browser || true
mkdir -p /etc/chromium/policies/managed /etc/chromium-browser/policies/managed
cat >/etc/chromium/policies/managed/live-browser.json <<JSON
{
"ExtensionInstallForcelist": [
"dhdgffkkebhmkfjojejmpbldmpobfkfo;https://clients2.google.com/service/update2/crx"
],
"DeveloperToolsAvailability": 1
}
JSON
cp /etc/chromium/policies/managed/live-browser.json /etc/chromium-browser/policies/managed/live-browser.json 2>/dev/null || true
mkdir -p "$BROWSER_PROFILE_DIR"
chown -R live:live "$(dirname "$BROWSER_PROFILE_DIR")"
chmod +x "$ROOT_DIR/scripts/linux/launch_browser.sh"
}
install_neko_browser() {
if [ "${ENABLE_NEKO:-0}" != "1" ]; then
return 0
fi
if ! have docker || ! (docker compose version >/dev/null 2>&1 || have docker-compose); then
log "Neko skipped: docker compose unavailable"
return 0
fi
log "Installing Neko Chromium (Docker, m1k1o/neko)"
set -a
[ -f "$STACK_ENV" ] && . "$STACK_ENV"
set +a
export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}"
compose_run "$ROOT_DIR/services/neko/docker-compose.yml" up -d
}
configure_adb_udev() {
log "Configuring Android udev access"
cat >/etc/udev/rules.d/51-live-android.rules <<RULES
SUBSYSTEM=="usb", ATTR{idVendor}=="2207", MODE="0660", GROUP="plugdev", TAG+="uaccess"
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev", TAG+="uaccess"
RULES
udevadm control --reload-rules || true
udevadm trigger || true
}
install_android_panel() {
if [ "${ENABLE_ANDROID_PANEL:-1}" != "1" ]; then
return 0
fi
log "Installing Android web panel (ws-scrcpy)"
mkdir -p "$(dirname "$ANDROID_PANEL_DIR")"
ANDROID_PANEL_GIT_URL="${ANDROID_PANEL_GIT_URL:-https://github.com/baixin1228/web-scrcpy.git}"
if [ ! -d "$ANDROID_PANEL_DIR/.git" ]; then
rm -rf "$ANDROID_PANEL_DIR"
git clone "$ANDROID_PANEL_GIT_URL" "$ANDROID_PANEL_DIR"
else
git -C "$ANDROID_PANEL_DIR" pull --ff-only
fi
python3 - <<PY
from pathlib import Path
path = Path(r"$ANDROID_PANEL_DIR/app.py")
text = path.read_text(encoding="utf-8")
if "import os" not in text:
text = "import os\n" + text
text = text.replace(
"socketio.run(app, host='0.0.0.0', port=5000)",
"socketio.run(app, host='0.0.0.0', port=int(os.environ.get('ANDROID_PANEL_PORT', '5000')), allow_unsafe_werkzeug=True)",
)
path.write_text(text, encoding="utf-8")
PY
chown -R live:live "$ANDROID_PANEL_DIR"
runuser -u live -- bash -lc "python3 -m venv '$ANDROID_PANEL_DIR/.venv' && . '$ANDROID_PANEL_DIR/.venv/bin/activate' && pip install --upgrade pip && pip install -r '$ANDROID_PANEL_DIR/requirements.txt'"
cat >/etc/systemd/system/live-android-panel.service <<UNIT
[Unit]
Description=Live Android Web Panel
After=network-online.target
Wants=network-online.target
[Service]
User=live
WorkingDirectory=$ANDROID_PANEL_DIR
Environment=ANDROID_PANEL_PORT=$ANDROID_PANEL_PORT
ExecStart=$ANDROID_PANEL_DIR/.venv/bin/python $ANDROID_PANEL_DIR/app.py --video_bit_rate $ANDROID_PANEL_VIDEO_BIT_RATE
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now live-android-panel.service
}
install_webtty() {
if [ "${ENABLE_WEBTTY:-1}" != "1" ]; then
return 0
fi
log "Installing WebTTY"
export DEBIAN_FRONTEND=noninteractive
if ! apt-get install -y --no-install-recommends ttyd >/dev/null 2>&1; then
log "ttyd package unavailable in apt repositories"
fi
if ! have ttyd; then
log "WebTTY skipped because ttyd is unavailable"
return 0
fi
systemctl disable --now ttyd.service >/dev/null 2>&1 || true
cat >/etc/systemd/system/live-webtty.service <<UNIT
[Unit]
Description=Live WebTTY
After=network-online.target
Wants=network-online.target
[Service]
User=live
WorkingDirectory=$ROOT_DIR
ExecStart=$(command -v ttyd) -W -p $WEBTTY_PORT -c $WEBTTY_USER:$WEBTTY_PASS $WEBTTY_SHELL
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now live-webtty.service
}
install_cockpit() {
if [ "${ENABLE_COCKPIT:-1}" != "1" ]; then
return 0
fi
log "Installing Cockpit"
export DEBIAN_FRONTEND=noninteractive
apt_install cockpit cockpit-networkmanager
systemctl enable --now cockpit.socket
}
install_netdata() {
if [ "${ENABLE_NETDATA:-1}" != "1" ]; then
return 0
fi
log "Installing Netdata"
export DEBIAN_FRONTEND=noninteractive
apt_install netdata
python3 - <<PY
from pathlib import Path
path = Path("/etc/netdata/netdata.conf")
text = path.read_text(encoding="utf-8")
if "bind socket to IP = 127.0.0.1" in text:
text = text.replace("bind socket to IP = 127.0.0.1", "bind socket to IP = 0.0.0.0")
elif "bind socket to IP =" not in text:
text += "\n[global]\n\tbind socket to IP = 0.0.0.0\n"
path.write_text(text, encoding="utf-8")
PY
systemctl enable --now netdata
systemctl restart netdata
}
render_homepage_services() {
local target="$ROOT_DIR/services/homepage/config/services.yaml"
log "Rendering Homepage service links"
cat >"$target" <<YAML
- Infrastructure:
- Hub Console:
href: http://${HOSTNAME_ALIAS}.local:${PORT}
description: Chinese-first control plane and modular service center
YAML
if [ "${ENABLE_WEBTTY:-1}" = "1" ]; then
cat >>"$target" <<YAML
- WebTTY:
href: http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}
description: Browser terminal as the live user
YAML
fi
if [ "${ENABLE_COCKPIT:-1}" = "1" ]; then
cat >>"$target" <<YAML
- Cockpit:
href: https://${HOSTNAME_ALIAS}.local:9090
description: System administration
YAML
fi
if [ "${ENABLE_FILEBROWSER:-1}" = "1" ]; then
cat >>"$target" <<YAML
- File Browser:
href: http://${HOSTNAME_ALIAS}.local:${FILEBROWSER_PORT}
description: Beginner-friendly file manager
YAML
fi
if [ "${ENABLE_ANDROID_PANEL:-1}" = "1" ]; then
cat >>"$target" <<YAML
- Android Center:
href: http://${HOSTNAME_ALIAS}.local:${PORT}/android
description: Device center with preview, ADB controls and embedded web-scrcpy
YAML
fi
if [ "${ENABLE_SHELLCRASH:-1}" = "1" ]; then
cat >>"$target" <<YAML
- ShellCrash:
href: http://${HOSTNAME_ALIAS}.local:${PORT}/shellcrash
description: Dedicated YAML, JSON and rule editing workspace
YAML
fi
if [ "${ENABLE_NETDATA:-1}" = "1" ]; then
cat >>"$target" <<YAML
- Netdata:
href: http://${HOSTNAME_ALIAS}.local:${NETDATA_PORT}
description: Metrics and health monitoring
YAML
fi
if [ "${ENABLE_SRS:-1}" = "1" ]; then
cat >>"$target" <<YAML
- SRS:
href: http://${HOSTNAME_ALIAS}.local:${SRS_HTTP_PORT}
description: RTMP, SRT and preview endpoint
YAML
fi
}
prepare_homepage() {
if [ "${ENABLE_HOMEPAGE:-1}" != "1" ] || ! need_docker; then
return 0
fi
log "Preparing Homepage"
render_homepage_services
compose_run "$ROOT_DIR/services/homepage/docker-compose.yml" up -d
}
prepare_filebrowser() {
if [ "${ENABLE_FILEBROWSER:-1}" != "1" ] || ! need_docker; then
return 0
fi
log "Preparing File Browser"
local fb_image="filebrowser/filebrowser:latest"
local fb_db_dir="$ROOT_DIR/services/filebrowser/database"
local fb_cfg_dir="$ROOT_DIR/services/filebrowser/config"
local fb_db="$fb_db_dir/filebrowser.db"
local fb_settings="$fb_cfg_dir/settings.json"
local fb_tmp_json fb_hash
mkdir -p "$ROOT_DIR/services/filebrowser/database" "$ROOT_DIR/services/filebrowser/config"
rm -f "$fb_db"
touch "$fb_db"
chmod -R 0777 "$ROOT_DIR/services/filebrowser/database" "$ROOT_DIR/services/filebrowser/config"
compose_run "$ROOT_DIR/services/filebrowser/docker-compose.yml" down >/dev/null 2>&1 || true
docker run --rm \
-v "$fb_db_dir:/database" \
-v "$fb_cfg_dir:/config" \
"$fb_image" \
config init --config /config/settings.json --database /database/filebrowser.db >/dev/null 2>&1 || true
docker run --rm \
-v "$fb_db_dir:/database" \
-v "$fb_cfg_dir:/config" \
"$fb_image" \
config set \
--address 0.0.0.0 \
--port 80 \
--locale zh-cn \
--auth.method json \
--minimumPasswordLength 8 \
--hideLoginButton=false \
--branding.name "Live File Browser" \
--config /config/settings.json \
--database /database/filebrowser.db >/dev/null
fb_hash="$(docker run --rm "$fb_image" hash "$FILEBROWSER_PASS" | tail -n 1)"
fb_tmp_json="$fb_cfg_dir/filebrowser-users.import.json"
FILEBROWSER_HASH="$fb_hash" FILEBROWSER_USER="$FILEBROWSER_USER" python3 - <<'PY' >"$fb_tmp_json"
import json
import os
password_hash = os.environ["FILEBROWSER_HASH"].strip()
username = os.environ["FILEBROWSER_USER"].strip() or "live"
base_user = {
"scope": ".",
"locale": "zh-cn",
"lockPassword": False,
"viewMode": "mosaic",
"singleClick": False,
"redirectAfterCopyMove": True,
"perm": {
"admin": True,
"execute": True,
"create": True,
"rename": True,
"modify": True,
"delete": True,
"share": True,
"download": True,
},
"commands": [],
"sorting": {"by": "name", "asc": False},
"rules": [],
"hideDotfiles": False,
"dateFormat": False,
"aceEditorTheme": "",
}
print(
json.dumps(
[
{"id": 1, "username": "admin", "password": password_hash, **base_user},
{"id": 0, "username": username, "password": password_hash, **base_user},
],
indent=2,
)
)
PY
chmod 0666 "$fb_tmp_json"
docker run --rm \
-w /config \
-v "$fb_db_dir:/database" \
-v "$fb_cfg_dir:/config" \
"$fb_image" \
users import $(basename "$fb_tmp_json") --overwrite --config /config/settings.json --database /database/filebrowser.db >/dev/null
rm -f "$fb_cfg_dir/users.backup.json"
rm -f "$fb_tmp_json"
compose_run "$ROOT_DIR/services/filebrowser/docker-compose.yml" up -d
}
prepare_srs() {
if [ "${ENABLE_SRS:-1}" != "1" ] || ! need_docker; then
return 0
fi
log "Starting SRS"
mkdir -p "$ROOT_DIR/services/srs/data"
compose_run "$ROOT_DIR/services/srs/docker-compose.yml" up -d
}
run_hardware_probe() {
log "Probing hardware profile"
as_live ". .venv/bin/activate && python scripts/hardware_probe.py --write"
}
print_summary() {
local profile="$1"
cat <<TXT
Profile: $profile
Control Plane: http://${HOSTNAME_ALIAS}.local:${PORT}
Homepage: http://${HOSTNAME_ALIAS}.local${HOMEPAGE_PORT:+:${HOMEPAGE_PORT}}
WebTTY: http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}
TXT
}