Skip to content

Commit 30f891f

Browse files
authored
Merge pull request #9168 from readthedocs/serve-static-files-same-domain
2 parents 134f5ca + 0e05700 commit 30f891f

File tree

17 files changed

+284
-116
lines changed

17 files changed

+284
-116
lines changed

dockerfiles/nginx/proxito.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ server {
3131
}
3232

3333
# Sendfile support to serve the actual files that Proxito has specified
34-
location /proxito/ {
34+
location ~* "^/(proxito|proxito-static)/(.+)" {
3535
internal;
3636
# Nginx will strip the `/proxito/` and pass just the `$storage/$type/$proj/$ver/$filename`
37-
proxy_pass http://storage:9000/;
37+
proxy_pass http://storage:9000/$2;
3838

3939
proxy_set_header Host storage:9000;
4040
proxy_set_header X-Real-IP $remote_addr;

readthedocs/api/v2/mixins.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
log = structlog.get_logger(__name__)
66

77

8-
class CachedResponseMixin:
8+
class CDNCacheTagsMixin:
99

1010
"""
1111
Add cache tags for project and version to the response of this view.
1212
1313
The view inheriting this mixin should implement the
1414
`self._get_project` and `self._get_version` methods.
1515
16+
If `self._get_version` returns `None`,
17+
only the project level tags are added.
18+
1619
You can add an extra per-project tag by overriding the `project_cache_tag` attribute.
1720
"""
1821

@@ -37,13 +40,18 @@ def _get_cache_tags(self):
3740
try:
3841
project = self._get_project()
3942
version = self._get_version()
40-
tags = [
41-
project.slug,
42-
get_cache_tag(project.slug, version.slug),
43-
]
44-
if self.project_cache_tag:
45-
tags.append(get_cache_tag(project.slug, self.project_cache_tag))
46-
return tags
47-
except Exception as e:
48-
log.debug('Error while retrieving project and version for this view.')
49-
return []
43+
except Exception:
44+
log.warning(
45+
"Error while retrieving project or version for this view.",
46+
exc_info=True,
47+
)
48+
return []
49+
50+
tags = []
51+
if project:
52+
tags.append(project.slug)
53+
if project and version:
54+
tags.append(get_cache_tag(project.slug, version.slug))
55+
if project and self.project_cache_tag:
56+
tags.append(get_cache_tag(project.slug, self.project_cache_tag))
57+
return tags

readthedocs/api/v2/views/footer_views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.views import APIView
1313
from rest_framework_jsonp.renderers import JSONPRenderer
1414

15-
from readthedocs.api.v2.mixins import CachedResponseMixin
15+
from readthedocs.api.v2.mixins import CDNCacheTagsMixin
1616
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
1717
from readthedocs.builds.constants import LATEST, TAG
1818
from readthedocs.builds.models import Version
@@ -83,7 +83,7 @@ def get_version_compare_data(project, base_version=None):
8383
return ret_val
8484

8585

86-
class BaseFooterHTML(CachedResponseMixin, APIView):
86+
class BaseFooterHTML(CDNCacheTagsMixin, APIView):
8787

8888
"""
8989
Render and return footer markup.

readthedocs/core/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class ProxiedAPIMixin:
3131
authentication_classes = []
3232

3333

34-
class CachedView:
34+
class CDNCacheControlMixin:
3535

3636
"""
3737
Allow to cache views at the CDN level when privacy levels are enabled.

readthedocs/doc_builder/backends/mkdocs.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ def get_absolute_static_url():
2727
"""
2828
static_url = settings.STATIC_URL
2929

30-
if not static_url.startswith('http'):
30+
if not static_url.startswith("http"):
3131
domain = settings.PRODUCTION_DOMAIN
32-
static_url = 'http://{}{}'.format(domain, static_url)
32+
static_url = "http://{}{}".format(domain, static_url)
3333

3434
return static_url
3535

@@ -147,8 +147,14 @@ def append_conf(self):
147147
self.create_index(extension='md')
148148
user_config['docs_dir'] = docs_dir
149149

150-
# Set mkdocs config values
151-
static_url = get_absolute_static_url()
150+
# MkDocs <=0.17.x doesn't support absolute paths,
151+
# it needs one with a full domain.
152+
if self.project.has_feature(Feature.DEFAULT_TO_MKDOCS_0_17_3):
153+
static_url = get_absolute_static_url()
154+
else:
155+
static_url = self.project.proxied_static_path
156+
157+
# Set mkdocs config values.
152158
extra_assets = {
153159
'extra_javascript': [
154160
'readthedocs-data.js',

readthedocs/doc_builder/backends/sphinx.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
"""
66
import codecs
77
import itertools
8-
import structlog
98
import os
109
import shutil
1110
import zipfile
1211
from glob import glob
1312
from pathlib import Path
1413

14+
import structlog
1515
from django.conf import settings
1616
from django.template import loader as template_loader
1717
from django.template.loader import render_to_string
@@ -169,21 +169,21 @@ def get_config_params(self):
169169
)
170170

171171
data = {
172-
'html_theme': 'sphinx_rtd_theme',
173-
'html_theme_import': 'sphinx_rtd_theme',
174-
'current_version': self.version.verbose_name,
175-
'project': self.project,
176-
'version': self.version,
177-
'settings': settings,
178-
'conf_py_path': conf_py_path,
179-
'api_host': settings.PUBLIC_API_URL,
180-
'commit': commit,
181-
'versions': versions,
182-
'downloads': downloads,
183-
'subproject_urls': subproject_urls,
184-
'build_url': build_url,
185-
'vcs_url': vcs_url,
186-
172+
"html_theme": "sphinx_rtd_theme",
173+
"html_theme_import": "sphinx_rtd_theme",
174+
"current_version": self.version.verbose_name,
175+
"project": self.project,
176+
"version": self.version,
177+
"settings": settings,
178+
"conf_py_path": conf_py_path,
179+
"api_host": settings.PUBLIC_API_URL,
180+
"commit": commit,
181+
"versions": versions,
182+
"downloads": downloads,
183+
"subproject_urls": subproject_urls,
184+
"build_url": build_url,
185+
"vcs_url": vcs_url,
186+
"proxied_static_path": self.project.proxied_static_path,
187187
# GitHub
188188
'github_user': github_user,
189189
'github_repo': github_repo,

readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ context = {
8181
'MEDIA_URL': "{{ settings.MEDIA_URL }}",
8282
'STATIC_URL': "{{ settings.STATIC_URL }}",
8383
'PRODUCTION_DOMAIN': "{{ settings.PRODUCTION_DOMAIN }}",
84+
'proxied_static_path': "{{ proxied_static_path }}",
8485
'versions': [{% for version in versions %}
8586
("{{ version.slug }}", "/{{ version.project.language }}/{{ version.slug}}/"),{% endfor %}
8687
],

readthedocs/embed/v3/views.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
import re
44
from urllib.parse import urlparse
5-
import requests
65

6+
import requests
77
import structlog
8-
9-
from selectolax.parser import HTMLParser
10-
118
from django.conf import settings
129
from django.core.cache import cache
1310
from django.shortcuts import get_object_or_404
@@ -17,8 +14,9 @@
1714
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
1815
from rest_framework.response import Response
1916
from rest_framework.views import APIView
17+
from selectolax.parser import HTMLParser
2018

21-
from readthedocs.api.v2.mixins import CachedResponseMixin
19+
from readthedocs.api.v2.mixins import CDNCacheTagsMixin
2220
from readthedocs.core.unresolver import unresolve
2321
from readthedocs.core.utils.extend import SettingsOverrideObject
2422
from readthedocs.embed.utils import clean_links
@@ -28,7 +26,7 @@
2826
log = structlog.get_logger(__name__)
2927

3028

31-
class EmbedAPIBase(CachedResponseMixin, APIView):
29+
class EmbedAPIBase(CDNCacheTagsMixin, APIView):
3230

3331
# pylint: disable=line-too-long
3432
# pylint: disable=no-self-use

readthedocs/embed/views.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import re
66

7+
import structlog
78
from django.shortcuts import get_object_or_404
89
from django.template.defaultfilters import slugify
910
from django.utils.functional import cached_property
@@ -14,15 +15,13 @@
1415
from rest_framework.response import Response
1516
from rest_framework.views import APIView
1617

17-
import structlog
18-
19-
from readthedocs.api.v2.mixins import CachedResponseMixin
18+
from readthedocs.api.v2.mixins import CDNCacheTagsMixin
2019
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
2120
from readthedocs.builds.constants import EXTERNAL
2221
from readthedocs.core.resolver import resolve
2322
from readthedocs.core.unresolver import unresolve
2423
from readthedocs.core.utils.extend import SettingsOverrideObject
25-
from readthedocs.embed.utils import recurse_while_none, clean_links
24+
from readthedocs.embed.utils import clean_links, recurse_while_none
2625
from readthedocs.projects.models import Project
2726
from readthedocs.storage import build_media_storage
2827

@@ -36,7 +35,7 @@ def escape_selector(selector):
3635
return ret
3736

3837

39-
class EmbedAPIBase(CachedResponseMixin, APIView):
38+
class EmbedAPIBase(CDNCacheTagsMixin, APIView):
4039

4140
# pylint: disable=line-too-long
4241

readthedocs/projects/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,11 @@ def proxied_api_url(self):
624624
"""
625625
return self.proxied_api_host.strip('/') + '/'
626626

627+
@property
628+
def proxied_static_path(self):
629+
"""Path for static files hosted on the user's doc domain."""
630+
return f"{self.proxied_api_host}/static/"
631+
627632
@property
628633
def regex_urlconf(self):
629634
"""

readthedocs/proxito/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ What can/can't be cached?
4242
This view is already cached at the application level.
4343
- ServeSitemapXML: can be cached. It displays only public versions, for everyone.
4444
This view is already cached at the application level.
45+
- ServeStaticFiles: can be cached, all files are the same for all projects and users.
4546
- Embed API: can be cached for public versions.
4647
- Search:
4748
This view checks for user permissions, can't be cached.

readthedocs/proxito/tests/test_full.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,26 @@ def test_sitemap_all_private_versions(self):
11381138
)
11391139
self.assertEqual(response.status_code, 404)
11401140

1141+
@override_settings(
1142+
STATICFILES_STORAGE="readthedocs.rtd_tests.storage.BuildMediaFileSystemStorageTest"
1143+
)
1144+
def test_serve_static_files(self):
1145+
resp = self.client.get(
1146+
reverse(
1147+
"proxito_static_files",
1148+
args=["javascript/readthedocs-doc-embed.js"],
1149+
),
1150+
HTTP_HOST="project.readthedocs.io",
1151+
)
1152+
self.assertEqual(resp.status_code, 200)
1153+
self.assertEqual(
1154+
resp.headers["x-accel-redirect"],
1155+
"/proxito-static/media/javascript/readthedocs-doc-embed.js",
1156+
)
1157+
self.assertEqual(
1158+
resp.headers["Cache-Tag"], "project,project:rtd-staticfiles,rtd-staticfiles"
1159+
)
1160+
11411161

11421162
@override_settings(
11431163
ALLOW_PRIVATE_REPOS=True,
@@ -1187,6 +1207,13 @@ def _test_cache_control_header_project(self, expected_value, host=None):
11871207
self.assertEqual(resp.headers['CDN-Cache-Control'], 'public', url)
11881208
self.assertEqual(resp.headers['Cache-Tag'], 'project', url)
11891209

1210+
# Proxied static files are always cached.
1211+
resp = self.client.get("/_/static/file.js", secure=True, HTTP_HOST=host)
1212+
self.assertEqual(resp.headers["CDN-Cache-Control"], "public")
1213+
self.assertEqual(
1214+
resp.headers["Cache-Tag"], "project,project:rtd-staticfiles,rtd-staticfiles"
1215+
)
1216+
11901217
# Slash redirect is done at the middleware level.
11911218
# So, it doesn't take into consideration the privacy level of the
11921219
# version, and always defaults to private.
@@ -1228,6 +1255,13 @@ def _test_cache_control_header_subproject(self, expected_value, host=None):
12281255
self.assertEqual(resp.headers['CDN-Cache-Control'], 'public', url)
12291256
self.assertEqual(resp.headers['Cache-Tag'], 'subproject', url)
12301257

1258+
# Proxied static files are always cached.
1259+
resp = self.client.get("/_/static/file.js", secure=True, HTTP_HOST=host)
1260+
self.assertEqual(resp.headers["CDN-Cache-Control"], "public")
1261+
self.assertEqual(
1262+
resp.headers["Cache-Tag"], "project,project:rtd-staticfiles,rtd-staticfiles"
1263+
)
1264+
12311265
# Slash redirect is done at the middleware level.
12321266
# So, it doesn't take into consideration the privacy level of the
12331267
# version, and always defaults to private.

readthedocs/proxito/urls.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@
4646
ServePageRedirect,
4747
ServeRobotsTXT,
4848
ServeSitemapXML,
49+
ServeStaticFiles,
4950
)
50-
from readthedocs.proxito.views.utils import proxito_404_page_handler, fast_404
51+
from readthedocs.proxito.views.utils import fast_404, proxito_404_page_handler
5152

5253
DOC_PATH_PREFIX = getattr(settings, 'DOC_PATH_PREFIX', '')
5354

@@ -106,6 +107,17 @@
106107
),
107108
include('readthedocs.api.v3.proxied_urls'),
108109
),
110+
# Serve static files
111+
# /_/static/file.js
112+
re_path(
113+
r"^{DOC_PATH_PREFIX}static/"
114+
r"(?P<filename>{filename_slug})$".format(
115+
DOC_PATH_PREFIX=DOC_PATH_PREFIX,
116+
**pattern_opts,
117+
),
118+
ServeStaticFiles.as_view(),
119+
name="proxito_static_files",
120+
),
109121
]
110122

111123
core_urls = [

0 commit comments

Comments
 (0)