22import mimetypes
33import os
44import pathlib
5+ import sys
6+ from contextlib import suppress
7+ from types import MappingProxyType
58from typing import (
69 IO ,
710 TYPE_CHECKING ,
3740
3841NOSENDFILE : Final [bool ] = bool (os .environ .get ("AIOHTTP_NOSENDFILE" ))
3942
43+ if sys .version_info < (3 , 9 ):
44+ mimetypes .encodings_map [".br" ] = "br"
45+
46+ # File extension to IANA encodings map that will be checked in the order defined.
47+ ENCODING_EXTENSIONS = MappingProxyType (
48+ {ext : mimetypes .encodings_map [ext ] for ext in (".br" , ".gz" )}
49+ )
50+
4051
4152class FileResponse (StreamResponse ):
4253 """A response object can be used to send files."""
@@ -121,34 +132,36 @@ async def _precondition_failed(
121132 self .content_length = 0
122133 return await super ().prepare (request )
123134
124- def _get_file_path_stat_and_gzip (
125- self , check_for_gzipped_file : bool
126- ) -> Tuple [pathlib .Path , os .stat_result , bool ]:
127- """Return the file path, stat result, and gzip status.
135+ def _get_file_path_stat_encoding (
136+ self , accept_encoding : str
137+ ) -> Tuple [pathlib .Path , os .stat_result , Optional [str ]]:
138+ """Return the file path, stat result, and encoding.
139+
140+ If an uncompressed file is returned, the encoding is set to
141+ :py:data:`None`.
128142
129143 This method should be called from a thread executor
130144 since it calls os.stat which may block.
131145 """
132- filepath = self ._path
133- if check_for_gzipped_file :
134- gzip_path = filepath . with_name ( filepath . name + ".gz" )
135- try :
136- return gzip_path , gzip_path . stat (), True
137- except OSError :
138- # Fall through and try the non-gzipped file
139- pass
146+ file_path = self ._path
147+ for file_extension , file_encoding in ENCODING_EXTENSIONS . items () :
148+ if file_encoding not in accept_encoding :
149+ continue
150+
151+ compressed_path = file_path . with_suffix ( file_path . suffix + file_extension )
152+ with suppress ( OSError ):
153+ return compressed_path , compressed_path . stat (), file_encoding
140154
141- return filepath , filepath .stat (), False
155+ # Fallback to the uncompressed file
156+ return file_path , file_path .stat (), None
142157
143158 async def prepare (self , request : "BaseRequest" ) -> Optional [AbstractStreamWriter ]:
144159 loop = asyncio .get_event_loop ()
145160 # Encoding comparisons should be case-insensitive
146161 # https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1
147- check_for_gzipped_file = (
148- "gzip" in request .headers .get (hdrs .ACCEPT_ENCODING , "" ).lower ()
149- )
150- filepath , st , gzip = await loop .run_in_executor (
151- None , self ._get_file_path_stat_and_gzip , check_for_gzipped_file
162+ accept_encoding = request .headers .get (hdrs .ACCEPT_ENCODING , "" ).lower ()
163+ file_path , st , file_encoding = await loop .run_in_executor (
164+ None , self ._get_file_path_stat_encoding , accept_encoding
152165 )
153166
154167 etag_value = f"{ st .st_mtime_ns :x} -{ st .st_size :x} "
@@ -181,11 +194,11 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
181194
182195 ct = None
183196 if hdrs .CONTENT_TYPE not in self .headers :
184- ct , encoding = mimetypes .guess_type (str (filepath ))
197+ ct , encoding = mimetypes .guess_type (str (file_path ))
185198 if not ct :
186199 ct = "application/octet-stream"
187200 else :
188- encoding = "gzip" if gzip else None
201+ encoding = file_encoding
189202
190203 status = self ._status
191204 file_size = st .st_size
@@ -265,7 +278,7 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
265278 self .content_type = ct
266279 if encoding :
267280 self .headers [hdrs .CONTENT_ENCODING ] = encoding
268- if gzip :
281+ if file_encoding :
269282 self .headers [hdrs .VARY ] = hdrs .ACCEPT_ENCODING
270283 # Disable compression if we are already sending
271284 # a compressed file since we don't want to double
@@ -289,7 +302,7 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
289302 if count == 0 or must_be_empty_body (request .method , self .status ):
290303 return await super ().prepare (request )
291304
292- fobj = await loop .run_in_executor (None , filepath .open , "rb" )
305+ fobj = await loop .run_in_executor (None , file_path .open , "rb" )
293306 if start : # be aware that start could be None or int=0 here.
294307 offset = start
295308 else :
0 commit comments