import hmac import os import platform import random import re import socket import statistics import time from datetime import timedelta from pathlib import Path from django import get_version as django_version from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.contrib.sessions.models import Session from django.core.cache import cache from django.db.models import Count, Sum from django.http import HttpResponseForbidden, JsonResponse from django.shortcuts import redirect, render from django.utils import timezone as tz from common.git_utils import get_git_commit_hash from common.middleware import gate_whitelist from library.models import ( Comment, FileIndex, Project, ProjectAsset, ProjectDraft, Rating, Tag, TempUpload, Version, VersionFile, ) def _under_lockout(request): lock_until = request.session.get('lock_until') if lock_until and time.time() < lock_until: return True return False def _start_lockout(request): duration = random.randint(30, 300) request.session['lock_until'] = time.time() + duration @gate_whitelist def gate(request): if request.method == 'POST': if _under_lockout(request): return render(request, 'landing/gate.html', { 'error': 'Incorrect password.', }) submitted = request.POST.get('password', '') master = settings.PASSWORD or '' if hmac.compare_digest(submitted, master): request.session['authorized'] = True request.session.pop('lock_until', None) return redirect('landing:home') else: _start_lockout(request) return render(request, 'landing/gate.html', { 'error': 'Incorrect password.', }) if request.session.get('authorized'): return redirect('landing:home') return render(request, 'landing/gate.html') def home(request): latest_projects = ( Project.objects.select_related('owner', 'thumbnail') .prefetch_related('tag_links__tag__category', 'versions') .order_by('-created_at')[:4] ) total_downloads = Version.objects.aggregate(total=Sum('downloads'))['total'] or 0 creators = ( get_user_model().objects.filter(projects__isnull=False).distinct().count() ) context = { 'latest_projects': latest_projects, 'stats': { 'creators': creators, 'packs': Project.objects.count(), 'downloads': total_downloads, 'versions': Version.objects.count(), }, } return render(request, 'landing/home.html', context) def _fmt_bytes(n): """Human-readable byte count.""" if n is None: return '—' size = float(n) for unit in ('B', 'KB', 'MB', 'GB', 'TB'): if size < 1024 or unit == 'TB': return f'{size:.1f} {unit}' size /= 1024 def _folder_usage(path): """Sum the allocated bytes of every file under ``path`` (like ``du -s``). Uses st_blocks (ext4/Unix allocation) when available, otherwise the apparent file size. Returns None if the path doesn't exist or can't be walked.""" root = Path(path) if not root.exists(): return None total = 0 try: for dirpath, dirnames, filenames in os.walk(root): for name in filenames: try: st = os.stat(os.path.join(dirpath, name)) except OSError: continue blocks = getattr(st, 'st_blocks', 0) total += blocks * 512 if blocks else st.st_size except OSError: return None return total def _system_snapshot(): """Collect a one-shot system/performance snapshot via psutil.""" try: import psutil except ImportError: return None snapshot = {} try: boot_ts = psutil.boot_time() snapshot['boot_time'] = tz.datetime.fromtimestamp( boot_ts, tz.get_current_timezone() ).isoformat(timespec='seconds') snapshot['uptime'] = round(time.time() - boot_ts) try: snapshot['load_avg'] = [round(x, 2) for x in psutil.getloadavg()] except Exception: snapshot['load_avg'] = None snapshot['cpus'] = { 'logical': psutil.cpu_count(logical=True) or 0, 'physical': psutil.cpu_count(logical=False) or 0, 'percent': psutil.cpu_percent(interval=0.1), } vm = psutil.virtual_memory() snapshot['memory'] = { 'total': vm.total, 'used': vm.used, 'available': vm.available, 'percent': vm.percent, } sw = psutil.swap_memory() snapshot['swap'] = {'total': sw.total, 'used': sw.used, 'percent': sw.percent} snapshot['disks'] = {} for label, path in (('Root /', '/'), ('Code', str(settings.BASE_DIR)), ('Media', str(settings.MEDIA_ROOT))): try: d = psutil.disk_usage(path) except OSError: snapshot['disks'][label] = None continue # For folders (Code/Media) report the space the folder itself # takes (like `du`), falling back to the whole-drive usage when the # tree can't be measured. Root / always shows the drive. used = d.used scope = 'drive' if label != 'Root /': folder = _folder_usage(path) if folder is not None: used = folder scope = 'folder' snapshot['disks'][label] = { 'path': path, 'total': d.total, 'used': used, 'free': d.free, 'scope': scope, 'percent': round(used / d.total * 100, 1) if d.total else 0, } try: snapshot['net'] = { 'sent': psutil.net_io_counters().bytes_sent, 'recv': psutil.net_io_counters().bytes_recv, } except Exception: snapshot['net'] = None running = 0 top_cpu = [] top_mem = [] gunicorn = 0 for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cpu_percent', 'memory_percent']): try: running += 1 info = proc.info cmd = ' '.join(info.get('cmdline') or []) if 'gunicorn' in cmd: gunicorn += 1 top_cpu.append({ 'name': info.get('name') or '?', 'pid': info.get('pid'), 'value': round(info.get('cpu_percent') or 0, 1), }) top_mem.append({ 'name': info.get('name') or '?', 'pid': info.get('pid'), 'value': round(info.get('memory_percent') or 0, 1), }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue snapshot['processes'] = running snapshot['gunicorn'] = gunicorn top_cpu.sort(key=lambda x: x['value'], reverse=True) top_mem.sort(key=lambda x: x['value'], reverse=True) snapshot['top_cpu'] = top_cpu[:6] snapshot['top_mem'] = top_mem[:6] except Exception: return None return snapshot @login_required def server_stats(request): """Staff-only stats page. Renders a lightweight shell; the snapshot data is fetched from the JSON endpoint so page loads stay cheap.""" if not request.user.is_staff: return HttpResponseForbidden('Only staff can view server stats.') return render(request, 'landing/server_stats.html', { 'disk_labels': ['Root /', 'Code', 'Media'], }) STATS_CACHE_KEY = 'stats:payload' STATS_CACHE_TTL = 20 GUNICORN_ACCESS_LOG = Path(settings.BASE_DIR).parent / 'logs' / 'gunicorn-access.log' _ACCESS_LOG_LINE = re.compile( r'^(?P\S+) \[(?P