8 Commits
v1.3.0 ... main

Author SHA1 Message Date
3438b271a5 Update dependencies in poetry.lock and pyproject.toml
Some checks failed
Build and Publish Docker Image / build (push) Failing after 3s
Run Tests / test (ubuntu-latest, 3.9) (push) Failing after 3s
Docker Build Test / build (3.10) (push) Successful in 56s
Run Tests / test (windows-latest, 3.13) (push) Has been cancelled
Run Tests / test (windows-latest, 3.9) (push) Has been cancelled
Docker Build Test / build (3.13) (push) Failing after 4s
Docker Build Test / build (3.9) (push) Failing after 4s
Safety / security (push) Failing after 3s
Run Tests / test (ubuntu-latest, 3.10) (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.11) (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.12) (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.13) (push) Failing after 3s
Docker Build Test / build (3.11) (push) Successful in 54s
Docker Build Test / build (3.12) (push) Successful in 52s
Run Tests / test (windows-latest, 3.10) (push) Has been cancelled
Run Tests / test (windows-latest, 3.11) (push) Has been cancelled
Run Tests / test (windows-latest, 3.12) (push) Has been cancelled
- Removed old version of `cryptography` (43.0.3) and updated to version 46.0.3 with adjusted markers.
- Added `typing-extensions` package (4.15.0) to support type hints for Python 3.9+.
- Updated Python version constraints in `pyproject.toml` for better compatibility.
2025-12-02 11:27:36 -06:00
d6228d6d63 Merge pull request 'refactor-pagenode-logic' (#1) from refactor-pagenode-logic into main
Some checks failed
Docker Build Test / build (3.10) (push) Failing after 3s
Run Tests / test (ubuntu-latest, 3.13) (push) Failing after 2s
Docker Build Test / build (3.11) (push) Failing after 2s
Docker Build Test / build (3.12) (push) Failing after 2s
Docker Build Test / build (3.13) (push) Failing after 2s
Docker Build Test / build (3.9) (push) Failing after 2s
Build and Publish Docker Image / build (push) Failing after 3s
Safety / security (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.10) (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.11) (push) Failing after 2s
Run Tests / test (ubuntu-latest, 3.12) (push) Failing after 1s
Run Tests / test (ubuntu-latest, 3.9) (push) Failing after 1s
Run Tests / test (windows-latest, 3.10) (push) Has been cancelled
Run Tests / test (windows-latest, 3.11) (push) Has been cancelled
Run Tests / test (windows-latest, 3.12) (push) Has been cancelled
Run Tests / test (windows-latest, 3.13) (push) Has been cancelled
Run Tests / test (windows-latest, 3.9) (push) Has been cancelled
Reviewed-on: Ivan/rns-page-node#1
2025-12-02 17:26:05 +00:00
ccf954681b Refactor path handling in PageNode class for improved reliability
Some checks failed
Docker Build Test / build (3.11) (pull_request) Successful in 1m9s
Docker Build Test / build (3.10) (pull_request) Successful in 1m12s
Docker Build Test / build (3.12) (pull_request) Successful in 1m8s
Docker Build Test / build (3.13) (pull_request) Failing after 2s
Build and Publish Docker Image / build (pull_request) Failing after 2s
Run Tests / test (windows-latest, 3.10) (pull_request) Has been cancelled
Run Tests / test (windows-latest, 3.11) (pull_request) Has been cancelled
Run Tests / test (windows-latest, 3.12) (pull_request) Has been cancelled
Run Tests / test (windows-latest, 3.13) (pull_request) Has been cancelled
Run Tests / test (windows-latest, 3.9) (pull_request) Has been cancelled
Docker Build Test / build (3.9) (pull_request) Failing after 2s
Run Tests / test (ubuntu-latest, 3.10) (pull_request) Failing after 2s
Run Tests / test (ubuntu-latest, 3.11) (pull_request) Failing after 2s
Run Tests / test (ubuntu-latest, 3.12) (pull_request) Failing after 1s
Run Tests / test (ubuntu-latest, 3.13) (pull_request) Failing after 1s
Run Tests / test (ubuntu-latest, 3.9) (pull_request) Failing after 2s
- Updated path resolution for pages and files to use `resolve()` method, ensuring absolute paths are handled correctly.
- Enhanced relative path calculation using `relative_to()` to improve robustness against invalid paths.
- Adjusted request path formatting to include a leading slash for consistency.
2025-12-02 11:03:58 -06:00
4ec44900cf add windows runner test 2025-12-02 11:02:01 -06:00
d4099fb9a2 Refactor _scan_pages method and enhance file reading logic in PageNode class
- Updated docstring for _scan_pages to clarify exclusion of .allowed files.
- Improved file reading logic to handle script detection and content retrieval more efficiently.
- Refined error handling during the announce process to catch specific exceptions.
2025-12-02 10:17:16 -06:00
1571b315b2 Add docstrings to PageNode methods for improved clarity 2025-12-02 10:06:56 -06:00
71bd49bd7d Refactor PageNode class to improve page and file registration logic
- Consolidated page and file scanning methods to return lists of served pages and files.
- Improved error handling in file reading operations.
- Updated the announce loop to use a more efficient waiting mechanism.
- Improved command-line argument handling for log level configuration.
2025-12-02 09:58:31 -06:00
382413dc08 Update to support immutable github releases/tags
Some checks failed
Docker Build Test / build (3.12) (push) Successful in 31s
Docker Build Test / build (3.13) (push) Successful in 37s
Docker Build Test / build (3.10) (push) Successful in 26s
Docker Build Test / build (3.11) (push) Successful in 25s
Docker Build Test / build (3.9) (push) Successful in 29s
Safety / security (push) Failing after 30s
Run Tests / test (3.10) (push) Successful in 40s
Run Tests / test (3.11) (push) Successful in 31s
Run Tests / test (3.12) (push) Successful in 35s
Build and Publish Docker Image / build (push) Failing after 1m15s
Run Tests / test (3.13) (push) Successful in 25s
Run Tests / test (3.9) (push) Successful in 23s
2025-11-23 11:45:41 -06:00
5 changed files with 200 additions and 115 deletions

View File

@@ -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"

View File

@@ -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
View File

@@ -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"

View File

@@ -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",

View File

@@ -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"