feat: add SQLite write-ahead log and shared memory files.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from threading import Lock
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config")
|
||||
SECURITY_FILE = os.path.join(CONFIG_DIR, "security.json")
|
||||
|
||||
# Ensure config directory exists
|
||||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||||
|
||||
_auth_lock = Lock()
|
||||
|
||||
def _load_security_data() -> dict:
|
||||
with _auth_lock:
|
||||
if not os.path.exists(SECURITY_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(SECURITY_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read security.json: {e}")
|
||||
return {}
|
||||
|
||||
def _save_security_data(data: dict) -> bool:
|
||||
with _auth_lock:
|
||||
try:
|
||||
with open(SECURITY_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write security.json: {e}")
|
||||
return False
|
||||
|
||||
def has_password() -> bool:
|
||||
"""Kiểm tra xem hệ thống đã thiết lập mật khẩu chưa."""
|
||||
data = _load_security_data()
|
||||
pwd = data.get("settings_password", "")
|
||||
return bool(pwd.strip())
|
||||
|
||||
def verify_password(pwd: str) -> bool:
|
||||
"""Xác thực mật khẩu. Trả về True nếu đúng hoặc nếu hệ thống chưa yêu cầu mật khẩu."""
|
||||
if not has_password():
|
||||
return True
|
||||
|
||||
data = _load_security_data()
|
||||
return data.get("settings_password", "") == pwd
|
||||
|
||||
def set_password(old_pwd: str, new_pwd: str) -> tuple[bool, str]:
|
||||
"""Cập nhật mật khẩu mới."""
|
||||
data = _load_security_data()
|
||||
current_pwd = data.get("settings_password", "")
|
||||
|
||||
# Nếu đang có pass, phải nhập đúng pass cũ
|
||||
if current_pwd and old_pwd != current_pwd:
|
||||
return False, "Mật khẩu cũ không chính xác."
|
||||
|
||||
data["settings_password"] = new_pwd
|
||||
if _save_security_data(data):
|
||||
if not new_pwd:
|
||||
return True, "Đã gỡ bỏ mật khẩu bảo vệ."
|
||||
return True, "Cập nhật mật khẩu thành công."
|
||||
return False, "Lỗi khi lưu mật khẩu, vui lòng xem log."
|
||||
+3
-1
@@ -227,7 +227,9 @@ class Backend:
|
||||
"""Xác minh tính hợp lệ của cấu hình"""
|
||||
if not self.name or not self.name.strip():
|
||||
return False, t("config.backend_name_empty")
|
||||
if self.type not in ["ollama", "openai", "claude", "other"]:
|
||||
# Accepted types: all keys from API_PROVIDERS + legacy types
|
||||
valid_types = set(API_PROVIDERS.keys()) | {"ollama", "openai", "claude", "other"}
|
||||
if self.type not in valid_types:
|
||||
return False, t("config.unsupported_type", type=self.type)
|
||||
if not self.base_url or not self.base_url.strip().startswith(("http://", "https://")):
|
||||
return False, t("config.base_url_invalid")
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
class Task:
|
||||
def __init__(self, name: str, task_type: str, metadata: Dict[str, Any] = None):
|
||||
self.id = str(uuid.uuid4())
|
||||
self.name = name
|
||||
self.type = task_type
|
||||
self.status = TaskStatus.PENDING
|
||||
self.progress = 0.0 # 0.0 to 100.0
|
||||
self.message = "Initializing..."
|
||||
self.metadata = metadata or {}
|
||||
self.created_at = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
self.result = None
|
||||
self.error = None
|
||||
self._stop_event = asyncio.Event()
|
||||
|
||||
def update(self, status: TaskStatus = None, progress: float = None, message: str = None, result: Any = None, error: str = None):
|
||||
if status: self.status = status
|
||||
if progress is not None: self.progress = progress
|
||||
if message: self.message = message
|
||||
if result: self.result = result
|
||||
if error: self.error = error
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def cancel(self):
|
||||
if self.status in [TaskStatus.PENDING, TaskStatus.RUNNING]:
|
||||
self.status = TaskStatus.CANCELLED
|
||||
self._stop_event.set()
|
||||
self.message = "Cancelled by user"
|
||||
|
||||
def is_cancelled(self):
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"type": self.type,
|
||||
"status": self.status,
|
||||
"progress": self.progress,
|
||||
"message": self.message,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"result": self.result,
|
||||
"error": self.error,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self):
|
||||
self.tasks: Dict[str, Task] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def create_task(self, name: str, task_type: str, metadata: Dict[str, Any] = None) -> Task:
|
||||
async with self._lock:
|
||||
task = Task(name, task_type, metadata)
|
||||
self.tasks[task.id] = task
|
||||
return task
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[Task]:
|
||||
return self.tasks.get(task_id)
|
||||
|
||||
def list_tasks(self, limit: int = 50) -> List[Dict]:
|
||||
return [t.to_dict() for t in sorted(self.tasks.values(), key=lambda x: x.created_at, reverse=True)[:limit]]
|
||||
|
||||
async def run_task(self, task_id: str, coro_func: Callable, *args, **kwargs):
|
||||
task = self.get_task(task_id)
|
||||
if not task:
|
||||
return
|
||||
|
||||
task.update(status=TaskStatus.RUNNING, message="Task started")
|
||||
try:
|
||||
# We pass the task object so the coroutine can update progress
|
||||
await coro_func(task, *args, **kwargs)
|
||||
if task.status == TaskStatus.RUNNING:
|
||||
task.update(status=TaskStatus.COMPLETED, progress=100.0, message="Task completed successfully")
|
||||
except asyncio.CancelledError:
|
||||
task.update(status=TaskStatus.CANCELLED, message="Task was cancelled")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in task {task_id}")
|
||||
task.update(status=TaskStatus.FAILED, message=f"Error: {str(e)}", error=str(e))
|
||||
|
||||
async def cleanup_old_tasks(self, max_age_seconds: int = 3600 * 24):
|
||||
# NOT implemented yet, but good for production
|
||||
pass
|
||||
|
||||
# Singleton instance
|
||||
task_manager = TaskManager()
|
||||
Reference in New Issue
Block a user