Skip to content

Addons: add CDN-Tags to endpoint and auto-purge cache #10704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 83 additions & 33 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
"""Views for hosting features."""

from functools import lru_cache

import packaging
import structlog
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http import Http404, JsonResponse
from django.views import View
from rest_framework.renderers import JSONRenderer
from rest_framework.views import APIView

from readthedocs.api.mixins import CDNCacheTagsMixin
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
from readthedocs.api.v3.serializers import (
BuildSerializer,
ProjectSerializer,
VersionSerializer,
)
from readthedocs.builds.models import Version
from readthedocs.core.mixins import CDNCacheControlMixin
from readthedocs.core.resolver import resolver
from readthedocs.core.unresolver import UnresolverError, unresolver
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.models import Feature

log = structlog.get_logger(__name__) # noqa
Expand All @@ -35,7 +40,7 @@ class ClientError(Exception):
)


class ReadTheDocsConfigJson(CDNCacheControlMixin, View):
class BaseReadTheDocsConfigJson(CDNCacheTagsMixin, APIView):

"""
API response consumed by our JavaScript client.
Expand All @@ -49,8 +54,52 @@ class ReadTheDocsConfigJson(CDNCacheControlMixin, View):
(e.g. ``window.location.href``)
"""

def get(self, request):
http_method_names = ["get"]
permission_classes = [IsAuthorizedToViewVersion]
renderer_classes = [JSONRenderer]
project_cache_tag = "rtd-addons"

@lru_cache(maxsize=1)
def _resolve_resources(self):
url = self.request.GET.get("url")
if not url:
# TODO: not sure what to return here when it fails on the `has_permission`
return None, None, None, None

unresolved_domain = self.request.unresolved_domain
project = unresolved_domain.project

try:
unresolved_url = unresolver.unresolve_url(url)
version = unresolved_url.version
filename = unresolved_url.filename
build = version.builds.last()

except UnresolverError as exc:
# If an exception is raised and there is a ``project`` in the
# exception, it's a partial match. This could be because of an
# invalid URL path, but on a valid project domain. In this case, we
# continue with the ``project``, but without a ``version``.
# Otherwise, we return 404 NOT FOUND.
project = getattr(exc, "project", None)
if not project:
raise Http404() from exc

version = None
filename = None
build = None

return project, version, build, filename

def _get_project(self):
project, version, build, filename = self._resolve_resources()
return project

def _get_version(self):
project, version, build, filename = self._resolve_resources()
return version

def get(self, request, format=None):
url = request.GET.get("url")
if not url:
return JsonResponse(
Expand Down Expand Up @@ -83,30 +132,16 @@ def get(self, request):
status=400,
)

unresolved_domain = request.unresolved_domain
project = unresolved_domain.project

try:
unresolved_url = unresolver.unresolve_url(url)
version = unresolved_url.version
filename = unresolved_url.filename
build = version.builds.last()

except UnresolverError as exc:
# If an exception is raised and there is a ``project`` in the
# exception, it's a partial match. This could be because of an
# invalid URL path, but on a valid project domain. In this case, we
# continue with the ``project``, but without a ``version``.
# Otherwise, we return 404 NOT FOUND.
project = getattr(exc, "project", None)
if not project:
raise Http404() from exc

version = None
filename = None
build = None
project, version, build, filename = self._resolve_resources()

data = AddonsResponse().get(addons_version, project, version, build, filename)
data = AddonsResponse().get(
addons_version,
project,
version,
build,
filename,
user=request.user,
)
return JsonResponse(data, json_dumps_params={"indent": 4, "sort_keys": True})


Expand Down Expand Up @@ -149,20 +184,28 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer):


class AddonsResponse:
def get(self, addons_version, project, version=None, build=None, filename=None):
def get(
self,
addons_version,
project,
version=None,
build=None,
filename=None,
user=None,
):
"""
Unique entry point to get the proper API response.

It will evaluate the ``addons_version`` passed and decide which is the
best JSON structure for that particular version.
"""
if addons_version.major == 0:
return self._v0(project, version, build, filename)
return self._v0(project, version, build, filename, user)

if addons_version.major == 1:
return self._v1(project, version, build, filename)
return self._v1(project, version, build, filename, user)

def _v0(self, project, version, build, filename):
def _v0(self, project, version, build, filename, user):
"""
Initial JSON data structure consumed by the JavaScript client.

Expand All @@ -180,7 +223,10 @@ def _v0(self, project, version, build, filename):
if not project.single_version:
versions_active_built_not_hidden = (
Version.internal.public(
project=project, only_active=True, only_built=True
project=project,
only_active=True,
only_built=True,
user=user,
)
.exclude(hidden=True)
.only("slug")
Expand Down Expand Up @@ -369,7 +415,11 @@ def _v0(self, project, version, build, filename):

return data

def _v1(self, project, version, build, filename):
def _v1(self, project, version, build, filename, user):
return {
"comment": "Undefined yet. Use v0 for now",
}


class ReadTheDocsConfigJson(SettingsOverrideObject):
_default_class = BaseReadTheDocsConfigJson