From c92872a81b74159638c49d9ff8aa1e05ee3fe78c Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 15 May 2026 20:12:07 +0200 Subject: [PATCH] Added download stats to rngit --- RNS/Utilities/rngit/pages.py | 15 ++++- RNS/Utilities/rngit/server.py | 115 ++++++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/RNS/Utilities/rngit/pages.py b/RNS/Utilities/rngit/pages.py index 1fbf72b0..ad7d8475 100644 --- a/RNS/Utilities/rngit/pages.py +++ b/RNS/Utilities/rngit/pages.py @@ -1116,6 +1116,8 @@ class NomadNetworkNode(): f_peak = stats["fetches"]["peak"] p_total = stats["pushes"]["total"] p_peak = stats["pushes"]["peak"] + d_total = stats["downloads_combined"]["total"] + d_peak = stats["downloads_combined"]["peak"] content_parts.append(f"\n`F66dViews`f : {v_total:>5} total {self.CLR_DIM}(peak: {v_peak:>3})`f\n") content_parts.append(f"`F0a0Fetches`f : {f_total:>5} total {self.CLR_DIM}(peak: {f_peak:>3})\n`f") @@ -1140,6 +1142,12 @@ class NomadNetworkNode(): content_parts.append("\n") content_parts.append(self.render_chart(stats["pushes"]["daily"], stats["timeline_labels"], color="aa0")) content_parts.append("\n") + + if d_total > 0: + content_parts.append(self.m_heading(f"Downloads", 2)) + content_parts.append("\n") + content_parts.append(self.render_chart(stats["downloads_combined"]["daily"], stats["timeline_labels"], color="a22")) + content_parts.append("\n") if stats["activity_score"] > 0: content_parts.append(self.m_heading("Combined Activity", 2)) @@ -1604,6 +1612,7 @@ class NomadNetworkNode(): RNS.log(f"Artifact file resolved for artifact request {group_name}/{repo_name}/{tag}/{artifact}", RNS.LOG_DEBUG) + self.owner.release_download_succeeded(group_name, repo_name, remote_identity) return [open(artifact_path, "rb"), {"name": artifact.encode("utf-8")}] def serve_download(self, path, data, request_id, link_id, remote_identity, requested_at): @@ -1641,7 +1650,10 @@ class NomadNetworkNode(): else: stream = self.get_blob_stream(repo_path, resolved_ref, file_path) - if stream is not None: return [stream, {"name": file_name.encode("utf-8")}] + if stream is not None: + self.owner.download_succeeded(group_name, repo_name, remote_identity) + return [stream, {"name": file_name.encode("utf-8")}] + else: RNS.log(f"Could not resolve blob stream for download request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING) return None @@ -1706,6 +1718,7 @@ class NomadNetworkNode(): if content: if fmt == "micron": file_name = f"{title}.mu" else: file_name = f"{title}.md" + self.owner.download_succeeded(group_name, repo_name, remote_identity) return [file_name, content.encode("utf-8")] return None diff --git a/RNS/Utilities/rngit/server.py b/RNS/Utilities/rngit/server.py index e5f0d719..373f5512 100644 --- a/RNS/Utilities/rngit/server.py +++ b/RNS/Utilities/rngit/server.py @@ -2976,6 +2976,13 @@ class ReticulumGitNode(): RNS.log(f"Error setting permissions: {e}", RNS.LOG_ERROR) return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error setting permissions" + ################### + # Node Statistics # + ################### + + STATS_INIT_REPO = {"view": {}, "fetch": {}, "push": {}, "download": {}, "release_download": {}} + STATS_INIT_GROUP = {"view": {}, "repositories": {}} + def repository_stats(self, remote_identity, group_name, repository_name, lookback_days=14): if not self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_STATS): return None else: @@ -2995,9 +3002,12 @@ class ReticulumGitNode(): repo_stats = { "group": group_name, "repository": repository_name, "lookback_days": lookback_days, "date_range": f"{day_labels[0]} - {day_labels[-1]}", "days": days, "day_labels": day_labels, "timeline_labels": timeline_labels, - "views": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, - "fetches": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, - "pushes": {"daily": [], "total": 0, "peak": 0, "peak_day": None} } + "views": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, + "fetches": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, + "pushes": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, + "downloads": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, + "release_downloads": {"daily": [], "total": 0, "peak": 0, "peak_day": None}, + "downloads_combined": {"daily": [], "total": 0, "peak": 0, "peak_day": None} } group_stats = self.stats.get("groups", {}).get(group_name, {}) repo_data = group_stats.get("repositories", {}).get(repository_name, {}) @@ -3029,9 +3039,38 @@ class ReticulumGitNode(): repo_stats["pushes"]["peak"] = count repo_stats["pushes"]["peak_day"] = day - total_score = ( repo_stats["views"]["total"] * 0.2 + - repo_stats["fetches"]["total"] * 2 + - repo_stats["pushes"]["total"] * 5 ) + download_stats = repo_data.get("download", {}) + for day in days: + count = download_stats.get(day, 0) + repo_stats["downloads"]["daily"].append(count) + repo_stats["downloads"]["total"] += count + if count > repo_stats["downloads"]["peak"]: + repo_stats["downloads"]["peak"] = count + repo_stats["downloads"]["peak_day"] = day + + release_download_stats = repo_data.get("release_download", {}) + for day in days: + count = release_download_stats.get(day, 0) + repo_stats["release_downloads"]["daily"].append(count) + repo_stats["release_downloads"]["total"] += count + if count > repo_stats["release_downloads"]["peak"]: + repo_stats["release_downloads"]["peak"] = count + repo_stats["release_downloads"]["peak_day"] = day + + for day in days: + count = download_stats.get(day, 0) + release_download_stats.get(day, 0) + repo_stats["downloads_combined"]["daily"].append(count) + repo_stats["downloads_combined"]["total"] += count + if count > repo_stats["downloads_combined"]["peak"]: + repo_stats["downloads_combined"]["peak"] = count + repo_stats["downloads_combined"]["peak_day"] = day + + view_total = repo_stats["views"]["total"] + repo_stats["downloads"]["total"] + repo_stats["release_downloads"]["total"] + fetch_total = repo_stats["fetches"]["total"] + push_total = repo_stats["pushes"]["total"] + total_score = ( view_total * 0.2 + + fetch_total * 2.0 + + push_total * 5 ) repo_stats["activity_score"] = int(total_score) @@ -3078,6 +3117,16 @@ class ReticulumGitNode(): if self.stats_enabled: if group_name and repository_name: self.record_push(group_name, repository_name) + def download_succeeded(self, group_name, repository_name, remote_identity): + if remote_identity and remote_identity.hash in self.stats_ignored: return + if self.stats_enabled: + if group_name and repository_name: self.record_download(group_name, repository_name) + + def release_download_succeeded(self, group_name, repository_name, remote_identity): + if remote_identity and remote_identity.hash in self.stats_ignored: return + if self.stats_enabled: + if group_name and repository_name: self.record_release_download(group_name, repository_name) + def _get_day(self): timefmt = "%Y-%m-%d" timestamp = time.localtime(time.time()) @@ -3100,7 +3149,8 @@ class ReticulumGitNode(): try: with self.stats_lock: day = self._get_day() - if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = {"view": {}, "repositories": {}} + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP + if not "view" in self.stats["groups"][group_name]: self.stats["groups"][group_name]["view"] = {} stats = self.stats["groups"][group_name]["view"] if not day in stats: stats[day] = 0 @@ -3115,9 +3165,10 @@ class ReticulumGitNode(): try: with self.stats_lock: day = self._get_day() - if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = {"view": {}, "repositories": {}} + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP repos = self.stats["groups"][group_name]["repositories"] - if not repository_name in repos: repos[repository_name] = {"view": {}, "fetch": {}, "push": {}} + if not repository_name in repos: repos[repository_name] = self.STATS_INIT_REPO + if not "view" in repos[repository_name]: repos[repository_name]["view"] = {} stats = repos[repository_name]["view"] if not day in stats: stats[day] = 0 @@ -3132,9 +3183,10 @@ class ReticulumGitNode(): try: with self.stats_lock: day = self._get_day() - if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = {"view": {}, "repositories": {}} + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP repos = self.stats["groups"][group_name]["repositories"] - if not repository_name in repos: repos[repository_name] = {"view": {}, "fetch": {}, "push": {}} + if not repository_name in repos: repos[repository_name] = self.STATS_INIT_REPO + if not "fetch" in repos[repository_name]: repos[repository_name]["fetch"] = {} stats = repos[repository_name]["fetch"] if not day in stats: stats[day] = 0 @@ -3149,9 +3201,10 @@ class ReticulumGitNode(): try: with self.stats_lock: day = self._get_day() - if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = {"view": {}, "repositories": {}} + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP repos = self.stats["groups"][group_name]["repositories"] - if not repository_name in repos: repos[repository_name] = {"view": {}, "fetch": {}, "push": {}} + if not repository_name in repos: repos[repository_name] = self.STATS_INIT_REPO + if not "push" in repos[repository_name]: repos[repository_name]["push"] = {} stats = repos[repository_name]["push"] if not day in stats: stats[day] = 0 @@ -3161,6 +3214,42 @@ class ReticulumGitNode(): threading.Thread(target=job, daemon=True).start() + def record_download(self, group_name, repository_name): + def job(): + try: + with self.stats_lock: + day = self._get_day() + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP + repos = self.stats["groups"][group_name]["repositories"] + if not repository_name in repos: repos[repository_name] = self.STATS_INIT_REPO + if not "download" in repos[repository_name]: repos[repository_name]["download"] = {} + + stats = repos[repository_name]["download"] + if not day in stats: stats[day] = 0 + stats[day] += 1 + + except Exception as e: RNS.log(f"Error while recording download stats: {e}", RNS.LOG_ERROR) + + threading.Thread(target=job, daemon=True).start() + + def record_release_download(self, group_name, repository_name): + def job(): + try: + with self.stats_lock: + day = self._get_day() + if not group_name in self.stats["groups"]: self.stats["groups"][group_name] = self.STATS_INIT_GROUP + repos = self.stats["groups"][group_name]["repositories"] + if not repository_name in repos: repos[repository_name] = self.STATS_INIT_REPO + if not "release_download" in repos[repository_name]: repos[repository_name]["release_download"] = {} + + stats = repos[repository_name]["release_download"] + if not day in stats: stats[day] = 0 + stats[day] += 1 + + except Exception as e: RNS.log(f"Error while recording release download stats: {e}", RNS.LOG_ERROR) + + threading.Thread(target=job, daemon=True).start() + __default_rngit_config__ = '''# This is the default rngit config file. # You will need to edit it to specify repository locations and