diff --git a/news/163.feature b/news/163.feature new file mode 100644 index 00000000..3891be30 --- /dev/null +++ b/news/163.feature @@ -0,0 +1 @@ +Set `Link` header with `rel="canonical"` for file downloads. @mamico diff --git a/plone/namedfile/browser.py b/plone/namedfile/browser.py index 0930b752..538b5d86 100644 --- a/plone/namedfile/browser.py +++ b/plone/namedfile/browser.py @@ -127,6 +127,13 @@ def handle_request_range(self, file): except ValueError: return default + def get_canonical(self, file): + filename = getattr(file, "filename", None) + if filename is None: + return f"{self.context.absolute_url()}/@@download/{self.fieldname}" + else: + return f"{self.context.absolute_url()}/@@download/{self.fieldname}/{filename}" + def set_headers(self, file): # With filename None, set_headers will not add the download headers. if not self.filename: @@ -135,7 +142,8 @@ def set_headers(self, file): self.filename = self.fieldname if self.filename is None: self.filename = "file.ext" - set_headers(file, self.request.response, filename=self.filename) + canonical = self.get_canonical(file) + set_headers(file, self.request.response, filename=self.filename, canonical=canonical) def _getFile(self): if not self.fieldname: @@ -185,4 +193,5 @@ def set_headers(self, file): if mimetype not in self.allowed_inline_mimetypes: # Let the Download view handle this. return super().set_headers(file) - set_headers(file, self.request.response) + canonical = self.get_canonical(file) + set_headers(file, self.request.response, canonical=canonical) diff --git a/plone/namedfile/usage.rst b/plone/namedfile/usage.rst index 759afe38..c18cf075 100644 --- a/plone/namedfile/usage.rst +++ b/plone/namedfile/usage.rst @@ -32,6 +32,9 @@ These store data with the following types:: ... self.image = namedfile.NamedImage() ... self.blob = namedfile.NamedBlobFile() ... self.blobimage = namedfile.NamedBlobImage() + ... + ... def absolute_url(self): + ... return "http://foo/bar" File data and content type @@ -226,6 +229,8 @@ We will test this with a dummy request, faking traversal:: 'text/plain' >>> request.response.getHeader('Content-Disposition') "attachment; filename*=UTF-8''test.txt" + >>> request.response.getHeader('Link') + '; rel="canonical"' >>> request = TestRequest() >>> download = Download(container, request).publishTraverse(request, 'blob') @@ -238,6 +243,8 @@ We will test this with a dummy request, faking traversal:: 'text/plain' >>> request.response.getHeader('Content-Disposition') "attachment; filename*=UTF-8''test.txt" + >>> request.response.getHeader('Link') + '; rel="canonical"' >>> request = TestRequest() >>> download = Download(container, request).publishTraverse(request, 'image') @@ -250,6 +257,8 @@ We will test this with a dummy request, faking traversal:: 'image/foo' >>> request.response.getHeader('Content-Disposition') "attachment; filename*=UTF-8''zpt.gif" + >>> request.response.getHeader('Link') + '; rel="canonical"' >>> request = TestRequest() >>> download = Download(container, request).publishTraverse(request, 'blobimage') @@ -262,6 +271,8 @@ We will test this with a dummy request, faking traversal:: 'image/foo' >>> request.response.getHeader('Content-Disposition') "attachment; filename*=UTF-8''zpt.gif" + >>> request.response.getHeader('Link') + '; rel="canonical"' Range support ------------- diff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py index 2f17ead9..a2ccf851 100644 --- a/plone/namedfile/utils/__init__.py +++ b/plone/namedfile/utils/__init__.py @@ -130,7 +130,7 @@ def get_contenttype(file=None, filename=None, default="application/octet-stream" return default -def set_headers(file, response, filename=None): +def set_headers(file, response, filename=None, canonical=None): """Set response headers for the given file. If filename is given, set the Content-Disposition to attachment. """ @@ -149,6 +149,8 @@ def set_headers(file, response, filename=None): "Content-Disposition", f"attachment; filename*=UTF-8''{filename}" ) + if canonical is not None: + response.setHeader("Link", f'<{quote(canonical, safe="/:&?=@")}>; rel="canonical"') def stream_data(file, start=0, end=None): """Return the given file as a stream if possible."""