Skip to content

Commit 2153806

Browse files
committed
feat: add filename length safety check with random suffix
1 parent 58b017d commit 2153806

File tree

2 files changed

+103
-2
lines changed

2 files changed

+103
-2
lines changed

filer/utils/files.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import mimetypes
22
import os
3+
import uuid
34

45
from django.http.multipartparser import ChunkIter, SkipFile, StopFutureHandlers, StopUpload, exhaust
56
from django.template.defaultfilters import slugify as slugify_django
@@ -121,6 +122,32 @@ def slugify(string):
121122
return slugify_django(force_str(string))
122123

123124

125+
def _ensure_safe_length(filename, max_length=255, random_suffix_length=16):
126+
"""
127+
Ensures that the filename does not exceed the maximum allowed length.
128+
If it does, the function truncates the filename and appends a random hexadecimal
129+
suffix of length `random_suffix_length` to ensure uniqueness and compliance with
130+
database constraints.
131+
132+
Parameters:
133+
filename (str): The filename to check.
134+
max_length (int): The maximum allowed length for the filename.
135+
random_suffix_length (int): The length of the random suffix to append.
136+
137+
Returns:
138+
str: The safe filename.
139+
140+
141+
Reference issue: https://github.com/django-cms/django-filer/issues/1270
142+
"""
143+
if len(filename) <= max_length:
144+
return filename
145+
146+
keep_length = max_length - random_suffix_length
147+
random_suffix = uuid.uuid4().hex[:random_suffix_length]
148+
return filename[:keep_length] + random_suffix
149+
150+
124151
def get_valid_filename(s):
125152
"""
126153
like the regular get_valid_filename, but also slugifies away
@@ -131,6 +158,9 @@ def get_valid_filename(s):
131158
filename = slugify(filename)
132159
ext = slugify(ext)
133160
if ext:
134-
return "{}.{}".format(filename, ext)
161+
valid_filename = "{}.{}".format(filename, ext)
135162
else:
136-
return "{}".format(filename)
163+
valid_filename = "{}".format(filename)
164+
165+
# Ensure the filename meets the maximum length requirements.
166+
return _ensure_safe_length(valid_filename)

tests/test_files.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import string
2+
3+
from django.test import TestCase
4+
5+
from filer.utils.files import get_valid_filename
6+
7+
8+
class GetValidFilenameTest(TestCase):
9+
def test_short_filename_remains_unchanged(self):
10+
"""
11+
Test that a filename under the maximum length remains unchanged.
12+
"""
13+
original = "example.jpg"
14+
result = get_valid_filename(original)
15+
self.assertEqual(result, "example.jpg")
16+
17+
def test_long_filename_is_truncated_and_suffix_appended(self):
18+
"""
19+
Test that a filename longer than the maximum allowed length is truncated and a random
20+
hexadecimal suffix is appended. The final filename must not exceed 255 characters.
21+
"""
22+
# Create a filename that is much longer than 255 characters.
23+
base = "a" * 300 # 300 characters
24+
original = f"{base}.jpg"
25+
result = get_valid_filename(original)
26+
# Assert that the result is within the maximum allowed length.
27+
self.assertTrue(len(result) <= 255, "Filename exceeds 255 characters.")
28+
29+
# When truncated, the filename should end with a random hexadecimal suffix of length 16.
30+
# We check that the suffix contains only hexadecimal digits.
31+
random_suffix = result[-16:]
32+
valid_hex_chars = set(string.hexdigits)
33+
self.assertTrue(all(c in valid_hex_chars for c in random_suffix),
34+
"The suffix is not a valid hexadecimal string.")
35+
36+
def test_filename_with_extension_preserved(self):
37+
"""
38+
Test that the file extension is preserved (and slugified) after processing.
39+
"""
40+
original = "This is a test IMAGE.JPG"
41+
result = get_valid_filename(original)
42+
# Since slugification converts characters to lowercase, we expect ".jpg"
43+
self.assertTrue(result.endswith(".jpg"),
44+
"File extension was not preserved correctly.")
45+
46+
def test_unicode_characters(self):
47+
"""
48+
Test that filenames with Unicode characters are handled correctly and result in a valid filename.
49+
"""
50+
original = "fiłęñâmé_üñîçødé.jpeg"
51+
result = get_valid_filename(original)
52+
# Verify that the result ends with the expected extension and contains only allowed characters.
53+
self.assertTrue(result.endswith(".jpeg"), "File extension is not preserved for unicode filename.")
54+
# Optionally, check that no unexpected characters remain (depends on your slugify behavior).
55+
for char in result:
56+
# Allow only alphanumeric characters, underscores, dashes, and the dot.
57+
self.assertIn(char, string.ascii_lowercase + string.digits + "._-",
58+
f"Unexpected character '{char}' found in filename.")
59+
60+
def test_edge_case_exact_length(self):
61+
"""
62+
Test an edge case where the filename is exactly the maximum allowed length.
63+
The function should leave such a filename unchanged.
64+
"""
65+
# Create a filename that is exactly 255 characters long.
66+
base = "b" * 250 # 250 characters for base
67+
original = f"{base}.png" # This may reach exactly or slightly above 255 depending on slugification
68+
result = get_valid_filename(original)
69+
# We check that the final result does not exceed 255 characters.
70+
self.assertTrue(len(result) <= 255,
71+
"Edge case filename exceeds the maximum allowed length.")

0 commit comments

Comments
 (0)