Skip to content

Commit fa67658

Browse files
PYTHON-5994 Use exact match for file ID in GridFS delete methods (#3046)
Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com>
1 parent 1011e57 commit fa67658

8 files changed

Lines changed: 728 additions & 56 deletions

File tree

‎gridfs/asynchronous/grid_file.py‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ async def delete(self, file_id: Any, session: Optional[AsyncClientSession] = Non
284284
``delete`` no longer ensures indexes.
285285
"""
286286
_disallow_transactions(session)
287-
await self._files.delete_one({"_id": file_id}, session=session)
288-
await self._chunks.delete_many({"files_id": file_id}, session=session)
287+
await self._files.delete_one({"_id": {"$eq": file_id}}, session=session)
288+
await self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session)
289289

290290
async def list(self, session: Optional[AsyncClientSession] = None) -> list[str]:
291291
"""List the names of all files stored in this instance of
@@ -341,7 +341,7 @@ async def find_one(
341341
Added ``session`` parameter.
342342
"""
343343
if filter is not None and not isinstance(filter, abc.Mapping):
344-
filter = {"_id": filter}
344+
filter = {"_id": {"$eq": filter}}
345345

346346
_disallow_transactions(session)
347347
async for f in self.find(filter, *args, session=session, **kwargs):
@@ -455,6 +455,8 @@ async def exists(
455455
if kwargs:
456456
f = await self._files.find_one(kwargs, ["_id"], session=session)
457457
else:
458+
if document_or_id is not None and not isinstance(document_or_id, abc.Mapping):
459+
document_or_id = {"_id": {"$eq": document_or_id}}
458460
f = await self._files.find_one(document_or_id, ["_id"], session=session)
459461

460462
return f is not None
@@ -831,8 +833,8 @@ async def delete(self, file_id: Any, session: Optional[AsyncClientSession] = Non
831833
Added ``session`` parameter.
832834
"""
833835
_disallow_transactions(session)
834-
res = await self._files.delete_one({"_id": file_id}, session=session)
835-
await self._chunks.delete_many({"files_id": file_id}, session=session)
836+
res = await self._files.delete_one({"_id": {"$eq": file_id}}, session=session)
837+
await self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session)
836838
if not res.deleted_count:
837839
raise NoFile(f"no file could be deleted because none matched {file_id}")
838840

@@ -1044,7 +1046,7 @@ async def rename(
10441046
"""
10451047
_disallow_transactions(session)
10461048
result = await self._files.update_one(
1047-
{"_id": file_id}, {"$set": {"filename": new_filename}}, session=session
1049+
{"_id": {"$eq": file_id}}, {"$set": {"filename": new_filename}}, session=session
10481050
)
10491051
if not result.matched_count:
10501052
raise NoFile(
@@ -1198,8 +1200,12 @@ async def _ensure_indexes(self) -> None:
11981200

11991201
async def abort(self) -> None:
12001202
"""Remove all chunks/files that may have been uploaded and close."""
1201-
await self._coll.chunks.delete_many({"files_id": self._file["_id"]}, session=self._session)
1202-
await self._coll.files.delete_one({"_id": self._file["_id"]}, session=self._session)
1203+
await self._coll.chunks.delete_many(
1204+
{"files_id": {"$eq": self._file["_id"]}}, session=self._session
1205+
)
1206+
await self._coll.files.delete_one(
1207+
{"_id": {"$eq": self._file["_id"]}}, session=self._session
1208+
)
12031209
object.__setattr__(self, "_closed", True)
12041210

12051211
@property
@@ -1248,7 +1254,9 @@ def __setattr__(self, name: str, value: Any) -> None:
12481254
self._file[name] = value
12491255
if self._closed:
12501256
if _IS_SYNC:
1251-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1257+
self._coll.files.update_one(
1258+
{"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}}
1259+
)
12521260
else:
12531261
raise AttributeError(
12541262
"AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead"
@@ -1257,7 +1265,9 @@ def __setattr__(self, name: str, value: Any) -> None:
12571265
async def set(self, name: str, value: Any) -> None:
12581266
self._file[name] = value
12591267
if self._closed:
1260-
await self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1268+
await self._coll.files.update_one(
1269+
{"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}}
1270+
)
12611271

12621272
async def _flush_data(self, data: Any, force: bool = False) -> None:
12631273
"""Flush `data` to a chunk."""
@@ -1582,7 +1592,9 @@ async def readlines(self, size: int = -1) -> list[bytes]:
15821592
async def open(self) -> None:
15831593
if not self._file:
15841594
_disallow_transactions(self._session)
1585-
self._file = await self._files.find_one({"_id": self._file_id}, session=self._session)
1595+
self._file = await self._files.find_one(
1596+
{"_id": {"$eq": self._file_id}}, session=self._session
1597+
)
15861598
if not self._file:
15871599
raise NoFile(
15881600
f"no file in gridfs collection {self._files!r} with _id {self._file_id!r}"
@@ -1853,7 +1865,7 @@ def __aiter__(self) -> _AsyncGridOutChunkIterator:
18531865
return self
18541866

18551867
def _create_cursor(self) -> None:
1856-
filter = {"files_id": self._id}
1868+
filter = {"files_id": {"$eq": self._id}}
18571869
if self._next_chunk > 0:
18581870
filter["n"] = {"$gte": self._next_chunk}
18591871
_disallow_transactions(self._session)

‎gridfs/grid_file_shared.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def setter(self: Any, value: Any) -> Any:
120120
if warn_str:
121121
warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning)
122122
if self._closed:
123-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {field_name: value}})
123+
self._coll.files.update_one(
124+
{"_id": {"$eq": self._file["_id"]}}, {"$set": {field_name: value}}
125+
)
124126
self._file[field_name] = value
125127

126128
if read_only:

‎gridfs/synchronous/grid_file.py‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ def delete(self, file_id: Any, session: Optional[ClientSession] = None) -> None:
284284
``delete`` no longer ensures indexes.
285285
"""
286286
_disallow_transactions(session)
287-
self._files.delete_one({"_id": file_id}, session=session)
288-
self._chunks.delete_many({"files_id": file_id}, session=session)
287+
self._files.delete_one({"_id": {"$eq": file_id}}, session=session)
288+
self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session)
289289

290290
def list(self, session: Optional[ClientSession] = None) -> list[str]:
291291
"""List the names of all files stored in this instance of
@@ -339,7 +339,7 @@ def find_one(
339339
Added ``session`` parameter.
340340
"""
341341
if filter is not None and not isinstance(filter, abc.Mapping):
342-
filter = {"_id": filter}
342+
filter = {"_id": {"$eq": filter}}
343343

344344
_disallow_transactions(session)
345345
for f in self.find(filter, *args, session=session, **kwargs):
@@ -453,6 +453,8 @@ def exists(
453453
if kwargs:
454454
f = self._files.find_one(kwargs, ["_id"], session=session)
455455
else:
456+
if document_or_id is not None and not isinstance(document_or_id, abc.Mapping):
457+
document_or_id = {"_id": {"$eq": document_or_id}}
456458
f = self._files.find_one(document_or_id, ["_id"], session=session)
457459

458460
return f is not None
@@ -827,8 +829,8 @@ def delete(self, file_id: Any, session: Optional[ClientSession] = None) -> None:
827829
Added ``session`` parameter.
828830
"""
829831
_disallow_transactions(session)
830-
res = self._files.delete_one({"_id": file_id}, session=session)
831-
self._chunks.delete_many({"files_id": file_id}, session=session)
832+
res = self._files.delete_one({"_id": {"$eq": file_id}}, session=session)
833+
self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session)
832834
if not res.deleted_count:
833835
raise NoFile(f"no file could be deleted because none matched {file_id}")
834836

@@ -1036,7 +1038,7 @@ def rename(
10361038
"""
10371039
_disallow_transactions(session)
10381040
result = self._files.update_one(
1039-
{"_id": file_id}, {"$set": {"filename": new_filename}}, session=session
1041+
{"_id": {"$eq": file_id}}, {"$set": {"filename": new_filename}}, session=session
10401042
)
10411043
if not result.matched_count:
10421044
raise NoFile(
@@ -1186,8 +1188,10 @@ def _ensure_indexes(self) -> None:
11861188

11871189
def abort(self) -> None:
11881190
"""Remove all chunks/files that may have been uploaded and close."""
1189-
self._coll.chunks.delete_many({"files_id": self._file["_id"]}, session=self._session)
1190-
self._coll.files.delete_one({"_id": self._file["_id"]}, session=self._session)
1191+
self._coll.chunks.delete_many(
1192+
{"files_id": {"$eq": self._file["_id"]}}, session=self._session
1193+
)
1194+
self._coll.files.delete_one({"_id": {"$eq": self._file["_id"]}}, session=self._session)
11911195
object.__setattr__(self, "_closed", True)
11921196

11931197
@property
@@ -1236,7 +1240,9 @@ def __setattr__(self, name: str, value: Any) -> None:
12361240
self._file[name] = value
12371241
if self._closed:
12381242
if _IS_SYNC:
1239-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1243+
self._coll.files.update_one(
1244+
{"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}}
1245+
)
12401246
else:
12411247
raise AttributeError(
12421248
"GridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use GridIn.set() instead"
@@ -1245,7 +1251,9 @@ def __setattr__(self, name: str, value: Any) -> None:
12451251
def set(self, name: str, value: Any) -> None:
12461252
self._file[name] = value
12471253
if self._closed:
1248-
self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}})
1254+
self._coll.files.update_one(
1255+
{"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}}
1256+
)
12491257

12501258
def _flush_data(self, data: Any, force: bool = False) -> None:
12511259
"""Flush `data` to a chunk."""
@@ -1570,7 +1578,9 @@ def readlines(self, size: int = -1) -> list[bytes]:
15701578
def open(self) -> None:
15711579
if not self._file:
15721580
_disallow_transactions(self._session)
1573-
self._file = self._files.find_one({"_id": self._file_id}, session=self._session)
1581+
self._file = self._files.find_one(
1582+
{"_id": {"$eq": self._file_id}}, session=self._session
1583+
)
15741584
if not self._file:
15751585
raise NoFile(
15761586
f"no file in gridfs collection {self._files!r} with _id {self._file_id!r}"
@@ -1841,7 +1851,7 @@ def __iter__(self) -> GridOutChunkIterator:
18411851
return self
18421852

18431853
def _create_cursor(self) -> None:
1844-
filter = {"files_id": self._id}
1854+
filter = {"files_id": {"$eq": self._id}}
18451855
if self._next_chunk > 0:
18461856
filter["n"] = {"$gte": self._next_chunk}
18471857
_disallow_transactions(self._session)

‎test/asynchronous/test_gridfs_bucket.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import gridfs
3434
from bson.binary import Binary
3535
from bson.int64 import Int64
36+
from bson.min_key import MinKey
3637
from bson.objectid import ObjectId
3738
from bson.son import SON
3839
from gridfs.errors import CorruptGridFile, NoFile
@@ -42,6 +43,7 @@
4243
NotPrimaryError,
4344
ServerSelectionTimeoutError,
4445
WriteConcernError,
46+
WriteError,
4547
)
4648
from pymongo.read_preferences import ReadPreference
4749
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
@@ -475,6 +477,34 @@ async def test_abort(self):
475477
await gin.write(b"test4")
476478
self.assertEqual(0, await self.db.fs.chunks.count_documents({"files_id": gin._id}))
477479

480+
# Server versions older than 5.0 do not support document values with
481+
# "$"-prefixed keys.
482+
@async_client_context.require_version_min(5, 0)
483+
async def test_abort_with_injected_file_id_does_not_delete_other_files(self):
484+
bucket = self.fs
485+
file1_bytes = b"\x11\x22\x33\x44"
486+
await bucket.upload_from_stream("file1", file1_bytes)
487+
488+
upload_stream = bucket.open_upload_stream_with_id(
489+
{"$gt": MinKey()}, "file2", chunk_size_bytes=2
490+
)
491+
await upload_stream.write(b"\x55\x66\x77\x88")
492+
try:
493+
await upload_stream.abort()
494+
except WriteError:
495+
# Newer server versions reject an equality match on "_id" whose
496+
# operand itself resembles a query operator, even when the
497+
# driver safely wraps it in "$eq". The chunks are still cleaned
498+
# up in that case; only the (never created) files document
499+
# delete is rejected.
500+
pass
501+
502+
self.assertEqual(
503+
file1_bytes, await (await bucket.open_download_stream_by_name("file1")).read()
504+
)
505+
with self.assertRaises(NoFile):
506+
await bucket.open_download_stream_by_name("file2")
507+
478508
async def test_download_to_stream(self):
479509
file1 = BytesIO(b"hello world")
480510
# Test with one chunk.

0 commit comments

Comments
 (0)