402 lines
13 KiB
Python
Executable File
402 lines
13 KiB
Python
Executable File
"""
|
|
Downloads Routes - File-centric RESTful API for download management.
|
|
Each endpoint operates on individual file downloads, not batches.
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from typing import List, Dict, Optional
|
|
import logging
|
|
|
|
from ..services import downloads_service
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api", tags=["Downloads"])
|
|
|
|
|
|
# ==================== REQUEST MODELS ====================
|
|
|
|
class FileInfo(BaseModel):
|
|
"""Single file information."""
|
|
name: str
|
|
path: str
|
|
isdir: bool = False
|
|
is_folder: Optional[bool] = None # Alias for isdir
|
|
|
|
|
|
class CreateBatchRequest(BaseModel):
|
|
"""Request to create a batch of file downloads (API mode)."""
|
|
files: List[FileInfo]
|
|
ge_id: str
|
|
lang: str
|
|
|
|
|
|
class CreateSharingBatchRequest(BaseModel):
|
|
"""Request to create batch downloads from sharing link."""
|
|
sharing_id: str
|
|
files: List[FileInfo]
|
|
ge_id: Optional[str] = None
|
|
lang: Optional[str] = None
|
|
|
|
|
|
class UpdateDownloadRequest(BaseModel):
|
|
"""Request to update a single download."""
|
|
action: str # "retry" or "cancel"
|
|
|
|
|
|
# ==================== DOWNLOAD ENDPOINTS ====================
|
|
|
|
@router.get('/downloads')
|
|
def get_all_downloads(
|
|
status: Optional[str] = Query(None, description="Filter by status"),
|
|
mode: Optional[str] = Query(
|
|
None, description="Filter by mode (api/sharing)"),
|
|
limit: int = Query(100, description="Max number of downloads to return")
|
|
):
|
|
"""
|
|
Get all file downloads with optional filtering.
|
|
|
|
Query params:
|
|
- status: pending, downloading, completed, failed, cancelled
|
|
- mode: api, sharing
|
|
- limit: Max results (default: 100)
|
|
|
|
Returns list of individual file downloads (not batched).
|
|
Frontend groups by batch_id for display.
|
|
"""
|
|
try:
|
|
downloads = downloads_service.get_all_downloads(
|
|
status=status,
|
|
mode=mode,
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"downloads": downloads,
|
|
"count": len(downloads)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting downloads: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.get('/downloads/{download_id}')
|
|
def get_download_by_id(download_id: int):
|
|
"""Get a single file download by ID."""
|
|
try:
|
|
download = downloads_service.get_download_by_id(download_id)
|
|
|
|
if not download:
|
|
raise HTTPException(
|
|
status_code=404, detail="Download không tồn tại")
|
|
|
|
return {
|
|
"success": True,
|
|
"download": download
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting download {download_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.get('/batches/{batch_id}')
|
|
def get_batch_downloads(batch_id: str):
|
|
"""Get all downloads in a specific batch."""
|
|
try:
|
|
downloads = downloads_service.get_downloads_by_batch(batch_id)
|
|
summary = downloads_service.get_batch_summary(batch_id)
|
|
|
|
if not downloads:
|
|
raise HTTPException(status_code=404, detail="Batch không tồn tại")
|
|
|
|
return {
|
|
"success": True,
|
|
"batch": summary,
|
|
"downloads": downloads,
|
|
"count": len(downloads)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting batch {batch_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.post('/batches/api')
|
|
def create_api_batch(payload: CreateBatchRequest):
|
|
"""
|
|
Create a batch of downloads for API mode (direct NAS access).
|
|
Each file becomes a separate download record.
|
|
"""
|
|
try:
|
|
from ..common import get_download_destination_path
|
|
from ..services import mongodb_service
|
|
|
|
logger.debug(
|
|
f"Creating API batch: {payload.ge_id}_{payload.lang}, {len(payload.files)} files")
|
|
|
|
if not payload.files:
|
|
raise HTTPException(
|
|
status_code=400, detail="Không có file nào được chọn")
|
|
|
|
# Get MongoDB path
|
|
mongodb_path = mongodb_service.get_path_from_tms_data(
|
|
payload.ge_id, payload.lang)
|
|
|
|
# Calculate destination
|
|
destination_path = get_download_destination_path(
|
|
payload.ge_id, payload.lang)
|
|
|
|
# Convert FileInfo to dicts
|
|
files_data = [
|
|
{
|
|
"name": f.name,
|
|
"path": f.path,
|
|
"isdir": f.isdir or f.is_folder or False
|
|
}
|
|
for f in payload.files
|
|
]
|
|
|
|
# Create batch
|
|
result = downloads_service.create_downloads_batch(
|
|
files=files_data,
|
|
ge_id=payload.ge_id,
|
|
lang=payload.lang,
|
|
mode='api',
|
|
mongodb_path=mongodb_path,
|
|
destination_path=destination_path
|
|
)
|
|
|
|
if not result["success"]:
|
|
raise HTTPException(status_code=500, detail=result["message"])
|
|
|
|
logger.debug(
|
|
f"Created API batch {result['batch_id']}: {result['file_count']} files")
|
|
|
|
return {
|
|
"success": True,
|
|
"batch_id": result["batch_id"],
|
|
"download_ids": result["download_ids"],
|
|
"file_count": result["file_count"],
|
|
"destination_path": destination_path,
|
|
"mongodb_path": mongodb_path,
|
|
"message": f"Đã tạo {result['file_count']} downloads"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error creating API batch: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.post('/batches/sharing')
|
|
def create_sharing_batch(payload: CreateSharingBatchRequest):
|
|
"""
|
|
Create a batch of downloads for Sharing mode (sharing link).
|
|
Each file becomes a separate download record.
|
|
"""
|
|
try:
|
|
from ..common import get_download_destination_path
|
|
from ..services import nas_service
|
|
|
|
logger.debug(
|
|
f"Creating Sharing batch: {payload.sharing_id}, {len(payload.files)} files")
|
|
|
|
if not payload.files:
|
|
raise HTTPException(
|
|
status_code=400, detail="Không có file nào được chọn")
|
|
|
|
# Determine GE ID and destination
|
|
if payload.ge_id and payload.lang:
|
|
from ..services import mongodb_service
|
|
destination_path = get_download_destination_path(
|
|
payload.ge_id, payload.lang)
|
|
# For sharing mode, mongodb_path = sharing link (linkRaw) from MongoDB
|
|
mongodb_path = mongodb_service.get_sharing_link_from_tms_data(
|
|
payload.ge_id, payload.lang)
|
|
ge_id = payload.ge_id
|
|
lang = payload.lang
|
|
else:
|
|
destination_path = nas_service.DESTINATION_PATH
|
|
mongodb_path = None
|
|
ge_id = f"SHARING_{payload.sharing_id}"
|
|
lang = "LINK"
|
|
|
|
# Convert FileInfo to dicts
|
|
files_data = [
|
|
{
|
|
"name": f.name,
|
|
"path": f.path,
|
|
"isdir": f.isdir or f.is_folder or False
|
|
}
|
|
for f in payload.files
|
|
]
|
|
|
|
# Create batch
|
|
result = downloads_service.create_downloads_batch(
|
|
files=files_data,
|
|
ge_id=ge_id,
|
|
lang=lang,
|
|
mode='sharing',
|
|
sharing_id=payload.sharing_id,
|
|
mongodb_path=mongodb_path,
|
|
destination_path=destination_path
|
|
)
|
|
|
|
if not result["success"]:
|
|
raise HTTPException(status_code=500, detail=result["message"])
|
|
|
|
logger.debug(
|
|
f"Created Sharing batch {result['batch_id']}: {result['file_count']} files")
|
|
|
|
return {
|
|
"success": True,
|
|
"batch_id": result["batch_id"],
|
|
"download_ids": result["download_ids"],
|
|
"file_count": result["file_count"],
|
|
"destination_path": destination_path,
|
|
"sharing_id": payload.sharing_id,
|
|
"message": f"Đã tạo {result['file_count']} downloads"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error creating Sharing batch: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.patch('/downloads/{download_id}')
|
|
def update_download(download_id: int, payload: UpdateDownloadRequest):
|
|
"""
|
|
Update a file download (cancel or retry).
|
|
|
|
Actions:
|
|
- "cancel": Cancel pending/downloading file
|
|
- "retry": Retry failed file
|
|
"""
|
|
try:
|
|
if payload.action == "cancel":
|
|
success = downloads_service.cancel_download(download_id)
|
|
|
|
if success:
|
|
return {
|
|
"success": True,
|
|
"message": f"Download {download_id} đã được hủy"
|
|
}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=404, detail="Download không tồn tại")
|
|
|
|
elif payload.action == "retry":
|
|
success = downloads_service.retry_download(download_id)
|
|
|
|
if success:
|
|
return {
|
|
"success": True,
|
|
"message": f"Download {download_id} đã được đưa vào queue"
|
|
}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=404, detail="Download không tồn tại")
|
|
|
|
else:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Action không hợp lệ: {payload.action}. Chỉ chấp nhận 'cancel' hoặc 'retry'"
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error updating download {download_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.delete('/downloads/{download_id}')
|
|
def delete_download(download_id: int):
|
|
"""
|
|
Delete a file download record.
|
|
Only completed/failed/cancelled downloads can be deleted.
|
|
"""
|
|
try:
|
|
# Check download exists and status
|
|
download = downloads_service.get_download_by_id(download_id)
|
|
|
|
if not download:
|
|
raise HTTPException(
|
|
status_code=404, detail="Download không tồn tại")
|
|
|
|
# Only delete terminal status
|
|
if download["status"] not in ["completed", "failed", "cancelled"]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Chỉ có thể xóa downloads đã hoàn thành hoặc thất bại"
|
|
)
|
|
|
|
success = downloads_service.delete_download(download_id)
|
|
|
|
if success:
|
|
return {
|
|
"success": True,
|
|
"message": f"Download {download_id} đã được xóa"
|
|
}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=500, detail="Không thể xóa download")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error deleting download {download_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|
|
|
|
|
|
@router.delete('/batches/{batch_id}')
|
|
def delete_batch(batch_id: str):
|
|
"""
|
|
Delete all downloads in a batch.
|
|
Only works if all downloads are in terminal status.
|
|
"""
|
|
try:
|
|
# Check all downloads in batch
|
|
downloads = downloads_service.get_downloads_by_batch(batch_id)
|
|
|
|
if not downloads:
|
|
raise HTTPException(status_code=404, detail="Batch không tồn tại")
|
|
|
|
# Verify all are terminal
|
|
active_count = sum(
|
|
1 for d in downloads
|
|
if d["status"] in ["pending", "downloading"]
|
|
)
|
|
|
|
if active_count > 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Batch còn {active_count} downloads đang active"
|
|
)
|
|
|
|
success = downloads_service.delete_batch(batch_id)
|
|
|
|
if success:
|
|
return {
|
|
"success": True,
|
|
"message": f"Batch {batch_id} đã được xóa ({len(downloads)} files)"
|
|
}
|
|
else:
|
|
raise HTTPException(status_code=500, detail="Không thể xóa batch")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error deleting batch {batch_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {str(e)}")
|