diff --git a/archivebox/core/views.py b/archivebox/core/views.py index 1c181fe4..7624c33c 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -3,6 +3,7 @@ __package__ = "archivebox.core" import json import os import posixpath +from hashlib import sha256 from glob import escape, glob from pathlib import Path from typing import ClassVar, cast @@ -18,8 +19,9 @@ from django.contrib.auth import HASH_SESSION_KEY, SESSION_KEY, get_user_model from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.sessions.models import Session from django.core import signing +from django.core.cache import cache from django.core.paginator import InvalidPage -from django.db.models import Case, IntegerField, Q, Value, When +from django.db.models import Case, Count, IntegerField, Max, Q, Value, When from django.http import Http404, HttpRequest, HttpResponse, HttpResponseForbidden, QueryDict from django.shortcuts import redirect, render from django.utils import timezone @@ -341,8 +343,48 @@ class SnapshotView(View): runtime_config = get_request_config(request) snapshot._runtime_config = runtime_config snapshot_permissions = get_snapshot_permissions(snapshot) - archive_results = list(snapshot.archiveresult_set.all().order_by("start_ts")) tags = list(snapshot.tags.all()) + related_snapshots_qs = ( + SnapshotView.find_snapshots_for_url(snapshot.url) + .select_related("crawl", "crawl__created_by") + .annotate( + num_outputs_cached=ArchiveResult.snapshot_count_expr(status=ArchiveResult.StatusChoices.SUCCEEDED), + num_failures_cached=ArchiveResult.snapshot_count_expr(status=ArchiveResult.StatusChoices.FAILED), + ) + ) + related_snapshots = list( + related_snapshots_qs.exclude(id=snapshot.id).order_by("-bookmarked_at", "-created_at", "-timestamp")[:25], + ) + result_state = ArchiveResult.objects.filter(snapshot_id=snapshot.id).aggregate( + count=Count("id"), + max_modified=Max("modified_at"), + ) + cache_key = ( + "snapshot-detail:" + + sha256( + repr( + ( + snapshot.id, + snapshot.modified_at, + snapshot.status, + result_state, + tuple(sorted(tag.name for tag in tags)), + tuple((snap.id, snap.modified_at, snap.num_outputs, snap.num_failures) for snap in related_snapshots), + "admin" if is_admin_user(request) else "direct", + request.get_host(), + request.is_secure(), + runtime_config.USES_SUBDOMAIN_ROUTING, + runtime_config.BASE_URL, + ), + ).encode(), + ).hexdigest() + ) + if snapshot.status == Snapshot.StatusChoices.SEALED: + cached_content = cache.get(cache_key) + if cached_content is not None: + return HttpResponse(cached_content, content_type="text/html") + + archive_results = list(snapshot.archiveresult_set.all().order_by("start_ts")) snapshot.__dict__["_admin_archiveresults"] = archive_results snapshot.__dict__["_tags_str_cached"] = ",".join(sorted(tag.name for tag in tags)) snapshot.__dict__["num_outputs_cached"] = sum(result.status == ArchiveResult.StatusChoices.SUCCEEDED for result in archive_results) @@ -393,17 +435,6 @@ class SnapshotView(View): best_result = archiveresults[result_type] break - related_snapshots_qs = ( - SnapshotView.find_snapshots_for_url(snapshot.url) - .select_related("crawl", "crawl__created_by") - .annotate( - num_outputs_cached=ArchiveResult.snapshot_count_expr(status=ArchiveResult.StatusChoices.SUCCEEDED), - num_failures_cached=ArchiveResult.snapshot_count_expr(status=ArchiveResult.StatusChoices.FAILED), - ) - ) - related_snapshots = list( - related_snapshots_qs.exclude(id=snapshot.id).order_by("-bookmarked_at", "-created_at", "-timestamp")[:25], - ) related_years_map: dict[int, list[Snapshot]] = {} for snap in [snapshot, *related_snapshots]: snap_dt = snap.bookmarked_at or snap.created_at or snap.downloaded_at @@ -498,7 +529,10 @@ class SnapshotView(View): "failed_items": failed_items, "title_tags": [{"name": tag.name, "style": tag_widget._tag_style(tag.name)} for tag in sorted(tags, key=lambda tag: tag.name)], } - return render(template_name="core/snapshot.html", request=request, context=context) + response = render(template_name="core/snapshot.html", request=request, context=context) + if snapshot.status == Snapshot.StatusChoices.SEALED: + cache.set(cache_key, response.content, timeout=15 * 60) + return response def get(self, request, path): snapshot = None diff --git a/archivebox/tests/test_ui_public_snapshot.py b/archivebox/tests/test_ui_public_snapshot.py index 5c6438d5..1ee8ffb7 100644 --- a/archivebox/tests/test_ui_public_snapshot.py +++ b/archivebox/tests/test_ui_public_snapshot.py @@ -250,6 +250,12 @@ def test_archive_url_with_multiple_snapshots_redirects_to_latest_snapshot(client assert b"4.0\xc2\xa0KB" in chooser.group() assert b"\xf0\x9f\x93\x81 2" not in chooser.group() + with CaptureQueriesContext(connection) as cached_queries: + cached_response = client.get(f"/archive/{url}", HTTP_HOST=WEB_TEST_HOST, follow=True) + assert len(cached_queries) < len(captured_queries) + assert cached_response.status_code == 200 + assert cached_response.content == response.content + def _login_admin_session_over_http(port: int, host: str) -> requests.Session: session = requests.Session()