cdn

Log | Files | Refs | Activity

commit a2b9713e0e5cf5db1cc3870305043665fc673b42
parent b84f4862006540065db9a701587ccdeb0768617b
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Fri,  4 Sep 2026 13:38:25 +0530

Merge pull request #13 from notamitgamer/feat/repo-stats-footer

Show total file count and total data size in footer
Diffstat:
Mapp/main.py | 24+++++++++++++++++-------
Mapp/storage.py | 59+++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mapp/templates/index.html | 5+++++
3 files changed, 73 insertions(+), 15 deletions(-)

diff --git a/app/main.py b/app/main.py @@ -10,7 +10,7 @@ from fastapi import FastAPI, Request, File, UploadFile, HTTPException from fastapi.responses import StreamingResponse, HTMLResponse, PlainTextResponse, RedirectResponse, FileResponse from fastapi.templating import Jinja2Templates -from .storage import is_file, list_directory, list_files_recursive, upload_temp_file, HF_REPO_ID +from .storage import is_file, list_directory, list_files_recursive, upload_temp_file, repo_stats, HF_REPO_ID app = FastAPI() @@ -57,13 +57,23 @@ RAW_DOMAIN = os.getenv("RAW_DOMAIN", "raw.cdn.amit.is-a.dev") RAW_BASE_URL = os.getenv("RAW_BASE_URL", f"https://{RAW_DOMAIN}") RAW_PREFIX = "raw/" +def render_context(extra: dict) -> dict: + """Merges page-specific context with repo-wide file count / size stats + (shown in the footer on every page). repo_stats() is cached, so this + doesn't add a fresh full-repo scan on every request.""" + stats = repo_stats() + ctx = dict(extra) + ctx["repo_file_count"] = stats["file_count"] if stats else None + ctx["repo_size_str"] = stats["size_str"] if stats else None + return ctx + @app.exception_handler(404) async def not_found_handler(request: Request, exc: HTTPException): # Keep the raw subdomain returning plain text errors if request.url.hostname == RAW_DOMAIN: return PlainTextResponse("404: File Not Found", status_code=404) # Render UI 404 page for the main CDN domain - return templates.TemplateResponse(request, "index.html", {"page": "404"}, status_code=404) + return templates.TemplateResponse(request, "index.html", render_context({"page": "404"}), status_code=404) async def stream_raw(path: str): hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}" @@ -214,24 +224,24 @@ async def serve(request: Request, path: str): return RedirectResponse(f"{RAW_BASE_URL}/{raw_path}") if clean_path == "upload": - return templates.TemplateResponse(request, "index.html", {"page": "upload"}) + return templates.TemplateResponse(request, "index.html", render_context({"page": "upload"})) if clean_path and await is_file(clean_path): filename = clean_path.split("/")[-1] - return templates.TemplateResponse(request, "index.html", { + return templates.TemplateResponse(request, "index.html", render_context({ "page": "file", "path": clean_path, "filename": filename, "raw_base_url": RAW_BASE_URL - }) + })) items = list_directory(clean_path) if clean_path and not items: raise HTTPException(status_code=404, detail="Not Found") - return templates.TemplateResponse(request, "index.html", { + return templates.TemplateResponse(request, "index.html", render_context({ "page": "listing", "path": clean_path, "items": items, "raw_base_url": RAW_BASE_URL - }) + })) diff --git a/app/storage.py b/app/storage.py @@ -74,14 +74,7 @@ def list_directory(path: str): is_dir = not hasattr(item, "size") size = getattr(item, "size", 0) - size_str = "-" - if not is_dir and size is not None: - if size < 1024: - size_str = f"{size} B" - elif size < 1024 * 1024: - size_str = f"{size / 1024:.1f} KB" - else: - size_str = f"{size / (1024 * 1024):.1f} MB" + size_str = format_size(size) if not is_dir else "-" # last_commit is only populated when expand_info=True succeeded; # otherwise this just stays "-" rather than breaking the listing. @@ -137,6 +130,56 @@ def list_files_recursive(path: str): ) return files +def format_size(size_bytes) -> str: + """ + Human-readable size with proper unit escalation: bytes stay whole, + KB/MB/GB/TB get 2 decimals, and it keeps dividing by 1024 as long as + the value would round up to the next unit (so 1024MB shows as 1.00GB, + not 1024.00MB). + """ + if size_bytes is None: + return "-" + size = float(size_bytes) + units = ["B", "KB", "MB", "GB", "TB"] + idx = 0 + while size >= 1024 and idx < len(units) - 1: + size /= 1024 + idx += 1 + if idx == 0: + return f"{int(size)} {units[idx]}" + return f"{size:.2f} {units[idx]}" + +# Repo-wide stats are more expensive to compute (full recursive tree scan) +# than a single-folder listing, so they get their own longer-lived cache +# entry rather than reusing the 60s per-folder TTL. +REPO_STATS_CACHE_TTL = 300 # 5 minutes + +def repo_stats(): + """Returns {'file_count': int, 'size_str': str} for the whole repo, cached.""" + cache_key = "repo_stats" + now = time.time() + if cache_key in _cache and _cache[cache_key][0] > now: + return _cache[cache_key][1] + + try: + items = list(api.list_repo_tree( + repo_id=HF_REPO_ID, path_in_repo="", repo_type="dataset", recursive=True + )) + except Exception as e: + print(f"[storage] repo_stats failed: {e}") + return None + + file_count = 0 + total_size = 0 + for item in items: + if hasattr(item, "size"): # files only, skip folder entries + file_count += 1 + total_size += getattr(item, "size", 0) or 0 + + result = {"file_count": file_count, "size_str": format_size(total_size)} + _cache[cache_key] = (now + REPO_STATS_CACHE_TTL, result) + return result + def upload_temp_file(temp_path: str, filename: str) -> str: """Uploads a local file to HF and returns the relative path.""" slug = str(uuid.uuid4())[:8] diff --git a/app/templates/index.html b/app/templates/index.html @@ -577,6 +577,11 @@ {% endif %} · &copy; 2026 · assets may move without notice </div> + {% if repo_file_count is not none %} + <div title="Across the whole repo"> + {{ repo_file_count }} file{{ '' if repo_file_count == 1 else 's' }} · {{ repo_size_str }} total + </div> + {% endif %} </footer> </div>
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror