#!/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 — 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