This commit is contained in:
hashbro
2026-08-05 05:42:10 +08:00
parent d4fb538997
commit 22942f1a78
19 changed files with 986 additions and 164 deletions
+127 -62
View File
@@ -1,19 +1,33 @@
#!/usr/bin/env python3
"""Reset server/public web/ + sync/ from source/, then patch_all --apply."""
"""Build server/public campaign trees: new channel + fixed domains.
Seeds are derived deterministically from the domain lists (not user-facing).
Same domains → same sync/ and same secondary domain patch; only channel differs.
Each run:
- creates web/<channel-id>/
- rebuilds sync/ from source + fixed-domain patch
- leaves other web/<channel>/ dirs intact
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import subprocess
import sys
from pathlib import Path
from _channel_patch import gen_channel_id, validate_channel_id
from _domain_patch import parse_domain_list
TOOLS = Path(__file__).resolve().parent
LAB_ROOT = TOOLS.parent
SOURCE_ROOT = LAB_ROOT / "source"
APPLY_ROOT = LAB_ROOT / "server" / "public"
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
@@ -27,105 +41,156 @@ def replace_tree(src: Path, dst: Path) -> None:
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
def copy_campaign_template(dst_campaign: Path) -> None:
src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
if not src.is_dir():
raise SystemExit(f"missing source campaign: {src}")
if dst_campaign.exists():
raise SystemExit(f"campaign already exists: {dst_campaign}")
shutil.copytree(src, dst_campaign, symlinks=False, ignore=_ignore_junk)
def seeds_from_domains(dep: list[str], rep: list[str]) -> tuple[str, str]:
"""32-hex seeds stable for a given ordered domain list (pool slots still need seeds)."""
def one(label: str, domains: list[str]) -> str:
material = label.encode("ascii") + b"\0" + b"\0".join(d.encode("ascii") for d in domains)
return hashlib.sha256(material).hexdigest()[:32]
dep_seed = one("deployment", dep)
rep_seed = one("reporting", rep)
if dep_seed == rep_seed:
rep_seed = one("reporting/alt", rep)
return dep_seed, rep_seed
def pick_channel(explicit: str | None, web_root: Path) -> str:
channel = validate_channel_id(explicit) if explicit else gen_channel_id()
while (web_root / channel).exists():
if explicit:
raise SystemExit(f"web/{channel} already exists; choose another --channel-id")
channel = gen_channel_id()
return channel
def run(cmd: list[str]) -> None:
print("+", " ".join(cmd), flush=True)
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Copy source/web + source/sync to server/public, then run "
"patch_all.py --apply --root server/public."
"From source/, create server/public/web/<channel-id>/ with fixed "
"Deployment/Reporting domains. Seeds are derived from domains "
"(no --*-seed / no reuse-sync shortcut)."
)
)
parser.add_argument(
"--deployment-seed",
help="optional; forwarded to patch_all (default: random)",
)
parser.add_argument(
"--reporting-seed",
help="optional; forwarded to patch_all (default: random)",
"--channel-id",
help="optional 32-hex channel id (default: random, unique under web/)",
)
parser.add_argument(
"--deployment-domains",
action="append",
default=[],
required=True,
help="fixed Deployment hosts (comma-separated or repeatable)",
)
parser.add_argument(
"--reporting-domains",
action="append",
default=[],
required=True,
help="fixed Reporting hosts (comma-separated or repeatable)",
)
parser.add_argument(
"-n",
"--count",
type=int,
default=5,
help="DGA candidates to print when not using fixed domains (default 5)",
)
parser.add_argument(
"--skip-patch",
action="store_true",
help="only copy source trees to server/public; do not run patch_all",
)
args = parser.parse_args()
if bool(args.deployment_domains) != bool(args.reporting_domains):
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
fixed_dep = parse_domain_list(args.deployment_domains, label="deployment")
fixed_rep = parse_domain_list(args.reporting_domains, label="reporting")
dep_seed, rep_seed = seeds_from_domains(fixed_dep, fixed_rep)
src_web = SOURCE_ROOT / "web"
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
src_sync = SOURCE_ROOT / "sync"
if not (src_web / CAMPAIGN_HASH).is_dir():
raise SystemExit(f"missing source campaign: {src_web / CAMPAIGN_HASH}")
if not src_campaign.is_dir():
raise SystemExit(f"missing source campaign: {src_campaign}")
if not src_sync.is_dir():
raise SystemExit(f"missing source sync: {src_sync}")
APPLY_ROOT.mkdir(parents=True, exist_ok=True)
dst_web = APPLY_ROOT / "web"
dst_sync = APPLY_ROOT / "sync"
web_root = APPLY_ROOT / "web"
sync_dir = APPLY_ROOT / "sync"
out_root = APPLY_ROOT / "out"
out_root.mkdir(parents=True, exist_ok=True)
web_root.mkdir(parents=True, exist_ok=True)
print("=== reset server/public from source ===")
print(f"from: {SOURCE_ROOT}")
print(f"to: {APPLY_ROOT}/{{web,sync}}")
replace_tree(src_web, dst_web)
replace_tree(src_sync, dst_sync)
print(f"copied web/ ({CAMPAIGN_HASH}) + sync/")
channel = pick_channel(args.channel_id, web_root)
campaign_dir = web_root / channel
if args.skip_patch:
print("skip-patch: done (source copy only)")
return 0
print("=== new_project ===")
print(f"channel: {channel}")
print(f"web dest: {campaign_dir}")
print(f" deployment: {', '.join(fixed_dep)}")
print(f" reporting: {', '.join(fixed_rep)}")
print(f" seeds: derived from domains (stable)")
print()
cmd = [
sys.executable,
str(TOOLS / "patch_all.py"),
"--apply",
"--root",
str(APPLY_ROOT),
"-n",
str(args.count),
]
if args.deployment_seed:
cmd += ["--deployment-seed", args.deployment_seed]
if args.reporting_seed:
cmd += ["--reporting-seed", args.reporting_seed]
for item in args.deployment_domains:
cmd += ["--deployment-domains", item]
for item in args.reporting_domains:
cmd += ["--reporting-domains", item]
print("=== copy campaign template ===")
print(f"from: {src_campaign}")
print(f"to: {campaign_dir}")
copy_campaign_template(campaign_dir)
print(f"created web/{channel}/")
print("=== reset sync from source ===")
replace_tree(src_sync, sync_dir)
print(f"copied sync/ -> {sync_dir}")
domain_args: list[str] = []
for item in fixed_dep:
domain_args += ["--deployment-domains", item]
for item in fixed_rep:
domain_args += ["--reporting-domains", item]
print()
print("=== patch_all --apply ===")
print("+", " ".join(cmd), flush=True)
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
run(
[
sys.executable,
str(TOOLS / "patch_all.py"),
"--apply",
"--root",
str(APPLY_ROOT),
"--channel-id",
channel,
"--deployment-seed",
dep_seed,
"--reporting-seed",
rep_seed,
*domain_args,
]
)
(out_root / "channel.json").write_text(
json.dumps(
{
"channel_id": channel,
"deployment_domains": fixed_dep,
"reporting_domains": fixed_rep,
},
indent=2,
)
+ "\n"
)
out_root = APPLY_ROOT / "out"
print()
print("=== ready ===")
print(f"web: {dst_web / CAMPAIGN_HASH}")
print(f"sync: {dst_sync}")
print(f"seeds: {out_root / 'seeds.json'}")
print(f"web: {campaign_dir}")
print(f"sync: {sync_dir}")
print(f"channel: {out_root / 'channel.json'}")
print(f"domains: {out_root / 'domains.json'}")
print()
print("served by Laravel public:")
print(f" /web/{CAMPAIGN_HASH}/support.html")
print(f" /web/{channel}/support.html")
print(" /sync/daily.html")
return 0