Skip to content

Commit 1c12ca4

Browse files
committed
Normalize filenames
1 parent 4badcd7 commit 1c12ca4

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

conveyor/views.py

+54-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
from aiohttp import web
2020
from botocore.config import Config as BotoCoreConfig
21+
from packaging.utils import parse_sdist_filename, parse_wheel_filename
22+
from packaging.utils import canonicalize_name, canonicalize_version
2123

2224
ANON_CONFIG = BotoCoreConfig(signature_version=botocore.UNSIGNED)
2325

@@ -30,6 +32,48 @@ async def not_found(request):
3032
return web.Response(status=404)
3133

3234

35+
async def _normalize_filename(filename):
36+
if filename.endswith(".whl"):
37+
name, ver, build, tags = parse_wheel_filename(filename)
38+
return (
39+
"-".join(
40+
[
41+
canonicalize_name(name),
42+
canonicalize_version(ver),
43+
]
44+
+ (["".join(str(x) for x in build)] if build else [])
45+
+ [
46+
"-".join(str(x) for x in tags),
47+
]
48+
)
49+
+ ".whl"
50+
)
51+
elif filename.endswith(".tar.gz"):
52+
name, ver = parse_sdist_filename(filename)
53+
return (
54+
"-".join(
55+
[
56+
canonicalize_name(name),
57+
canonicalize_version(ver),
58+
]
59+
)
60+
+ ".tar.gz"
61+
)
62+
elif filename.endswith(".zip"):
63+
name, ver = parse_sdist_filename(filename)
64+
return (
65+
"-".join(
66+
[
67+
canonicalize_name(name),
68+
canonicalize_version(ver),
69+
]
70+
)
71+
+ ".zip"
72+
)
73+
else:
74+
return filename
75+
76+
3377
async def redirect(request):
3478
python_version = request.match_info["python_version"]
3579
project_l = request.match_info["project_l"]
@@ -38,7 +82,9 @@ async def redirect(request):
3882

3983
# If the letter bucket doesn't match the first letter of the project, then
4084
# there is no point to going any further since it will be a 404 regardless.
41-
if project_l != project_name[0]:
85+
# Allow specifiying the exact first character of the actual filename (which
86+
# might not be lowercase, to maintain backwards compatibility
87+
if project_l != project_name[0].lower() and project_l != project_name[0]:
4288
return web.Response(status=404, headers={"Reason": "Incorrect project bucket"})
4389

4490
# If the filename we're looking for is a signature, then we'll need to turn
@@ -72,8 +118,13 @@ async def redirect(request):
72118
# 302 redirect to that URL.
73119
for release in data.get("releases", {}).values():
74120
for file_ in release:
75-
if (file_["filename"] == filename
76-
and file_["python_version"] == python_version):
121+
if (
122+
# Prefer that the normalized filename has been specified
123+
_normalize_filename(file_["filename"]) == filename
124+
# But also allow specifying the exact filename, to maintain
125+
# backwards compatiblity
126+
or file_["filename"] == filename
127+
) and file_["python_version"] == python_version:
77128
# If we've found our filename, but we were actually looking for
78129
# the *signature* of that file, then we need to check if it has
79130
# a signature associated with it, and if so redirect to that,

tests/test_views.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pytest
14+
15+
from conveyor.views import _normalize_filename
16+
17+
18+
@pytest.mark.asyncio
19+
@pytest.mark.parametrize(
20+
"filename, expected",
21+
[
22+
("Flask-Common-0.2.0.tar.gz", "flask-common-0.2.tar.gz"),
23+
("websocket_client-0.52.0.tar.gz", "websocket-client-0.52.tar.gz"),
24+
("Sphinx-7.1.1.tar.gz", "sphinx-7.1.1.tar.gz"),
25+
("Foo_Bar-24.0.0.0.tar.gz", "foo-bar-24.tar.gz"),
26+
("Foo_Bar-24.0.0.0-py3-none-any.whl", "foo-bar-24-py3-none-any.whl"),
27+
("foo-24-py3-none-any.whl", "foo-24-py3-none-any.whl"),
28+
(
29+
"spam-1.0-420yolo-py3-none-any.whl",
30+
"spam-1-420yolo-py3-none-any.whl",
31+
), # Build tag
32+
("Foo_bar-24.0.0.0.zip", "foo-bar-24.zip"),
33+
],
34+
)
35+
async def test_normalize_filename(filename, expected):
36+
result = await _normalize_filename(filename)
37+
38+
assert result == expected

0 commit comments

Comments
 (0)