Files
NoN-Site/nonpacks/landing/views.py
T

330 lines
11 KiB
Python

import hmac
import platform
import random
import socket
import time
from datetime import timedelta
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 _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)
snapshot['disks'][label] = {
'path': path, 'total': d.total, 'used': d.used,
'free': d.free, 'percent': d.percent,
}
except OSError:
snapshot['disks'][label] = None
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
@login_required
def server_stats_api(request):
"""JSON snapshot of server state / performance / recent activity.
The payload is cached server-side for STATS_CACHE_TTL seconds so polling
clients share one computation per interval instead of hammering the DB."""
if not request.user.is_staff:
return JsonResponse({'detail': 'Only staff can view server stats.'}, status=403)
payload = cache.get(STATS_CACHE_KEY)
if payload is None:
start = time.time()
payload = _collect_stats()
payload['app']['page_gen_ms'] = round((time.time() - start) * 1000, 1)
cache.set(STATS_CACHE_KEY, payload, STATS_CACHE_TTL)
return JsonResponse(payload)
def _collect_stats(page_gen_ms=None):
"""One-shot, JSON-safe snapshot of server state and recent activity."""
now = tz.now()
users = get_user_model().objects
category_breakdown = dict(
Project.objects.values_list('category').annotate(n=Count('pk')).order_by()
)
project_categories = [c for c, _ in Project.CATEGORY_CHOICES]
categories_list = [
[c, category_breakdown.get(c, 0)] for c in project_categories
]
try:
import importlib.metadata as _md
gunicorn_version = _md.version('gunicorn')
except Exception:
gunicorn_version = 'unknown'
content = {
'users': users.count(),
'staff': users.filter(is_staff=True).count(),
'superusers': users.filter(is_superuser=True).count(),
'active_24h': users.filter(last_login__gte=now - timedelta(hours=24)).count(),
'active_7d': users.filter(last_login__gte=now - timedelta(days=7)).count(),
'projects': sum(category_breakdown.get(c, 0) for c in project_categories),
'categories': category_breakdown,
'versions': Version.objects.count(),
'files': FileIndex.objects.count(),
'storage': _fmt_bytes(
FileIndex.objects.aggregate(s=Sum('size'))['s'] or 0
),
'assets': ProjectAsset.objects.count(),
'downloads': Version.objects.aggregate(t=Sum('downloads'))['t'] or 0,
'ratings': Rating.objects.count(),
'ratings_positive': Rating.objects.filter(value=True).count(),
'ratings_negative': Rating.objects.filter(value=False).count(),
'comments': Comment.objects.count(),
'tags': Tag.objects.count(),
'sessions': Session.objects.filter(expire_date__gte=now).count(),
'pending_uploads': TempUpload.objects.filter(status='pending').count(),
'drafts': ProjectDraft.objects.count(),
}
def _iso(dt):
return dt.isoformat(timespec='seconds')
recent = {
'versions': [
{
'project': v.project.title,
'slug': v.project.slug,
'version': v.version_name,
'created_at': _iso(v.created_at),
}
for v in Version.objects.select_related('project').order_by('-created_at')[:8]
],
'comments': [
{
'user': c.user.username,
'project': c.project.title,
'slug': c.project.slug,
'body': c.body,
'created_at': _iso(c.created_at),
}
for c in Comment.objects.select_related('user', 'project').order_by('-created_at')[:8]
],
'projects': [
{
'title': p.title,
'slug': p.slug,
'category': p.category,
'owner': p.owner.username,
'created_at': _iso(p.created_at),
}
for p in Project.objects.select_related('owner').order_by('-created_at')[:5]
],
'ratings': [
{
'user': r.user.username,
'project': r.project.title,
'slug': r.project.slug,
'value': r.value,
'created_at': _iso(r.created_at),
}
for r in Rating.objects.select_related('user', 'project').order_by('-created_at')[:5]
],
}
app = {
'django': django_version(),
'python': platform.python_version(),
'hostname': socket.gethostname(),
'app_env': 'dev' if settings.DEBUG else 'prod',
'debug': settings.DEBUG,
'git': get_git_commit_hash(),
'db': settings.DATABASES['default']['ENGINE'].rsplit('.', 1)[-1],
'tz': settings.TIME_ZONE,
'media_root': str(settings.MEDIA_ROOT),
'gunicorn': gunicorn_version,
'page_gen_ms': page_gen_ms,
}
return {
'generated_at': _iso(now),
'system': _system_snapshot(),
'app': app,
'content': content,
'categories_list': categories_list,
'project_categories': project_categories,
'recent': recent,
}