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
This commit is contained in:
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user