Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ History and Contributors

Changes:

- 2.4.0: Unreleased
- 3.0.0: Unreleased

- `#125 <https://github.com/dcrosta/flask-pymongo/pull/125>`_ Drop
MongoDB 3.2 support.
Expand All @@ -191,8 +191,12 @@ Changes:
- `#62 <https://github.com/dcrosta/flask-pymongo/issues/62>`_ Add
support for :func:`~flask.json.jsonify()`.
- `#131 <https://github.com/dcrosta/flask-pymongo/pulls/131>`_ Drop
support for Flask 0.11 and Python 3.4; Add support for MongoDB 4.2,
PyMongo 3.9, and Flask 1.1.
support for Flask 0.11 and Python < 3.8; Change test matrix to test
latest MongoDB 4.x, 5.x, 6.x series
- Support conditional GET with explicit ETag value passed in to
``save_file``; MongoDB has dropped support for automatic MD5 generation
in GridFS


- 2.3.0: April 24, 2019

Expand Down
27 changes: 24 additions & 3 deletions flask_pymongo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,15 @@ def init_app(self, app, uri=None, *args, **kwargs):
app.json_encoder = self._json_encoder

# view helpers
def send_file(self, filename, base="fs", version=-1, cache_for=31536000):
def send_file(self, filename, base="fs", version=-1, cache_for=31536000): # noqa: C901, E501
"""Respond with a file from GridFS.

Returns an instance of the :attr:`~flask.Flask.response_class`
containing the named file, and implement conditional GET semantics
(using :meth:`~werkzeug.wrappers.ETagResponseMixin.make_conditional`).
Conditional GET with ``If-Match`` is supported using the ``etag`` key
of the grid file's ``metadata`` dict, or the file's ``md5`` attribute,
if present.

.. code-block:: python

Expand Down Expand Up @@ -163,13 +166,24 @@ def get_upload(filename):
)
response.content_length = fileobj.length
response.last_modified = fileobj.upload_date
response.set_etag(fileobj.md5)
if fileobj.metadata and fileobj.metadata.get("etag"):
response.set_etag(fileobj.metadata["etag"])
elif fileobj.md5:
response.set_etag(fileobj.md5)
response.cache_control.max_age = cache_for
response.cache_control.public = True
response.make_conditional(request)
return response

def save_file(self, filename, fileobj, base="fs", content_type=None, **kwargs):
def save_file(
self,
filename,
fileobj,
base="fs",
content_type=None,
etag=None,
**kwargs,
):
"""Save a file-like object to GridFS using the given filename.

.. code-block:: python
Expand All @@ -185,6 +199,9 @@ def save_upload(filename):
:param str content_type: the MIME content-type of the file. If
``None``, the content-type is guessed from the filename using
:func:`~mimetypes.guess_type`
:param str etag: value to be used for HTTP entity tags in support
of conditional ``GET``. See `ETag on MDN
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag>`_.
:param kwargs: extra attributes to be stored in the file's document,
passed directly to :meth:`gridfs.GridFS.put`
"""
Expand All @@ -196,6 +213,10 @@ def save_upload(filename):
if content_type is None:
content_type, _ = guess_type(filename)

if etag:
kwargs.setdefault("metadata", {})
kwargs["metadata"]["etag"] = etag

storage = GridFS(self.db, base)
id = storage.put(fileobj, filename=filename, content_type=content_type, **kwargs)
return id
24 changes: 21 additions & 3 deletions flask_pymongo/tests/test_gridfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def setUp(self):

# make it bigger than 1 gridfs chunk
self.myfile = BytesIO(b"a" * 500 * 1024)
self.mongo.save_file("myfile.txt", self.myfile)
self.etag = md5(self.myfile.getvalue()).hexdigest()
self.mongo.save_file("myfile.txt", self.myfile, etag=self.etag)

def test_it_404s_for_missing_files(self):
with pytest.raises(NotFound):
Expand All @@ -77,19 +78,36 @@ def test_it_sets_content_length(self):
resp = self.mongo.send_file("myfile.txt")
assert resp.content_length == len(self.myfile.getvalue())

def test_it_sets_supports_conditional_gets(self):
def test_it_sets_supports_conditional_gets_from_etag(self):
# a basic conditional GET
environ_args = {
"method": "GET",
"headers": {
"If-None-Match": md5(self.myfile.getvalue()).hexdigest(),
"If-None-Match": self.etag,
},
}

with self.app.test_request_context(**environ_args):
resp = self.mongo.send_file("myfile.txt")
assert resp.status_code == 304

def test_it_sets_supports_conditional_gets_from_md5(self):
myfile = BytesIO(b"hello world")
etag = md5(myfile.getvalue()).hexdigest()
self.mongo.save_file("myfile-md5.txt", myfile, md5=etag)

# a basic conditional GET
environ_args = {
"method": "GET",
"headers": {
"If-None-Match": etag,
},
}

with self.app.test_request_context(**environ_args):
resp = self.mongo.send_file("myfile-md5.txt")
assert resp.status_code == 304

def test_it_sets_cache_headers(self):
resp = self.mongo.send_file("myfile.txt", cache_for=60)
assert resp.cache_control.max_age == 60
Expand Down
5 changes: 2 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
[tox]

; pymongo{311,312,40,41,42,43}-mongo{4x,5x,6x}-flask{10,11,2x}, style
envlist=
pymongo{311,312}-mongo{4x,5x,6x}-flask{10,11,2x}, style
pymongo{311,312,40,41,42,43}-mongo{4x,5x,6x}-flask{10,11,2x}, style

[testenv]
docker =
Expand All @@ -28,7 +27,7 @@ deps =
flask2x: flask>=2.0,<3.0

commands =
{envbindir}/py.test --tb=native {toxinidir}
py.test --tb=native {toxinidir}/flask_pymongo {posargs}

[testenv:style]
skipsdist = true
Expand Down