mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
feat: Rename subdomain to folder_name, add Tor bootstrap progress, and auto-start server after init
This commit is contained in:
1 parent
956a836ff0
commit
e429a6e21b
20 files changed
+92
-81
No files matched your search
@@ -94,7 +94,7 @@ voxvera quickstart
|
||||
| Field | Max | Where it appears |
|
||||
|---|---|---|
|
||||
| **name** | 60 chars | Browser tab title. Not printed on the flyer itself. |
|
||||
| **subdomain** | 63 chars | Used internally as the folder name under `host/`. Lowercase letters, numbers, and hyphens only. |
|
||||
| **folder_name** | 63 chars | Used internally as the folder name under `host/`. Lowercase letters, numbers, and hyphens only. |
|
||||
| **title** | 60 chars | Large heading at the top-right of the flyer (e.g. "TOP SECRET"). |
|
||||
| **subtitle** | 80 chars | Smaller text directly below the title (e.g. "DO NOT DISTRIBUTE"). Supports basic HTML like `<span class="redacted">`. |
|
||||
| **headline** | 80 chars | Second heading below the subtitle (e.g. "OPERATION VOX VERA"). |
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ EOI
|
||||
"$voxvera_cmd" build
|
||||
|
||||
# Verify build output exists in host/
|
||||
subdomain=$(python3 -c "import json; print(json.load(open('voxvera/src/config.json'))['subdomain'])")
|
||||
host_dir="voxvera/host/$subdomain"
|
||||
folder_name=$(python3 -c "import json; print(json.load(open('voxvera/src/config.json'))['folder_name'])")
|
||||
host_dir="voxvera/host/$folder_name"
|
||||
ls -R "$host_dir" >>"$LOG_DIR/tree.txt"
|
||||
|
||||
# Verify essential files
|
||||
|
||||
@@ -26,7 +26,7 @@ If no running Tor is found, the defaults (9050/9051) are used and OnionShare wil
|
||||
|
||||
## OnionShare crashes
|
||||
If `voxvera serve` prints "OnionShare exited unexpectedly", check the log file
|
||||
`host/<subdomain>/onionshare.log` for details. Common causes:
|
||||
`host/<folder_name>/onionshare.log` for details. Common causes:
|
||||
|
||||
- Tor is not running (`sudo systemctl start tor`)
|
||||
- A port conflict with another service
|
||||
@@ -34,7 +34,7 @@ If `voxvera serve` prints "OnionShare exited unexpectedly", check the log file
|
||||
|
||||
## Onion URL changed unexpectedly
|
||||
The onion URL is tied to the Ed25519 keypair stored in
|
||||
`host/<subdomain>/.onionshare-session`. If this file is deleted, a new
|
||||
`host/<folder_name>/.onionshare-session`. If this file is deleted, a new
|
||||
keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ All build dependencies (QR generation, HTML minification, PDF parsing) are Pytho
|
||||
```
|
||||
This automatically detects your Tor instance, starts OnionShare, and writes the generated .onion address into the flyer's tear-off links. QR codes are regenerated with the new address.
|
||||
|
||||
The generated `host/<subdomain>` directory contains all flyer files. The `index.html` file fetches `config.json` at runtime, so the flyer must be served via a web server (OnionShare handles this). Visitors can use the **Download** button to retrieve any bundled files.
|
||||
The generated `host/<folder_name>` directory contains all flyer files. The `index.html` file fetches `config.json` at runtime, so the flyer must be served via a web server (OnionShare handles this). Visitors can use the **Download** button to retrieve any bundled files.
|
||||
|
||||
## How URLs Work
|
||||
|
||||
@@ -55,7 +55,7 @@ Place configuration files in an `imports/` directory at the project root and run
|
||||
voxvera import
|
||||
```
|
||||
|
||||
Each JSON file is copied to `src/config.json` and processed with `voxvera build`. Existing folders under `host/` with the same subdomain are cleaned before new files are written. OnionShare session keys (`.onionshare-session`) are preserved so the onion URL stays the same across re-imports.
|
||||
Each JSON file is copied to `src/config.json` and processed with `voxvera build`. Existing folders under `host/` with the same folder_name are cleaned before new files are written. OnionShare session keys (`.onionshare-session`) are preserved so the onion URL stays the same across re-imports.
|
||||
|
||||
## Hosting with OnionShare
|
||||
|
||||
@@ -73,7 +73,7 @@ TOR_SOCKS_PORT=9150 TOR_CONTROL_PORT=9151 voxvera serve
|
||||
|
||||
The command launches `onionshare-cli` in persistent website mode, waits for the generated onion URL, writes it into `config.json` as the tear-off link, regenerates QR codes, and copies updated files into the `host` directory.
|
||||
|
||||
The onion URL is derived from an Ed25519 keypair stored in `host/<subdomain>/.onionshare-session`. As long as this file exists, the URL stays the same even if you rebuild with new content. Keep OnionShare running to continue hosting.
|
||||
The onion URL is derived from an Ed25519 keypair stored in `host/<folder_name>/.onionshare-session`. As long as this file exists, the URL stays the same even if you rebuild with new content. Keep OnionShare running to continue hosting.
|
||||
|
||||
## All-in-One
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<h1>VoxVera</h1>
|
||||
<form id="config-form">
|
||||
<div><label>Name <input type="text" id="name"></label></div>
|
||||
<div><label>Subdomain <input type="text" id="subdomain"></label></div>
|
||||
<div><label>Subdomain <input type="text" id="folder_name"></label></div>
|
||||
<div><label>Title <input type="text" id="title"></label></div>
|
||||
<div><label>Subtitle <input type="text" id="subtitle"></label></div>
|
||||
<div><label>Headline <input type="text" id="headline"></label></div>
|
||||
@@ -42,7 +42,7 @@
|
||||
document.addEventListener('DOMContentLoaded', load);
|
||||
|
||||
document.getElementById('quickstart').addEventListener('click', async () => {
|
||||
const ids = ['name','subdomain','title','subtitle','headline','content','url_message','url','tear_off_link','binary_message'];
|
||||
const ids = ['name','folder_name','title','subtitle','headline','content','url_message','url','tear_off_link','binary_message'];
|
||||
const cfg = {};
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AOS Project",
|
||||
"subdomain": "aosp",
|
||||
"folder_name": "aosp",
|
||||
"title": "ANDROID UNLEASHED",
|
||||
"subtitle": "WITHOUT BOUNDARIES",
|
||||
"headline": "OPEN-SOURCE FREEDOM",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Bitcoin Revolution Flyers",
|
||||
"subdomain": "bitcoin",
|
||||
"folder_name": "bitcoin",
|
||||
"title": "THE FUTURE OF MONEY",
|
||||
"subtitle": "DO NOT IGNORE",
|
||||
"headline": "JOIN THE BITCOIN REVOLUTION",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "",
|
||||
"subdomain": "",
|
||||
"folder_name": "",
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"headline": "",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "name",
|
||||
"subdomain": "subdomain",
|
||||
"folder_name": "folder_name",
|
||||
"title": "title",
|
||||
"subtitle": "subtitle",
|
||||
"headline": "headline",
|
||||
"content": "content",
|
||||
"url_message": "url message",
|
||||
"url": "url",
|
||||
"tear_off_link": "http://example-subdomain.6dshf2gnj7yzxlfcaczlyi57up4mvbtd5orinuj5bjsfycnhz2w456yd.onion",
|
||||
"tear_off_link": "http://example-folder_name.6dshf2gnj7yzxlfcaczlyi57up4mvbtd5orinuj5bjsfycnhz2w456yd.onion",
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100"
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Life Changing Message",
|
||||
"subdomain": "jesus",
|
||||
"folder_name": "jesus",
|
||||
"title": "GOOD NEWS",
|
||||
"subtitle": "DO NOT IGNORE",
|
||||
"headline": "DISCOVER TRUE HOPE",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Meshtastic Network Flyers",
|
||||
"subdomain": "meshtastic",
|
||||
"folder_name": "meshtastic",
|
||||
"title": "SECURE COMMUNICATIONS",
|
||||
"subtitle": "DO NOT IGNORE",
|
||||
"headline": "BUILD YOUR EMERGENCY NETWORK",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Nostr Protocol Flyers",
|
||||
"subdomain": "voxvera",
|
||||
"folder_name": "voxvera",
|
||||
"title": "SOCIAL FREEDOM",
|
||||
"subtitle": "DO NOT IGNORE",
|
||||
"headline": "JOIN THE CENSORSHIP-RESISTANT NETWORK",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Vox Vera Printable Flyers",
|
||||
"subdomain": "voxvera",
|
||||
"folder_name": "voxvera",
|
||||
"title": "TOP SECRET",
|
||||
"subtitle": "DO <span class=\"redacted\">NOT</span> DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Vox Vera Printable Flyers",
|
||||
"subdomain": "voxvera",
|
||||
"folder_name": "voxvera",
|
||||
"title": "TOP SECRET",
|
||||
"subtitle": "DO <span class=\"redacted\">NOT</span> DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
|
||||
+16
-16
@@ -65,7 +65,7 @@ def test_import(tmp_path, monkeypatch):
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
for sub in ["foo", "bar"]:
|
||||
data = dict(base_data)
|
||||
data["subdomain"] = sub
|
||||
data["folder_name"] = sub
|
||||
with open(imports_dir / f"{sub}.json", "w") as fh:
|
||||
json.dump(data, fh)
|
||||
cli.main(["import"])
|
||||
@@ -81,7 +81,7 @@ def test_import_preserves_session(tmp_path, monkeypatch):
|
||||
imports_dir = tmp_path / "imports"
|
||||
imports_dir.mkdir()
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
base_data["subdomain"] = "keep"
|
||||
base_data["folder_name"] = "keep"
|
||||
with open(imports_dir / "keep.json", "w") as fh:
|
||||
json.dump(base_data, fh)
|
||||
|
||||
@@ -125,12 +125,12 @@ def test_check_missing(capsys, monkeypatch):
|
||||
def test_build_download_zip(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
subdomain = config["subdomain"]
|
||||
folder_name = config["folder_name"]
|
||||
zip_path = tmp_path / "file.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as zf:
|
||||
zf.writestr("dummy.txt", "data")
|
||||
cli.main(["build", "--download", str(zip_path)])
|
||||
dest = tmp_path / "host" / subdomain / "download" / "download.zip"
|
||||
dest = tmp_path / "host" / folder_name / "download" / "download.zip"
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
subdomain = config["subdomain"]
|
||||
dir_path = tmp_path / "host" / subdomain
|
||||
folder_name = config["folder_name"]
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
|
||||
# Set a custom URL that should be preserved (content link)
|
||||
config["url"] = "https://example.com/external-resource"
|
||||
@@ -152,7 +152,7 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
orig_build = cli.build_assets
|
||||
|
||||
def safe_build_assets(cfg, download_path=None):
|
||||
dest = cli.ROOT / "host" / json.load(open(cfg))["subdomain"] / "config.json"
|
||||
dest = cli.ROOT / "host" / json.load(open(cfg))["folder_name"] / "config.json"
|
||||
if Path(cfg) == dest:
|
||||
return
|
||||
return orig_build(cfg, download_path=download_path)
|
||||
@@ -193,14 +193,14 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
"""Test quickstart auto-generates onion URL for tear-off links."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
subdomain = config["subdomain"]
|
||||
folder_name = config["folder_name"]
|
||||
|
||||
# No need for TOR_* env vars anymore - auto-detection should work
|
||||
monkeypatch.setattr(cli, "require_cmd", lambda c: True)
|
||||
orig_build = cli.build_assets
|
||||
|
||||
def safe_build_assets(cfg, download_path=None):
|
||||
dest = cli.ROOT / "host" / json.load(open(cfg))["subdomain"] / "config.json"
|
||||
dest = cli.ROOT / "host" / json.load(open(cfg))["folder_name"] / "config.json"
|
||||
if Path(cfg) == dest:
|
||||
return
|
||||
return orig_build(cfg, download_path=download_path)
|
||||
@@ -228,7 +228,7 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
|
||||
cli.main(["quickstart", "--non-interactive"])
|
||||
|
||||
dir_path = tmp_path / "host" / subdomain
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
assert (dir_path / "index.html").exists()
|
||||
log_file = dir_path / "onionshare.log"
|
||||
assert onion_url in log_file.read_text()
|
||||
@@ -287,7 +287,7 @@ def test_build_without_url_skips_qr(tmp_path, monkeypatch, capsys):
|
||||
# Build should complete without error, just skip QR generation
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["subdomain"]
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -315,7 +315,7 @@ def test_build_with_only_url(tmp_path, monkeypatch):
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["subdomain"]
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
# Only content QR should exist (tear-off will be set by serve)
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert not (dest / "qrcode-tear-offs.png").exists()
|
||||
@@ -340,7 +340,7 @@ def test_build_with_only_tear_off_link(tmp_path, monkeypatch):
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["subdomain"]
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
# Only tear-off QR should exist (will be updated by serve)
|
||||
assert not (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
@@ -351,7 +351,7 @@ def test_serve_passes_tor_ports_to_env(monkeypatch, tmp_path):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
subdomain = config["subdomain"]
|
||||
folder_name = config["folder_name"]
|
||||
|
||||
captured_env = {}
|
||||
|
||||
@@ -397,7 +397,7 @@ def test_backward_compatibility_with_https_config(tmp_path, monkeypatch):
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["subdomain"]
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
|
||||
@@ -415,6 +415,6 @@ def test_long_urls_in_qr(tmp_path, monkeypatch):
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["subdomain"]
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
+4
-4
@@ -111,7 +111,7 @@ class TestFullWorkflow:
|
||||
# Create test config
|
||||
config = {
|
||||
"name": "Test Flyer",
|
||||
"subdomain": "testflyer",
|
||||
"folder_name": "testflyer",
|
||||
"title": "TEST TITLE",
|
||||
"subtitle": "Test Subtitle",
|
||||
"headline": "Test Headline",
|
||||
@@ -203,7 +203,7 @@ class TestFullWorkflow:
|
||||
|
||||
config = {
|
||||
"name": "Reachability Test",
|
||||
"subdomain": "reachtest",
|
||||
"folder_name": "reachtest",
|
||||
"title": "Reachability Test",
|
||||
"subtitle": "Testing Tor reachability",
|
||||
"headline": "Test Headline",
|
||||
@@ -323,7 +323,7 @@ class TestSiteFiles:
|
||||
|
||||
config = {
|
||||
"name": "Test",
|
||||
"subdomain": "filetest",
|
||||
"folder_name": "filetest",
|
||||
"title": "Title",
|
||||
"subtitle": "Subtitle",
|
||||
"headline": "Headline",
|
||||
@@ -369,7 +369,7 @@ class TestErrorHandling:
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"subdomain": "nonexistent",
|
||||
"folder_name": "nonexistent",
|
||||
"name": "Test",
|
||||
"title": "Title",
|
||||
"subtitle": "Subtitle",
|
||||
|
||||
+49
-38
@@ -160,9 +160,9 @@ def _len_validator(limit: int):
|
||||
return _v
|
||||
|
||||
|
||||
def _subdomain_validator(val: str):
|
||||
def _folder_name_validator(val: str):
|
||||
if len(val) > 63:
|
||||
return "Subdomain must be at most 63 characters"
|
||||
return "Folder name must be at most 63 characters"
|
||||
if not re.fullmatch(r"[a-z0-9-]+", val):
|
||||
return "Use only lowercase letters, numbers and '-'"
|
||||
return True
|
||||
@@ -184,11 +184,11 @@ def interactive_update(config_path: str):
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"message": "Subdomain",
|
||||
"name": "subdomain",
|
||||
"default": data.get("subdomain", ""),
|
||||
"message": "Folder Name",
|
||||
"name": "folder_name",
|
||||
"default": data.get("folder_name", ""),
|
||||
"transformer": _len_transform(63),
|
||||
"validate": _subdomain_validator,
|
||||
"validate": _folder_name_validator,
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
@@ -299,8 +299,8 @@ def build_assets(
|
||||
|
||||
with open(src_dir / "index.html", "w") as fh:
|
||||
fh.write(html)
|
||||
subdomain = data["subdomain"]
|
||||
dest = ROOT / "host" / subdomain
|
||||
folder_name = data["folder_name"]
|
||||
dest = ROOT / "host" / folder_name
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
if download_path:
|
||||
os.makedirs(dest / "download", exist_ok=True)
|
||||
@@ -372,8 +372,8 @@ def serve(config_path: str):
|
||||
|
||||
socks, ctl = get_tor_ports()
|
||||
|
||||
subdomain = load_config(config_path)["subdomain"]
|
||||
dir_path = ROOT / "host" / subdomain
|
||||
folder_name = load_config(config_path)["folder_name"]
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
if not dir_path.is_dir():
|
||||
print(f"Directory {dir_path} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -405,20 +405,29 @@ def serve(config_path: str):
|
||||
import re as _re
|
||||
|
||||
onion_url = None
|
||||
while onion_url is None:
|
||||
time.sleep(1)
|
||||
if proc.poll() is not None:
|
||||
print("OnionShare exited unexpectedly", file=sys.stderr)
|
||||
with open(logfile) as fh:
|
||||
sys.stderr.write(fh.read())
|
||||
pid_file.unlink(missing_ok=True)
|
||||
sys.exit(1)
|
||||
if os.path.exists(logfile):
|
||||
with open(logfile) as fh:
|
||||
content = fh.read()
|
||||
m = _re.search(r"https?://[a-z0-9]+\.onion", content)
|
||||
if m:
|
||||
onion_url = m.group(0)
|
||||
console = Console()
|
||||
with console.status("[bold green]Connecting to Tor relays and setting up hidden service...") as status:
|
||||
while onion_url is None:
|
||||
time.sleep(1)
|
||||
if proc.poll() is not None:
|
||||
print("OnionShare exited unexpectedly", file=sys.stderr)
|
||||
with open(logfile) as fh:
|
||||
sys.stderr.write(fh.read())
|
||||
pid_file.unlink(missing_ok=True)
|
||||
sys.exit(1)
|
||||
if os.path.exists(logfile):
|
||||
with open(logfile) as fh:
|
||||
content = fh.read()
|
||||
|
||||
# Look for Tor bootstrap progress in logs to give feedback
|
||||
m_progress = _re.findall(r"Bootstrapped (\d+)%", content)
|
||||
if m_progress:
|
||||
status.update(f"[bold green]Tor Bootstrapping: {m_progress[-1]}% ...")
|
||||
|
||||
m = _re.search(r"https?://[a-z0-9]+\.onion", content)
|
||||
if m:
|
||||
onion_url = m.group(0)
|
||||
|
||||
print(f"Onion URL: {onion_url}")
|
||||
# update config with the onion URL
|
||||
# tear_off_link is set to the onion address (for tear-off tabs)
|
||||
@@ -462,8 +471,8 @@ def import_configs():
|
||||
print(f"Processing {json_file}")
|
||||
dest_config = ROOT / "src" / "config.json"
|
||||
shutil.copy(json_file, dest_config)
|
||||
subdomain = load_config(json_file)["subdomain"]
|
||||
_clear_host_dir(ROOT / "host" / subdomain)
|
||||
folder_name = load_config(json_file)["folder_name"]
|
||||
_clear_host_dir(ROOT / "host" / folder_name)
|
||||
build_assets(dest_config)
|
||||
|
||||
|
||||
@@ -478,8 +487,8 @@ def get_servers() -> list[str]:
|
||||
return sorted(servers)
|
||||
|
||||
|
||||
def is_server_running(subdomain: str) -> bool:
|
||||
pid_file = ROOT / "host" / subdomain / "server.pid"
|
||||
def is_server_running(folder_name: str) -> bool:
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
return False
|
||||
try:
|
||||
@@ -517,10 +526,10 @@ def is_server_running(subdomain: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def stop_server(subdomain: str):
|
||||
pid_file = ROOT / "host" / subdomain / "server.pid"
|
||||
def stop_server(folder_name: str):
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
print(f"Server {subdomain} is not running.")
|
||||
print(f"Server {folder_name} is not running.")
|
||||
return
|
||||
try:
|
||||
with open(pid_file, "r") as f:
|
||||
@@ -530,20 +539,20 @@ def stop_server(subdomain: str):
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
print(f"Stopped {subdomain} (PID {pid})")
|
||||
print(f"Stopped {folder_name} (PID {pid})")
|
||||
except Exception as e:
|
||||
print(f"Error stopping {subdomain}: {e}")
|
||||
print(f"Error stopping {folder_name}: {e}")
|
||||
finally:
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def delete_server(subdomain: str):
|
||||
if is_server_running(subdomain):
|
||||
stop_server(subdomain)
|
||||
dir_path = ROOT / "host" / subdomain
|
||||
def delete_server(folder_name: str):
|
||||
if is_server_running(folder_name):
|
||||
stop_server(folder_name)
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
if dir_path.exists():
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
print(f"Deleted server {subdomain}")
|
||||
print(f"Deleted server {folder_name}")
|
||||
|
||||
|
||||
def manage_servers():
|
||||
@@ -640,6 +649,8 @@ def main(argv=None):
|
||||
return
|
||||
elif not args.non_interactive:
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
serve(config_path)
|
||||
elif args.command == "build":
|
||||
build_assets(config_path, download_path=args.download)
|
||||
elif args.command == "serve":
|
||||
|
||||
@@ -14,7 +14,7 @@ The `voxvera build` command processes these source files:
|
||||
1. **QR code generation** -- Reads URLs from `config.json` and generates PNG QR codes using the `qrcode` and `Pillow` Python libraries.
|
||||
2. **HTML minification** -- Minifies embedded JavaScript with `jsmin` and the overall HTML with `htmlmin2`. Produces `index.html` and `nostr.html` from the master templates.
|
||||
3. **Binary message injection** -- Writes the configured binary message into the built HTML.
|
||||
4. **Asset packaging** -- Copies all built files into `host/<subdomain>/`.
|
||||
4. **Asset packaging** -- Copies all built files into `host/<folder_name>/`.
|
||||
|
||||
No external tools (Node.js, ImageMagick, qrencode, etc.) are required. The entire build pipeline uses pure Python packages.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Vox Vera Printable Flyers",
|
||||
"subdomain": "voxvera",
|
||||
"folder_name": "voxvera",
|
||||
"title": "TOP SECRET",
|
||||
"subtitle": "DO <span class=\"redacted\">NOT</span> DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Vox Vera Printable Flyers",
|
||||
"subdomain": "voxvera",
|
||||
"folder_name": "voxvera",
|
||||
"title": "TOP SECRET",
|
||||
"subtitle": "DO <span class=\\\"redacted\\\">NOT</span> DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
|
||||
Reference in new issue
Block a user