Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6601adb38 | ||
|
|
98c71a888e | ||
|
|
65bd70c05a | ||
|
|
b77b73576f |
100
.github/workflows/publish.yml
vendored
Normal file
100
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to release (e.g., 0.6.8)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build distribution 📦
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5.3.0
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: python3 -m pip install build --user
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: python3 -m build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: actions/upload-artifact@v4.5.0
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
publish-to-pypi:
|
||||||
|
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/rns-page-node
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.12.3
|
||||||
|
|
||||||
|
github-release:
|
||||||
|
name: Sign the Python 🐍 distribution 📦 and create GitHub Release
|
||||||
|
needs:
|
||||||
|
- publish-to-pypi
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Sign the dists with Sigstore
|
||||||
|
uses: sigstore/gh-action-sigstore-python@v3.0.0
|
||||||
|
with:
|
||||||
|
inputs: >-
|
||||||
|
./dist/*.tar.gz
|
||||||
|
./dist/*.whl
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: >-
|
||||||
|
gh release create
|
||||||
|
"$GITHUB_REF_NAME"
|
||||||
|
--repo "$GITHUB_REPOSITORY"
|
||||||
|
--notes ""
|
||||||
|
- name: Upload artifact signatures to GitHub Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: >-
|
||||||
|
gh release upload
|
||||||
|
"$GITHUB_REF_NAME" dist/**
|
||||||
|
--repo "$GITHUB_REPOSITORY"
|
||||||
18
README.md
18
README.md
@@ -38,6 +38,24 @@ podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config
|
|||||||
|
|
||||||
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Build wheels:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Wheels in Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-wheels
|
||||||
|
```
|
||||||
|
|
||||||
## Page formats
|
## Page formats
|
||||||
|
|
||||||
- Micron `.mu`
|
- Micron `.mu`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "rns-page-node"
|
name = "rns-page-node"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
description = "A simple way to serve pages and files over the Reticulum network."
|
description = "A simple way to serve pages and files over the Reticulum network."
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import threading
|
|||||||
import subprocess
|
import subprocess
|
||||||
import RNS
|
import RNS
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_INDEX = '''>Default Home Page
|
DEFAULT_INDEX = '''>Default Home Page
|
||||||
|
|
||||||
@@ -23,6 +26,9 @@ You are not authorised to carry out the request.
|
|||||||
|
|
||||||
class PageNode:
|
class PageNode:
|
||||||
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
|
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self.logger = logging.getLogger(f"{__name__}.PageNode")
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
self.name = name
|
self.name = name
|
||||||
self.pagespath = pagespath
|
self.pagespath = pagespath
|
||||||
@@ -46,12 +52,15 @@ class PageNode:
|
|||||||
|
|
||||||
self.destination.set_link_established_callback(self.on_connect)
|
self.destination.set_link_established_callback(self.on_connect)
|
||||||
|
|
||||||
threading.Thread(target=self._announce_loop, daemon=True).start()
|
self._announce_thread = threading.Thread(target=self._announce_loop, daemon=True)
|
||||||
threading.Thread(target=self._refresh_loop, daemon=True).start()
|
self._announce_thread.start()
|
||||||
|
self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
|
||||||
|
self._refresh_thread.start()
|
||||||
|
|
||||||
def register_pages(self):
|
def register_pages(self):
|
||||||
self.servedpages = []
|
with self._lock:
|
||||||
self._scan_pages(self.pagespath)
|
self.servedpages = []
|
||||||
|
self._scan_pages(self.pagespath)
|
||||||
|
|
||||||
if not os.path.isfile(os.path.join(self.pagespath, "index.mu")):
|
if not os.path.isfile(os.path.join(self.pagespath, "index.mu")):
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
@@ -70,8 +79,9 @@ class PageNode:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def register_files(self):
|
def register_files(self):
|
||||||
self.servedfiles = []
|
with self._lock:
|
||||||
self._scan_files(self.filespath)
|
self.servedfiles = []
|
||||||
|
self._scan_files(self.filespath)
|
||||||
|
|
||||||
for full_path in self.servedfiles:
|
for full_path in self.servedfiles:
|
||||||
rel = full_path[len(self.filespath):]
|
rel = full_path[len(self.filespath):]
|
||||||
@@ -132,25 +142,45 @@ class PageNode:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _announce_loop(self):
|
def _announce_loop(self):
|
||||||
while True:
|
while not self._stop_event.is_set():
|
||||||
if time.time() - self.last_announce > self.announce_interval:
|
try:
|
||||||
if self.name:
|
if time.time() - self.last_announce > self.announce_interval:
|
||||||
self.destination.announce(app_data=self.name.encode('utf-8'))
|
if self.name:
|
||||||
else:
|
self.destination.announce(app_data=self.name.encode('utf-8'))
|
||||||
self.destination.announce()
|
else:
|
||||||
self.last_announce = time.time()
|
self.destination.announce()
|
||||||
time.sleep(1)
|
self.last_announce = time.time()
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error in announce loop")
|
||||||
|
|
||||||
def _refresh_loop(self):
|
def _refresh_loop(self):
|
||||||
while True:
|
while not self._stop_event.is_set():
|
||||||
now = time.time()
|
try:
|
||||||
if self.page_refresh_interval > 0 and now - self.last_page_refresh > self.page_refresh_interval:
|
now = time.time()
|
||||||
self.register_pages()
|
if self.page_refresh_interval > 0 and now - self.last_page_refresh > self.page_refresh_interval:
|
||||||
self.last_page_refresh = now
|
self.register_pages()
|
||||||
if self.file_refresh_interval > 0 and now - self.last_file_refresh > self.file_refresh_interval:
|
self.last_page_refresh = now
|
||||||
self.register_files()
|
if self.file_refresh_interval > 0 and now - self.last_file_refresh > self.file_refresh_interval:
|
||||||
self.last_file_refresh = now
|
self.register_files()
|
||||||
time.sleep(1)
|
self.last_file_refresh = now
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error in refresh loop")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.logger.info("Shutting down PageNode...")
|
||||||
|
self._stop_event.set()
|
||||||
|
try:
|
||||||
|
self._announce_thread.join(timeout=5)
|
||||||
|
self._refresh_thread.join(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error waiting for threads to shut down")
|
||||||
|
try:
|
||||||
|
if hasattr(self.destination, 'close'):
|
||||||
|
self.destination.close()
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("Error closing RNS destination")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -163,6 +193,7 @@ def main():
|
|||||||
parser.add_argument('-i', '--identity-dir', dest='identity_dir', help='Directory to store node identity', default=os.path.join(os.getcwd(), 'node-config'))
|
parser.add_argument('-i', '--identity-dir', dest='identity_dir', help='Directory to store node identity', default=os.path.join(os.getcwd(), 'node-config'))
|
||||||
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
||||||
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
||||||
|
parser.add_argument('-l', '--log-level', dest='log_level', choices=['DEBUG','INFO','WARNING','ERROR','CRITICAL'], default='INFO', help='Logging level')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
configpath = args.configpath
|
configpath = args.configpath
|
||||||
@@ -173,6 +204,8 @@ def main():
|
|||||||
identity_dir = args.identity_dir
|
identity_dir = args.identity_dir
|
||||||
page_refresh_interval = args.page_refresh_interval
|
page_refresh_interval = args.page_refresh_interval
|
||||||
file_refresh_interval = args.file_refresh_interval
|
file_refresh_interval = args.file_refresh_interval
|
||||||
|
numeric_level = getattr(logging, args.log_level.upper(), logging.INFO)
|
||||||
|
logging.basicConfig(level=numeric_level, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
|
||||||
|
|
||||||
RNS.Reticulum(configpath)
|
RNS.Reticulum(configpath)
|
||||||
os.makedirs(identity_dir, exist_ok=True)
|
os.makedirs(identity_dir, exist_ok=True)
|
||||||
@@ -187,13 +220,14 @@ def main():
|
|||||||
os.makedirs(files_dir, exist_ok=True)
|
os.makedirs(files_dir, exist_ok=True)
|
||||||
|
|
||||||
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
|
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
|
||||||
print("Page node running. Press Ctrl-C to exit.")
|
logger.info("Page node running. Press Ctrl-C to exit.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Shutting down.")
|
logger.info("Keyboard interrupt received, shutting down...")
|
||||||
|
node.shutdown()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='rns-page-node',
|
name='rns-page-node',
|
||||||
version='0.1.0',
|
version='0.1.2',
|
||||||
author='Sudo-Ivan',
|
author='Sudo-Ivan',
|
||||||
author_email='',
|
author_email='',
|
||||||
description='A simple way to serve pages and files over the Reticulum network.',
|
description='A simple way to serve pages and files over the Reticulum network.',
|
||||||
|
|||||||
Reference in New Issue
Block a user