mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-31 07:27:57 +00:00
fix export case download with non-ascii names (#24100)
This commit is contained in:
parent
5197881ef7
commit
ca18b8dc13
@ -9,6 +9,7 @@ import zipfile
|
||||
from collections import deque
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
@ -453,6 +454,22 @@ def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]:
|
||||
yield from buffer.drain()
|
||||
|
||||
|
||||
def _content_disposition(filename: str, ascii_fallback: str) -> str:
|
||||
"""Build an attachment Content-Disposition that survives non-ASCII names.
|
||||
|
||||
Header values are encoded as latin-1, so a name outside that range cannot
|
||||
go in filename at all. RFC 6266 handles this with a pair: a plain ASCII
|
||||
filename for old clients, plus a percent-encoded UTF-8 filename* that
|
||||
every current browser prefers.
|
||||
"""
|
||||
ascii_name = filename if filename.isascii() else ascii_fallback
|
||||
|
||||
return (
|
||||
f'attachment; filename="{ascii_name}"; '
|
||||
f"filename*=UTF-8''{quote(filename, safe='')}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cases/{case_id}/download",
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
@ -495,7 +512,9 @@ def download_export_case(
|
||||
_stream_case_archive(exports),
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{archive_base}.zip"',
|
||||
"Content-Disposition": _content_disposition(
|
||||
f"{archive_base}.zip", f"{case_id}.zip"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from frigate.jobs.export import (
|
||||
@ -1431,3 +1433,79 @@ class TestHttpExport(BaseTestHttp):
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_download_export_case_with_multibyte_name(self):
|
||||
"""A case name outside latin-1 must not break the response headers."""
|
||||
case = ExportCase.create(
|
||||
id="case_multibyte",
|
||||
name="テスト事案",
|
||||
description="",
|
||||
created_at=10,
|
||||
updated_at=10,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
video_path = os.path.join(tmpdir, "multibyte_export.mp4")
|
||||
with open(video_path, "wb") as handle:
|
||||
handle.write(b"video")
|
||||
|
||||
Export.create(
|
||||
id="export_multibyte",
|
||||
camera="front_door",
|
||||
name="現場カメラ",
|
||||
date=100,
|
||||
video_path=video_path,
|
||||
thumb_path=os.path.join(tmpdir, "multibyte_export.webp"),
|
||||
in_progress=False,
|
||||
export_case=case,
|
||||
)
|
||||
|
||||
with AuthTestClient(self.app) as client:
|
||||
response = client.get(f"/cases/{case.id}/download")
|
||||
|
||||
assert response.status_code == 200
|
||||
# RFC 5987/6266: the UTF-8 name rides in filename*, and a latin-1 safe
|
||||
# fallback stays in filename for old clients.
|
||||
assert response.headers["content-disposition"] == (
|
||||
'attachment; filename="case_multibyte.zip"; '
|
||||
"filename*=UTF-8''%E3%83%86%E3%82%B9%E3%83%88%E4%BA%8B%E6%A1%88.zip"
|
||||
)
|
||||
|
||||
archive = zipfile.ZipFile(io.BytesIO(response.content))
|
||||
assert archive.namelist() == ["現場カメラ.mp4"]
|
||||
|
||||
def test_download_export_case_with_ascii_name(self):
|
||||
"""An ASCII case name still gets a plain, readable filename."""
|
||||
case = ExportCase.create(
|
||||
id="case_ascii",
|
||||
name="Burglary 2026-08",
|
||||
description="",
|
||||
created_at=10,
|
||||
updated_at=10,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
video_path = os.path.join(tmpdir, "ascii_export.mp4")
|
||||
with open(video_path, "wb") as handle:
|
||||
handle.write(b"video")
|
||||
|
||||
Export.create(
|
||||
id="export_ascii",
|
||||
camera="front_door",
|
||||
name="Front door",
|
||||
date=100,
|
||||
video_path=video_path,
|
||||
thumb_path=os.path.join(tmpdir, "ascii_export.webp"),
|
||||
in_progress=False,
|
||||
export_case=case,
|
||||
)
|
||||
|
||||
with AuthTestClient(self.app) as client:
|
||||
response = client.get(f"/cases/{case.id}/download")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
response.headers["content-disposition"]
|
||||
== 'attachment; filename="Burglary 2026-08.zip"; '
|
||||
"filename*=UTF-8''Burglary%202026-08.zip"
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user