Push-to-deploy (OFF by default): vanguard-deploy applies only changed parts with a live-stream guard for MediaMTX, rollback, pending timer; dumpd /deploy webhook gated by DEPLOY_AUTO; configure --apply; switchToClone.sh; draft CI workflow; committed plugin zip
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
50
bin/dumpd.py
50
bin/dumpd.py
@@ -9,6 +9,9 @@ GET /me/stream-token -> short-lived publish token for the caller (member of str
|
||||
GET /me/clips -> Syncthing self-service status for the caller: our device id, their folder/device state
|
||||
POST /me/clips -> {"device_id": "..."}: create clips/<user> (+ .stignore), add their device to our Syncthing,
|
||||
create the receive-only folder clips-<user> shared with them (min free 300 GB). Idempotent.
|
||||
POST /deploy -> Gitea push webhook (bridge port only, HMAC-verified with DEPLOY_WEBHOOK_SECRET from
|
||||
/etc/vanguard/deploy.env). Runs /usr/local/bin/vanguard-deploy pull ONLY when DEPLOY_AUTO=1;
|
||||
otherwise logs the push and answers 202 "auto-deploy disabled".
|
||||
POST /mtx-auth -> MediaMTX authHTTP hook: allows read/playback; allows publish iff user/pass carries a valid
|
||||
token whose user owns the path prefix. Tokens are HMAC-signed with /etc/vanguard/stream-token.secret.
|
||||
|
||||
@@ -244,6 +247,34 @@ def clips_setup(user, device_id, device_name=None):
|
||||
print(f"{datetime.now(TZ):%F %T} clips setup user={user} device={device_id[:7]}… folder={fid}", flush=True)
|
||||
return clips_status(user)
|
||||
|
||||
DEPLOY_ENV = Path(os.environ.get("DEPLOY_ENV_FILE", "/etc/vanguard/deploy.env"))
|
||||
def deploy_env():
|
||||
d = {}
|
||||
try:
|
||||
for line in DEPLOY_ENV.read_text().splitlines():
|
||||
if "=" in line and not line.startswith("#"): k, v = line.split("=", 1); d[k.strip()] = v.strip()
|
||||
except OSError:
|
||||
pass
|
||||
return d
|
||||
|
||||
_deploy_lock = {"running": False}
|
||||
def run_deploy(reason):
|
||||
import subprocess, threading
|
||||
if _deploy_lock["running"]: return False
|
||||
_deploy_lock["running"] = True
|
||||
def go():
|
||||
try:
|
||||
print(f"{datetime.now(TZ):%F %T} deploy: start ({reason})", flush=True)
|
||||
r = subprocess.run(["/usr/local/bin/vanguard-deploy", "pull"], capture_output=True, text=True, timeout=1800)
|
||||
for line in (r.stdout + r.stderr).splitlines()[-40:]: print(f"deploy| {line}", flush=True)
|
||||
print(f"{datetime.now(TZ):%F %T} deploy: exit {r.returncode}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"{datetime.now(TZ):%F %T} deploy: error {e}", flush=True)
|
||||
finally:
|
||||
_deploy_lock["running"] = False
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
return True
|
||||
|
||||
def token_secret():
|
||||
try:
|
||||
sec = TOKEN_SECRET_FILE.read_bytes().strip()
|
||||
@@ -354,6 +385,25 @@ class H(BaseHTTPRequestHandler):
|
||||
"files": [str(f.relative_to(ROOT)) for f in files],
|
||||
"gb": round(sum(f.stat().st_size for f in files) / 1e9, 2)})
|
||||
def do_POST(self):
|
||||
if self.server.server_port == AUTH_LISTEN[1] and self.path.startswith("/deploy"):
|
||||
# Gitea (container) -> host gateway -> here. Verify HMAC-SHA256 of the raw body with the webhook secret.
|
||||
n = int(self.headers.get("Content-Length") or 0); raw = self.rfile.read(n)
|
||||
env = deploy_env(); secret = env.get("DEPLOY_WEBHOOK_SECRET", "")
|
||||
sig = self.headers.get("X-Gitea-Signature") or self.headers.get("X-Hub-Signature-256", "").replace("sha256=", "")
|
||||
if not secret or not sig or not hmac.compare_digest(hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest(), sig):
|
||||
print(f"{datetime.now(TZ):%F %T} deploy webhook: bad signature from {self.client_address[0]}", flush=True)
|
||||
return self._json(401, {"error": "bad signature"})
|
||||
try: payload = json.loads(raw or b"{}")
|
||||
except json.JSONDecodeError: return self._json(400, {"error": "bad json"})
|
||||
ref = payload.get("ref", ""); branch = env.get("DEPLOY_BRANCH", "main")
|
||||
after = (payload.get("after") or "")[:10]; pusher = (payload.get("pusher") or {}).get("username", "?")
|
||||
if ref != f"refs/heads/{branch}":
|
||||
return self._json(200, {"ignored": f"ref {ref} is not {branch}"})
|
||||
if env.get("DEPLOY_AUTO", "0") != "1":
|
||||
print(f"{datetime.now(TZ):%F %T} deploy webhook: push {after} by {pusher} on {branch} — auto-deploy disabled (DEPLOY_AUTO=0)", flush=True)
|
||||
return self._json(202, {"queued": False, "note": "auto-deploy disabled (DEPLOY_AUTO=0); run vanguard-deploy pull by hand"})
|
||||
started = run_deploy(f"push {after} by {pusher}")
|
||||
return self._json(202, {"queued": started, "note": "deploy started" if started else "a deploy is already running"})
|
||||
if self.server.server_port == AUTH_LISTEN[1] and not self.path.startswith("/mtx-auth"):
|
||||
return self._json(404, {"error": "not found"})
|
||||
if self.path.startswith("/mtx-auth"):
|
||||
|
||||
Reference in New Issue
Block a user