Skip to content

Commit 31526f3

Browse files
committed
PYTHON-4947 - GridFS spec: Add performant 'delete revisions by filename' feature - delete_by_name
1 parent e99818d commit 31526f3

File tree

5 files changed

+296
-2
lines changed

5 files changed

+296
-2
lines changed

gridfs/asynchronous/grid_file.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,36 @@ async def delete(self, file_id: Any, session: Optional[AsyncClientSession] = Non
834834
if not res.deleted_count:
835835
raise NoFile("no file could be deleted because none matched %s" % file_id)
836836

837+
@_csot.apply
838+
async def delete_by_name(
839+
self, filename: str, session: Optional[AsyncClientSession] = None
840+
) -> None:
841+
"""Given a filename, delete this stored file's files collection document(s)
842+
and associated chunks from a GridFS bucket.
843+
844+
For example::
845+
846+
my_db = AsyncMongoClient().test
847+
fs = AsyncGridFSBucket(my_db)
848+
await fs.upload_from_stream("test_file", "data I want to store!")
849+
await fs.delete_by_name("test_file")
850+
851+
Raises :exc:`~gridfs.errors.NoFile` if no file with filename exists.
852+
853+
:param filename: The name of the file to be deleted.
854+
:param session: a
855+
:class:`~pymongo.client_session.AsyncClientSession`
856+
857+
.. versionadded:: 4.13
858+
"""
859+
_disallow_transactions(session)
860+
file_ids = self._files.find({"filename": filename}, {"_id": 1}, session=session)
861+
file_ids = [file_id["_id"] async for file_id in file_ids]
862+
res = await self._files.delete_many({"_id": {"$in": file_ids}}, session=session)
863+
await self._chunks.delete_many({"files_id": {"$in": file_ids}}, session=session)
864+
if not res.deleted_count:
865+
raise NoFile("no file could be deleted because none matched %s" % filename)
866+
837867
def find(self, *args: Any, **kwargs: Any) -> AsyncGridOutCursor:
838868
"""Find and return the files collection documents that match ``filter``
839869

gridfs/synchronous/grid_file.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,34 @@ def delete(self, file_id: Any, session: Optional[ClientSession] = None) -> None:
830830
if not res.deleted_count:
831831
raise NoFile("no file could be deleted because none matched %s" % file_id)
832832

833+
@_csot.apply
834+
def delete_by_name(self, filename: str, session: Optional[ClientSession] = None) -> None:
835+
"""Given a filename, delete this stored file's files collection document(s)
836+
and associated chunks from a GridFS bucket.
837+
838+
For example::
839+
840+
my_db = MongoClient().test
841+
fs = GridFSBucket(my_db)
842+
fs.upload_from_stream("test_file", "data I want to store!")
843+
fs.delete_by_name("test_file")
844+
845+
Raises :exc:`~gridfs.errors.NoFile` if no file with filename exists.
846+
847+
:param filename: The name of the file to be deleted.
848+
:param session: a
849+
:class:`~pymongo.client_session.ClientSession`
850+
851+
.. versionadded:: 4.13
852+
"""
853+
_disallow_transactions(session)
854+
file_ids = self._files.find({"filename": filename}, {"_id": 1}, session=session)
855+
file_ids = [file_id["_id"] for file_id in file_ids]
856+
res = self._files.delete_many({"_id": {"$in": file_ids}}, session=session)
857+
self._chunks.delete_many({"files_id": {"$in": file_ids}}, session=session)
858+
if not res.deleted_count:
859+
raise NoFile("no file could be deleted because none matched %s" % filename)
860+
833861
def find(self, *args: Any, **kwargs: Any) -> GridOutCursor:
834862
"""Find and return the files collection documents that match ``filter``
835863

test/asynchronous/unified_format.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
from bson import SON, json_util
6767
from bson.codec_options import DEFAULT_CODEC_OPTIONS
6868
from bson.objectid import ObjectId
69-
from gridfs import AsyncGridFSBucket, GridOut
69+
from gridfs import AsyncGridFSBucket, GridOut, NoFile
7070
from pymongo import ASCENDING, AsyncMongoClient, CursorType, _csot
7171
from pymongo.asynchronous.change_stream import AsyncChangeStream
7272
from pymongo.asynchronous.client_session import AsyncClientSession, TransactionOptions, _TxnState
@@ -630,6 +630,9 @@ def process_error(self, exception, spec):
630630
self.assertNotIsInstance(error, NotPrimaryError)
631631
elif isinstance(error, (InvalidOperation, ConfigurationError, EncryptionError)):
632632
pass
633+
# gridfs NoFile errors are considered client errors.
634+
elif isinstance(error, NoFile):
635+
pass
633636
else:
634637
self.assertNotIsInstance(error, PyMongoError)
635638

test/gridfs/deleteByName.json

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
{
2+
"description": "gridfs-deleteByName",
3+
"schemaVersion": "1.0",
4+
"createEntities": [
5+
{
6+
"client": {
7+
"id": "client0"
8+
}
9+
},
10+
{
11+
"database": {
12+
"id": "database0",
13+
"client": "client0",
14+
"databaseName": "gridfs-tests"
15+
}
16+
},
17+
{
18+
"bucket": {
19+
"id": "bucket0",
20+
"database": "database0"
21+
}
22+
},
23+
{
24+
"collection": {
25+
"id": "bucket0_files_collection",
26+
"database": "database0",
27+
"collectionName": "fs.files"
28+
}
29+
},
30+
{
31+
"collection": {
32+
"id": "bucket0_chunks_collection",
33+
"database": "database0",
34+
"collectionName": "fs.chunks"
35+
}
36+
}
37+
],
38+
"initialData": [
39+
{
40+
"collectionName": "fs.files",
41+
"databaseName": "gridfs-tests",
42+
"documents": [
43+
{
44+
"_id": {
45+
"$oid": "000000000000000000000001"
46+
},
47+
"length": 0,
48+
"chunkSize": 4,
49+
"uploadDate": {
50+
"$date": "1970-01-01T00:00:00.000Z"
51+
},
52+
"filename": "filename",
53+
"metadata": {}
54+
},
55+
{
56+
"_id": {
57+
"$oid": "000000000000000000000002"
58+
},
59+
"length": 0,
60+
"chunkSize": 4,
61+
"uploadDate": {
62+
"$date": "1970-01-01T00:00:00.000Z"
63+
},
64+
"filename": "filename",
65+
"metadata": {}
66+
},
67+
{
68+
"_id": {
69+
"$oid": "000000000000000000000003"
70+
},
71+
"length": 2,
72+
"chunkSize": 4,
73+
"uploadDate": {
74+
"$date": "1970-01-01T00:00:00.000Z"
75+
},
76+
"filename": "filename",
77+
"metadata": {}
78+
},
79+
{
80+
"_id": {
81+
"$oid": "000000000000000000000004"
82+
},
83+
"length": 8,
84+
"chunkSize": 4,
85+
"uploadDate": {
86+
"$date": "1970-01-01T00:00:00.000Z"
87+
},
88+
"filename": "otherfilename",
89+
"metadata": {}
90+
}
91+
]
92+
},
93+
{
94+
"collectionName": "fs.chunks",
95+
"databaseName": "gridfs-tests",
96+
"documents": [
97+
{
98+
"_id": {
99+
"$oid": "000000000000000000000001"
100+
},
101+
"files_id": {
102+
"$oid": "000000000000000000000002"
103+
},
104+
"n": 0,
105+
"data": {
106+
"$binary": {
107+
"base64": "",
108+
"subType": "00"
109+
}
110+
}
111+
},
112+
{
113+
"_id": {
114+
"$oid": "000000000000000000000002"
115+
},
116+
"files_id": {
117+
"$oid": "000000000000000000000003"
118+
},
119+
"n": 0,
120+
"data": {
121+
"$binary": {
122+
"base64": "",
123+
"subType": "00"
124+
}
125+
}
126+
},
127+
{
128+
"_id": {
129+
"$oid": "000000000000000000000003"
130+
},
131+
"files_id": {
132+
"$oid": "000000000000000000000003"
133+
},
134+
"n": 0,
135+
"data": {
136+
"$binary": {
137+
"base64": "",
138+
"subType": "00"
139+
}
140+
}
141+
},
142+
{
143+
"_id": {
144+
"$oid": "000000000000000000000004"
145+
},
146+
"files_id": {
147+
"$oid": "000000000000000000000004"
148+
},
149+
"n": 0,
150+
"data": {
151+
"$binary": {
152+
"base64": "",
153+
"subType": "00"
154+
}
155+
}
156+
}
157+
]
158+
}
159+
],
160+
"tests": [
161+
{
162+
"description": "delete when multiple revisions of the file exist",
163+
"operations": [
164+
{
165+
"name": "deleteByName",
166+
"object": "bucket0",
167+
"arguments": {
168+
"filename": "filename"
169+
}
170+
}
171+
],
172+
"outcome": [
173+
{
174+
"collectionName": "fs.files",
175+
"databaseName": "gridfs-tests",
176+
"documents": [
177+
{
178+
"_id": {
179+
"$oid": "000000000000000000000004"
180+
},
181+
"length": 8,
182+
"chunkSize": 4,
183+
"uploadDate": {
184+
"$date": "1970-01-01T00:00:00.000Z"
185+
},
186+
"filename": "otherfilename",
187+
"metadata": {}
188+
}
189+
]
190+
},
191+
{
192+
"collectionName": "fs.chunks",
193+
"databaseName": "gridfs-tests",
194+
"documents": [
195+
{
196+
"_id": {
197+
"$oid": "000000000000000000000004"
198+
},
199+
"files_id": {
200+
"$oid": "000000000000000000000004"
201+
},
202+
"n": 0,
203+
"data": {
204+
"$binary": {
205+
"base64": "",
206+
"subType": "00"
207+
}
208+
}
209+
}
210+
]
211+
}
212+
]
213+
},
214+
{
215+
"description": "delete when file name does not exist",
216+
"operations": [
217+
{
218+
"name": "deleteByName",
219+
"object": "bucket0",
220+
"arguments": {
221+
"filename": "missing-file"
222+
},
223+
"expectError": {
224+
"isClientError": true
225+
}
226+
}
227+
]
228+
}
229+
]
230+
}

test/unified_format.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from bson import SON, json_util
6666
from bson.codec_options import DEFAULT_CODEC_OPTIONS
6767
from bson.objectid import ObjectId
68-
from gridfs import GridFSBucket, GridOut
68+
from gridfs import GridFSBucket, GridOut, NoFile
6969
from pymongo import ASCENDING, CursorType, MongoClient, _csot
7070
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
7171
from pymongo.errors import (
@@ -629,6 +629,9 @@ def process_error(self, exception, spec):
629629
self.assertNotIsInstance(error, NotPrimaryError)
630630
elif isinstance(error, (InvalidOperation, ConfigurationError, EncryptionError)):
631631
pass
632+
# gridfs NoFile errors are considered client errors.
633+
elif isinstance(error, NoFile):
634+
pass
632635
else:
633636
self.assertNotIsInstance(error, PyMongoError)
634637

0 commit comments

Comments
 (0)