ge-tool/backend/routes/raw_sharing_routes.py

607 lines
19 KiB
Python
Raw Normal View History

2025-12-10 06:41:43 +00:00
"""
Raw Download Routes - Sharing Link Mode
Handles Synology sharing link downloads (với Selenium + OTP).
"""
from fastapi import APIRouter, HTTPException, Request, Response
from pydantic import BaseModel
from typing import List, Dict, Optional
import logging
import os
import uuid
import time
from collections import defaultdict
from ..services import mongodb_service, nas_service, nas_sharing_service, supabase_service, downloads_service
from ..common import get_download_destination_path
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/sharing-link", tags=["Raw Sharing"])
# ==================== RATE LIMITING ====================
# Track last request time per sharing_id to prevent NAS API rate limit errors
_last_request_time: Dict[str, float] = defaultdict(float)
_rate_limit_window_ms = 200 # Minimum 200ms between requests for same sharing_id
# ==================== REQUEST MODELS ====================
class SharingLinkRequest(BaseModel):
url: str
class SharingLinkDownloadRequest(BaseModel):
sharing_id: str
files: List[Dict] # List of file objects with name, path, isdir
ge_id: Optional[str] = None # Optional: for organizing files by project
lang: Optional[str] = None # Optional: for organizing files by project
class SharingLinkFromDbRequest(BaseModel):
ge_id: str
lang: str
class SharingOtpSubmit(BaseModel):
otp_code: str
# ==================== SHARING LINK PROCESSING ====================
@router.post('/get-from-db')
def get_sharing_link_from_db(payload: SharingLinkFromDbRequest):
"""
Query MongoDB titles_data collection to get linkRaw field.
Returns sharing link from database or throws error with record details.
"""
try:
# Query MongoDB
collection = mongodb_service.get_titles_collection()
query = {
"geId": str(payload.ge_id).strip(),
"lang": str(payload.lang).strip().upper()
}
# Find all matching records
documents = list(collection.find(query))
# Validation 1: No records found
if len(documents) == 0:
raise HTTPException(
status_code=404,
detail={
"error": "Không tìm thấy record",
"query": {"geId": payload.ge_id, "lang": payload.lang.upper()}
}
)
# Validation 2: Multiple records (should not happen with unique constraint)
if len(documents) > 1:
record_info = [
{
"geId": doc.get("geId"),
"lang": doc.get("lang"),
"linkRaw": doc.get("linkRaw"),
"path": doc.get("path")
}
for doc in documents
]
raise HTTPException(
status_code=400,
detail={
"error": "Tìm thấy nhiều hơn 1 record",
"records": record_info
}
)
# Get single record
document = documents[0]
link_raw = document.get("linkRaw")
# Validation 3: linkRaw is empty or null
if not link_raw or not isinstance(link_raw, str) or link_raw.strip() == "":
record_info = {
"geId": document.get("geId"),
"lang": document.get("lang"),
"linkRaw": link_raw,
"path": document.get("path")
}
raise HTTPException(
status_code=400,
detail={
"error": "Trường linkRaw trống hoặc null",
"record": record_info
}
)
# Validation 4: linkRaw is not a valid link
link_raw_stripped = link_raw.strip()
if not (link_raw_stripped.startswith("http://") or link_raw_stripped.startswith("https://")):
record_info = {
"geId": document.get("geId"),
"lang": document.get("lang"),
"linkRaw": link_raw,
"path": document.get("path")
}
raise HTTPException(
status_code=400,
detail={
"error": "linkRaw không phải là liên kết hợp lệ (phải bắt đầu bằng http:// hoặc https://)",
"record": record_info
}
)
# Success - return sharing link
return {
"success": True,
"sharing_link": link_raw_stripped,
"record": {
"geId": document.get("geId"),
"lang": document.get("lang"),
"path": document.get("path")
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error querying MongoDB for sharing link: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.post('/process')
def process_sharing_link(payload: SharingLinkRequest):
"""
Process sharing link to extract file list.
Returns: request_id for polling result
"""
try:
# Validate URL format
if not payload.url or 'sharing' not in payload.url:
raise HTTPException(status_code=400, detail="URL không hợp lệ")
# Submit to worker queue
result = nas_sharing_service.process_sharing_link(payload.url)
return {
"success": True,
"request_id": result['request_id'],
"status": result['status'],
"message": "Đang xử lý sharing link..."
}
except Exception as e:
logger.error(f"Error processing sharing link: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.get('/related-projects')
def get_related_projects(link_raw: str):
"""
Tìm tất cả GE projects cùng sharing link (linkRaw).
Query params:
link_raw: Sharing link URL
Returns:
{
"success": true,
"projects": [
{"ge_id": "1000", "lang": "DE"},
{"ge_id": "2000", "lang": "KO"}
],
"total": 2
}
"""
try:
if not link_raw or not link_raw.strip():
raise HTTPException(
status_code=400, detail="link_raw không được rỗng")
link_raw_normalized = link_raw.strip()
# Query MongoDB titles_data - tìm tất cả records có cùng linkRaw
collection = mongodb_service.get_titles_collection()
documents = list(collection.find(
{"linkRaw": link_raw_normalized},
{"geId": 1, "lang": 1, "_id": 0} # Chỉ lấy geId và lang
))
# Format kết quả
projects = [
{
"ge_id": doc.get("geId"),
"lang": doc.get("lang")
}
for doc in documents
]
logger.debug(
f"Found {len(projects)} projects with linkRaw: {link_raw_normalized}")
return {
"success": True,
"projects": projects,
"total": len(projects)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error querying related projects: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.get('/related-projects-by-ge')
def get_related_projects_by_ge(ge_id: str, lang: str):
"""
Tìm tất cả GE projects cùng sharing link dựa trên ge_id lang.
Query params:
ge_id: GE ID
lang: Language code
Returns:
{
"success": true,
"projects": [
{"ge_id": "1000", "lang": "DE"},
{"ge_id": "2000", "lang": "KO"}
],
"total": 2
}
"""
try:
if not ge_id or not ge_id.strip():
raise HTTPException(
status_code=400, detail="ge_id không được rỗng")
if not lang or not lang.strip():
raise HTTPException(status_code=400, detail="lang không được rỗng")
# Step 1: Get linkRaw from titles_data
link_raw = mongodb_service.get_sharing_link_from_tms_data(ge_id, lang)
if not link_raw:
return {
"success": True,
"projects": [],
"total": 0
}
# Step 2: Find all projects with same linkRaw
collection = mongodb_service.get_titles_collection()
documents = list(collection.find(
{"linkRaw": link_raw},
{"geId": 1, "lang": 1, "_id": 0}
))
# Format kết quả
projects = [
{
"ge_id": doc.get("geId"),
"lang": doc.get("lang")
}
for doc in documents
]
logger.debug(
f"Found {len(projects)} related projects for GE {ge_id} {lang}")
return {
"success": True,
"projects": projects,
"total": len(projects)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error querying related projects by GE: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.get('/project-note')
def get_project_note(ge_id: str, lang: str):
"""
Lấy note từ collection titlelist_data cho GE project cụ thể.
Query params:
ge_id: GE ID
lang: Language code
Returns:
{
"success": true,
"note": "Content of note field"
}
"""
try:
if not ge_id or not ge_id.strip():
raise HTTPException(
status_code=400, detail="ge_id không được rỗng")
if not lang or not lang.strip():
raise HTTPException(status_code=400, detail="lang không được rỗng")
# Query MongoDB titlelist_data collection
db = mongodb_service.get_db_connection()
collection = db['titlelist_data']
document = collection.find_one(
{
"geId": str(ge_id).strip(),
"lang": str(lang).strip().upper()
},
{"note": 1, "_id": 0}
)
if not document:
return {
"success": True,
"note": None
}
note_content = document.get("note")
logger.debug(f"Found note for {ge_id} {lang}: {bool(note_content)}")
return {
"success": True,
"note": note_content if note_content else None
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error querying project note: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.get('/result/{request_id}')
def get_sharing_result(request_id: str):
"""
Poll result of sharing link processing.
Returns:
- status: "pending" | "success" | "error"
- If success: sharing_id, path, files, total_files
- If error: message
"""
try:
result = nas_sharing_service.get_sharing_result(request_id)
if not result:
raise HTTPException(
status_code=404, detail="Request không tồn tại")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting sharing result: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
# ==================== FOLDER NAVIGATION ====================
@router.post('/list-folder')
def list_sharing_folder(payload: dict):
"""
List contents of a subfolder in sharing link.
Used when user double-clicks a folder in sharing link file list.
Payload:
- sharing_id: Sharing ID from initial process
- folder_path: Path to folder to list (e.g., "/subfolder_name")
Returns:
- status: "success" | "error"
- files: List of files/folders
- path: Current folder path
"""
try:
sharing_id = payload.get('sharing_id')
folder_path = payload.get('folder_path', '/')
if not sharing_id:
raise HTTPException(
status_code=400, detail="sharing_id là bắt buộc")
# CRITICAL FIX: Rate limiting to prevent Error 407 from rapid requests
current_time = time.time() * 1000 # Convert to milliseconds
last_time = _last_request_time[sharing_id]
time_since_last = current_time - last_time
if time_since_last < _rate_limit_window_ms:
wait_time = (_rate_limit_window_ms - time_since_last) / 1000
logger.warning(
f"Rate limit hit for {sharing_id}, rejecting request (wait {wait_time:.2f}s)")
raise HTTPException(
status_code=429,
detail=f"Vui lòng chậm lại, đợi {wait_time:.1f}s trước khi thao tác tiếp"
)
# Update last request time
_last_request_time[sharing_id] = current_time
# Get sharing worker instance
worker = nas_sharing_service.get_sharing_worker()
if not worker or not worker.driver:
raise HTTPException(
status_code=503, detail="Sharing worker không sẵn sàng")
# CRITICAL FIX: Lock driver to prevent race conditions
# Without lock, multiple users can navigate simultaneously and conflict
with worker.driver_lock:
# List folder using nas_sharing_api package
from ..services.nas_sharing_api import get_file_list
logger.debug(
f"📂 [Navigation] Lấy danh sách subfolder: {folder_path}")
files = get_file_list(
driver=worker.driver,
sharing_id=sharing_id,
folder_path=folder_path
)
logger.debug(
f"✅ [Navigation] Tìm thấy {len(files)} items trong: {folder_path}")
return {
"status": "success",
"files": files,
"path": folder_path,
"message": f"Tìm thấy {len(files)} item(s)"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing sharing folder: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
# ==================== DOWNLOAD ENDPOINTS ====================
@router.post('/download')
def download_sharing_files(payload: SharingLinkDownloadRequest):
"""
Download files from sharing link to NAS raw folder.
If ge_id and lang provided, files go to \\NAS_PATH\\{ge_id}_{lang}\\ (same as API download).
Otherwise, files go directly to \\NAS_PATH\\ root.
"""
try:
logger.debug(
f"Creating sharing link download job: {payload.sharing_id}, {len(payload.files)} files")
if not payload.files:
return {
"status": "error",
"message": "Không có file nào được chọn để tải xuống"
}
# Calculate destination path
if payload.ge_id and payload.lang:
# Use same logic as API download: \\172.16.14.240\\raw\\11_US\\
destination_path = get_download_destination_path(
payload.ge_id, payload.lang)
ge_id_for_db = payload.ge_id
lang_for_db = payload.lang
else:
# Fallback: direct to root (backward compatible with old logic)
destination_path = nas_service.DESTINATION_PATH
ge_id_for_db = f"SHARING_{payload.sharing_id}"
lang_for_db = "LINK"
# ✅ FIX: Use downloads_service (NEW) instead of supabase_service (OLD)
result = downloads_service.create_downloads_batch(
files=payload.files,
ge_id=ge_id_for_db,
lang=lang_for_db,
mode='sharing', # Sharing link mode
sharing_id=payload.sharing_id,
mongodb_path=None, # No MongoDB path for sharing links
destination_path=destination_path
)
if not result['success']:
return {
"status": "error",
"message": result.get('message', 'Không thể tạo batch downloads')
}
logger.debug(
f"Created sharing batch: {result['batch_id']} ({result['file_count']} files)")
return {
"status": "pending",
"message": "Batch đã được tạo và đang chờ xử lý",
"batch_id": result['batch_id'],
"download_ids": result['download_ids'],
"file_count": result['file_count'],
"destination_path": destination_path,
"sharing_id": payload.sharing_id
}
except Exception as e:
logger.error(f"Error creating sharing download job: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
# ==================== OTP HANDLING ====================
@router.get('/download-status/{batch_id}')
def get_sharing_download_status(batch_id: str):
"""
Get the status of a sharing link download batch.
Returns batch summary with all files' status.
"""
try:
summary = downloads_service.get_batch_summary(batch_id)
if not summary:
raise HTTPException(status_code=404, detail="Batch không tồn tại")
return {
"success": True,
"batch": summary
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting sharing download job status: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
# ==================== OTP HANDLING ====================
@router.get('/otp-status')
def check_sharing_otp_status():
"""
Check if sharing worker is waiting for OTP
Returns:
- otp_required: bool
"""
try:
otp_required = nas_sharing_service.is_otp_required()
return {
"otp_required": otp_required,
"message": "Vui lòng nhập mã OTP" if otp_required else "Không cần OTP"
}
except Exception as e:
logger.error(f"Error checking OTP status: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")
@router.post('/submit-otp')
def submit_sharing_otp(payload: SharingOtpSubmit):
"""
Submit OTP code for sharing link login
Args:
otp_code: OTP code from user
Returns:
- status: "success" | "error"
- message: Status message
"""
try:
status, message = nas_sharing_service.submit_otp(payload.otp_code)
if status == "success":
return {
"success": True,
"message": message
}
else:
raise HTTPException(status_code=400, detail=message)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error submitting OTP: {e}")
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}")