1
+ import datetime
2
+ import logging
1
3
import mimetypes
2
4
import os
5
+ import urllib .parse
6
+ from email .utils import format_datetime
3
7
from pathlib import Path
4
- from typing import AsyncIterator , Union
8
+ from typing import AsyncIterator , Optional , Union
5
9
6
10
import aiofiles
7
11
from fastapi import APIRouter , HTTPException , Request
8
12
from fastapi .responses import FileResponse , HTMLResponse , StreamingResponse
9
13
10
14
from ..utils .static import ETaggedStaticFiles
11
15
16
+ logger = logging .getLogger (__name__ )
17
+
12
18
# Get current module path
13
19
PACKAGE_DIR = Path (__file__ ).parent .parent
14
20
@@ -27,9 +33,28 @@ async def stream_file_range(file_path: Path, start: int, end: int) -> AsyncItera
27
33
bytes_remaining -= len (chunk )
28
34
yield chunk
29
35
36
+ def get_content_disposition_header (filename : str , as_attachment : bool = False ) -> str :
37
+ """Generate Content-Disposition header value with RFC 5987 compatible filename encoding
38
+
39
+ Args:
40
+ filename: The filename to encode
41
+ as_attachment: Whether to use attachment or inline disposition
42
+
43
+ Returns:
44
+ str: The properly encoded Content-Disposition header value
45
+ """
46
+ disposition_type = "attachment" if as_attachment else "inline"
47
+ try :
48
+ filename .encode ('ascii' )
49
+ return f'{ disposition_type } ; filename="{ filename } "'
50
+ except UnicodeEncodeError :
51
+ encoded_filename = urllib .parse .quote (filename )
52
+ return f'{ disposition_type } ; filename*=UTF-8\' \' { encoded_filename } '
53
+
54
+
30
55
async def handle_file_request (
31
56
file_path : Path ,
32
- range_header : str = None ,
57
+ range_header : Optional [ str ] = None ,
33
58
as_attachment : bool = False
34
59
) -> Union [FileResponse , StreamingResponse ]:
35
60
"""处理文件请求的通用函数"""
@@ -46,11 +71,13 @@ async def handle_file_request(
46
71
47
72
# 如果没有 Range 头,直接返回完整文件
48
73
if not range_header :
49
- headers = {}
50
- if as_attachment :
51
- headers ["Content-Disposition" ] = f'attachment; filename="{ file_path .name } "'
52
- else :
53
- headers ["Content-Disposition" ] = f'inline; filename="{ file_path .name } "'
74
+ headers = {
75
+ "Last-Modified" : format_datetime (datetime .datetime .fromtimestamp (os .path .getmtime (file_path ))),
76
+ "Accept-Ranges" : "bytes" ,
77
+ "Content-Length" : str (file_size ),
78
+ }
79
+
80
+ headers ["Content-Disposition" ] = get_content_disposition_header (file_path .name , as_attachment )
54
81
55
82
return FileResponse (
56
83
path = file_path ,
@@ -66,13 +93,14 @@ async def handle_file_request(
66
93
67
94
if start >= file_size :
68
95
raise HTTPException (status_code = 416 , detail = "Range not satisfiable" )
69
-
96
+
70
97
headers = {
71
98
"Content-Range" : f"bytes { start } -{ end } /{ file_size } " ,
72
99
"Accept-Ranges" : "bytes" ,
73
100
"Content-Length" : str (end - start + 1 ),
74
101
"Content-Type" : mime_type ,
75
- "Content-Disposition" : "attachment" if as_attachment else "inline" + f'; filename="{ file_path .name } "'
102
+ "Content-Disposition" : get_content_disposition_header (file_path .name , as_attachment ),
103
+ "Last-Modified" : format_datetime (datetime .datetime .fromtimestamp (os .path .getmtime (file_path )))
76
104
}
77
105
78
106
return StreamingResponse (
@@ -83,8 +111,10 @@ async def handle_file_request(
83
111
84
112
except (ValueError , IndexError ):
85
113
raise HTTPException (status_code = 416 , detail = "Invalid range header" )
86
-
87
114
except Exception as e :
115
+ logger .exception ("Error processing file request" )
116
+ if isinstance (e , HTTPException ):
117
+ raise e
88
118
raise HTTPException (status_code = 500 , detail = str (e ))
89
119
90
120
# Mount static files for direct access to static assets
@@ -108,9 +138,9 @@ async def serve_blob_path(path: str):
108
138
async def get_raw_file (file_path : str , request : Request ):
109
139
"""Get raw file content with Range support"""
110
140
try :
111
- file_path = request .app .state .ROOT_DIR / file_path
141
+ full_path : Path = request .app .state .ROOT_DIR / file_path
112
142
return await handle_file_request (
113
- file_path = file_path ,
143
+ file_path = full_path ,
114
144
range_header = request .headers .get ("range" ),
115
145
as_attachment = False
116
146
)
@@ -123,9 +153,9 @@ async def get_raw_file(file_path: str, request: Request):
123
153
async def download_file (file_path : str , request : Request ):
124
154
"""Download file with Range support"""
125
155
try :
126
- file_path = request .app .state .ROOT_DIR / file_path
156
+ full_path : Path = request .app .state .ROOT_DIR / file_path
127
157
return await handle_file_request (
128
- file_path = file_path ,
158
+ file_path = full_path ,
129
159
range_header = request .headers .get ("range" ),
130
160
as_attachment = True
131
161
)
0 commit comments