Spaces:
Running
Running
Nguyễn Quốc Vỹ commited on
Commit ·
ab66a23
1
Parent(s): 0bcda63
Fix lỗi upload file không được lưu trữ lại lâu dài
Browse files- backend/admin_services.py +7 -0
- backend/db.py +5 -0
- backend/db_sync.py +139 -3
backend/admin_services.py
CHANGED
|
@@ -16,6 +16,7 @@ if ROOT_DIR not in sys.path:
|
|
| 16 |
from backend import db
|
| 17 |
from backend.auth import is_admin
|
| 18 |
from backend.runtime_paths import PDF_DIR
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
# ======================== UserService ========================
|
|
@@ -108,6 +109,7 @@ def create_system_doc(file_path: str, filename: str = None) -> tuple[bool, str]:
|
|
| 108 |
else:
|
| 109 |
shutil.copy2(file_path, dest)
|
| 110 |
db.insert_tai_lieu_he_thong(ma_tai_lieu=ma, ten_file=filename, duong_dan=os.path.abspath(dest))
|
|
|
|
| 111 |
return True, "Đã thêm tài liệu. Hãy chạy 'Tái lập chỉ mục' để cập nhật RAG."
|
| 112 |
except Exception as e:
|
| 113 |
return False, str(e)
|
|
@@ -131,6 +133,7 @@ def update_system_doc(ma_tai_lieu: str, file_path: str = None, ten_file: str = N
|
|
| 131 |
return False, str(e)
|
| 132 |
# Cập nhật tên trong DB nếu đổi tên (cần hàm update trong db - hiện chỉ có insert upsert)
|
| 133 |
db.insert_tai_lieu_he_thong(ma_tai_lieu=ma_tai_lieu, ten_file=new_name, duong_dan=old_path)
|
|
|
|
| 134 |
return True, "Đã cập nhật. Chạy 'Tái lập chỉ mục' nếu đã thay file."
|
| 135 |
|
| 136 |
|
|
@@ -140,12 +143,15 @@ def delete_system_doc(ma_tai_lieu: str) -> tuple[bool, str]:
|
|
| 140 |
if not docs:
|
| 141 |
return False, "Không tìm thấy tài liệu"
|
| 142 |
path = docs[0].get("duong_dan")
|
|
|
|
| 143 |
try:
|
| 144 |
if path and os.path.isfile(path):
|
| 145 |
os.remove(path)
|
| 146 |
except OSError:
|
| 147 |
pass
|
| 148 |
db.delete_tai_lieu_he_thong(ma_tai_lieu)
|
|
|
|
|
|
|
| 149 |
return True, "Đã xóa tài liệu. Chạy 'Tái lập chỉ mục' để cập nhật RAG."
|
| 150 |
|
| 151 |
|
|
@@ -170,6 +176,7 @@ def reindex_all() -> tuple[bool, str]:
|
|
| 170 |
except Exception:
|
| 171 |
pass
|
| 172 |
create_vector_database(chunks)
|
|
|
|
| 173 |
return True, f"Đã tái lập chỉ mục: {len(documents)} tài liệu, {len(chunks)} chunks."
|
| 174 |
except Exception as e:
|
| 175 |
return False, f"Lỗi: {str(e)}"
|
|
|
|
| 16 |
from backend import db
|
| 17 |
from backend.auth import is_admin
|
| 18 |
from backend.runtime_paths import PDF_DIR
|
| 19 |
+
from backend.db_sync import schedule_pdf_upload, schedule_pdf_delete, schedule_vector_sync
|
| 20 |
|
| 21 |
|
| 22 |
# ======================== UserService ========================
|
|
|
|
| 109 |
else:
|
| 110 |
shutil.copy2(file_path, dest)
|
| 111 |
db.insert_tai_lieu_he_thong(ma_tai_lieu=ma, ten_file=filename, duong_dan=os.path.abspath(dest))
|
| 112 |
+
schedule_pdf_upload(dest, filename)
|
| 113 |
return True, "Đã thêm tài liệu. Hãy chạy 'Tái lập chỉ mục' để cập nhật RAG."
|
| 114 |
except Exception as e:
|
| 115 |
return False, str(e)
|
|
|
|
| 133 |
return False, str(e)
|
| 134 |
# Cập nhật tên trong DB nếu đổi tên (cần hàm update trong db - hiện chỉ có insert upsert)
|
| 135 |
db.insert_tai_lieu_he_thong(ma_tai_lieu=ma_tai_lieu, ten_file=new_name, duong_dan=old_path)
|
| 136 |
+
schedule_pdf_upload(old_path, new_name)
|
| 137 |
return True, "Đã cập nhật. Chạy 'Tái lập chỉ mục' nếu đã thay file."
|
| 138 |
|
| 139 |
|
|
|
|
| 143 |
if not docs:
|
| 144 |
return False, "Không tìm thấy tài liệu"
|
| 145 |
path = docs[0].get("duong_dan")
|
| 146 |
+
filename = docs[0].get("ten_file") or (os.path.basename(path) if path else "")
|
| 147 |
try:
|
| 148 |
if path and os.path.isfile(path):
|
| 149 |
os.remove(path)
|
| 150 |
except OSError:
|
| 151 |
pass
|
| 152 |
db.delete_tai_lieu_he_thong(ma_tai_lieu)
|
| 153 |
+
if filename:
|
| 154 |
+
schedule_pdf_delete(filename)
|
| 155 |
return True, "Đã xóa tài liệu. Chạy 'Tái lập chỉ mục' để cập nhật RAG."
|
| 156 |
|
| 157 |
|
|
|
|
| 176 |
except Exception:
|
| 177 |
pass
|
| 178 |
create_vector_database(chunks)
|
| 179 |
+
schedule_vector_sync()
|
| 180 |
return True, f"Đã tái lập chỉ mục: {len(documents)} tài liệu, {len(chunks)} chunks."
|
| 181 |
except Exception as e:
|
| 182 |
return False, f"Lỗi: {str(e)}"
|
backend/db.py
CHANGED
|
@@ -482,6 +482,7 @@ def save_tai_lieu(ma_tai_lieu: str, ma_nguoi_dung: str, ten_file: str,
|
|
| 482 |
conn.commit()
|
| 483 |
finally:
|
| 484 |
conn.close()
|
|
|
|
| 485 |
|
| 486 |
|
| 487 |
def update_trang_thai_tai_lieu(ma_tai_lieu: str, trang_thai: str):
|
|
@@ -495,6 +496,7 @@ def update_trang_thai_tai_lieu(ma_tai_lieu: str, trang_thai: str):
|
|
| 495 |
conn.commit()
|
| 496 |
finally:
|
| 497 |
conn.close()
|
|
|
|
| 498 |
|
| 499 |
|
| 500 |
def load_tai_lieu_by_user(ma_nguoi_dung: str) -> list:
|
|
@@ -520,6 +522,7 @@ def delete_tai_lieu(ma_tai_lieu: str):
|
|
| 520 |
conn.commit()
|
| 521 |
finally:
|
| 522 |
conn.close()
|
|
|
|
| 523 |
|
| 524 |
|
| 525 |
# ======================== PHẢN HỒI NGƯỜI DÙNG ========================
|
|
@@ -725,6 +728,7 @@ def insert_tai_lieu_he_thong(ma_tai_lieu: str, ten_file: str, duong_dan: str):
|
|
| 725 |
conn.commit()
|
| 726 |
finally:
|
| 727 |
conn.close()
|
|
|
|
| 728 |
|
| 729 |
|
| 730 |
def delete_tai_lieu_he_thong(ma_tai_lieu: str):
|
|
@@ -735,6 +739,7 @@ def delete_tai_lieu_he_thong(ma_tai_lieu: str):
|
|
| 735 |
conn.commit()
|
| 736 |
finally:
|
| 737 |
conn.close()
|
|
|
|
| 738 |
|
| 739 |
|
| 740 |
# Khởi tạo DB khi import module
|
|
|
|
| 482 |
conn.commit()
|
| 483 |
finally:
|
| 484 |
conn.close()
|
| 485 |
+
_schedule_sync()
|
| 486 |
|
| 487 |
|
| 488 |
def update_trang_thai_tai_lieu(ma_tai_lieu: str, trang_thai: str):
|
|
|
|
| 496 |
conn.commit()
|
| 497 |
finally:
|
| 498 |
conn.close()
|
| 499 |
+
_schedule_sync()
|
| 500 |
|
| 501 |
|
| 502 |
def load_tai_lieu_by_user(ma_nguoi_dung: str) -> list:
|
|
|
|
| 522 |
conn.commit()
|
| 523 |
finally:
|
| 524 |
conn.close()
|
| 525 |
+
_schedule_sync()
|
| 526 |
|
| 527 |
|
| 528 |
# ======================== PHẢN HỒI NGƯỜI DÙNG ========================
|
|
|
|
| 728 |
conn.commit()
|
| 729 |
finally:
|
| 730 |
conn.close()
|
| 731 |
+
_schedule_sync()
|
| 732 |
|
| 733 |
|
| 734 |
def delete_tai_lieu_he_thong(ma_tai_lieu: str):
|
|
|
|
| 739 |
conn.commit()
|
| 740 |
finally:
|
| 741 |
conn.close()
|
| 742 |
+
_schedule_sync()
|
| 743 |
|
| 744 |
|
| 745 |
# Khởi tạo DB khi import module
|
backend/db_sync.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Đồng bộ
|
| 3 |
|
| 4 |
Chỉ hoạt động khi chạy trên HF Space và có cấu hình:
|
| 5 |
- HF_DATASET_REPO
|
|
@@ -13,6 +13,7 @@ import shutil
|
|
| 13 |
import sqlite3
|
| 14 |
import threading
|
| 15 |
import time
|
|
|
|
| 16 |
|
| 17 |
_lock = threading.Lock()
|
| 18 |
_last_sync: float = 0
|
|
@@ -77,6 +78,142 @@ def _do_upload(db_path: str, repo: str, token: str, revision: str) -> bool:
|
|
| 77 |
return False
|
| 78 |
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
def schedule_sync(db_path: str | None = None) -> None:
|
| 81 |
"""
|
| 82 |
Lên lịch đồng bộ DB lên dataset repo (chạy background, có rate-limit).
|
|
@@ -109,5 +246,4 @@ def schedule_sync(db_path: str | None = None) -> None:
|
|
| 109 |
_last_sync = time.time()
|
| 110 |
_do_upload(db_path, repo, token, revision)
|
| 111 |
|
| 112 |
-
|
| 113 |
-
thread.start()
|
|
|
|
| 1 |
"""
|
| 2 |
+
Đồng bộ dữ liệu runtime lên Hugging Face Dataset repo để dữ liệu bền vững.
|
| 3 |
|
| 4 |
Chỉ hoạt động khi chạy trên HF Space và có cấu hình:
|
| 5 |
- HF_DATASET_REPO
|
|
|
|
| 13 |
import sqlite3
|
| 14 |
import threading
|
| 15 |
import time
|
| 16 |
+
from datetime import datetime, timezone
|
| 17 |
|
| 18 |
_lock = threading.Lock()
|
| 19 |
_last_sync: float = 0
|
|
|
|
| 78 |
return False
|
| 79 |
|
| 80 |
|
| 81 |
+
def _spawn_sync(task) -> None:
|
| 82 |
+
"""Chạy tác vụ sync trong background thread."""
|
| 83 |
+
thread = threading.Thread(target=task, daemon=True)
|
| 84 |
+
thread.start()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _upload_manifest(repo: str, token: str, revision: str, include_pdf: bool, include_vector: bool) -> None:
|
| 88 |
+
"""Cập nhật manifest.json cơ bản trong dataset repo."""
|
| 89 |
+
try:
|
| 90 |
+
from huggingface_hub import HfApi
|
| 91 |
+
import json
|
| 92 |
+
import tempfile
|
| 93 |
+
|
| 94 |
+
payload = {
|
| 95 |
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
| 96 |
+
"includes": {
|
| 97 |
+
"chatbot_db": True,
|
| 98 |
+
"pdf": include_pdf,
|
| 99 |
+
"csdl_vector": include_vector,
|
| 100 |
+
},
|
| 101 |
+
}
|
| 102 |
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
|
| 103 |
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
| 104 |
+
temp_manifest = f.name
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
api = HfApi(token=token)
|
| 108 |
+
api.upload_file(
|
| 109 |
+
path_or_fileobj=temp_manifest,
|
| 110 |
+
path_in_repo="manifest.json",
|
| 111 |
+
repo_id=repo,
|
| 112 |
+
repo_type="dataset",
|
| 113 |
+
revision=revision,
|
| 114 |
+
commit_message="Auto-update manifest",
|
| 115 |
+
)
|
| 116 |
+
finally:
|
| 117 |
+
try:
|
| 118 |
+
os.remove(temp_manifest)
|
| 119 |
+
except OSError:
|
| 120 |
+
pass
|
| 121 |
+
except Exception as exc:
|
| 122 |
+
print(f"[DB_SYNC] Manifest update failed: {exc}")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def schedule_pdf_upload(local_pdf_path: str, remote_filename: str | None = None) -> None:
|
| 126 |
+
"""Upload 1 file PDF mới/chỉnh sửa lên dataset repo tại pdf/<filename>."""
|
| 127 |
+
config = _get_config()
|
| 128 |
+
if not config:
|
| 129 |
+
return
|
| 130 |
+
if not os.path.exists(local_pdf_path):
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
repo, token, revision = config
|
| 134 |
+
remote_name = remote_filename or os.path.basename(local_pdf_path)
|
| 135 |
+
|
| 136 |
+
def _task():
|
| 137 |
+
with _lock:
|
| 138 |
+
try:
|
| 139 |
+
from huggingface_hub import HfApi
|
| 140 |
+
api = HfApi(token=token)
|
| 141 |
+
api.upload_file(
|
| 142 |
+
path_or_fileobj=local_pdf_path,
|
| 143 |
+
path_in_repo=f"pdf/{remote_name}",
|
| 144 |
+
repo_id=repo,
|
| 145 |
+
repo_type="dataset",
|
| 146 |
+
revision=revision,
|
| 147 |
+
commit_message=f"Auto-sync PDF: {remote_name}",
|
| 148 |
+
)
|
| 149 |
+
_upload_manifest(repo, token, revision, include_pdf=True, include_vector=False)
|
| 150 |
+
print(f"[DB_SYNC] Uploaded pdf/{remote_name} to {repo}")
|
| 151 |
+
except Exception as exc:
|
| 152 |
+
print(f"[DB_SYNC] PDF upload failed: {exc}")
|
| 153 |
+
|
| 154 |
+
_spawn_sync(_task)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def schedule_pdf_delete(remote_filename: str) -> None:
|
| 158 |
+
"""Xóa 1 file PDF từ dataset repo tại pdf/<filename>."""
|
| 159 |
+
config = _get_config()
|
| 160 |
+
if not config or not remote_filename:
|
| 161 |
+
return
|
| 162 |
+
repo, token, revision = config
|
| 163 |
+
|
| 164 |
+
def _task():
|
| 165 |
+
with _lock:
|
| 166 |
+
try:
|
| 167 |
+
from huggingface_hub import HfApi
|
| 168 |
+
api = HfApi(token=token)
|
| 169 |
+
api.delete_file(
|
| 170 |
+
path_in_repo=f"pdf/{remote_filename}",
|
| 171 |
+
repo_id=repo,
|
| 172 |
+
repo_type="dataset",
|
| 173 |
+
revision=revision,
|
| 174 |
+
commit_message=f"Auto-remove PDF: {remote_filename}",
|
| 175 |
+
)
|
| 176 |
+
_upload_manifest(repo, token, revision, include_pdf=True, include_vector=False)
|
| 177 |
+
print(f"[DB_SYNC] Deleted pdf/{remote_filename} from {repo}")
|
| 178 |
+
except Exception as exc:
|
| 179 |
+
print(f"[DB_SYNC] PDF delete failed: {exc}")
|
| 180 |
+
|
| 181 |
+
_spawn_sync(_task)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def schedule_vector_sync() -> None:
|
| 185 |
+
"""Upload toàn bộ thư mục csdl_vector lên dataset repo."""
|
| 186 |
+
config = _get_config()
|
| 187 |
+
if not config:
|
| 188 |
+
return
|
| 189 |
+
from backend.runtime_paths import VECTOR_DIR
|
| 190 |
+
|
| 191 |
+
if not os.path.isdir(VECTOR_DIR):
|
| 192 |
+
return
|
| 193 |
+
|
| 194 |
+
repo, token, revision = config
|
| 195 |
+
|
| 196 |
+
def _task():
|
| 197 |
+
with _lock:
|
| 198 |
+
try:
|
| 199 |
+
from huggingface_hub import HfApi
|
| 200 |
+
api = HfApi(token=token)
|
| 201 |
+
api.upload_folder(
|
| 202 |
+
folder_path=VECTOR_DIR,
|
| 203 |
+
path_in_repo="csdl_vector",
|
| 204 |
+
repo_id=repo,
|
| 205 |
+
repo_type="dataset",
|
| 206 |
+
revision=revision,
|
| 207 |
+
commit_message="Auto-sync vector store",
|
| 208 |
+
)
|
| 209 |
+
_upload_manifest(repo, token, revision, include_pdf=True, include_vector=True)
|
| 210 |
+
print(f"[DB_SYNC] Uploaded csdl_vector to {repo}")
|
| 211 |
+
except Exception as exc:
|
| 212 |
+
print(f"[DB_SYNC] Vector sync failed: {exc}")
|
| 213 |
+
|
| 214 |
+
_spawn_sync(_task)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
def schedule_sync(db_path: str | None = None) -> None:
|
| 218 |
"""
|
| 219 |
Lên lịch đồng bộ DB lên dataset repo (chạy background, có rate-limit).
|
|
|
|
| 246 |
_last_sync = time.time()
|
| 247 |
_do_upload(db_path, repo, token, revision)
|
| 248 |
|
| 249 |
+
_spawn_sync(_sync)
|
|
|