Added releases to rngit page node

This commit is contained in:
Mark Qvist
2026-05-04 20:13:35 +02:00
parent 4cbd4ed60c
commit 3de16e085e
+227 -38
View File
@@ -56,6 +56,8 @@ class NomadNetworkNode():
PATH_COMMIT = "/page/commit.mu"
PATH_REFS = "/page/refs.mu"
PATH_STATS = "/page/stats.mu"
PATH_RELEASES = "/page/releases.mu"
PATH_RELEASE = "/page/release.mu"
BLOB_SIZE_LIMIT = 256 * 1024
TREE_ENTRIES_PER_PAGE = 1000
@@ -73,6 +75,7 @@ class NomadNetworkNode():
U_ICON_COMMITS = "🖹"
U_ICON_STATS = "🗠"
U_ICON_HEART = ""
U_ICON_PACKAGE = ""
NF_ICON_SEP = ""
NF_ICON_FOLDER = "󰉖"
@@ -82,6 +85,7 @@ class NomadNetworkNode():
NF_ICON_COMMITS = "󰋚"
NF_ICON_STATS = ""
NF_ICON_HEART = "󰋑"
NF_ICON_PACKAGE = "󰏗"
CLR_FOLDER = "`Ffe6"
CLR_FILE = "`F66d"
@@ -94,32 +98,34 @@ class NomadNetworkNode():
def __init__(self, owner=None):
if not owner: raise TypeError(f"Invalid owner {owner} for {self}")
self._ready = False
self._should_run = False
self.owner = owner
self.identity = owner.identity
self.node_name = owner.node_name
self.announce_interval = owner.announce_interval
self.last_announce = 0
self.null_ident = RNS.Identity.from_bytes(bytes(64))
self._ready = False
self._should_run = False
self.owner = owner
self.identity = owner.identity
self.node_name = owner.node_name
self.announce_interval = owner.announce_interval
self.last_announce = 0
self.null_ident = RNS.Identity.from_bytes(bytes(64))
self.templates = {}
self.templates["base"] = DEFAULT_BASE_TEMPLATE
self.templates["front"] = DEFAULT_FRONT_TEMPLATE
self.templates["group"] = DEFAULT_GROUP_TEMPLATE
self.templates["repo"] = DEFAULT_REPO_TEMPLATE
self.templates["tree"] = DEFAULT_TREE_TEMPLATE
self.templates["blob"] = DEFAULT_BLOB_TEMPLATE
self.templates["commits"] = DEFAULT_COMMITS_TEMPLATE
self.templates["commit"] = DEFAULT_COMMIT_TEMPLATE
self.templates["refs"] = DEFAULT_REFS_TEMPLATE
self.templates["stats"] = DEFAULT_STATS_TEMPLATE
self.templatesdir = self.owner.configdir+"/templates"
self.use_nerdfonts = self.USE_NERDFONTS
self.highlight_syntax = True
self.highlighter = SyntaxHighlighter()
self.mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter)
self.thanks_deque = deque(maxlen=256)
self.templates = {}
self.templates["base"] = DEFAULT_BASE_TEMPLATE
self.templates["front"] = DEFAULT_FRONT_TEMPLATE
self.templates["group"] = DEFAULT_GROUP_TEMPLATE
self.templates["repo"] = DEFAULT_REPO_TEMPLATE
self.templates["releases"] = DEFAULT_RELEASES_TEMPLATE
self.templates["release"] = DEFAULT_RELEASE_TEMPLATE
self.templates["tree"] = DEFAULT_TREE_TEMPLATE
self.templates["blob"] = DEFAULT_BLOB_TEMPLATE
self.templates["commits"] = DEFAULT_COMMITS_TEMPLATE
self.templates["commit"] = DEFAULT_COMMIT_TEMPLATE
self.templates["refs"] = DEFAULT_REFS_TEMPLATE
self.templates["stats"] = DEFAULT_STATS_TEMPLATE
self.templatesdir = self.owner.configdir+"/templates"
self.use_nerdfonts = self.USE_NERDFONTS
self.highlight_syntax = True
self.highlighter = SyntaxHighlighter()
self.mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter)
self.thanks_deque = deque(maxlen=256)
if not os.path.isdir(self.templatesdir):
try: os.makedirs(self.templatesdir)
@@ -151,6 +157,7 @@ class NomadNetworkNode():
elif name == "tag": return self.NF_ICON_TAG
elif name == "stats": return self.NF_ICON_STATS
elif name == "heart": return self.NF_ICON_HEART
elif name == "package": return self.NF_ICON_PACKAGE
else: return ""
else:
@@ -162,6 +169,7 @@ class NomadNetworkNode():
elif name == "tag": return self.U_ICON_TAG
elif name == "stats": return self.U_ICON_STATS
elif name == "heart": return self.U_ICON_HEART
elif name == "package": return self.U_ICON_PACKAGE
else: return ""
def jobs(self):
@@ -186,15 +194,17 @@ class NomadNetworkNode():
return self.owner.resolve_permission(remote_identity, group_name, repository_name, permission)
def register_request_handlers(self):
self.destination.register_request_handler(self.PATH_INDEX, response_generator=self.serve_front_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_GROUP, response_generator=self.serve_group_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_REPO, response_generator=self.serve_repo_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_TREE, response_generator=self.serve_tree_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_BLOB, response_generator=self.serve_blob_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_COMMITS, response_generator=self.serve_commits_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_COMMIT, response_generator=self.serve_commit_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_REFS, response_generator=self.serve_refs_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_STATS, response_generator=self.serve_stats_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_INDEX, response_generator=self.serve_front_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_GROUP, response_generator=self.serve_group_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_REPO, response_generator=self.serve_repo_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_TREE, response_generator=self.serve_tree_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_BLOB, response_generator=self.serve_blob_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_COMMITS, response_generator=self.serve_commits_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_COMMIT, response_generator=self.serve_commit_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_REFS, response_generator=self.serve_refs_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_STATS, response_generator=self.serve_stats_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_RELEASES, response_generator=self.serve_releases_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_RELEASE, response_generator=self.serve_release_page, allow=RNS.Destination.ALLOW_ALL)
def get_template(self, template):
filename = f"{template}.mu"
@@ -317,7 +327,7 @@ class NomadNetworkNode():
group_name = data.get("var_g", "") if data else ""
if not group_name:
content = self.m_heading("Error", 1) + "\nInvalid request.\n"
content = self.m_heading("Error", 2) + "\nInvalid request.\n"
return self.render_template(content, st=st)
accessible_repos = self.get_accessible_repositories(remote_identity, group_name)
@@ -363,7 +373,7 @@ class NomadNetworkNode():
thanks = True if data.get("var_thanks", "") else False
if not group_name or not repo_name:
content = self.m_heading("Error", 1) + "\nInvalid request.\n"
content = self.m_heading("Error", 2) + "\nInvalid request.\n"
return self.render_template(content, st=st)
content_parts = []
@@ -395,8 +405,15 @@ class NomadNetworkNode():
branch_count = len(refs.get("heads", [])) if refs else 0
tag_count = len(refs["tags"]) if refs else 0
# Get releases count
releases_path = f"{repo['path']}.releases"
releases_count = 0
releases = self.owner.releases_list_data(releases_path)
if releases: releases_count = len([r for r in releases if r.get("status") == "published"])
sep = self.icon("sep")
content_parts.append(f"{self.m_link_r(self.icon("folder")+" Files", self.PATH_TREE, g=group_name, r=repo_name, ref='HEAD')} {sep} ")
if releases_count: content_parts.append(f"{self.m_link_r(self.icon("package")+f" Releases ({releases_count})", self.PATH_RELEASES, g=group_name, r=repo_name)} {sep} ")
content_parts.append(f"{self.m_link_r(self.icon("commits")+f" Commits ({commits_count})", self.PATH_COMMITS, g=group_name, r=repo_name, ref='HEAD')} {sep} ")
content_parts.append(f"{self.m_link_r(self.icon("branch")+f" Branches ({branch_count})", self.PATH_REFS, g=group_name, r=repo_name, type="heads")} {sep} ")
content_parts.append(f"{self.m_link_r(self.icon("tag")+f" Tags ({tag_count})", self.PATH_REFS, g=group_name, r=repo_name, type="tags")} {sep} ")
@@ -825,7 +842,7 @@ class NomadNetworkNode():
commit_info = self.get_commit_info(repo_path, resolved_hash)
if not commit_info:
content_parts.append(self.m_heading("Error", 1) + "\n\nCould not retrieve commit information.\n")
content_parts.append(self.m_heading("Error", 2) + "\n\nCould not retrieve commit information.\n")
page_content = "".join(content_parts)
return self.render_template(page_content, st=st)
@@ -1016,7 +1033,7 @@ class NomadNetworkNode():
repo_name = data.get("var_r", "") if data else ""
if not group_name or not repo_name:
content = self.m_heading("Error", 1) + "\nInvalid request.\n"
content = self.m_heading("Error", 2) + "\nInvalid request.\n"
return self.render_template(content, st=st)
content_parts = []
@@ -1091,6 +1108,147 @@ class NomadNetworkNode():
nav_content = "".join(nav_parts)
return self.render_template(page_content, nav_content=nav_content, template="stats", st=st)
def serve_releases_page(self, path, data, request_id, link_id, remote_identity, requested_at):
st = time.time()
RNS.log(f"Releases page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
if not group_name or not repo_name:
content = self.m_heading("Error", 2) + "\nInvalid request.\n"
return self.render_template(content, st=st)
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
if not repo:
content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n"
return self.render_template(content, st=st)
content_parts = []
nav_parts = []
# Breadcrumb navigation
breadcrumb = f">>\n{self.m_link("Node", self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / releases"
nav_parts.append(breadcrumb + "\n")
nav_content = "".join(nav_parts)
releases_path = f"{repo['path']}.releases"
releases = self.owner.releases_list_data(releases_path)
if not releases:
content_parts.append(self.m_heading("Releases", 2))
content_parts.append("\nNo releases available for this repository.\n")
page_content = "".join(content_parts)
return self.render_template(page_content, nav_content=nav_content, template="repo", st=st)
published_releases = [r for r in releases if r.get("status") == "published"]
content_parts.append(self.m_heading(f"Releases ({len(published_releases)})", 2))
content_parts.append("\n")
for rel in published_releases:
tag = rel.get("tag", "unknown")
created_ts = rel.get("created", 0)
date_str = time.strftime("%Y-%m-%d", time.localtime(created_ts)) if created_ts else "unknown"
artifacts = rel.get("artifacts", 0)
preview = rel.get("preview", "")[:256]
if len(rel.get("preview", "")) > len(preview): preview += ""
link = self.m_link(tag, self.PATH_RELEASE, g=group_name, r=repo_name, t=tag)
sep = self.icon("sep")
artifacts_str = f"`*{artifacts} artifact{'s' if artifacts != 1 else ''}`*"
content_parts.append(f"{link} {self.CLR_DIM}{date_str} {sep} {artifacts_str}`f\n")
if preview: content_parts.append(f"{self.m_escape(preview)}\n")
content_parts.append("\n")
self.owner.view_succeeded(group_name, repo_name, remote_identity)
page_content = "".join(content_parts)
return self.render_template(page_content, nav_content=nav_content, template="releases", st=st)
def serve_release_page(self, path, data, request_id, link_id, remote_identity, requested_at):
st = time.time()
RNS.log(f"Release page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
tag = data.get("var_t", "") if data else ""
if not group_name or not repo_name or not tag:
content = self.m_heading("Error", 2) + "\nInvalid request.\n"
return self.render_template(content, st=st)
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
if not repo:
content = self.m_heading("Error", 2) + "\nThe requested repository was not found.\n"
return self.render_template(content, st=st)
content_parts = []
nav_parts = []
# Breadcrumb navigation
breadcrumb = f">>\n{self.m_link("Node", self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / {self.m_link('releases', self.PATH_RELEASES, g=group_name, r=repo_name)} / {tag}"
nav_parts.append(breadcrumb + "\n")
nav_content = "".join(nav_parts)
releases_path = f"{repo['path']}.releases"
release_dir = os.path.join(releases_path, tag)
if not os.path.isdir(release_dir):
content = self.m_heading("Release Not Found", 2) + f"\nThe release {tag} does not exist.\n"
return self.render_template(content, nav_content=nav_content, st=st)
release_info = self.owner.release_data(release_dir, tag)
if not release_info:
content = self.m_heading("Error", 2) + "\nCould not load release data.\n"
return self.render_template(content, nav_content=nav_content, st=st)
# Only show published releases
if release_info.get("status") != "published":
content = self.m_heading("Release Not Found", 2) + f"\nThe release {tag} does not exist.\n"
return self.render_template(content, nav_content=nav_content, st=st)
sep = self.icon("sep")
heart = self.icon("heart")
created_ts = release_info.get("created", 0)
ts_str = f" {sep} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created_ts))}" if created_ts else ""
content_parts.append(self.m_heading(f"Release {tag}{ts_str}", 2))
content_parts.append("\n")
# Release notes
notes = release_info.get("notes", "")
if notes:
notes_format = release_info.get("notes_format", "text")
if notes_format == "micron": content_parts.append(f"{notes}\n")
elif notes_format == "markdown": content_parts.append(f"{self.mdc.format_block(notes)}\n")
else: content_parts.append(f"`={notes}`=\n")
content_parts.append("\n")
# Artifacts
artifacts = release_info.get("artifacts", [])
if artifacts:
content_parts.append(self.m_heading(f"Artifacts ({len(artifacts)})", 2))
content_parts.append("\n")
for art in artifacts:
name = art.get("name", "unknown")
size = art.get("size", 0)
size_str = RNS.prettysize(size) if size else "0 B"
content_parts.append(f"{self.icon('file')} {self.m_escape(name)} {self.CLR_DIM}({size_str})`f\n")
content_parts.append("\n")
else:
content_parts.append(self.m_heading("Artifacts", 2))
content_parts.append("\nNo artifacts for this release.\n\n")
thanks = True if data.get("var_thanks", "") else False
thanks_count = self.release_thanks(release_dir, add=thanks, link_id=link_id)
content_parts.append(f"{self.m_link_r(self.icon("heart")+f" Thanks ({thanks_count})", self.PATH_RELEASE, g=group_name, r=repo_name, t=tag, thanks="y")}\n")
self.owner.view_succeeded(group_name, repo_name, remote_identity)
page_content = "".join(content_parts)
return self.render_template(page_content, nav_content=nav_content, template="release", st=st)
#######################
# Git Data Extraction #
#######################
@@ -1615,6 +1773,31 @@ class NomadNetworkNode():
except Exception as e: RNS.log(f"Error while processing repository thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
return 0
def release_thanks(self, release_path, add=False, link_id=None):
if add:
thanks_hash = RNS.Identity.full_hash(link_id+release_path.encode("utf-8"))
if thanks_hash in self.thanks_deque: add = False
else: self.thanks_deque.append(thanks_hash)
try:
thanks_path = f"{release_path}/THANKS"
if not os.path.isfile(thanks_path):
thanks_count = 1 if add else 0
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
else:
with open(thanks_path, "rb") as fh:
thanks_data = mp.unpackb(fh.read())
if "count" in thanks_data: thanks_count = thanks_data["count"]
else: raise ValueError("Invalid data in thanks file")
if add: thanks_count += 1
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
return thanks_count
except Exception as e: RNS.log(f"Error while processing release thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
return 0
###################
# Stats Renderers #
###################
@@ -1788,6 +1971,12 @@ DEFAULT_GROUP_TEMPLATE = """{PAGE_CONTENT}"""
# Repository page template
DEFAULT_REPO_TEMPLATE = """{PAGE_CONTENT}"""
# Repository page template
DEFAULT_RELEASES_TEMPLATE = """{PAGE_CONTENT}"""
# Repository page template
DEFAULT_RELEASE_TEMPLATE = """{PAGE_CONTENT}"""
# Tree page template
DEFAULT_TREE_TEMPLATE = """{PAGE_CONTENT}"""