From d948bfeebd283cf2ec14e92308f364623b988547 Mon Sep 17 00:00:00 2001 From: davisagli Date: Thu, 21 Nov 2024 11:17:36 -0800 Subject: [PATCH] [fc] Repository: plone.namedfile Branch: refs/heads/master Date: 2024-10-28T01:28:15+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/d72e456918db99ee8ca78e92bb2c7b9ea273270f feat: set canonical header for file Files changed: M plone/namedfile/browser.py M plone/namedfile/tests/test_display_file.py M plone/namedfile/utils/__init__.py Repository: plone.namedfile Branch: refs/heads/master Date: 2024-10-28T01:35:22+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/5a9fc2f369e50c826ada3939294900325d30ea83 fix test Files changed: M plone/namedfile/usage.rst Repository: plone.namedfile Branch: refs/heads/master Date: 2024-10-28T01:48:30+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/36fe1a40779787bbd025b4ce82b792473a7014f9 fix test Files changed: M plone/namedfile/browser.py M plone/namedfile/tests/test_display_file.py M plone/namedfile/usage.rst Repository: plone.namedfile Branch: refs/heads/master Date: 2024-10-28T14:13:31+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/5a808ba73b98be70ce0786f5f4fe87937248761c changelog Files changed: A news/163.feature Repository: plone.namedfile Branch: refs/heads/master Date: 2024-10-28T21:59:49-07:00 Author: David Glick (davisagli) Commit: https://github.com/plone/plone.namedfile/commit/6296627099f9524d545d4e2de8d195a6bddea6d6 Update news/163.feature Files changed: M news/163.feature Repository: plone.namedfile Branch: refs/heads/master Date: 2024-11-16T23:50:52+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/39abbc0d7af268fe443603886ce8bf24335b2963 The URI (absolute or relative) must percent-encode character codes greater than 255 (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) Files changed: M plone/namedfile/utils/__init__.py Repository: plone.namedfile Branch: refs/heads/master Date: 2024-11-16T23:50:52+01:00 Author: Mauro Amico (mamico) Commit: https://github.com/plone/plone.namedfile/commit/2e54f717898c9b717a5100682e3cb0f43a91b68f fix quote Files changed: M plone/namedfile/utils/__init__.py Repository: plone.namedfile Branch: refs/heads/master Date: 2024-11-21T11:17:36-08:00 Author: David Glick (davisagli) Commit: https://github.com/plone/plone.namedfile/commit/db74ee2200c9f4d2332d930bc5c1c00478e50592 Merge pull request #163 from plone/canonical feat: set canonical header for file Files changed: A news/163.feature M plone/namedfile/browser.py M plone/namedfile/usage.rst M plone/namedfile/utils/__init__.py --- last_commit.txt | 134 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 13 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 4c876357a7..34c2489412 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,21 +1,129 @@ -Repository: plone.restapi +Repository: plone.namedfile -Branch: refs/heads/main -Date: 2024-11-20T15:30:02-08:00 -Author: Andrea Cecchi (cekk) -Commit: https://github.com/plone/plone.restapi/commit/694ec892dd992c5187ada334f55175d5ba7d9f08 +Branch: refs/heads/master +Date: 2024-10-28T01:28:15+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/d72e456918db99ee8ca78e92bb2c7b9ea273270f -Do not change request during serialization of relation fields (#1845) +feat: set canonical header for file -* Do not change request during serialization - -* Add changelog +Files changed: +M plone/namedfile/browser.py +M plone/namedfile/tests/test_display_file.py +M plone/namedfile/utils/__init__.py + +b'diff --git a/plone/namedfile/browser.py b/plone/namedfile/browser.py\nindex 0930b75..0b753cb 100644\n--- a/plone/namedfile/browser.py\n+++ b/plone/namedfile/browser.py\n@@ -127,6 +127,13 @@ def handle_request_range(self, file):\n except ValueError:\n return default\n \n+ def get_canonical(self, file):\n+ filename = getattr(file, "filename", None)\n+ if filename is None:\n+ return f"{self.context.absolute_url()}/@@download/{self.fieldname}"\n+ else:\n+ return f"{self.context.absolute_url()}/@@download/{self.fieldname}/filename"\n+\n def set_headers(self, file):\n # With filename None, set_headers will not add the download headers.\n if not self.filename:\n@@ -135,7 +142,8 @@ def set_headers(self, file):\n self.filename = self.fieldname\n if self.filename is None:\n self.filename = "file.ext"\n- set_headers(file, self.request.response, filename=self.filename)\n+ canonical = self.get_canonical(file)\n+ set_headers(file, self.request.response, filename=self.filename, canonical=canonical)\n \n def _getFile(self):\n if not self.fieldname:\n@@ -185,4 +193,5 @@ def set_headers(self, file):\n if mimetype not in self.allowed_inline_mimetypes:\n # Let the Download view handle this.\n return super().set_headers(file)\n- set_headers(file, self.request.response)\n+ canonical = self.get_canonical(file)\n+ set_headers(file, self.request.response, canonical=canonical)\ndiff --git a/plone/namedfile/tests/test_display_file.py b/plone/namedfile/tests/test_display_file.py\nindex 4ab467d..88e4151 100644\n--- a/plone/namedfile/tests/test_display_file.py\n+++ b/plone/namedfile/tests/test_display_file.py\n@@ -49,6 +49,15 @@ def get_disposition_header(browser):\n return browser.headers.get(name, None)\n \n \n+def get_canonical_header(browser):\n+ name = "Link"\n+ for header, value in browser.headers.items():\n+ if header == name or header == name.lower():\n+ if \'rel="canonical"\' in map(str.strip, value.split(";")):\n+ return value\n+ return None\n+\n+\n def custom_available_sizes():\n # Define available image scales.\n return {"custom": (10, 10)}\n@@ -96,12 +105,16 @@ def assert_download_works(self, base_url):\n self.assertIsNotNone(header)\n self.assertIn("attachment", header)\n self.assertIn("filename", header)\n+ header = get_canonical_header(browser)\n+ self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_display_inline_works(self, base_url):\n # Test that displaying this file inline works.\n browser = self.get_anon_browser()\n browser.open(base_url + f"/@@display-file/{self.field_name}")\n self.assertIsNone(get_disposition_header(browser))\n+ header = get_canonical_header(browser)\n+ self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_display_inline_is_download(self, base_url):\n # Test that displaying this file inline turns into a download.\n@@ -111,6 +124,8 @@ def assert_display_inline_is_download(self, base_url):\n self.assertIsNotNone(header)\n self.assertIn("attachment", header)\n self.assertIn("filename", header)\n+ header = get_canonical_header(browser)\n+ self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_scale_view_works(self, base_url):\n # Test that accessing a scale view shows the image inline.\ndiff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py\nindex 2f17ead..6509aec 100644\n--- a/plone/namedfile/utils/__init__.py\n+++ b/plone/namedfile/utils/__init__.py\n@@ -130,7 +130,7 @@ def get_contenttype(file=None, filename=None, default="application/octet-stream"\n return default\n \n \n-def set_headers(file, response, filename=None):\n+def set_headers(file, response, filename=None, canonical=None):\n """Set response headers for the given file. If filename is given, set\n the Content-Disposition to attachment.\n """\n@@ -149,6 +149,8 @@ def set_headers(file, response, filename=None):\n "Content-Disposition", f"attachment; filename*=UTF-8\'\'{filename}"\n )\n \n+ if canonical is not None:\n+ response.setHeader("Link", f\'<{canonical}>; rel="canonical"\')\n \n def stream_data(file, start=0, end=None):\n """Return the given file as a stream if possible."""\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-10-28T01:35:22+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/5a9fc2f369e50c826ada3939294900325d30ea83 + +fix test + +Files changed: +M plone/namedfile/usage.rst + +b'diff --git a/plone/namedfile/usage.rst b/plone/namedfile/usage.rst\nindex 759afe3..bec7325 100644\n--- a/plone/namedfile/usage.rst\n+++ b/plone/namedfile/usage.rst\n@@ -32,6 +32,9 @@ These store data with the following types::\n ... self.image = namedfile.NamedImage()\n ... self.blob = namedfile.NamedBlobFile()\n ... self.blobimage = namedfile.NamedBlobImage()\n+ ...\n+ ... def absolute_url(self):\n+ ... return "http://foo/bar"\n \n \n File data and content type\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-10-28T01:48:30+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/36fe1a40779787bbd025b4ce82b792473a7014f9 + +fix test + +Files changed: +M plone/namedfile/browser.py +M plone/namedfile/tests/test_display_file.py +M plone/namedfile/usage.rst + +b'diff --git a/plone/namedfile/browser.py b/plone/namedfile/browser.py\nindex 0b753cb..538b5d8 100644\n--- a/plone/namedfile/browser.py\n+++ b/plone/namedfile/browser.py\n@@ -132,7 +132,7 @@ def get_canonical(self, file):\n if filename is None:\n return f"{self.context.absolute_url()}/@@download/{self.fieldname}"\n else:\n- return f"{self.context.absolute_url()}/@@download/{self.fieldname}/filename"\n+ return f"{self.context.absolute_url()}/@@download/{self.fieldname}/{filename}"\n \n def set_headers(self, file):\n # With filename None, set_headers will not add the download headers.\ndiff --git a/plone/namedfile/tests/test_display_file.py b/plone/namedfile/tests/test_display_file.py\nindex 88e4151..4ab467d 100644\n--- a/plone/namedfile/tests/test_display_file.py\n+++ b/plone/namedfile/tests/test_display_file.py\n@@ -49,15 +49,6 @@ def get_disposition_header(browser):\n return browser.headers.get(name, None)\n \n \n-def get_canonical_header(browser):\n- name = "Link"\n- for header, value in browser.headers.items():\n- if header == name or header == name.lower():\n- if \'rel="canonical"\' in map(str.strip, value.split(";")):\n- return value\n- return None\n-\n-\n def custom_available_sizes():\n # Define available image scales.\n return {"custom": (10, 10)}\n@@ -105,16 +96,12 @@ def assert_download_works(self, base_url):\n self.assertIsNotNone(header)\n self.assertIn("attachment", header)\n self.assertIn("filename", header)\n- header = get_canonical_header(browser)\n- self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_display_inline_works(self, base_url):\n # Test that displaying this file inline works.\n browser = self.get_anon_browser()\n browser.open(base_url + f"/@@display-file/{self.field_name}")\n self.assertIsNone(get_disposition_header(browser))\n- header = get_canonical_header(browser)\n- self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_display_inline_is_download(self, base_url):\n # Test that displaying this file inline turns into a download.\n@@ -124,8 +111,6 @@ def assert_display_inline_is_download(self, base_url):\n self.assertIsNotNone(header)\n self.assertIn("attachment", header)\n self.assertIn("filename", header)\n- header = get_canonical_header(browser)\n- self.assertTrue(header.startswith(f"<{base_url}/@@download/{self.field_name}>"))\n \n def assert_scale_view_works(self, base_url):\n # Test that accessing a scale view shows the image inline.\ndiff --git a/plone/namedfile/usage.rst b/plone/namedfile/usage.rst\nindex bec7325..c18cf07 100644\n--- a/plone/namedfile/usage.rst\n+++ b/plone/namedfile/usage.rst\n@@ -229,6 +229,8 @@ We will test this with a dummy request, faking traversal::\n \'text/plain\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'test.txt"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'blob\')\n@@ -241,6 +243,8 @@ We will test this with a dummy request, faking traversal::\n \'text/plain\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'test.txt"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'image\')\n@@ -253,6 +257,8 @@ We will test this with a dummy request, faking traversal::\n \'image/foo\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'zpt.gif"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'blobimage\')\n@@ -265,6 +271,8 @@ We will test this with a dummy request, faking traversal::\n \'image/foo\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'zpt.gif"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n Range support\n -------------\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-10-28T14:13:31+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/5a808ba73b98be70ce0786f5f4fe87937248761c + +changelog + +Files changed: +A news/163.feature + +b'diff --git a/news/163.feature b/news/163.feature\nnew file mode 100644\nindex 00000000..08cf3f9e\n--- /dev/null\n+++ b/news/163.feature\n@@ -0,0 +1 @@\n+set canonical header for file. @mamico\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-10-28T21:59:49-07:00 +Author: David Glick (davisagli) +Commit: https://github.com/plone/plone.namedfile/commit/6296627099f9524d545d4e2de8d195a6bddea6d6 + +Update news/163.feature + +Files changed: +M news/163.feature + +b'diff --git a/news/163.feature b/news/163.feature\nindex 08cf3f9..3891be3 100644\n--- a/news/163.feature\n+++ b/news/163.feature\n@@ -1 +1 @@\n-set canonical header for file. @mamico\n+Set `Link` header with `rel="canonical"` for file downloads. @mamico\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-11-16T23:50:52+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/39abbc0d7af268fe443603886ce8bf24335b2963 + +The URI (absolute or relative) must percent-encode character codes greater than 255 (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) + +Files changed: +M plone/namedfile/utils/__init__.py + +b'diff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py\nindex 6509aec..dd9da38 100644\n--- a/plone/namedfile/utils/__init__.py\n+++ b/plone/namedfile/utils/__init__.py\n@@ -150,7 +150,7 @@ def set_headers(file, response, filename=None, canonical=None):\n )\n \n if canonical is not None:\n- response.setHeader("Link", f\'<{canonical}>; rel="canonical"\')\n+ response.setHeader("Link", f\'<{quote(canonical, safe=\'\')}>; rel="canonical"\')\n \n def stream_data(file, start=0, end=None):\n """Return the given file as a stream if possible."""\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-11-16T23:50:52+01:00 +Author: Mauro Amico (mamico) +Commit: https://github.com/plone/plone.namedfile/commit/2e54f717898c9b717a5100682e3cb0f43a91b68f + +fix quote + +Files changed: +M plone/namedfile/utils/__init__.py + +b'diff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py\nindex dd9da38..a2ccf85 100644\n--- a/plone/namedfile/utils/__init__.py\n+++ b/plone/namedfile/utils/__init__.py\n@@ -150,7 +150,7 @@ def set_headers(file, response, filename=None, canonical=None):\n )\n \n if canonical is not None:\n- response.setHeader("Link", f\'<{quote(canonical, safe=\'\')}>; rel="canonical"\')\n+ response.setHeader("Link", f\'<{quote(canonical, safe="/:&?=@")}>; rel="canonical"\')\n \n def stream_data(file, start=0, end=None):\n """Return the given file as a stream if possible."""\n' + +Repository: plone.namedfile + + +Branch: refs/heads/master +Date: 2024-11-21T11:17:36-08:00 +Author: David Glick (davisagli) +Commit: https://github.com/plone/plone.namedfile/commit/db74ee2200c9f4d2332d930bc5c1c00478e50592 + +Merge pull request #163 from plone/canonical + +feat: set canonical header for file Files changed: -A news/1845.bugfix -M src/plone/restapi/serializer/relationfield.py -M src/plone/restapi/tests/test_dxfield_serializer.py +A news/163.feature +M plone/namedfile/browser.py +M plone/namedfile/usage.rst +M plone/namedfile/utils/__init__.py -b'diff --git a/news/1845.bugfix b/news/1845.bugfix\nnew file mode 100644\nindex 000000000..d1d853ab6\n--- /dev/null\n+++ b/news/1845.bugfix\n@@ -0,0 +1,2 @@\n+Do not change request during relation fields serialization\n+[cekk]\ndiff --git a/src/plone/restapi/serializer/relationfield.py b/src/plone/restapi/serializer/relationfield.py\nindex d54cadb9d..3115568d0 100644\n--- a/src/plone/restapi/serializer/relationfield.py\n+++ b/src/plone/restapi/serializer/relationfield.py\n@@ -18,7 +18,7 @@\n @implementer(IJsonCompatible)\n def relationvalue_converter(value):\n if value.to_object:\n- request = getRequest()\n+ request = getRequest().clone()\n request.form["metadata_fields"] = ["UID"]\n summary = getMultiAdapter((value.to_object, request), ISerializeToJsonSummary)()\n return json_compatible(summary)\ndiff --git a/src/plone/restapi/tests/test_dxfield_serializer.py b/src/plone/restapi/tests/test_dxfield_serializer.py\nindex bac16ffcc..625ea83d5 100644\n--- a/src/plone/restapi/tests/test_dxfield_serializer.py\n+++ b/src/plone/restapi/tests/test_dxfield_serializer.py\n@@ -325,6 +325,19 @@ def test_relationlist_field_serialization_returns_list(self):\n value,\n )\n \n+ def test_relation_field_serialization_do_not_change_request(self):\n+ self.request.form["metadata_fields"] = ["foo", "bar"]\n+ doc2 = self.portal[\n+ self.portal.invokeFactory(\n+ "DXTestDocument",\n+ id="doc2",\n+ title="Referenceable Document",\n+ description="Description 2",\n+ )\n+ ]\n+ self.serialize("test_relationchoice_field", doc2)\n+ self.assertEqual(self.request.form["metadata_fields"], ["foo", "bar"])\n+\n def test_remoteurl_field_in_links_get_converted(self):\n link = self.portal[\n self.portal.invokeFactory(\n' +b'diff --git a/news/163.feature b/news/163.feature\nnew file mode 100644\nindex 00000000..3891be30\n--- /dev/null\n+++ b/news/163.feature\n@@ -0,0 +1 @@\n+Set `Link` header with `rel="canonical"` for file downloads. @mamico\ndiff --git a/plone/namedfile/browser.py b/plone/namedfile/browser.py\nindex 0930b752..538b5d86 100644\n--- a/plone/namedfile/browser.py\n+++ b/plone/namedfile/browser.py\n@@ -127,6 +127,13 @@ def handle_request_range(self, file):\n except ValueError:\n return default\n \n+ def get_canonical(self, file):\n+ filename = getattr(file, "filename", None)\n+ if filename is None:\n+ return f"{self.context.absolute_url()}/@@download/{self.fieldname}"\n+ else:\n+ return f"{self.context.absolute_url()}/@@download/{self.fieldname}/{filename}"\n+\n def set_headers(self, file):\n # With filename None, set_headers will not add the download headers.\n if not self.filename:\n@@ -135,7 +142,8 @@ def set_headers(self, file):\n self.filename = self.fieldname\n if self.filename is None:\n self.filename = "file.ext"\n- set_headers(file, self.request.response, filename=self.filename)\n+ canonical = self.get_canonical(file)\n+ set_headers(file, self.request.response, filename=self.filename, canonical=canonical)\n \n def _getFile(self):\n if not self.fieldname:\n@@ -185,4 +193,5 @@ def set_headers(self, file):\n if mimetype not in self.allowed_inline_mimetypes:\n # Let the Download view handle this.\n return super().set_headers(file)\n- set_headers(file, self.request.response)\n+ canonical = self.get_canonical(file)\n+ set_headers(file, self.request.response, canonical=canonical)\ndiff --git a/plone/namedfile/usage.rst b/plone/namedfile/usage.rst\nindex 759afe38..c18cf075 100644\n--- a/plone/namedfile/usage.rst\n+++ b/plone/namedfile/usage.rst\n@@ -32,6 +32,9 @@ These store data with the following types::\n ... self.image = namedfile.NamedImage()\n ... self.blob = namedfile.NamedBlobFile()\n ... self.blobimage = namedfile.NamedBlobImage()\n+ ...\n+ ... def absolute_url(self):\n+ ... return "http://foo/bar"\n \n \n File data and content type\n@@ -226,6 +229,8 @@ We will test this with a dummy request, faking traversal::\n \'text/plain\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'test.txt"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'blob\')\n@@ -238,6 +243,8 @@ We will test this with a dummy request, faking traversal::\n \'text/plain\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'test.txt"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'image\')\n@@ -250,6 +257,8 @@ We will test this with a dummy request, faking traversal::\n \'image/foo\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'zpt.gif"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n >>> request = TestRequest()\n >>> download = Download(container, request).publishTraverse(request, \'blobimage\')\n@@ -262,6 +271,8 @@ We will test this with a dummy request, faking traversal::\n \'image/foo\'\n >>> request.response.getHeader(\'Content-Disposition\')\n "attachment; filename*=UTF-8\'\'zpt.gif"\n+ >>> request.response.getHeader(\'Link\')\n+ \'; rel="canonical"\'\n \n Range support\n -------------\ndiff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py\nindex 2f17ead9..a2ccf851 100644\n--- a/plone/namedfile/utils/__init__.py\n+++ b/plone/namedfile/utils/__init__.py\n@@ -130,7 +130,7 @@ def get_contenttype(file=None, filename=None, default="application/octet-stream"\n return default\n \n \n-def set_headers(file, response, filename=None):\n+def set_headers(file, response, filename=None, canonical=None):\n """Set response headers for the given file. If filename is given, set\n the Content-Disposition to attachment.\n """\n@@ -149,6 +149,8 @@ def set_headers(file, response, filename=None):\n "Content-Disposition", f"attachment; filename*=UTF-8\'\'{filename}"\n )\n \n+ if canonical is not None:\n+ response.setHeader("Link", f\'<{quote(canonical, safe="/:&?=@")}>; rel="canonical"\')\n \n def stream_data(file, start=0, end=None):\n """Return the given file as a stream if possible."""\n'