mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 17:50:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f530e54f45 | ||
|
|
8be7d0761b | ||
|
|
6d747f719f | ||
|
|
dc83b76317 | ||
|
|
9c699eb36b | ||
|
|
3cba4c7e9f | ||
|
|
69ca9f6411 | ||
|
|
994744ee60 |
No files matched your search
@@ -34,7 +34,9 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade setuptools pkg_resources || pip install --upgrade setuptools
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
pip install pyinstaller pyinstaller-hooks-contrib
|
||||
|
||||
- name: Download Tor Binaries
|
||||
@@ -48,6 +50,16 @@ jobs:
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then SEP=";"; else SEP=":"; fi
|
||||
pyinstaller --onefile \
|
||||
--collect-all pkg_resources \
|
||||
--collect-all setuptools \
|
||||
--hidden-import pkg_resources \
|
||||
--hidden-import setuptools \
|
||||
--hidden-import jaraco.text \
|
||||
--hidden-import jaraco.functools \
|
||||
--hidden-import jaraco.context \
|
||||
--hidden-import more_itertools \
|
||||
--hidden-import packaging \
|
||||
--hidden-import platformdirs \
|
||||
--collect-all onionshare_cli \
|
||||
--collect-all flask_socketio \
|
||||
--collect-all engineio \
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
# VoxVera v0.2.4
|
||||
|
||||
## New Features
|
||||
- **Optional File Attachment:** Users can now attach a file (e.g., a .torrent, image, or document) to their flyers. This file is packaged into the Tor-hosted site and made available via a "Download Attachment" button on the landing page.
|
||||
- **Interactive Attachment Selection:** Added a prompt during `voxvera init` to easily specify the path to an attachment.
|
||||
|
||||
## Localization
|
||||
- Added new tokens for attachment handling across all 14+ supported languages.
|
||||
+6
-3
@@ -137,15 +137,18 @@ if command_exists curl; then
|
||||
# For Windows, PyInstaller might add .exe but here we focus on linux/mac
|
||||
|
||||
msg "Downloading from: $url"
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest" || echo "failed")
|
||||
set +e
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
if [ $curl_exit -eq 0 ] && [ "$status" = "200" ]; then
|
||||
chmod +x "$dest"
|
||||
msg "VoxVera binary (${ARCH}) installed to $dest"
|
||||
[[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && warn "Add \$HOME/.local/bin to your PATH."
|
||||
exit 0
|
||||
else
|
||||
warn "Binary download failed (status: $status). Falling back to source installation."
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
|
||||
+3
-1
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.2.6"
|
||||
version = "0.2.13"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
@@ -29,6 +29,8 @@ dependencies = [
|
||||
"pypdf",
|
||||
"wcwidth",
|
||||
"onionshare-cli",
|
||||
"flask<2.3.0",
|
||||
"werkzeug<3.0.0",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -7,4 +7,6 @@ htmlmin2
|
||||
pypdf
|
||||
wcwidth
|
||||
onionshare-cli
|
||||
flask<2.3.0
|
||||
werkzeug<3.0.0
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
import setuptools
|
||||
print("pkg_resources available")
|
||||
except ImportError as e:
|
||||
print(f"Failed to import: {e}")
|
||||
|
||||
from voxvera import cli
|
||||
cli._internal_onionshare()
|
||||
@@ -0,0 +1,2 @@
|
||||
import pkg_resources
|
||||
print("Success")
|
||||
@@ -0,0 +1,2 @@
|
||||
import pkg_resources
|
||||
print("Success")
|
||||
+90
-50
@@ -14,11 +14,26 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
def _setup_tmp(monkeypatch, tmp_path):
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", tmp_path / "locales")
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
|
||||
# Use a unique subdirectory for this test run to ensure isolation
|
||||
test_data_dir = tmp_path / "data"
|
||||
test_data_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Internal resources (ROOT)
|
||||
res_root = test_data_dir / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", test_data_dir)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
# Create a default config.json in DATA_DIR
|
||||
shutil.copy(res_root / "src" / "config.json", test_data_dir / "config.json")
|
||||
return repo_root
|
||||
|
||||
|
||||
@@ -31,10 +46,11 @@ def test_help(capsys):
|
||||
|
||||
def test_init_template(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
# Mock choose_language to return 'en'
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
cli.main(["init", "--template", "voxvera"])
|
||||
dest = tmp_path / "host" / "voxvera"
|
||||
dest = test_data_dir / "host" / "voxvera"
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -43,7 +59,8 @@ def test_init_template(tmp_path, monkeypatch):
|
||||
def test_build(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
dest = tmp_path / "host" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / "voxvera"
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -54,14 +71,16 @@ def test_build_with_download(tmp_path, monkeypatch):
|
||||
download_file = tmp_path / "sample.zip"
|
||||
download_file.write_text("dummy")
|
||||
cli.main(["build", "--download", str(download_file)])
|
||||
dest = tmp_path / "host" / "voxvera" / "download" / "extra-content.zip"
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / "voxvera" / "download" / "extra-content.zip"
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
def test_import(tmp_path, monkeypatch):
|
||||
repo = _setup_tmp(monkeypatch, tmp_path)
|
||||
imports_dir = tmp_path / "imports"
|
||||
imports_dir.mkdir()
|
||||
test_data_dir = tmp_path / "data"
|
||||
imports_dir = test_data_dir / "imports"
|
||||
imports_dir.mkdir(parents=True, exist_ok=True)
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
for sub in ["foo", "bar"]:
|
||||
data = dict(base_data)
|
||||
@@ -70,7 +89,8 @@ def test_import(tmp_path, monkeypatch):
|
||||
json.dump(data, fh)
|
||||
cli.main(["batch-import"])
|
||||
for sub in ["foo", "bar"]:
|
||||
dest = tmp_path / "host" / sub
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / sub
|
||||
assert dest.is_dir()
|
||||
assert (dest / "index.html").exists()
|
||||
|
||||
@@ -78,8 +98,9 @@ def test_import(tmp_path, monkeypatch):
|
||||
def test_import_preserves_session(tmp_path, monkeypatch):
|
||||
"""Re-importing a config must not destroy the OnionShare session key."""
|
||||
repo = _setup_tmp(monkeypatch, tmp_path)
|
||||
imports_dir = tmp_path / "imports"
|
||||
imports_dir.mkdir()
|
||||
test_data_dir = tmp_path / "data"
|
||||
imports_dir = test_data_dir / "imports"
|
||||
imports_dir.mkdir(parents=True, exist_ok=True)
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
base_data["folder_name"] = "keep"
|
||||
with open(imports_dir / "keep.json", "w") as fh:
|
||||
@@ -87,7 +108,8 @@ def test_import_preserves_session(tmp_path, monkeypatch):
|
||||
|
||||
# first import creates the host dir
|
||||
cli.main(["batch-import"])
|
||||
host_dir = tmp_path / "host" / "keep"
|
||||
test_data_dir = tmp_path / "data"
|
||||
host_dir = test_data_dir / "host" / "keep"
|
||||
assert host_dir.is_dir()
|
||||
|
||||
# simulate OnionShare having written a session key
|
||||
@@ -129,13 +151,15 @@ 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"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
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" / folder_name / "download" / "extra-content.zip"
|
||||
dest = test_data_dir / "host" / folder_name / "download" / "extra-content.zip"
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
@@ -143,9 +167,11 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
"""Test that serve() auto-generates onion URL for tear-off links."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
folder_name = config["folder_name"]
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
dir_path = test_data_dir / "host" / folder_name
|
||||
|
||||
# Set a custom URL that should be preserved (content link)
|
||||
config["url"] = "https://example.com/external-resource"
|
||||
@@ -197,7 +223,9 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
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"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
folder_name = config["folder_name"]
|
||||
|
||||
# No need for TOR_* env vars anymore - auto-detection should work
|
||||
@@ -232,9 +260,12 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cli.subprocess, "Popen", FakePopen)
|
||||
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
config_path = test_data_dir / "config.json"
|
||||
cli.main(["quickstart", "--non-interactive"])
|
||||
# quickstart no longer calls serve, so we call it manually to test URL update
|
||||
cli.serve(str(config_path))
|
||||
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
dir_path = test_data_dir / "host" / folder_name
|
||||
assert (dir_path / "index.html").exists()
|
||||
log_file = dir_path / "onionshare.log"
|
||||
assert onion_url in log_file.read_text()
|
||||
@@ -285,22 +316,23 @@ def test_build_without_url_skips_qr(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Remove URLs from config
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = ""
|
||||
config["tear_off_link"] = ""
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
# Build should complete without error, just skip QR generation
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -314,21 +346,22 @@ def test_build_with_only_url(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Set only url (content link)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = "https://example.com/external-resource"
|
||||
config["tear_off_link"] = ""
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "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()
|
||||
@@ -339,21 +372,22 @@ def test_build_with_only_tear_off_link(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Set only tear_off_link (before serve sets it to onion)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = ""
|
||||
config["tear_off_link"] = "http://preconfigured.onion"
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "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()
|
||||
@@ -400,15 +434,18 @@ def test_backward_compatibility_with_https_config(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Simulate old config with HTTPS URLs
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = "https://example.com/old-style"
|
||||
config["tear_off_link"] = "https://example.com/tear-off"
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
|
||||
@@ -417,15 +454,18 @@ def test_long_urls_in_qr(tmp_path, monkeypatch):
|
||||
"""Test that long URLs still generate valid QR codes."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
# Very long URL (close to 200 char limit)
|
||||
config["url"] = "http://" + "a" * 190 + ".onion"
|
||||
config["tear_off_link"] = "https://example.com/" + "path/" * 20
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
+42
-23
@@ -107,13 +107,18 @@ class TestFullWorkflow:
|
||||
|
||||
# Setup temp workspace
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", tmp_path / "locales")
|
||||
# Internal resources (ROOT)
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
# Monkeypatch ROOT to use temp directory
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
# Create test config
|
||||
config = {
|
||||
@@ -129,7 +134,7 @@ class TestFullWorkflow:
|
||||
"binary_message": "0101010",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -203,12 +208,17 @@ class TestFullWorkflow:
|
||||
|
||||
# Setup
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", tmp_path / "locales")
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
config = {
|
||||
"name": "Reachability Test",
|
||||
@@ -223,7 +233,7 @@ class TestFullWorkflow:
|
||||
"binary_message": "101010",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -333,12 +343,17 @@ class TestSiteFiles:
|
||||
|
||||
# Setup
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", tmp_path / "locales")
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
config = {
|
||||
"name": "Test",
|
||||
@@ -353,7 +368,7 @@ class TestSiteFiles:
|
||||
"binary_message": "",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -383,8 +398,8 @@ class TestErrorHandling:
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
|
||||
# Try to serve without building first
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -407,11 +422,13 @@ class TestErrorHandling:
|
||||
class TestTorIntegration:
|
||||
"""Tests for Tor integration."""
|
||||
|
||||
def test_tor_auto_detection_falls_back_to_defaults(self, monkeypatch):
|
||||
def test_tor_auto_detection_falls_back_to_defaults(self, monkeypatch, tmp_path):
|
||||
"""Test that Tor port detection falls back to defaults when Tor isn't running."""
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from voxvera import cli
|
||||
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
|
||||
# Clear environment variables
|
||||
monkeypatch.delenv("TOR_SOCKS_PORT", raising=False)
|
||||
monkeypatch.delenv("TOR_CONTROL_PORT", raising=False)
|
||||
@@ -440,11 +457,13 @@ class TestTorIntegration:
|
||||
assert socks == "9050"
|
||||
assert ctl == "9051"
|
||||
|
||||
def test_tor_env_vars_override_detection(self, monkeypatch):
|
||||
def test_tor_env_vars_override_detection(self, monkeypatch, tmp_path):
|
||||
"""Test that environment variables override auto-detection."""
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from voxvera import cli
|
||||
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
|
||||
monkeypatch.setenv("TOR_SOCKS_PORT", "9999")
|
||||
monkeypatch.setenv("TOR_CONTROL_PORT", "8888")
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ def test_export_import_roundtrip(tmp_path, monkeypatch):
|
||||
shutil.copytree(REPO_ROOT / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(REPO_ROOT / "voxvera" / "locales", tmp_path / "locales")
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
|
||||
|
||||
+6
-3
@@ -87,15 +87,18 @@ install_voxvera() {
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/download/${latest}/${BINARY}-${ARCH}"
|
||||
|
||||
msg "Downloading from: $url"
|
||||
set +e
|
||||
local status
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest" || echo "failed")
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
local curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
if [ $curl_exit -eq 0 ] && [ "$status" = "200" ]; then
|
||||
chmod +x "$dest"
|
||||
msg "VoxVera binary (${ARCH}) installed to $dest"
|
||||
return 0
|
||||
else
|
||||
warn "Binary download failed (status: $status). Falling back to source installation."
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
|
||||
+6
-3
@@ -1,13 +1,16 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.2.6"
|
||||
__version__ = "0.2.13"
|
||||
|
||||
# Add the vendor directory to sys.path as a fallback.
|
||||
# Add the vendor directory to sys.path. We prepend it to ensure our bundled
|
||||
# dependencies take priority over potentially incompatible system packages
|
||||
# (e.g. Flask/Werkzeug version mismatches on Debian).
|
||||
if getattr(sys, 'frozen', False):
|
||||
vendor_dir = Path(sys._MEIPASS).joinpath("voxvera", "vendor")
|
||||
else:
|
||||
vendor_dir = Path(__file__).parent / "vendor"
|
||||
|
||||
if vendor_dir.exists():
|
||||
if str(vendor_dir) not in sys.path:
|
||||
sys.path.append(str(vendor_dir))
|
||||
sys.path.insert(0, str(vendor_dir))
|
||||
+113
-74
@@ -10,6 +10,17 @@ import time
|
||||
from pathlib import Path
|
||||
from importlib import resources
|
||||
from importlib.resources.abc import Traversable
|
||||
|
||||
# Force PyInstaller to bundle these critical dependencies for onionshare-cli
|
||||
try:
|
||||
import pkg_resources
|
||||
import setuptools
|
||||
import jaraco
|
||||
import platformdirs
|
||||
import packaging
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from InquirerPy import prompt, inquirer
|
||||
from rich.console import Console
|
||||
from voxvera import __version__
|
||||
@@ -17,9 +28,15 @@ from voxvera import __version__
|
||||
# package root (contains bundled templates and src/)
|
||||
if getattr(sys, 'frozen', False):
|
||||
ROOT = Path(sys._MEIPASS).joinpath("voxvera")
|
||||
# Ensure the bundle root is in sys.path for collected packages
|
||||
if sys._MEIPASS not in sys.path:
|
||||
sys.path.insert(0, sys._MEIPASS)
|
||||
else:
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# Persistent data directory (where host/ and projects live)
|
||||
DATA_DIR = Path(os.environ.get("VOXVERA_DIR", Path.cwd()))
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
|
||||
@@ -174,7 +191,7 @@ def _open_editor_terminal(initial: str) -> str:
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fh.write(initial or "")
|
||||
subprocess.call([editor, path])
|
||||
subprocess.run([editor, path], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
except Exception as e:
|
||||
@@ -369,11 +386,18 @@ def interactive_update(config_path: str):
|
||||
|
||||
console.rule(t("cli.init_links"))
|
||||
link_qs = [
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.attachment_path_label"),
|
||||
"name": "attachment_path",
|
||||
"default": data.get("attachment_path", ""),
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.url_label"),
|
||||
"name": "url",
|
||||
"default": data.get("url", "https://voxvera.org/"),
|
||||
"default": data.get("url", "https://voxvera.org/") if not data.get("url", "").startswith("download/") else "",
|
||||
"when": lambda result: not result.get("attachment_path", "").strip(),
|
||||
"transformer": _len_transform(200),
|
||||
"validate": _len_validator(200),
|
||||
},
|
||||
@@ -385,21 +409,6 @@ def interactive_update(config_path: str):
|
||||
"transformer": _len_transform(120),
|
||||
"validate": _len_validator(120),
|
||||
},
|
||||
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.binary_message_label"),
|
||||
"name": "binary_message",
|
||||
"default": data.get("binary_message", ""),
|
||||
"transformer": _len_transform(120),
|
||||
"validate": _len_validator(120),
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.attachment_path_label"),
|
||||
"name": "attachment_path",
|
||||
"default": data.get("attachment_path", ""),
|
||||
},
|
||||
]
|
||||
data.update(prompt(link_qs))
|
||||
|
||||
@@ -412,9 +421,13 @@ def copy_template(name: str) -> str:
|
||||
if not src.is_dir():
|
||||
print(f"Template {name} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
dest = ROOT / "host" / name
|
||||
os.makedirs(ROOT / "host", exist_ok=True)
|
||||
dest = DATA_DIR / "host" / name
|
||||
os.makedirs(DATA_DIR / "host", exist_ok=True)
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
# Clear any QR codes that might have been in the template
|
||||
# to ensure a clean build slate.
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
(dest / qr_file).unlink(missing_ok=True)
|
||||
print(f"Template copied to {dest}")
|
||||
return str(dest)
|
||||
|
||||
@@ -460,8 +473,6 @@ def build_assets(
|
||||
|
||||
with resources.as_file(_src_res()) as src_dir:
|
||||
config_path = Path(config_path)
|
||||
# generate QR codes (pure Python)
|
||||
generate_qr(config_path, src_dir)
|
||||
|
||||
data = load_config(config_path)
|
||||
with open(src_dir / "index-master.html", "r") as fh:
|
||||
@@ -534,23 +545,43 @@ def build_assets(
|
||||
val_str = re.sub(r"~~(.*?)~~", r'<span class="redacted">\1</span>', val_str)
|
||||
html = html.replace(f"{{{{{key}}}}}", val_str)
|
||||
|
||||
# Handle optional file attachment
|
||||
# Handle optional file attachment logic
|
||||
attachment_path = data.get("attachment_path")
|
||||
attachment_url = ""
|
||||
attachment_display = "none"
|
||||
if attachment_path and os.path.isfile(attachment_path):
|
||||
att_filename = os.path.basename(attachment_path)
|
||||
attachment_url = f"download/{att_filename}"
|
||||
attachment_display = "inline-block"
|
||||
|
||||
html = html.replace("{{attachment_url}}", attachment_url)
|
||||
html = html.replace("{{attachment_display}}", attachment_display)
|
||||
|
||||
# We don't replace the URL here if it's already an onion link (e.g. during serve rebuild)
|
||||
if not data.get("url", "").startswith("http://") or ".onion" not in data.get("url", ""):
|
||||
data["url"] = f"download/{att_filename}"
|
||||
# Must write back to config so generate_qr uses the new URL
|
||||
save_config(data, config_path)
|
||||
|
||||
# Also update the HTML we just replaced if we changed it
|
||||
html = html.replace("{{url}}", data["url"])
|
||||
|
||||
folder_name = data["folder_name"]
|
||||
dest = ROOT / "host" / folder_name
|
||||
dest = DATA_DIR / "host" / folder_name
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
# 1. Refresh QR codes directly in destination
|
||||
# generate_qr only creates them if URLs are present in config
|
||||
|
||||
# We generate to a temp dir first to ensure we ONLY get what's in current config
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as qr_tmp:
|
||||
qr_tmp_path = Path(qr_tmp)
|
||||
generate_qr(config_path, qr_tmp_path)
|
||||
|
||||
# Clear old QR codes from destination and copy new ones
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_dest = dest / qr_file
|
||||
qr_src = qr_tmp_path / qr_file
|
||||
if qr_src.exists():
|
||||
shutil.copy(qr_src, qr_dest)
|
||||
elif qr_dest.exists():
|
||||
qr_dest.unlink()
|
||||
|
||||
# Update download link to point to our viral source bundle
|
||||
|
||||
html = html.replace("download/download.zip", "download/voxvera-source.zip")
|
||||
|
||||
with open(dest / "index.html", "w") as fh:
|
||||
@@ -571,12 +602,6 @@ def build_assets(
|
||||
if Path(config_path) != dest / "config.json":
|
||||
shutil.copy(config_path, dest / "config.json")
|
||||
|
||||
# Copy QR codes if they exist
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_src = Path(src_dir) / qr_file
|
||||
if qr_src.exists():
|
||||
shutil.copy(qr_src, dest / qr_file)
|
||||
|
||||
print(f"Flyer files created under {dest}")
|
||||
|
||||
|
||||
@@ -648,12 +673,15 @@ def _internal_onionshare():
|
||||
|
||||
try:
|
||||
from onionshare_cli import main
|
||||
# sys.argv is ['voxvera', '_internal_onionshare', '--website', ...]
|
||||
# We need to make it look like ['onionshare-cli', '--website', ...]
|
||||
# sys.argv is ['voxvera', '_internal_onionshare', '--website', ... ]
|
||||
# We need to make it look like ['onionshare-cli', '--website', ... ]
|
||||
sys.argv = ["onionshare-cli"] + sys.argv[2:]
|
||||
sys.exit(main())
|
||||
except ImportError:
|
||||
print("Error: onionshare-cli not bundled or installed.", file=sys.stderr)
|
||||
except ImportError as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"Error: onionshare-cli not bundled or installed correctly: {e}", file=sys.stderr)
|
||||
print(f"DEBUG: sys.path = {sys.path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -661,7 +689,7 @@ def serve(config_path: str) -> str | None:
|
||||
socks, ctl = get_tor_ports()
|
||||
|
||||
folder_name = load_config(config_path)["folder_name"]
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
dir_path = DATA_DIR / "host" / folder_name
|
||||
if not dir_path.is_dir():
|
||||
print(f"Directory {dir_path} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -779,9 +807,9 @@ def get_export_dir() -> Path:
|
||||
|
||||
def export_site(folder_name: str, output_path: str = None):
|
||||
"""Export a site folder and its keys into a zip archive."""
|
||||
source_dir = ROOT / "host" / folder_name
|
||||
source_dir = DATA_DIR / "host" / folder_name
|
||||
if not source_dir.exists():
|
||||
print(f"Error: Site folder '{folder_name}' not found in {ROOT / 'host'}")
|
||||
print(f"Error: Site folder '{folder_name}' not found in {DATA_DIR / 'host'}")
|
||||
return
|
||||
|
||||
if not output_path:
|
||||
@@ -837,7 +865,7 @@ def import_site(zip_path: str):
|
||||
print("Error: Invalid config.json. Missing 'folder_name'.")
|
||||
return
|
||||
|
||||
dest_dir = ROOT / "host" / folder_name
|
||||
dest_dir = DATA_DIR / "host" / folder_name
|
||||
if dest_dir.exists():
|
||||
confirm = inquirer.confirm(message=f"Site '{folder_name}' already exists. Overwrite?").execute()
|
||||
if not confirm:
|
||||
@@ -928,25 +956,31 @@ def build_docs():
|
||||
|
||||
def build_site():
|
||||
"""Refresh the main site/ directory with the latest template, QRs, and portable bundle."""
|
||||
# When frozen, site/ source is in sys._MEIPASS, but we might want to output to CWD/site
|
||||
if getattr(sys, 'frozen', False):
|
||||
site_dir = Path(sys._MEIPASS).joinpath("site")
|
||||
src_site = Path(sys._MEIPASS).joinpath("site")
|
||||
dest_site = Path.cwd() / "site"
|
||||
else:
|
||||
site_dir = ROOT.parent / "site"
|
||||
src_site = ROOT.parent / "site"
|
||||
dest_site = src_site
|
||||
|
||||
if not site_dir.exists():
|
||||
print(f"Error: {site_dir} directory not found.")
|
||||
if not src_site.exists():
|
||||
print(f"Error: {src_site} directory not found.")
|
||||
return
|
||||
|
||||
config_path = site_dir / "config.json"
|
||||
config_path = dest_site / "config.json"
|
||||
if not config_path.exists():
|
||||
print(f"Error: {config_path} not found.")
|
||||
return
|
||||
# Fallback to source if dest doesn't have it (e.g. first run)
|
||||
config_path = src_site / "config.json"
|
||||
if not config_path.exists():
|
||||
print(f"Error: {config_path} not found.")
|
||||
return
|
||||
|
||||
print("Refreshing site/ assets...")
|
||||
print(f"Refreshing {dest_site} assets...")
|
||||
|
||||
# 1. Refresh QR codes using site/config.json
|
||||
from voxvera.build import generate_qr
|
||||
generate_qr(config_path, site_dir)
|
||||
generate_qr(config_path, dest_site)
|
||||
|
||||
# 2. Build the HTML from master template
|
||||
data = load_config(config_path)
|
||||
@@ -1026,11 +1060,12 @@ def build_site():
|
||||
for key, value in data.items():
|
||||
html = html.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
with open(site_dir / "index.html", "w") as fh:
|
||||
os.makedirs(dest_site, exist_ok=True)
|
||||
with open(dest_site / "index.html", "w") as fh:
|
||||
fh.write(html)
|
||||
|
||||
# 5. Create the download assets in site/download/
|
||||
download_dir = site_dir / "download"
|
||||
download_dir = dest_site / "download"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
# Bundle source version
|
||||
@@ -1070,33 +1105,39 @@ def vendorize():
|
||||
|
||||
|
||||
def batch_import_configs():
|
||||
files = sorted(glob.glob(str(ROOT / "imports" / "*.json")))
|
||||
files = sorted(glob.glob(str(DATA_DIR / "imports" / "*.json")))
|
||||
if not files:
|
||||
print("No JSON files found in imports")
|
||||
return
|
||||
for json_file in files:
|
||||
print(f"Processing {json_file}")
|
||||
dest_config = ROOT / "src" / "config.json"
|
||||
# Use a temp config path in DATA_DIR instead of bundled ROOT
|
||||
dest_config = DATA_DIR / "temp_config.json"
|
||||
shutil.copy(json_file, dest_config)
|
||||
folder_name = load_config(json_file)["folder_name"]
|
||||
_clear_host_dir(ROOT / "host" / folder_name)
|
||||
build_assets(dest_config)
|
||||
_clear_host_dir(DATA_DIR / "host" / folder_name)
|
||||
build_assets(str(dest_config))
|
||||
dest_config.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def get_servers() -> list[str]:
|
||||
# Check both bundled ROOT/host and local CWD/host
|
||||
search_paths = [ROOT / "host", Path.cwd() / "host"]
|
||||
# Check both bundled ROOT/host and local DATA_DIR/host
|
||||
search_paths = []
|
||||
if ROOT != DATA_DIR:
|
||||
search_paths.append(ROOT / "host")
|
||||
search_paths.append(DATA_DIR / "host")
|
||||
|
||||
servers = set()
|
||||
for host_dir in search_paths:
|
||||
if host_dir.exists():
|
||||
for d in host_dir.iterdir():
|
||||
if d.is_dir() and (d / "config.json").exists():
|
||||
servers.append(d.name) if hasattr(servers, 'append') else servers.add(d.name)
|
||||
servers.add(d.name)
|
||||
return sorted(list(servers))
|
||||
|
||||
|
||||
def is_server_running(folder_name: str) -> bool:
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
pid_file = DATA_DIR / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
return False
|
||||
try:
|
||||
@@ -1135,7 +1176,7 @@ def is_server_running(folder_name: str) -> bool:
|
||||
|
||||
|
||||
def stop_server(folder_name: str):
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
pid_file = DATA_DIR / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
print(f"Server {folder_name} is not running.")
|
||||
return
|
||||
@@ -1157,7 +1198,7 @@ def stop_server(folder_name: str):
|
||||
def delete_server(folder_name: str):
|
||||
if is_server_running(folder_name):
|
||||
stop_server(folder_name)
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
dir_path = DATA_DIR / "host" / folder_name
|
||||
if dir_path.exists():
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
print(f"Deleted server {folder_name}")
|
||||
@@ -1179,7 +1220,7 @@ def manage_servers():
|
||||
label = f"{status_text} {s}"
|
||||
if running:
|
||||
try:
|
||||
site_data = load_config(ROOT / "host" / s / "config.json")
|
||||
site_data = load_config(DATA_DIR / "host" / s / "config.json")
|
||||
url = site_data.get("tear_off_link")
|
||||
if url:
|
||||
label += f" ({url})"
|
||||
@@ -1204,7 +1245,7 @@ def manage_servers():
|
||||
break
|
||||
|
||||
if selected == "create_new":
|
||||
config_path = ROOT / "src" / "config.json"
|
||||
config_path = DATA_DIR / "config.json"
|
||||
# Reset config from template for a fresh start
|
||||
try:
|
||||
template_config = _template_res("voxvera", "config.json")
|
||||
@@ -1228,7 +1269,7 @@ def manage_servers():
|
||||
results = []
|
||||
for s in servers:
|
||||
if not is_server_running(s):
|
||||
config_path = ROOT / "host" / s / "config.json"
|
||||
config_path = DATA_DIR / "host" / s / "config.json"
|
||||
try:
|
||||
url = serve(str(config_path))
|
||||
if url:
|
||||
@@ -1275,13 +1316,13 @@ def manage_servers():
|
||||
if running:
|
||||
stop_server(selected)
|
||||
else:
|
||||
config_path = ROOT / "host" / selected / "config.json"
|
||||
config_path = DATA_DIR / "host" / selected / "config.json"
|
||||
try:
|
||||
serve(str(config_path))
|
||||
except SystemExit:
|
||||
pass
|
||||
elif action == "edit":
|
||||
config_path = ROOT / "host" / selected / "config.json"
|
||||
config_path = DATA_DIR / "host" / selected / "config.json"
|
||||
try:
|
||||
interactive_update(str(config_path))
|
||||
# Re-build assets after update
|
||||
@@ -1340,7 +1381,7 @@ def main(argv=None):
|
||||
parser = argparse.ArgumentParser(prog="voxvera")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=str(ROOT / "src" / "config.json"),
|
||||
default=str(DATA_DIR / "config.json"),
|
||||
help="Path to config.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -1453,7 +1494,6 @@ def main(argv=None):
|
||||
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 == "export":
|
||||
@@ -1481,7 +1521,6 @@ def main(argv=None):
|
||||
sys.exit(1)
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
serve(config_path)
|
||||
elif args.command == "check":
|
||||
check_deps()
|
||||
elif args.command == "vendorize":
|
||||
|
||||
@@ -579,9 +579,6 @@
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
|
||||
</a>
|
||||
<a id="attachment-link" href="{{attachment_url}}" download style="display: {{attachment_display}};">
|
||||
<button class="download-button" data-i18n="web.download_attachment">{{t_web_download_attachment}}</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user