Spaces:
Configuration error
Configuration error
new test on storage management
Browse files- index.html +5 -5
- main.py +88 -50
- 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
|
| 1596 |
</label>
|
| 1597 |
</div>
|
| 1598 |
<div class="admin-actions">
|
| 1599 |
-
<button type="button" id="wipeDataBtn" class="danger-btn">Delete all
|
| 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
|
| 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
|
| 3648 |
return;
|
| 3649 |
}
|
| 3650 |
|
| 3651 |
setAdminStatus(
|
| 3652 |
-
`Deleted
|
| 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 |
-
"""
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# Define paths relative to the base directory for persistent storage
|
| 317 |
-
|
|
|
|
| 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 =
|
| 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
|
| 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
|
| 2894 |
-
|
| 2895 |
-
|
| 2896 |
-
|
| 2897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2898 |
|
| 2899 |
-
|
| 2900 |
-
normalized = os.path.abspath(path)
|
| 2901 |
-
# Must be an absolute subpath, never root.
|
| 2902 |
-
if normalized in {"/", ""}:
|
| 2903 |
return False
|
| 2904 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2966 |
-
|
| 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 |
-
|
| 2979 |
-
|
| 2980 |
-
|
| 2981 |
-
|
| 2982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2983 |
|
| 2984 |
try:
|
| 2985 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2986 |
logger.warning(
|
| 2987 |
-
"Admin data wipe executed for %s (files=%s, dirs=%s)",
|
| 2988 |
-
|
| 2989 |
-
|
| 2990 |
-
|
| 2991 |
)
|
| 2992 |
return JSONResponse(
|
| 2993 |
content={
|
| 2994 |
"ok": True,
|
| 2995 |
-
"message":
|
| 2996 |
-
"
|
| 2997 |
-
"
|
|
|
|
|
|
|
| 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 |
-
"""
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|