Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3438b271a5
|
|||
| d6228d6d63 | |||
|
ccf954681b
|
|||
|
4ec44900cf
|
|||
|
d4099fb9a2
|
|||
|
1571b315b2
|
|||
|
71bd49bd7d
|
|||
|
382413dc08
|
36
.github/workflows/publish.yml
vendored
36
.github/workflows/publish.yml
vendored
@@ -1,5 +1,14 @@
|
|||||||
name: Publish Python 🐍 distribution 📦 to PyPI
|
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||||
|
|
||||||
|
# This workflow creates immutable releases:
|
||||||
|
# 1. Build packages
|
||||||
|
# 2. Publish to PyPI (only on tag push)
|
||||||
|
# 3. After successful PyPI publish:
|
||||||
|
# - Sign artifacts
|
||||||
|
# - Check if GitHub release exists (idempotent)
|
||||||
|
# - Create release with all artifacts atomically
|
||||||
|
# This ensures releases cannot be modified once published.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -83,18 +92,27 @@ jobs:
|
|||||||
inputs: >-
|
inputs: >-
|
||||||
./dist/*.tar.gz
|
./dist/*.tar.gz
|
||||||
./dist/*.whl
|
./dist/*.whl
|
||||||
- name: Create GitHub Release
|
- name: Check if release exists
|
||||||
|
id: check_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $GITHUB_REF_NAME already exists, skipping creation"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $GITHUB_REF_NAME does not exist, will create"
|
||||||
|
fi
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Create GitHub Release with artifacts
|
||||||
|
if: steps.check_release.outputs.exists != 'true'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: >-
|
run: >-
|
||||||
gh release create
|
gh release create
|
||||||
"$GITHUB_REF_NAME"
|
"$GITHUB_REF_NAME"
|
||||||
--repo "$GITHUB_REPOSITORY"
|
--repo "$GITHUB_REPOSITORY"
|
||||||
--notes ""
|
--title "Release $GITHUB_REF_NAME"
|
||||||
- name: Upload artifact signatures to GitHub Release
|
--notes "PyPI: https://pypi.org/project/rns-page-node/$GITHUB_REF_NAME/"
|
||||||
env:
|
dist/*
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: >-
|
|
||||||
gh release upload
|
|
||||||
"$GITHUB_REF_NAME" dist/**
|
|
||||||
--repo "$GITHUB_REPOSITORY"
|
|
||||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -8,13 +8,18 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
os: ["ubuntu-latest", "windows-latest"]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -40,5 +45,5 @@ jobs:
|
|||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-logs-python-${{ matrix.python-version }}
|
name: test-logs-${{ matrix.os }}-${{ matrix.python-version }}
|
||||||
path: tests/node.log
|
path: tests/node.log
|
||||||
|
|||||||
70
poetry.lock
generated
70
poetry.lock
generated
@@ -98,57 +98,6 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cryptography"
|
|
||||||
version = "43.0.3"
|
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
groups = ["main"]
|
|
||||||
markers = "python_full_version < \"3.14.0\" or platform_python_implementation == \"PyPy\""
|
|
||||||
files = [
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
|
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
|
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
|
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
|
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
|
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
|
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
|
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
|
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
|
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
|
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
|
|
||||||
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
|
||||||
nox = ["nox"]
|
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
|
||||||
sdist = ["build"]
|
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
|
||||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
|
||||||
test-randomorder = ["pytest-randomly"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "46.0.3"
|
version = "46.0.3"
|
||||||
@@ -156,7 +105,6 @@ description = "cryptography is a package which provides cryptographic recipes an
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "python_version >= \"3.14\" and platform_python_implementation != \"PyPy\""
|
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
||||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
||||||
@@ -216,6 +164,7 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||||
|
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
@@ -301,7 +250,20 @@ files = [
|
|||||||
{file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
|
{file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "python_full_version < \"3.11.0\""
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9"
|
python-versions = ">3.9.0,<3.9.1 || >3.9.1"
|
||||||
content-hash = "77e36900b1ae8e63ed10aaf461a3fada9c572a606865eaa01af02aec20ce3a73"
|
content-hash = "1be824dfb2b426a79853223eb90b868160d299c549d6eca850d8a982b7336aef"
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ authors = [
|
|||||||
{name = "Sudo-Ivan"}
|
{name = "Sudo-Ivan"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">3.9.0,<3.9.1 || >3.9.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rns (>=1.0.4,<1.5.0)"
|
"rns (>=1.0.4,<1.5.0)",
|
||||||
|
"cryptography>=46.0.3"
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|||||||
@@ -124,11 +124,12 @@ class PageNode:
|
|||||||
|
|
||||||
def register_pages(self):
|
def register_pages(self):
|
||||||
"""Scan pages directory and register request handlers for all .mu files."""
|
"""Scan pages directory and register request handlers for all .mu files."""
|
||||||
with self._lock:
|
pages = self._scan_pages(self.pagespath)
|
||||||
self.servedpages = []
|
|
||||||
self._scan_pages(self.pagespath)
|
|
||||||
|
|
||||||
pagespath = Path(self.pagespath)
|
with self._lock:
|
||||||
|
self.servedpages = pages
|
||||||
|
|
||||||
|
pagespath = Path(self.pagespath).resolve()
|
||||||
|
|
||||||
if not (pagespath / "index.mu").is_file():
|
if not (pagespath / "index.mu").is_file():
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
@@ -137,11 +138,13 @@ class PageNode:
|
|||||||
allow=RNS.Destination.ALLOW_ALL,
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
for full_path in self.servedpages:
|
for full_path in pages:
|
||||||
rel = full_path[len(str(pagespath)) :]
|
page_path = Path(full_path).resolve()
|
||||||
if not rel.startswith("/"):
|
try:
|
||||||
rel = "/" + rel
|
rel = page_path.relative_to(pagespath).as_posix()
|
||||||
request_path = f"/page{rel}"
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/page/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_page,
|
response_generator=self.serve_page,
|
||||||
@@ -150,17 +153,20 @@ class PageNode:
|
|||||||
|
|
||||||
def register_files(self):
|
def register_files(self):
|
||||||
"""Scan files directory and register request handlers for all files."""
|
"""Scan files directory and register request handlers for all files."""
|
||||||
|
files = self._scan_files(self.filespath)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.servedfiles = []
|
self.servedfiles = files
|
||||||
self._scan_files(self.filespath)
|
|
||||||
|
|
||||||
filespath = Path(self.filespath)
|
filespath = Path(self.filespath).resolve()
|
||||||
|
|
||||||
for full_path in self.servedfiles:
|
for full_path in files:
|
||||||
rel = full_path[len(str(filespath)) :]
|
file_path = Path(full_path).resolve()
|
||||||
if not rel.startswith("/"):
|
try:
|
||||||
rel = "/" + rel
|
rel = file_path.relative_to(filespath).as_posix()
|
||||||
request_path = f"/file{rel}"
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/file/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_file,
|
response_generator=self.serve_file,
|
||||||
@@ -169,24 +175,34 @@ class PageNode:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _scan_pages(self, base):
|
def _scan_pages(self, base):
|
||||||
|
"""Return a list of page paths under the given directory, excluding .allowed files."""
|
||||||
base_path = Path(base)
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
for entry in base_path.iterdir():
|
for entry in base_path.iterdir():
|
||||||
if entry.name.startswith("."):
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
self._scan_pages(str(entry))
|
served.extend(self._scan_pages(entry))
|
||||||
elif entry.is_file() and not entry.name.endswith(".allowed"):
|
elif entry.is_file() and not entry.name.endswith(".allowed"):
|
||||||
self.servedpages.append(str(entry))
|
served.append(str(entry))
|
||||||
|
return served
|
||||||
|
|
||||||
def _scan_files(self, base):
|
def _scan_files(self, base):
|
||||||
|
"""Return all file paths under the given directory."""
|
||||||
base_path = Path(base)
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
for entry in base_path.iterdir():
|
for entry in base_path.iterdir():
|
||||||
if entry.name.startswith("."):
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
self._scan_files(str(entry))
|
served.extend(self._scan_files(entry))
|
||||||
elif entry.is_file():
|
elif entry.is_file():
|
||||||
self.servedfiles.append(str(entry))
|
served.append(str(entry))
|
||||||
|
return served
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serve_default_index(
|
def serve_default_index(
|
||||||
@@ -216,17 +232,25 @@ class PageNode:
|
|||||||
|
|
||||||
if not str(file_path).startswith(str(pagespath)):
|
if not str(file_path).startswith(str(pagespath)):
|
||||||
return DEFAULT_NOTALLOWED.encode("utf-8")
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
try:
|
|
||||||
with file_path.open("rb") as _f:
|
|
||||||
first_line = _f.readline()
|
|
||||||
is_script = first_line.startswith(b"#!")
|
|
||||||
except Exception:
|
|
||||||
is_script = False
|
is_script = False
|
||||||
|
file_content = None
|
||||||
|
try:
|
||||||
|
with file_path.open("rb") as file_handle:
|
||||||
|
first_line = file_handle.readline()
|
||||||
|
is_script = first_line.startswith(b"#!")
|
||||||
|
file_handle.seek(0)
|
||||||
|
if not is_script:
|
||||||
|
return file_handle.read()
|
||||||
|
file_content = file_handle.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
except OSError as err:
|
||||||
|
RNS.log(f"Error reading page {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
if is_script and os.access(str(file_path), os.X_OK):
|
if is_script and os.access(str(file_path), os.X_OK):
|
||||||
try:
|
try:
|
||||||
env_map = {}
|
env_map = os.environ.copy()
|
||||||
if "PATH" in os.environ:
|
|
||||||
env_map["PATH"] = os.environ["PATH"]
|
|
||||||
if _link_id is not None:
|
if _link_id is not None:
|
||||||
env_map["link_id"] = RNS.hexrep(_link_id, delimit=False)
|
env_map["link_id"] = RNS.hexrep(_link_id, delimit=False)
|
||||||
if remote_identity is not None:
|
if remote_identity is not None:
|
||||||
@@ -249,8 +273,21 @@ class PageNode:
|
|||||||
return result.stdout
|
return result.stdout
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
||||||
with file_path.open("rb") as f:
|
if file_content is not None:
|
||||||
return f.read()
|
return file_content
|
||||||
|
try:
|
||||||
|
return self._read_file_bytes(file_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
except OSError as err:
|
||||||
|
RNS.log(f"Error reading page fallback {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_file_bytes(file_path):
|
||||||
|
"""Read a file's bytes and return the contents."""
|
||||||
|
with file_path.open("rb") as file_handle:
|
||||||
|
return file_handle.read()
|
||||||
|
|
||||||
def serve_file(
|
def serve_file(
|
||||||
self,
|
self,
|
||||||
@@ -278,35 +315,76 @@ class PageNode:
|
|||||||
"""Handle new link connections."""
|
"""Handle new link connections."""
|
||||||
|
|
||||||
def _announce_loop(self):
|
def _announce_loop(self):
|
||||||
|
"""Periodically announce the node until shutdown is requested."""
|
||||||
|
interval_seconds = max(self.announce_interval, 0) * 60
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
if time.time() - self.last_announce > self.announce_interval * 60:
|
now = time.time()
|
||||||
|
if (
|
||||||
|
self.last_announce == 0
|
||||||
|
or now - self.last_announce >= interval_seconds
|
||||||
|
):
|
||||||
|
try:
|
||||||
if self.name:
|
if self.name:
|
||||||
self.destination.announce(app_data=self.name.encode("utf-8"))
|
self.destination.announce(
|
||||||
|
app_data=self.name.encode("utf-8"),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.destination.announce()
|
self.destination.announce()
|
||||||
self.last_announce = time.time()
|
self.last_announce = time.time()
|
||||||
time.sleep(1)
|
except (TypeError, ValueError) as announce_error:
|
||||||
|
RNS.log(
|
||||||
|
f"Error during announce: {announce_error}",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
|
wait_time = max(
|
||||||
|
(self.last_announce + interval_seconds) - time.time()
|
||||||
|
if self.last_announce
|
||||||
|
else 0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
def _refresh_loop(self):
|
def _refresh_loop(self):
|
||||||
|
"""Refresh page and file registrations at configured intervals."""
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if (
|
if (
|
||||||
self.page_refresh_interval > 0
|
self.page_refresh_interval > 0
|
||||||
and now - self.last_page_refresh > self.page_refresh_interval
|
and now - self.last_page_refresh >= self.page_refresh_interval
|
||||||
):
|
):
|
||||||
self.register_pages()
|
self.register_pages()
|
||||||
self.last_page_refresh = now
|
self.last_page_refresh = time.time()
|
||||||
if (
|
if (
|
||||||
self.file_refresh_interval > 0
|
self.file_refresh_interval > 0
|
||||||
and now - self.last_file_refresh > self.file_refresh_interval
|
and now - self.last_file_refresh >= self.file_refresh_interval
|
||||||
):
|
):
|
||||||
self.register_files()
|
self.register_files()
|
||||||
self.last_file_refresh = now
|
self.last_file_refresh = time.time()
|
||||||
time.sleep(1)
|
|
||||||
|
wait_candidates = []
|
||||||
|
if self.page_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_page_refresh + self.page_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self.file_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_file_refresh + self.file_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_time = min(wait_candidates) if wait_candidates else 1.0
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
@@ -415,7 +493,7 @@ def main():
|
|||||||
return arg_value
|
return arg_value
|
||||||
if config_key in config:
|
if config_key in config:
|
||||||
try:
|
try:
|
||||||
if value_type == int:
|
if value_type is int:
|
||||||
return int(config[config_key])
|
return int(config[config_key])
|
||||||
return config[config_key]
|
return config[config_key]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -430,19 +508,40 @@ def main():
|
|||||||
files_dir = get_config_value(args.files_dir, str(Path.cwd() / "files"), "files-dir")
|
files_dir = get_config_value(args.files_dir, str(Path.cwd() / "files"), "files-dir")
|
||||||
node_name = get_config_value(args.node_name, None, "node-name")
|
node_name = get_config_value(args.node_name, None, "node-name")
|
||||||
announce_interval = get_config_value(
|
announce_interval = get_config_value(
|
||||||
args.announce_interval, 360, "announce-interval", int,
|
args.announce_interval,
|
||||||
|
360,
|
||||||
|
"announce-interval",
|
||||||
|
int,
|
||||||
)
|
)
|
||||||
identity_dir = get_config_value(
|
identity_dir = get_config_value(
|
||||||
args.identity_dir, str(Path.cwd() / "node-config"), "identity-dir",
|
args.identity_dir,
|
||||||
|
str(Path.cwd() / "node-config"),
|
||||||
|
"identity-dir",
|
||||||
)
|
)
|
||||||
page_refresh_interval = get_config_value(
|
page_refresh_interval = get_config_value(
|
||||||
args.page_refresh_interval, 0, "page-refresh-interval", int,
|
args.page_refresh_interval,
|
||||||
|
0,
|
||||||
|
"page-refresh-interval",
|
||||||
|
int,
|
||||||
)
|
)
|
||||||
file_refresh_interval = get_config_value(
|
file_refresh_interval = get_config_value(
|
||||||
args.file_refresh_interval, 0, "file-refresh-interval", int,
|
args.file_refresh_interval,
|
||||||
|
0,
|
||||||
|
"file-refresh-interval",
|
||||||
|
int,
|
||||||
)
|
)
|
||||||
log_level = get_config_value(args.log_level, "INFO", "log-level")
|
log_level = get_config_value(args.log_level, "INFO", "log-level")
|
||||||
|
|
||||||
|
# Set RNS log level based on command line argument
|
||||||
|
log_level_map = {
|
||||||
|
"CRITICAL": RNS.LOG_CRITICAL,
|
||||||
|
"ERROR": RNS.LOG_ERROR,
|
||||||
|
"WARNING": RNS.LOG_WARNING,
|
||||||
|
"INFO": RNS.LOG_INFO,
|
||||||
|
"DEBUG": RNS.LOG_DEBUG,
|
||||||
|
}
|
||||||
|
RNS.loglevel = log_level_map.get(log_level.upper(), RNS.LOG_INFO)
|
||||||
|
|
||||||
RNS.Reticulum(configpath)
|
RNS.Reticulum(configpath)
|
||||||
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
||||||
identity_file = Path(identity_dir) / "identity"
|
identity_file = Path(identity_dir) / "identity"
|
||||||
|
|||||||
Reference in New Issue
Block a user