Coverage for src / local_deep_research / research_library / deletion / routes / delete_routes.py: 95%
160 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
1"""
2Delete API Routes
4Provides endpoints for delete operations:
5- Delete document
6- Delete document blob only
7- Delete documents in bulk
8- Delete blobs in bulk
9- Remove document from collection
10- Delete collection
11- Delete collection index only
12"""
14from flask import Blueprint, jsonify, request, session
17from ....web.auth.decorators import login_required
18from ...utils import handle_api_error
19from ..services.document_deletion import DocumentDeletionService
20from ..services.collection_deletion import CollectionDeletionService
21from ..services.bulk_deletion import BulkDeletionService
24delete_bp = Blueprint("delete", __name__, url_prefix="/library/api")
25# NOTE: Routes use session["username"] (not .get()) intentionally.
26# @login_required guarantees the key exists; direct access fails fast
27# if the decorator is ever removed.
30class _ValidationError(ValueError):
31 """Raised when request validation fails."""
33 def __init__(self, message: str):
34 super().__init__(message)
35 self.message = message
38def _validate_document_ids() -> list:
39 """Extract and validate document_ids from the JSON request body.
41 Returns:
42 list: The validated document IDs.
44 Raises:
45 _ValidationError: If the request body is missing or invalid.
46 """
47 data = request.get_json()
48 if not data or "document_ids" not in data:
49 raise _ValidationError("document_ids required in request body")
51 document_ids = data["document_ids"]
52 if not isinstance(document_ids, list) or not document_ids:
53 raise _ValidationError("document_ids must be a non-empty list")
55 return document_ids
58# =============================================================================
59# Document Delete Endpoints
60# =============================================================================
63@delete_bp.route("/document/<string:document_id>", methods=["DELETE"])
64@login_required
65def delete_document(document_id):
66 """
67 Delete a document and all related data.
69 Tooltip: "Permanently delete this document, including PDF and text content.
70 This cannot be undone."
72 Returns:
73 JSON with deletion details including chunks deleted, blob size freed
74 """
75 try:
76 username = session["username"]
77 service = DocumentDeletionService(username)
78 result = service.delete_document(document_id)
80 if result.get("deleted"):
81 return jsonify({"success": True, **result})
82 return jsonify({"success": False, **result}), 404
84 except Exception as e:
85 return handle_api_error("deleting document", e)
88@delete_bp.route("/document/<string:document_id>/blob", methods=["DELETE"])
89@login_required
90def delete_document_blob(document_id):
91 """
92 Delete PDF binary but keep document metadata and text content.
94 Tooltip: "Remove the PDF file to save space. Text content will be
95 preserved for searching."
97 Returns:
98 JSON with bytes freed
99 """
100 try:
101 username = session["username"]
102 service = DocumentDeletionService(username)
103 result = service.delete_blob_only(document_id)
105 if result.get("deleted"):
106 return jsonify({"success": True, **result})
107 error_code = (
108 404 if "not found" in result.get("error", "").lower() else 400
109 )
110 return jsonify({"success": False, **result}), error_code
112 except Exception as e:
113 return handle_api_error("deleting document blob", e)
116@delete_bp.route("/document/<string:document_id>/preview", methods=["GET"])
117@login_required
118def get_document_deletion_preview(document_id):
119 """
120 Get a preview of what will be deleted.
122 Returns information about the document to help user confirm deletion.
123 """
124 try:
125 username = session["username"]
126 service = DocumentDeletionService(username)
127 result = service.get_deletion_preview(document_id)
129 if result.get("found"): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 return jsonify({"success": True, **result})
131 return jsonify({"success": False, "error": "Document not found"}), 404
133 except Exception as e:
134 return handle_api_error("getting document preview", e)
137# =============================================================================
138# Collection Document Endpoints
139# =============================================================================
142@delete_bp.route(
143 "/collection/<string:collection_id>/document/<string:document_id>",
144 methods=["DELETE"],
145)
146@login_required
147def remove_document_from_collection(collection_id, document_id):
148 """
149 Remove document from a collection.
151 If the document is not in any other collection, it will be deleted.
153 Tooltip: "Remove from this collection. If not in any other collection,
154 the document will be deleted."
156 Returns:
157 JSON with unlink status and whether document was deleted
158 """
159 try:
160 username = session["username"]
161 service = DocumentDeletionService(username)
162 result = service.remove_from_collection(document_id, collection_id)
164 if result.get("unlinked"):
165 return jsonify({"success": True, **result})
166 return jsonify({"success": False, **result}), 404
168 except Exception as e:
169 return handle_api_error("removing document from collection", e)
172# =============================================================================
173# Collection Delete Endpoints
174# =============================================================================
177@delete_bp.route("/collections/<string:collection_id>", methods=["DELETE"])
178@login_required
179def delete_collection(collection_id):
180 """
181 Delete a collection and clean up all related data.
183 Deletes the collection, its RAG index, chunks, and any orphaned
184 documents (documents not in any other collection).
186 Returns:
187 JSON with deletion details including orphaned documents deleted
188 """
189 try:
190 username = session["username"]
191 service = CollectionDeletionService(username)
192 result = service.delete_collection(
193 collection_id, delete_orphaned_documents=True
194 )
196 if result.get("deleted"):
197 return jsonify({"success": True, **result})
198 error = result.get("error", "Unknown error")
199 status_code = 404 if "not found" in error.lower() else 400
200 return jsonify({"success": False, **result}), status_code
202 except Exception as e:
203 return handle_api_error("deleting collection", e)
206@delete_bp.route(
207 "/collections/<string:collection_id>/index", methods=["DELETE"]
208)
209@login_required
210def delete_collection_index(collection_id):
211 """
212 Delete only the RAG index for a collection, keeping the collection itself.
214 Useful for rebuilding an index from scratch.
216 Returns:
217 JSON with deletion details
218 """
219 try:
220 username = session["username"]
221 service = CollectionDeletionService(username)
222 result = service.delete_collection_index_only(collection_id)
224 if result.get("deleted"):
225 return jsonify({"success": True, **result})
226 return jsonify({"success": False, **result}), 404
228 except Exception as e:
229 return handle_api_error("deleting collection index", e)
232@delete_bp.route("/collections/<string:collection_id>/preview", methods=["GET"])
233@login_required
234def get_collection_deletion_preview(collection_id):
235 """
236 Get a preview of what will be deleted.
238 Returns information about the collection to help user confirm deletion.
239 """
240 try:
241 username = session["username"]
242 service = CollectionDeletionService(username)
243 result = service.get_deletion_preview(collection_id)
245 if result.get("found"): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 return jsonify({"success": True, **result})
247 return jsonify({"success": False, "error": "Collection not found"}), 404
249 except Exception as e:
250 return handle_api_error("getting collection preview", e)
253# =============================================================================
254# Bulk Delete Endpoints
255# =============================================================================
258@delete_bp.route("/documents/bulk", methods=["DELETE"])
259@login_required
260def delete_documents_bulk():
261 """
262 Delete multiple documents at once.
264 Tooltip: "Permanently delete all selected documents and their associated data."
266 Request body:
267 {"document_ids": ["id1", "id2", ...]}
269 Returns:
270 JSON with bulk deletion results
271 """
272 try:
273 document_ids = _validate_document_ids()
274 username = session["username"]
275 service = BulkDeletionService(username)
276 result = service.delete_documents(document_ids)
278 return jsonify({"success": True, **result})
280 except _ValidationError as e:
281 return jsonify({"success": False, "error": e.message}), 400
282 except Exception as e:
283 return handle_api_error("bulk deleting documents", e)
286@delete_bp.route("/documents/blobs", methods=["DELETE"])
287@login_required
288def delete_documents_blobs_bulk():
289 """
290 Delete PDF binaries for multiple documents.
292 Tooltip: "Remove PDF files from selected documents to free up database space.
293 Text content is preserved."
295 Request body:
296 {"document_ids": ["id1", "id2", ...]}
298 Returns:
299 JSON with bulk blob deletion results
300 """
301 try:
302 document_ids = _validate_document_ids()
303 username = session["username"]
304 service = BulkDeletionService(username)
305 result = service.delete_blobs(document_ids)
307 return jsonify({"success": True, **result})
309 except _ValidationError as e:
310 return jsonify({"success": False, "error": e.message}), 400
311 except Exception as e:
312 return handle_api_error("bulk deleting blobs", e)
315@delete_bp.route(
316 "/collection/<string:collection_id>/documents/bulk", methods=["DELETE"]
317)
318@login_required
319def remove_documents_from_collection_bulk(collection_id):
320 """
321 Remove multiple documents from a collection.
323 Documents that are not in any other collection will be deleted.
325 Request body:
326 {"document_ids": ["id1", "id2", ...]}
328 Returns:
329 JSON with bulk removal results
330 """
331 try:
332 document_ids = _validate_document_ids()
333 username = session["username"]
334 service = BulkDeletionService(username)
335 result = service.remove_documents_from_collection(
336 document_ids, collection_id
337 )
339 return jsonify({"success": True, **result})
341 except _ValidationError as e:
342 return jsonify({"success": False, "error": e.message}), 400
343 except Exception as e:
344 return handle_api_error("bulk removing documents from collection", e)
347@delete_bp.route("/documents/preview", methods=["POST"])
348@login_required
349def get_bulk_deletion_preview():
350 """
351 Get a preview of what will be affected by a bulk operation.
353 Request body:
354 {
355 "document_ids": ["id1", "id2", ...],
356 "operation": "delete" or "delete_blobs"
357 }
359 Returns:
360 JSON with preview information
361 """
362 try:
363 document_ids = _validate_document_ids()
364 data = request.get_json()
365 operation = data.get("operation", "delete")
367 username = session["username"]
368 service = BulkDeletionService(username)
369 result = service.get_bulk_preview(document_ids, operation)
371 return jsonify({"success": True, **result})
373 except _ValidationError as e:
374 return jsonify({"success": False, "error": e.message}), 400
375 except Exception as e:
376 return handle_api_error("getting bulk preview", e)