quanliang commited on
Commit
9ec5a92
·
1 Parent(s): c7b3e63

new test on storage management

Browse files
Files changed (3) hide show
  1. index.html +5 -5
  2. main.py +88 -50
  3. vectorstore.py +27 -7
index.html CHANGED
@@ -1592,11 +1592,11 @@
1592
  <div class="admin-row">
1593
  <label>
1594
  Type confirmation phrase
1595
- <input type="text" id="adminDeleteConfirmInput" placeholder="DELETE /data">
1596
  </label>
1597
  </div>
1598
  <div class="admin-actions">
1599
- <button type="button" id="wipeDataBtn" class="danger-btn">Delete all /data contents</button>
1600
  </div>
1601
  </div>
1602
  <div id="adminPanelStatus"></div>
@@ -3626,7 +3626,7 @@
3626
  }
3627
 
3628
  const proceed = window.confirm(
3629
- 'This will permanently delete all files/folders inside /data. Continue?'
3630
  );
3631
  if (!proceed) {
3632
  return;
@@ -3644,12 +3644,12 @@
3644
 
3645
  const data = await response.json();
3646
  if (!response.ok || !data.ok) {
3647
- setAdminStatus(data.message || 'Failed to delete /data contents.', true);
3648
  return;
3649
  }
3650
 
3651
  setAdminStatus(
3652
- `Deleted /data contents (files: ${data.deleted_files ?? 0}, folders: ${data.deleted_dirs ?? 0}).`
3653
  );
3654
  adminDeleteConfirmInput.value = '';
3655
  } catch (error) {
 
1592
  <div class="admin-row">
1593
  <label>
1594
  Type confirmation phrase
1595
+ <input type="text" id="adminDeleteConfirmInput" placeholder="DELETE APP DATA">
1596
  </label>
1597
  </div>
1598
  <div class="admin-actions">
1599
+ <button type="button" id="wipeDataBtn" class="danger-btn">Delete all app data contents</button>
1600
  </div>
1601
  </div>
1602
  <div id="adminPanelStatus"></div>
 
3626
  }
3627
 
3628
  const proceed = window.confirm(
3629
+ 'This will permanently delete all files/folders inside the app data directories. Continue?'
3630
  );
3631
  if (!proceed) {
3632
  return;
 
3644
 
3645
  const data = await response.json();
3646
  if (!response.ok || !data.ok) {
3647
+ setAdminStatus(data.message || 'Failed to delete app data contents.', true);
3648
  return;
3649
  }
3650
 
3651
  setAdminStatus(
3652
+ `Deleted app data contents (files: ${data.deleted_files ?? 0}, folders: ${data.deleted_dirs ?? 0}).`
3653
  );
3654
  adminDeleteConfirmInput.value = '';
3655
  } catch (error) {
main.py CHANGED
@@ -50,7 +50,7 @@ from pdf_text_processing import (
50
  get_pdf_title,
51
  entity_match
52
  )
53
- from vectorstore import get_vectorstore, compute_file_hash
54
  from system_chart_extraction import system_chart_extraction
55
  from system_chart_service import SystemChartService
56
  from hypotheses_service import HypothesesService
@@ -304,17 +304,37 @@ def resource_path(relative_path: str) -> str:
304
  return os.path.join(sys._MEIPASS, relative_path)
305
  return os.path.join(os.path.dirname(__file__), relative_path)
306
 
307
- def get_base_directory():
308
- """Return base directory, preferring persistent cloud storage when provided."""
309
- persistent_dir = os.environ.get("PERSISTENT_STORAGE_PATH")
310
- if persistent_dir:
311
- return persistent_dir
312
- if hasattr(sys, "_MEIPASS"): # Running as PyInstaller executable
313
- return os.path.dirname(sys.executable)
314
- return os.path.dirname(os.path.abspath(__file__))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  # Define paths relative to the base directory for persistent storage
317
- UPLOAD_DIR = os.path.join(get_base_directory(), "uploaded files")
 
318
  os.makedirs(UPLOAD_DIR, exist_ok=True)
319
  logger.info(f"UPLOAD_DIR set to: {os.path.abspath(UPLOAD_DIR)}")
320
 
@@ -367,7 +387,7 @@ def load_existing_databases():
367
 
368
  # Get all database directories from root databases
369
  database_dirs = []
370
- chroma_db_base = os.path.join(get_base_directory(), "root databases")
371
 
372
  if os.path.exists(chroma_db_base):
373
  for item in os.listdir(chroma_db_base):
@@ -647,8 +667,7 @@ embedding_model = None
647
  llm_QA = None
648
  llm_system_chart_extraction = None
649
 
650
- ADMIN_DELETE_CONFIRMATION_TEXT = "DELETE /data"
651
- DEFAULT_ADMIN_PURGE_PATH = "/data"
652
 
653
  def get_model_provider(model: str) -> Optional[str]:
654
  if model in OPENAI_MODELS:
@@ -2890,24 +2909,29 @@ def get_admin_password() -> Optional[str]:
2890
  return password.strip()
2891
  return None
2892
 
2893
- def get_admin_purge_root() -> str:
2894
- configured = (os.environ.get("ADMIN_PURGE_PATH") or DEFAULT_ADMIN_PURGE_PATH).strip()
2895
- if not configured:
2896
- configured = DEFAULT_ADMIN_PURGE_PATH
2897
- return os.path.abspath(configured)
 
 
 
 
2898
 
2899
- def is_safe_admin_purge_root(path: str) -> bool:
2900
- normalized = os.path.abspath(path)
2901
- # Must be an absolute subpath, never root.
2902
- if normalized in {"/", ""}:
2903
  return False
2904
- if not normalized.startswith("/data"):
 
 
 
2905
  return False
2906
- return True
2907
 
2908
  def delete_directory_contents(path: str) -> Dict[str, int]:
2909
  deleted_files = 0
2910
  deleted_dirs = 0
 
 
2911
  for name in os.listdir(path):
2912
  target = os.path.join(path, name)
2913
  if os.path.isfile(target) or os.path.islink(target):
@@ -2962,39 +2986,54 @@ async def admin_wipe_data(
2962
  status_code=400
2963
  )
2964
 
2965
- purge_root = get_admin_purge_root()
2966
- if not is_safe_admin_purge_root(purge_root):
2967
- return JSONResponse(
2968
- content={"ok": False, "message": f"Unsafe ADMIN_PURGE_PATH resolved to '{purge_root}'."},
2969
- status_code=400
2970
- )
2971
-
2972
- if not os.path.exists(purge_root):
2973
- return JSONResponse(
2974
- content={"ok": False, "message": f"Purge path does not exist: {purge_root}"},
2975
- status_code=404
2976
- )
2977
 
2978
- if not os.path.isdir(purge_root):
2979
- return JSONResponse(
2980
- content={"ok": False, "message": f"Purge path is not a directory: {purge_root}"},
2981
- status_code=400
2982
- )
 
 
 
 
 
 
2983
 
2984
  try:
2985
- result = delete_directory_contents(purge_root)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2986
  logger.warning(
2987
- "Admin data wipe executed for %s (files=%s, dirs=%s)",
2988
- purge_root,
2989
- result["deleted_files"],
2990
- result["deleted_dirs"]
2991
  )
2992
  return JSONResponse(
2993
  content={
2994
  "ok": True,
2995
- "message": f"Deleted contents of {purge_root}.",
2996
- "deleted_files": result["deleted_files"],
2997
- "deleted_dirs": result["deleted_dirs"]
 
 
2998
  },
2999
  status_code=200
3000
  )
@@ -3216,7 +3255,6 @@ if __name__ == "__main__":
3216
  logger.info(f"Executable path: {sys.executable}")
3217
  logger.info(f"Base directory for storage: {get_base_directory()}")
3218
  logger.info(f"UPLOAD_DIR: {os.path.abspath(UPLOAD_DIR)}")
3219
- from vectorstore import CHROMA_DB_BASE
3220
  logger.info(f"CHROMA_DB_BASE: {os.path.abspath(CHROMA_DB_BASE)}")
3221
  logger.info("=" * 50)
3222
 
 
50
  get_pdf_title,
51
  entity_match
52
  )
53
+ from vectorstore import get_vectorstore, compute_file_hash, CHROMA_DB_BASE
54
  from system_chart_extraction import system_chart_extraction
55
  from system_chart_service import SystemChartService
56
  from hypotheses_service import HypothesesService
 
304
  return os.path.join(sys._MEIPASS, relative_path)
305
  return os.path.join(os.path.dirname(__file__), relative_path)
306
 
307
+ def get_base_directory() -> str:
308
+ """
309
+ HF Spaces-safe data root:
310
+ - Prefer explicit override via APP_DATA_DIR or PERSISTENT_STORAGE_PATH
311
+ - Else use /data/<app> only if it exists and is writable
312
+ - Else use $HOME/.cache/<app>
313
+ - Else use /tmp/<app>
314
+ """
315
+ override = (os.environ.get("APP_DATA_DIR") or os.environ.get("PERSISTENT_STORAGE_PATH") or "").strip()
316
+ if override:
317
+ os.makedirs(override, exist_ok=True)
318
+ return override
319
+
320
+ if os.path.isdir("/data") and os.access("/data", os.W_OK):
321
+ base = "/data/pdf-rag"
322
+ os.makedirs(base, exist_ok=True)
323
+ return base
324
+
325
+ home = os.environ.get("HOME")
326
+ if home:
327
+ base = os.path.join(home, ".cache", "pdf-rag")
328
+ os.makedirs(base, exist_ok=True)
329
+ return base
330
+
331
+ base = "/tmp/pdf-rag"
332
+ os.makedirs(base, exist_ok=True)
333
+ return base
334
 
335
  # Define paths relative to the base directory for persistent storage
336
+ APP_DATA_ROOT = get_base_directory()
337
+ UPLOAD_DIR = os.path.join(APP_DATA_ROOT, "uploaded_files")
338
  os.makedirs(UPLOAD_DIR, exist_ok=True)
339
  logger.info(f"UPLOAD_DIR set to: {os.path.abspath(UPLOAD_DIR)}")
340
 
 
387
 
388
  # Get all database directories from root databases
389
  database_dirs = []
390
+ chroma_db_base = CHROMA_DB_BASE
391
 
392
  if os.path.exists(chroma_db_base):
393
  for item in os.listdir(chroma_db_base):
 
667
  llm_QA = None
668
  llm_system_chart_extraction = None
669
 
670
+ ADMIN_DELETE_CONFIRMATION_TEXT = "DELETE APP DATA"
 
671
 
672
  def get_model_provider(model: str) -> Optional[str]:
673
  if model in OPENAI_MODELS:
 
2909
  return password.strip()
2910
  return None
2911
 
2912
+ def get_admin_wipe_targets() -> List[str]:
2913
+ return [
2914
+ os.path.abspath(UPLOAD_DIR),
2915
+ os.path.abspath(CHROMA_DB_BASE),
2916
+ ]
2917
+
2918
+ def is_safe_target(path: str, data_root: str) -> bool:
2919
+ normalized_path = os.path.abspath(path)
2920
+ normalized_root = os.path.abspath(data_root)
2921
 
2922
+ if normalized_path in {"/", ""}:
 
 
 
2923
  return False
2924
+
2925
+ try:
2926
+ return os.path.commonpath([normalized_root, normalized_path]) == normalized_root
2927
+ except ValueError:
2928
  return False
 
2929
 
2930
  def delete_directory_contents(path: str) -> Dict[str, int]:
2931
  deleted_files = 0
2932
  deleted_dirs = 0
2933
+ if not os.path.isdir(path):
2934
+ return {"deleted_files": 0, "deleted_dirs": 0}
2935
  for name in os.listdir(path):
2936
  target = os.path.join(path, name)
2937
  if os.path.isfile(target) or os.path.islink(target):
 
2986
  status_code=400
2987
  )
2988
 
2989
+ targets = get_admin_wipe_targets()
2990
+ data_root = os.path.abspath(APP_DATA_ROOT)
 
 
 
 
 
 
 
 
 
 
2991
 
2992
+ for target in targets:
2993
+ if not is_safe_target(target, data_root):
2994
+ return JSONResponse(
2995
+ content={
2996
+ "ok": False,
2997
+ "message": f"Refusing to wipe unsafe target: {target}",
2998
+ "data_root": data_root,
2999
+ "targets": targets
3000
+ },
3001
+ status_code=400
3002
+ )
3003
 
3004
  try:
3005
+ results = []
3006
+ total_files = 0
3007
+ total_dirs = 0
3008
+
3009
+ for target in targets:
3010
+ if not os.path.exists(target):
3011
+ results.append({"target": target, "status": "missing"})
3012
+ continue
3013
+
3014
+ result = delete_directory_contents(target)
3015
+ total_files += result["deleted_files"]
3016
+ total_dirs += result["deleted_dirs"]
3017
+ results.append({"target": target, "status": "wiped", **result})
3018
+
3019
+ FILE_REGISTRY.clear()
3020
+ EMBEDDING_PROGRESS.clear()
3021
+ HYPOTHESIS_GENERATION_CONTEXT.clear()
3022
+
3023
  logger.warning(
3024
+ "Admin data wipe executed for targets=%s (files=%s, dirs=%s)",
3025
+ targets,
3026
+ total_files,
3027
+ total_dirs
3028
  )
3029
  return JSONResponse(
3030
  content={
3031
  "ok": True,
3032
+ "message": "Wipe completed.",
3033
+ "data_root": data_root,
3034
+ "targets": results,
3035
+ "deleted_files": total_files,
3036
+ "deleted_dirs": total_dirs
3037
  },
3038
  status_code=200
3039
  )
 
3255
  logger.info(f"Executable path: {sys.executable}")
3256
  logger.info(f"Base directory for storage: {get_base_directory()}")
3257
  logger.info(f"UPLOAD_DIR: {os.path.abspath(UPLOAD_DIR)}")
 
3258
  logger.info(f"CHROMA_DB_BASE: {os.path.abspath(CHROMA_DB_BASE)}")
3259
  logger.info("=" * 50)
3260
 
vectorstore.py CHANGED
@@ -1,15 +1,35 @@
1
  import os
2
- import sys
3
  import hashlib
4
  from langchain_community.vectorstores import Chroma
5
  from langchain_openai import OpenAIEmbeddings
6
 
7
- def get_base_directory():
8
- """Return the base directory for persistent storage (next to executable when packaged)"""
9
- if hasattr(sys, '_MEIPASS'): # Running as PyInstaller executable
10
- return os.path.dirname(sys.executable)
11
- else: # Running as normal Python script
12
- return os.path.dirname(os.path.abspath(__file__))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  # Define path relative to the base directory
15
  CHROMA_DB_BASE = os.path.join(get_base_directory(), "root databases")
 
1
  import os
 
2
  import hashlib
3
  from langchain_community.vectorstores import Chroma
4
  from langchain_openai import OpenAIEmbeddings
5
 
6
+ def get_base_directory() -> str:
7
+ """
8
+ HF Spaces-safe data root:
9
+ - Prefer explicit override via APP_DATA_DIR or PERSISTENT_STORAGE_PATH
10
+ - Else use /data/<app> only if it exists and is writable
11
+ - Else use $HOME/.cache/<app>
12
+ - Else use /tmp/<app>
13
+ """
14
+ override = (os.environ.get("APP_DATA_DIR") or os.environ.get("PERSISTENT_STORAGE_PATH") or "").strip()
15
+ if override:
16
+ os.makedirs(override, exist_ok=True)
17
+ return override
18
+
19
+ if os.path.isdir("/data") and os.access("/data", os.W_OK):
20
+ base = "/data/pdf-rag"
21
+ os.makedirs(base, exist_ok=True)
22
+ return base
23
+
24
+ home = os.environ.get("HOME")
25
+ if home:
26
+ base = os.path.join(home, ".cache", "pdf-rag")
27
+ os.makedirs(base, exist_ok=True)
28
+ return base
29
+
30
+ base = "/tmp/pdf-rag"
31
+ os.makedirs(base, exist_ok=True)
32
+ return base
33
 
34
  # Define path relative to the base directory
35
  CHROMA_DB_BASE = os.path.join(get_base_directory(), "root databases")