Files
share/install.sh
T
eberkheev 7af9aec0b3 feat(deploy): add install script and untrack share.config
Package the service for setup on a new machine:

- install.sh generates a token, writes share.config (mode 600, token
  stored as a SHA-256 hash) and installs the systemd user unit from
  share.service.template
- share.config is now gitignored, with share.config.example as the
  tracked template — the config holds the secret and must not be in
  the repo
- warn at startup when token is empty, since that means open access
- SETUP.md documents install, verification and security posture
2026-07-27 02:07:21 +04:00

147 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Share — setup on a new machine.
#
# ./install.sh generate a random token, install + start service
# ./install.sh --token mysecret use your own token
# ./install.sh --port 4000 --dir ~/box override port / upload dir
# ./install.sh --san 192.168.0.1 extra IP in the TLS cert SAN (repeatable)
# ./install.sh --no-service write config only, don't touch systemd
#
# Re-running is safe: an existing share.config is kept unless you pass --force.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG="$DIR/share.config"
UNIT_DIR="$HOME/.config/systemd/user"
TOKEN="" PORT="" UPLOAD_DIR="" LANG_OPT="" TTL=""
SANS=() SERVICE=1 FORCE=0
die() { printf '\033[31merror:\033[0m %s\n' "$1" >&2; exit 1; }
info() { printf ' %s\n' "$1"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--token) TOKEN="${2:-}"; shift 2 ;;
--port) PORT="${2:-}"; shift 2 ;;
--dir) UPLOAD_DIR="${2:-}"; shift 2 ;;
--ttl) TTL="${2:-}"; shift 2 ;;
--lang) LANG_OPT="${2:-}"; shift 2 ;;
--san) SANS+=("${2:-}"); shift 2 ;;
--no-service) SERVICE=0; shift ;;
--force) FORCE=1; shift ;;
-h|--help) sed -n '2,10p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
# ── Dependencies ──────────────────────────────────────────────────────────────
PYTHON="$(command -v python3)" || die "python3 not found"
"$PYTHON" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' \
|| die "python3 >= 3.10 required (found $("$PYTHON" -V 2>&1))"
command -v openssl >/dev/null || die "openssl not found — needed to generate the TLS cert"
# ── Token ─────────────────────────────────────────────────────────────────────
GENERATED=0
if [[ -z "$TOKEN" ]]; then
TOKEN="$("$PYTHON" -c 'import secrets; print(secrets.token_urlsafe(24))')"
GENERATED=1
fi
TOKEN_HASH="$("$PYTHON" -c 'import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())' "$TOKEN")"
# ── Config ────────────────────────────────────────────────────────────────────
KEPT=0
if [[ -f "$CONFIG" && $FORCE -eq 0 ]]; then
KEPT=1
info "share.config exists — keeping it (pass --force to overwrite)"
else
[[ -f "$DIR/share.config.example" ]] || die "share.config.example missing"
cp "$DIR/share.config.example" "$CONFIG"
set_opt() { # set_opt <key> <value> — replace the first bare "key =" line
local key="$1" val="$2"
"$PYTHON" - "$CONFIG" "$key" "$val" <<'PY'
import re, sys
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
text = open(path).read()
text, n = re.subn(rf'(?m)^{re.escape(key)}\s*=.*$', f'{key} = {val}', text, count=1)
if n == 0:
raise SystemExit(f'key not found in config template: {key}')
open(path, 'w').write(text)
PY
}
set_opt token "$TOKEN_HASH"
[[ -n "$PORT" ]] && set_opt port "$PORT"
[[ -n "$UPLOAD_DIR" ]] && set_opt dir "$UPLOAD_DIR"
[[ -n "$TTL" ]] && set_opt ttl "$TTL"
[[ -n "$LANG_OPT" ]] && set_opt lang "$LANG_OPT"
[[ ${#SANS[@]} -gt 0 ]] && set_opt san "${SANS[*]}"
chmod 600 "$CONFIG"
info "wrote share.config (token stored as SHA-256 hash)"
fi
EFF_PORT="$("$PYTHON" -c '
import configparser, sys
cp = configparser.ConfigParser(); cp.read(sys.argv[1]); print(cp.get("server", "port", fallback="3001"))' "$CONFIG")"
# ── systemd user service ──────────────────────────────────────────────────────
if [[ $SERVICE -eq 1 ]]; then
if ! command -v systemctl >/dev/null; then
info "systemctl not found — skipping service install"
SERVICE=0
else
mkdir -p "$UNIT_DIR"
sed -e "s|__DIR__|$DIR|g" -e "s|__PYTHON__|$PYTHON|g" \
-e '/^;;/d' "$DIR/share.service.template" > "$UNIT_DIR/share.service"
systemctl --user daemon-reload
systemctl --user enable share.service
# restart, not `enable --now`: --now is a no-op on an already-running service,
# so a re-run would leave the old unit/config in effect.
systemctl --user restart share.service
info "installed $UNIT_DIR/share.service and (re)started it"
# Give it a moment to bind the port / fail loudly.
for _ in 1 2 3 4 5 6 7 8 9 10; do
systemctl --user is-active --quiet share.service && break
sleep 0.3
done
if ! systemctl --user is-active --quiet share.service; then
systemctl --user status share.service --no-pager -n 20 || true
die "service failed to start — see the status output above"
fi
fi
fi
# ── Summary ───────────────────────────────────────────────────────────────────
IP="$("$PYTHON" -c '
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.connect(("8.8.8.8", 80))
print(s.getsockname()[0]); s.close()
except OSError:
print("127.0.0.1")')"
echo
printf '\033[32m Share is set up.\033[0m\n\n'
info "Local: https://localhost:$EFF_PORT"
info "Network: https://$IP:$EFF_PORT"
echo
if [[ $KEPT -eq 1 ]]; then
info "Token: unchanged — the existing share.config was kept."
info " To set a new one: ./install.sh --force --token 'newsecret'"
echo
elif [[ $GENERATED -eq 1 ]]; then
printf '\033[33m Token (generated — save it now, it is not stored in plaintext):\033[0m\n\n'
printf ' %s\n\n' "$TOKEN"
else
info "Token: the one you passed via --token"
echo
fi
info "The cert is self-signed — the browser will warn on first visit. That is expected."
if [[ $SERVICE -eq 1 ]]; then
info "Logs: journalctl --user -u share -f"
info "Autostart at boot without login: sudo loginctl enable-linger $USER"
else
info "Run manually: python3 $DIR/share.py"
fi