yugbirla commited on
Commit
7be4ee1
·
1 Parent(s): 742f3fd

Add document storage status and backend delete

Browse files
app/main.py CHANGED
@@ -1,3 +1,4 @@
 
1
 
2
  import os
3
  from fastapi import Request, Query
@@ -827,3 +828,15 @@ def auth_dev_session(
827
  @app.get("/auth/logout")
828
  def auth_logout(request: Request):
829
  return clear_session(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.product.document_storage_manager import get_document_storage_status, delete_document_storage
2
 
3
  import os
4
  from fastapi import Request, Query
 
828
  @app.get("/auth/logout")
829
  def auth_logout(request: Request):
830
  return clear_session(request)
831
+
832
+
833
+ # Document storage status and delete endpoints
834
+
835
+ @app.get("/documents/{document_id}/storage")
836
+ def document_storage_status(document_id: str):
837
+ return get_document_storage_status(document_id)
838
+
839
+
840
+ @app.delete("/documents/{document_id}/delete")
841
+ def delete_document_runtime_storage(document_id: str):
842
+ return delete_document_storage(document_id)
app/product/document_storage_manager.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import Dict, Any, List
5
+
6
+ from app.core.config import settings
7
+
8
+
9
+ def as_path(value, fallback: str) -> Path:
10
+ try:
11
+ if value:
12
+ return Path(value)
13
+ except Exception:
14
+ pass
15
+
16
+ return Path(fallback)
17
+
18
+
19
+ def get_storage_paths() -> Dict[str, Path]:
20
+ upload_dir = as_path(
21
+ getattr(settings, "UPLOAD_DIR", None),
22
+ "/tmp/graphrag/uploads"
23
+ )
24
+
25
+ processed_dir = as_path(
26
+ getattr(settings, "PROCESSED_DIR", None),
27
+ "/tmp/graphrag/processed"
28
+ )
29
+
30
+ qdrant_dir = as_path(
31
+ getattr(settings, "QDRANT_LOCAL_PATH", None),
32
+ "/tmp/graphrag/qdrant"
33
+ )
34
+
35
+ evaluation_dir = as_path(
36
+ getattr(settings, "EVALUATION_DIR", None),
37
+ "/tmp/graphrag/evaluation"
38
+ )
39
+
40
+ return {
41
+ "upload_dir": upload_dir,
42
+ "processed_dir": processed_dir,
43
+ "qdrant_dir": qdrant_dir,
44
+ "evaluation_dir": evaluation_dir
45
+ }
46
+
47
+
48
+ def path_size_bytes(path: Path) -> int:
49
+ if not path.exists():
50
+ return 0
51
+
52
+ if path.is_file():
53
+ try:
54
+ return path.stat().st_size
55
+ except Exception:
56
+ return 0
57
+
58
+ total = 0
59
+
60
+ for item in path.rglob("*"):
61
+ if item.is_file():
62
+ try:
63
+ total += item.stat().st_size
64
+ except Exception:
65
+ pass
66
+
67
+ return total
68
+
69
+
70
+ def find_matching_items(root: Path, document_id: str) -> List[Dict[str, Any]]:
71
+ matches = []
72
+
73
+ if not root.exists():
74
+ return matches
75
+
76
+ for item in root.rglob("*"):
77
+ try:
78
+ item_name = item.name
79
+ item_path = str(item)
80
+ except Exception:
81
+ continue
82
+
83
+ if document_id in item_name or document_id in item_path:
84
+ matches.append({
85
+ "path": str(item),
86
+ "type": "directory" if item.is_dir() else "file",
87
+ "size_bytes": path_size_bytes(item)
88
+ })
89
+
90
+ return matches
91
+
92
+
93
+ def get_document_storage_status(document_id: str) -> Dict[str, Any]:
94
+ paths = get_storage_paths()
95
+
96
+ processed_doc_dir = paths["processed_dir"] / document_id
97
+
98
+ upload_matches = find_matching_items(paths["upload_dir"], document_id)
99
+ processed_matches = find_matching_items(paths["processed_dir"], document_id)
100
+ evaluation_matches = find_matching_items(paths["evaluation_dir"], document_id)
101
+
102
+ processed_exists = processed_doc_dir.exists()
103
+
104
+ return {
105
+ "document_id": document_id,
106
+ "storage_type": "runtime_ephemeral_storage",
107
+ "important_note": (
108
+ "On free/basic Hugging Face Spaces, files stored under /tmp can disappear "
109
+ "after rebuild, restart, or runtime reset unless persistent storage is configured."
110
+ ),
111
+ "paths": {
112
+ "upload_dir": str(paths["upload_dir"]),
113
+ "processed_dir": str(paths["processed_dir"]),
114
+ "processed_document_dir": str(processed_doc_dir),
115
+ "qdrant_dir": str(paths["qdrant_dir"]),
116
+ "evaluation_dir": str(paths["evaluation_dir"])
117
+ },
118
+ "exists": {
119
+ "processed_document_dir": processed_exists,
120
+ "upload_matches_found": len(upload_matches),
121
+ "processed_matches_found": len(processed_matches),
122
+ "evaluation_matches_found": len(evaluation_matches)
123
+ },
124
+ "sizes": {
125
+ "processed_document_size_bytes": path_size_bytes(processed_doc_dir),
126
+ "upload_matches_size_bytes": sum(x["size_bytes"] for x in upload_matches),
127
+ "evaluation_matches_size_bytes": sum(x["size_bytes"] for x in evaluation_matches)
128
+ },
129
+ "matches": {
130
+ "uploads": upload_matches[:50],
131
+ "processed": processed_matches[:50],
132
+ "evaluation": evaluation_matches[:50]
133
+ },
134
+ "status": "available" if processed_exists or upload_matches or processed_matches else "missing_or_runtime_reset",
135
+ "recommendation": (
136
+ "If this says missing_or_runtime_reset but the UI still shows the document, "
137
+ "clear workspace cache and re-upload the document."
138
+ )
139
+ }
140
+
141
+
142
+ def remove_path(path: Path) -> Dict[str, Any]:
143
+ result = {
144
+ "path": str(path),
145
+ "existed": path.exists(),
146
+ "deleted": False,
147
+ "error": None
148
+ }
149
+
150
+ if not path.exists():
151
+ return result
152
+
153
+ try:
154
+ if path.is_dir():
155
+ shutil.rmtree(path)
156
+ else:
157
+ path.unlink()
158
+
159
+ result["deleted"] = True
160
+ except Exception as exc:
161
+ result["error"] = str(exc)
162
+
163
+ return result
164
+
165
+
166
+ def delete_product_db_records(document_id: str) -> Dict[str, Any]:
167
+ result = {
168
+ "attempted": True,
169
+ "deleted_records": {},
170
+ "error": None
171
+ }
172
+
173
+ try:
174
+ from app.product.product_db import get_connection, init_product_database
175
+
176
+ init_product_database()
177
+ conn = get_connection()
178
+ cur = conn.cursor()
179
+
180
+ tables = [
181
+ ("messages", "conversation_id", "SELECT conversation_id FROM conversations WHERE document_id = ?"),
182
+ ("conversations", "document_id", None),
183
+ ("user_documents", "document_id", None)
184
+ ]
185
+
186
+ # Delete messages belonging to conversations for this document
187
+ cur.execute("SELECT conversation_id FROM conversations WHERE document_id = ?", (document_id,))
188
+ conversation_ids = [row["conversation_id"] for row in cur.fetchall()]
189
+
190
+ msg_count = 0
191
+ for cid in conversation_ids:
192
+ cur.execute("DELETE FROM messages WHERE conversation_id = ?", (cid,))
193
+ msg_count += cur.rowcount
194
+
195
+ result["deleted_records"]["messages"] = msg_count
196
+
197
+ cur.execute("DELETE FROM conversations WHERE document_id = ?", (document_id,))
198
+ result["deleted_records"]["conversations"] = cur.rowcount
199
+
200
+ cur.execute("DELETE FROM user_documents WHERE document_id = ?", (document_id,))
201
+ result["deleted_records"]["user_documents"] = cur.rowcount
202
+
203
+ conn.commit()
204
+ conn.close()
205
+
206
+ except Exception as exc:
207
+ result["error"] = str(exc)
208
+
209
+ return result
210
+
211
+
212
+ def delete_document_storage(document_id: str) -> Dict[str, Any]:
213
+ before = get_document_storage_status(document_id)
214
+ paths = get_storage_paths()
215
+
216
+ deleted_items = []
217
+
218
+ processed_doc_dir = paths["processed_dir"] / document_id
219
+ deleted_items.append(remove_path(processed_doc_dir))
220
+
221
+ for match in before["matches"]["uploads"]:
222
+ deleted_items.append(remove_path(Path(match["path"])))
223
+
224
+ for match in before["matches"]["evaluation"]:
225
+ deleted_items.append(remove_path(Path(match["path"])))
226
+
227
+ db_result = delete_product_db_records(document_id)
228
+
229
+ after = get_document_storage_status(document_id)
230
+
231
+ return {
232
+ "document_id": document_id,
233
+ "status": "delete_attempt_complete",
234
+ "before": before,
235
+ "deleted_items": deleted_items,
236
+ "database_cleanup": db_result,
237
+ "after": after,
238
+ "qdrant_note": (
239
+ "This endpoint removes runtime files and DB records. "
240
+ "Vector DB point-level deletion is not attempted here unless your existing vector store exposes a safe delete method. "
241
+ "If stale vector results appear, rebuild/restart or add vector deletion in a later phase."
242
+ )
243
+ }
scripts/phase35_document_storage_delete.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ # Clean BOM
4
+ for path in Path("app").rglob("*.py"):
5
+ text = path.read_text(encoding="utf-8-sig")
6
+ text = text.replace("\ufeff", "")
7
+ path.write_text(text, encoding="utf-8")
8
+
9
+ Path("app/product").mkdir(parents=True, exist_ok=True)
10
+
11
+ Path("app/product/document_storage_manager.py").write_text(r'''
12
+ import shutil
13
+ from pathlib import Path
14
+ from typing import Dict, Any, List
15
+
16
+ from app.core.config import settings
17
+
18
+
19
+ def as_path(value, fallback: str) -> Path:
20
+ try:
21
+ if value:
22
+ return Path(value)
23
+ except Exception:
24
+ pass
25
+
26
+ return Path(fallback)
27
+
28
+
29
+ def get_storage_paths() -> Dict[str, Path]:
30
+ upload_dir = as_path(
31
+ getattr(settings, "UPLOAD_DIR", None),
32
+ "/tmp/graphrag/uploads"
33
+ )
34
+
35
+ processed_dir = as_path(
36
+ getattr(settings, "PROCESSED_DIR", None),
37
+ "/tmp/graphrag/processed"
38
+ )
39
+
40
+ qdrant_dir = as_path(
41
+ getattr(settings, "QDRANT_LOCAL_PATH", None),
42
+ "/tmp/graphrag/qdrant"
43
+ )
44
+
45
+ evaluation_dir = as_path(
46
+ getattr(settings, "EVALUATION_DIR", None),
47
+ "/tmp/graphrag/evaluation"
48
+ )
49
+
50
+ return {
51
+ "upload_dir": upload_dir,
52
+ "processed_dir": processed_dir,
53
+ "qdrant_dir": qdrant_dir,
54
+ "evaluation_dir": evaluation_dir
55
+ }
56
+
57
+
58
+ def path_size_bytes(path: Path) -> int:
59
+ if not path.exists():
60
+ return 0
61
+
62
+ if path.is_file():
63
+ try:
64
+ return path.stat().st_size
65
+ except Exception:
66
+ return 0
67
+
68
+ total = 0
69
+
70
+ for item in path.rglob("*"):
71
+ if item.is_file():
72
+ try:
73
+ total += item.stat().st_size
74
+ except Exception:
75
+ pass
76
+
77
+ return total
78
+
79
+
80
+ def find_matching_items(root: Path, document_id: str) -> List[Dict[str, Any]]:
81
+ matches = []
82
+
83
+ if not root.exists():
84
+ return matches
85
+
86
+ for item in root.rglob("*"):
87
+ try:
88
+ item_name = item.name
89
+ item_path = str(item)
90
+ except Exception:
91
+ continue
92
+
93
+ if document_id in item_name or document_id in item_path:
94
+ matches.append({
95
+ "path": str(item),
96
+ "type": "directory" if item.is_dir() else "file",
97
+ "size_bytes": path_size_bytes(item)
98
+ })
99
+
100
+ return matches
101
+
102
+
103
+ def get_document_storage_status(document_id: str) -> Dict[str, Any]:
104
+ paths = get_storage_paths()
105
+
106
+ processed_doc_dir = paths["processed_dir"] / document_id
107
+
108
+ upload_matches = find_matching_items(paths["upload_dir"], document_id)
109
+ processed_matches = find_matching_items(paths["processed_dir"], document_id)
110
+ evaluation_matches = find_matching_items(paths["evaluation_dir"], document_id)
111
+
112
+ processed_exists = processed_doc_dir.exists()
113
+
114
+ return {
115
+ "document_id": document_id,
116
+ "storage_type": "runtime_ephemeral_storage",
117
+ "important_note": (
118
+ "On free/basic Hugging Face Spaces, files stored under /tmp can disappear "
119
+ "after rebuild, restart, or runtime reset unless persistent storage is configured."
120
+ ),
121
+ "paths": {
122
+ "upload_dir": str(paths["upload_dir"]),
123
+ "processed_dir": str(paths["processed_dir"]),
124
+ "processed_document_dir": str(processed_doc_dir),
125
+ "qdrant_dir": str(paths["qdrant_dir"]),
126
+ "evaluation_dir": str(paths["evaluation_dir"])
127
+ },
128
+ "exists": {
129
+ "processed_document_dir": processed_exists,
130
+ "upload_matches_found": len(upload_matches),
131
+ "processed_matches_found": len(processed_matches),
132
+ "evaluation_matches_found": len(evaluation_matches)
133
+ },
134
+ "sizes": {
135
+ "processed_document_size_bytes": path_size_bytes(processed_doc_dir),
136
+ "upload_matches_size_bytes": sum(x["size_bytes"] for x in upload_matches),
137
+ "evaluation_matches_size_bytes": sum(x["size_bytes"] for x in evaluation_matches)
138
+ },
139
+ "matches": {
140
+ "uploads": upload_matches[:50],
141
+ "processed": processed_matches[:50],
142
+ "evaluation": evaluation_matches[:50]
143
+ },
144
+ "status": "available" if processed_exists or upload_matches or processed_matches else "missing_or_runtime_reset",
145
+ "recommendation": (
146
+ "If this says missing_or_runtime_reset but the UI still shows the document, "
147
+ "clear workspace cache and re-upload the document."
148
+ )
149
+ }
150
+
151
+
152
+ def remove_path(path: Path) -> Dict[str, Any]:
153
+ result = {
154
+ "path": str(path),
155
+ "existed": path.exists(),
156
+ "deleted": False,
157
+ "error": None
158
+ }
159
+
160
+ if not path.exists():
161
+ return result
162
+
163
+ try:
164
+ if path.is_dir():
165
+ shutil.rmtree(path)
166
+ else:
167
+ path.unlink()
168
+
169
+ result["deleted"] = True
170
+ except Exception as exc:
171
+ result["error"] = str(exc)
172
+
173
+ return result
174
+
175
+
176
+ def delete_product_db_records(document_id: str) -> Dict[str, Any]:
177
+ result = {
178
+ "attempted": True,
179
+ "deleted_records": {},
180
+ "error": None
181
+ }
182
+
183
+ try:
184
+ from app.product.product_db import get_connection, init_product_database
185
+
186
+ init_product_database()
187
+ conn = get_connection()
188
+ cur = conn.cursor()
189
+
190
+ tables = [
191
+ ("messages", "conversation_id", "SELECT conversation_id FROM conversations WHERE document_id = ?"),
192
+ ("conversations", "document_id", None),
193
+ ("user_documents", "document_id", None)
194
+ ]
195
+
196
+ # Delete messages belonging to conversations for this document
197
+ cur.execute("SELECT conversation_id FROM conversations WHERE document_id = ?", (document_id,))
198
+ conversation_ids = [row["conversation_id"] for row in cur.fetchall()]
199
+
200
+ msg_count = 0
201
+ for cid in conversation_ids:
202
+ cur.execute("DELETE FROM messages WHERE conversation_id = ?", (cid,))
203
+ msg_count += cur.rowcount
204
+
205
+ result["deleted_records"]["messages"] = msg_count
206
+
207
+ cur.execute("DELETE FROM conversations WHERE document_id = ?", (document_id,))
208
+ result["deleted_records"]["conversations"] = cur.rowcount
209
+
210
+ cur.execute("DELETE FROM user_documents WHERE document_id = ?", (document_id,))
211
+ result["deleted_records"]["user_documents"] = cur.rowcount
212
+
213
+ conn.commit()
214
+ conn.close()
215
+
216
+ except Exception as exc:
217
+ result["error"] = str(exc)
218
+
219
+ return result
220
+
221
+
222
+ def delete_document_storage(document_id: str) -> Dict[str, Any]:
223
+ before = get_document_storage_status(document_id)
224
+ paths = get_storage_paths()
225
+
226
+ deleted_items = []
227
+
228
+ processed_doc_dir = paths["processed_dir"] / document_id
229
+ deleted_items.append(remove_path(processed_doc_dir))
230
+
231
+ for match in before["matches"]["uploads"]:
232
+ deleted_items.append(remove_path(Path(match["path"])))
233
+
234
+ for match in before["matches"]["evaluation"]:
235
+ deleted_items.append(remove_path(Path(match["path"])))
236
+
237
+ db_result = delete_product_db_records(document_id)
238
+
239
+ after = get_document_storage_status(document_id)
240
+
241
+ return {
242
+ "document_id": document_id,
243
+ "status": "delete_attempt_complete",
244
+ "before": before,
245
+ "deleted_items": deleted_items,
246
+ "database_cleanup": db_result,
247
+ "after": after,
248
+ "qdrant_note": (
249
+ "This endpoint removes runtime files and DB records. "
250
+ "Vector DB point-level deletion is not attempted here unless your existing vector store exposes a safe delete method. "
251
+ "If stale vector results appear, rebuild/restart or add vector deletion in a later phase."
252
+ )
253
+ }
254
+ ''', encoding="utf-8")
255
+
256
+
257
+ main_path = Path("app/main.py")
258
+ text = main_path.read_text(encoding="utf-8-sig")
259
+ text = text.replace("\ufeff", "")
260
+
261
+ if "from app.product.document_storage_manager import" not in text:
262
+ text = (
263
+ "from app.product.document_storage_manager import get_document_storage_status, delete_document_storage\n"
264
+ + text
265
+ )
266
+
267
+ if "# Document storage status and delete endpoints" not in text:
268
+ text += '''
269
+
270
+ # Document storage status and delete endpoints
271
+
272
+ @app.get("/documents/{document_id}/storage")
273
+ def document_storage_status(document_id: str):
274
+ return get_document_storage_status(document_id)
275
+
276
+
277
+ @app.delete("/documents/{document_id}/delete")
278
+ def delete_document_runtime_storage(document_id: str):
279
+ return delete_document_storage(document_id)
280
+ '''
281
+
282
+ main_path.write_text(text, encoding="utf-8")
283
+
284
+ print("Phase 35 document storage status and backend delete added.")