Repository: bottlepy/bottle

Issue:
static_file is serving gzipped files as uncompressed
When using [`static_file`](https://github.com/bottlepy/bottle/blob/71bb933649ba009cd537c2e973519d6e024c02a2/bottle.py#L2842) without `mimetype` to serve a file compressed with gzip, bottle seems to uncompress it on the fly.

How to reproduce it:

1. create a gzipped text file:
```shell
$ echo 'Hello, world!' > hello
$ gzip hello
```

2. serve the file with and without `mimetype`:
```python
>>> from bottle import route, run, static_file
>>> @route("/static/<filename:path>")
... def send_static(filename):
...     return static_file(filename, root=".")
...
>>> @route("/mimetype/<filename:path>")
... def send_static_with_mimetype(filename):
...     return static_file(filename, root=".", mimetype="application/x-gzip")
...
>>> run(host="localhost", port=8080, debug=True)
Bottle v0.12.19 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.
```
3. in a second shell, download the file using both endpoint and check its type:
```python
>>> import requests
>>> import os
>>> def dl_file(type):
...     url = f"http://localhost:8080/{type}/hello.gz"
...     response = requests.get(url)
...     path = type
...     if not os.path.exists(path):
...         os.makedirs(path)
...     else:
...         os.remove(f"{path}/hello.gz")
...     open(f"{path}/hello.gz", "wb").write(response.content)
...     os.system(f"file {path}/hello.gz")
... 
>>> dl_file("static")
static/hello.gz: ASCII text
>>> dl_file("mimetype")
mimetype/hello.gz: gzip compressed data, was "hello", last modified: Wed Sep 15 08:29:37 2021, from Unix, truncated
```

Discussion:

Patch:
diff --git a/bottle.py b/bottle.py
index b1467a2..283f6bb 100755
--- a/bottle.py
+++ b/bottle.py
@@ -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.")
@@ -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)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c4256ae..244acdc 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -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
 ==============
 


Test patch:
diff --git a/test/test_sendfile.py b/test/test_sendfile.py
index ef8eef5..53e2738 100755
--- a/test/test_sendfile.py
+++ b/test/test_sendfile.py
@@ -85,6 +85,17 @@ class TestSendFile(unittest.TestCase):
         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())
@@ -118,7 +129,6 @@ class TestSendFile(unittest.TestCase):
         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 """