Skip to content

Commit

Permalink
Fix #1350: Serve compressed files as is in static_file()
Browse files Browse the repository at this point in the history
Do not instruct browsers to transparently uncompress gzipped files.
  • Loading branch information
defnull committed Sep 5, 2024
1 parent 90d749b commit ee61a68
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 17 deletions.
31 changes: 16 additions & 15 deletions bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2869,12 +2869,12 @@ def static_file(filename, root,
``If-None-Match``) are answered with ``304 Not Modified`` whenever
possible. ``HEAD`` and ``Range`` requests (used by download managers to
check or continue partial downloads) are also handled automatically.
"""

root = os.path.join(os.path.abspath(root), '')
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
headers = headers.copy() if headers else {}
getenv = request.environ.get

if not filename.startswith(root):
return HTTPError(403, "Access denied.")
Expand All @@ -2884,31 +2884,32 @@ def static_file(filename, root,
return HTTPError(403, "You do not have permission to access this file.")

if mimetype is True:
if download and download is not True:
mimetype, encoding = mimetypes.guess_type(download)
else:
mimetype, encoding = mimetypes.guess_type(filename)
if encoding:
headers['Content-Encoding'] = encoding
name = download if isinstance(download, str) else filename
mimetype, encoding = mimetypes.guess_type(name)
if encoding == 'gzip':
mimetype = 'application/gzip'
elif encoding: # e.g. bzip2 -> application/x-bzip2
mimetype = 'application/x-' + encoding

if charset and mimetype and 'charset=' not in mimetype \
and (mimetype[:5] == 'text/' or mimetype == 'application/javascript'):
mimetype += '; charset=%s' % charset

if mimetype:
if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
and charset and 'charset' not in mimetype:
mimetype += '; charset=%s' % charset
headers['Content-Type'] = mimetype

if download is True:
download = os.path.basename(filename)

if download:
download = os.path.basename(filename if download is True else download)
download = download.replace('"','')
headers['Content-Disposition'] = 'attachment; filename="%s"' % download

stats = os.stat(filename)
headers['Content-Length'] = clen = stats.st_size
headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime,
usegmt=True)
headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True)
headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)

getenv = request.environ.get

if etag is None:
etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
clen, filename)
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ These changes might require special care when updating.
* :meth:`Bottle.mount` now recognizes instances of :class:`Bottle` and mounts them with significantly less overhead than other WSGI applications.
* The :attr:`BaseRequest.json` property now accepts ``application/json-rpc`` requests.
* :func:`static_file` gained support for ``ETag`` headers. It will generate ETags and recognizes ``If-None-Match`` headers.
* :func:`static_file` will now guess the mime type of ``*.gz`` and other compressed files correctly (e.g. ``application/gzip``) and NOT set the ``Content-Encoding`` header.
* Jinja2 templates will produce better error messages than before.




Release 0.12
==============

Expand Down
12 changes: 11 additions & 1 deletion test/test_sendfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ def test_mime(self):
f = static_file(basename, root=root, mimetype='text/foo', charset='latin1')
self.assertEqual('text/foo; charset=latin1', f.headers['Content-Type'])

def test_mime_gzip(self):
""" SendFile: Mime Guessing"""
try:
fp, fn = tempfile.mkstemp(suffix=".txt.gz")
f = static_file(fn, root='/')
self.assertTrue(f.headers['Content-Type'][0] in ('application/gzip'))
self.assertFalse('Content-Encoding' in f.headers)
finally:
os.close(fp)
os.unlink(fn)

def test_ims(self):
""" SendFile: If-Modified-Since"""
request.environ['HTTP_IF_MODIFIED_SINCE'] = bottle.http_date(time.time())
Expand Down Expand Up @@ -118,7 +129,6 @@ def test_etag(self):
self.assertTrue('ETag' in res.headers)
self.assertNotEqual(etag, res.headers['ETag'])
self.assertEqual(200, res.status_code)


def test_download(self):
""" SendFile: Download as attachment """
Expand Down

0 comments on commit ee61a68

Please sign in to comment.