Skip to content

Commit f346791

Browse files
authored
Merge pull request presenton#609 from Wvssim/patch-2
fix: truncate long filenames to prevent OSError on Japanese themes
2 parents 2918b8b + 9b19988 commit f346791

3 files changed

Lines changed: 53 additions & 3 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from pathvalidate import sanitize_filename
2+
3+
from utils.filename_utils import safe_export_basename, MAX_EXPORT_BASENAME_BYTES
4+
5+
6+
def test_safe_export_basename_short_name_unchanged():
7+
assert safe_export_basename("Hello World") == "Hello World"
8+
9+
10+
def test_safe_export_basename_empty_falls_back_to_presentation():
11+
assert safe_export_basename("") == "presentation"
12+
assert safe_export_basename(" ") == "presentation"
13+
14+
15+
def test_safe_export_basename_long_ascii_truncated_with_hash():
16+
long_name = "a" * 300
17+
result = safe_export_basename(long_name)
18+
assert len(result.encode("utf-8")) <= MAX_EXPORT_BASENAME_BYTES
19+
assert "_" in result
20+
21+
22+
def test_safe_filename_japanese_under_os_limit():
23+
title = "2026下半期営業戦略 " + "あ" * 80
24+
safe = safe_export_basename(sanitize_filename(title))
25+
assert len(safe.encode("utf-8")) + len(".pptx") <= 255
26+
27+
28+
def test_safe_export_basename_exactly_at_limit_unchanged():
29+
name = "x" * MAX_EXPORT_BASENAME_BYTES
30+
result = safe_export_basename(name)
31+
assert result == name
32+
assert len(result.encode("utf-8")) == MAX_EXPORT_BASENAME_BYTES

‎servers/fastapi/utils/export_utils.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathvalidate import sanitize_filename
88

99
from models.presentation_and_path import PresentationAndPath
10+
from utils.filename_utils import safe_export_basename
1011
from services.export_task_service import EXPORT_TASK_SERVICE
1112
from utils.runtime_limits import log_memory
1213

@@ -28,7 +29,6 @@ def _build_presentation_export_url(presentation_id: uuid.UUID) -> tuple[str, str
2829
fastapi_url = _get_next_public_fastapi_url()
2930
if fastapi_url:
3031
params["fastapiUrl"] = fastapi_url
31-
3232
return (
3333
f"{_get_next_public_url().rstrip('/')}/pdf-maker?{urlencode(params)}",
3434
fastapi_url,
@@ -48,9 +48,10 @@ async def export_presentation(
4848
export_as=export_as,
4949
)
5050
export_url, fastapi_url = _build_presentation_export_url(presentation_id)
51+
name = (title or "").strip() or str(uuid.uuid4())
5152
export_result = await EXPORT_TASK_SERVICE.export_from_url(
5253
url=export_url,
53-
title=sanitize_filename(title or str(uuid.uuid4())),
54+
title=safe_export_basename(sanitize_filename(name)),
5455
export_as=export_as,
5556
fastapi_url=fastapi_url,
5657
cookie_header=cookie_header,
@@ -61,7 +62,6 @@ async def export_presentation(
6162
presentation_id=str(presentation_id),
6263
export_as=export_as,
6364
)
64-
6565
return PresentationAndPath(
6666
presentation_id=presentation_id,
6767
path=export_result.path,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import hashlib
2+
3+
MAX_EXPORT_BASENAME_BYTES = 200
4+
5+
6+
def safe_export_basename(name: str, max_bytes: int = MAX_EXPORT_BASENAME_BYTES) -> str:
7+
name = (name or "").strip() or "presentation"
8+
9+
encoded = name.encode("utf-8")
10+
11+
if len(encoded) <= max_bytes:
12+
return name
13+
14+
suffix = hashlib.md5(encoded).hexdigest()[:8]
15+
budget = max_bytes - len(suffix) - 1
16+
truncated = encoded[:budget].decode("utf-8", errors="ignore").rstrip()
17+
18+
return f"{truncated}_{suffix}" if truncated else suffix

0 commit comments

Comments
 (0)