ge-tool/backend/services/nas_sharing_service.py
2025-12-10 13:41:43 +07:00

412 lines
16 KiB
Python
Executable File

"""
NAS Sharing Service - Public API wrapper
"""
import os
import sys
import logging
from typing import Dict, Optional, Any, Callable
from .nas_sharing_worker import SharingLinkWorker
logger = logging.getLogger(__name__)
_worker: Optional[SharingLinkWorker] = None
USE_ARIA2 = os.getenv('USE_ARIA2', 'true').lower() == 'true'
def get_worker() -> SharingLinkWorker:
global _worker
if _worker is None:
_worker = SharingLinkWorker()
_worker.start()
return _worker
def process_sharing_link(url: str) -> Dict[str, str]:
worker = get_worker()
request_id = worker.submit_request(url)
return {'request_id': request_id, 'status': 'pending'}
def get_sharing_result(request_id: str) -> Optional[Dict]:
return get_worker().get_result(request_id)
def is_otp_required() -> bool:
worker = get_worker()
# Show modal only once and before submission
if worker.otp_pending and not worker.otp_modal_shown and not worker.otp_submitted:
worker.otp_modal_shown = True
return True
return False
def submit_otp(code: str) -> Dict[str, str]:
get_worker().otp_code = code
return {'status': 'ok', 'message': 'OTP đã nhận'}
def download_file(
sharing_id: str,
file_path: str,
save_path: str,
is_folder: bool = False,
progress_callback=None,
max_speed: Optional[str] = None,
validate_link: bool = True
) -> Dict[str, Any]:
"""
Download file from sharing link.
Args:
sharing_id: Sharing ID
file_path: Remote file path
save_path: Local save path
is_folder: Whether file is a folder
progress_callback: Progress callback function
max_speed: Optional bandwidth limit (e.g., '100K')
validate_link: Validate link trước khi tải (default: True)
Returns:
Dict with status, message, save_path
"""
worker = get_worker()
try:
# Step 0: Clean up existing file to force re-download
# aria2 with continue=true will SKIP if file exists and is complete.
# We must delete it to ensure a fresh download.
# Use the shared cleanup function with delete_dirs=False (Sharing mode specific)
try:
from .nas_api.file_operations import cleanup_duplicates_before_download
file_name = os.path.basename(save_path)
dest_dir = os.path.dirname(save_path)
# Clean up duplicates AND the exact file
cleanup_duplicates_before_download(
dest_path=dest_dir,
file_name_pattern=file_name.replace(
'.zip', ''), # Basic pattern from filename
exact_filename=file_name,
delete_dirs=False
)
# Safety check: If file still exists (could not be deleted), append _NEW
if os.path.exists(save_path):
logger.warning(
f"[download_file] Could not delete existing file {save_path}, appending _NEW")
name, ext = os.path.splitext(save_path)
save_path = f"{name}_NEW{ext}"
logger.debug(f"[download_file] New save path: {save_path}")
except ImportError:
logger.warning(
"Could not import cleanup_duplicates_before_download")
# Fallback to simple delete
if os.path.exists(save_path):
try:
os.remove(save_path)
except:
pass
except Exception as e:
logger.warning(f"[download_file] Cleanup failed: {e}")
# Also delete .aria2 control file if exists (for the new path)
aria2_file = f"{save_path}.aria2"
if os.path.exists(aria2_file):
try:
os.remove(aria2_file)
logger.debug(
f"[download_file] Deleted existing .aria2 file: {aria2_file}")
except Exception as e:
logger.warning(
f"[download_file] Failed to delete .aria2 file: {e}")
# Step 1: Extract cookies and build URL (NEEDS driver lock - FAST ~1s)
with worker.driver_lock:
# Always ensure driver is ready and ALIVE (check current_url)
# Previously only checked 'if not worker.driver', which missed dead drivers
worker._ensure_driver_ready()
if not worker.driver:
return {'status': 'error', 'message': 'Worker driver failed to initialize'}
# Ensure driver is on the correct sharing page to get valid cookies
# If driver is fresh (data:,) or on another page, cookies will be missing/wrong
expected_url = f'https://disk.lezhin.com:5001/sharing/{sharing_id}'
try:
if sharing_id not in worker.driver.current_url:
logger.debug(
f"[download_file] Driver not on sharing page, navigating to: {expected_url}")
worker.driver.get(expected_url)
except Exception as e:
logger.warning(
f"[download_file] Failed to check/navigate URL: {e}")
# Try to restart driver if navigation fails
worker._ensure_driver_ready()
if worker.driver:
worker.driver.get(expected_url)
# Import here to avoid circular dependency
from .nas_sharing_api.selenium_operations import prepare_download_url, validate_download_link
# Extract cookies and build download URL (FAST - only needs driver briefly)
file_name = os.path.basename(save_path)
download_url, cookie_string = prepare_download_url(
driver=worker.driver,
sharing_id=sharing_id,
remote_path=file_path,
file_name=file_name
)
# Lock released here - driver now free for other requests!
# Step 1.5: Validate link (OPTIONAL - phát hiện link chết trước khi tải)
if validate_link:
logger.debug(
f"[download_file] Validating link before download: {file_name}")
try:
is_valid, error_msg, content_length = validate_download_link(
download_url=download_url,
cookie_string=cookie_string,
timeout=10
)
if not is_valid:
# Link đã chết → Fail ngay, không tải
logger.error(
f"[download_file] ❌ Link validation failed: {error_msg}")
return {
'status': 'error',
'message': f'Link không hợp lệ: {error_msg}',
'save_path': None
}
# Link OK → Log size nếu có
if content_length:
logger.debug(
f"[download_file] ✅ Link valid, file size: {content_length:,} bytes")
else:
logger.debug(
f"[download_file] ✅ Link valid (size unknown)")
except RuntimeError as e:
# Validation error (network/timeout) → Log warning nhưng vẫn tiếp tục tải
logger.warning(
f"[download_file] ⚠️ Link validation failed with error: {e}")
logger.warning(
f"[download_file] Continuing download anyway...")
# Step 2: Download with aria2 (NO driver lock needed - SLOW ~minutes)
from .nas_sharing_api.selenium_operations import get_aria2_manager
manager = get_aria2_manager()
logger.debug(f"[download_file] Starting aria2 download: {file_name}")
success, error_msg, gid = manager.download_file(
url=download_url,
dest_path=save_path,
cookies=cookie_string,
referer="https://disk.lezhin.com:5001/",
progress_callback=progress_callback,
max_download_limit=max_speed
)
if success:
return {
'status': 'success',
'message': 'Đã tải thành công',
'save_path': save_path,
'aria2_gid': gid # ✅ Return GID for cancellation support
}
else:
# ✅ QUAN TRỌNG: Return GID ngay cả khi fail
# Vì GID đã tồn tại từ khi aria2 task được tạo
return {
'status': 'error',
'message': f'Download failed: {error_msg}',
'save_path': None,
'aria2_gid': gid # ✅ Return GID để có thể cancel hoặc cleanup
}
except Exception as e:
logger.error(f"[download_file] Exception: {e}", exc_info=True)
return {'status': 'error', 'message': str(e), 'aria2_gid': None}
def shutdown_worker():
global _worker
if _worker:
_worker.stop()
_worker = None
def start_sharing_worker():
"""Initialize and start the sharing link worker on startup"""
get_worker() # This will create and start the worker if not exists
def get_sharing_worker() -> Optional[SharingLinkWorker]:
"""Get the global sharing worker instance (for external use)"""
return _worker
def download_sharing_files(
worker_instance: SharingLinkWorker,
sharing_id: str,
files_info: list,
dest_path: str,
job_id: Optional[str] = None,
progress_callback: Optional[Callable] = None
) -> tuple:
"""
Download multiple files from sharing link to destination
Args:
worker_instance: SharingLinkWorker instance
sharing_id: Sharing ID
files_info: List of file dicts with 'path', 'name', 'is_folder'
dest_path: Destination directory
job_id: Optional job ID for progress updates
progress_callback: Optional callback(file_index, total_files, file_progress_data)
Returns:
(status, results, message) tuple
"""
try:
import os
# Ensure driver is ready
if not worker_instance.driver:
worker_instance._ensure_driver_ready()
if not worker_instance.driver:
raise RuntimeError("Worker driver failed to initialize")
results = []
success_count = 0
total_files = len(files_info)
# Initialize files status for progress tracking
files_status = [
{
"name": f.get('name', ''),
"status": "pending",
"is_folder": f.get('is_folder', False),
"size": f.get('size_bytes', 0)
}
for f in files_info
]
for idx, file_info in enumerate(files_info):
file_path = file_info.get('path', '')
file_name = file_info.get('name', os.path.basename(file_path))
is_folder = file_info.get('is_folder', False)
file_size = file_info.get('size_bytes', 0)
# Add .zip for folders
if is_folder and not file_name.endswith('.zip'):
file_name = f"{file_name}.zip"
save_path = os.path.join(dest_path, file_name)
# Update status to downloading
files_status[idx]["status"] = "downloading"
files_status[idx]["progress"] = 0
print(
f"[Download] ({idx + 1}/{total_files}) {file_name}{save_path}")
# Progress callback for individual file
def file_progress_callback(downloaded_bytes: int, total_bytes: int):
# Update progress for both files and folders
files_status[idx]["downloaded"] = downloaded_bytes
if total_bytes > 0:
# File with known size - calculate percentage
progress_pct = (downloaded_bytes / total_bytes) * 100
files_status[idx]["progress"] = round(progress_pct, 1)
files_status[idx]["total"] = total_bytes
else:
# Folder (no total size) - just track downloaded bytes
files_status[idx]["progress"] = None
files_status[idx]["total"] = None
# Call parent progress callback
if progress_callback:
progress_callback(idx, total_files, {
"current_file": file_name,
"current_file_index": idx + 1,
"total_files": total_files,
"current_file_progress": files_status[idx].get("progress"),
"current_file_downloaded": downloaded_bytes,
"current_file_total": total_bytes if total_bytes > 0 else None,
"files_status": files_status
})
# Download (always use aria2)
try:
# Step 1: Extract cookies and build URL WITH VALIDATION (LOCK driver briefly ~1s)
with worker_instance.driver_lock:
# Ensure driver is on correct sharing page before extracting cookies
expected_url = f'https://disk.lezhin.com:5001/sharing/{sharing_id}'
if sharing_id not in worker_instance.driver.current_url:
logger.debug(
f"[download_sharing_files] Driver not on sharing page, navigating to: {expected_url}")
worker_instance.driver.get(expected_url)
from .nas_sharing_api.selenium_operations import prepare_download_url
download_url, cookie_string = prepare_download_url(
driver=worker_instance.driver,
sharing_id=sharing_id,
remote_path=file_path,
file_name=file_name
)
# Lock released - driver free for other requests!
# Step 2: Download with aria2 (NO lock - allows parallel downloads)
from .nas_sharing_api.selenium_operations import get_aria2_manager
manager = get_aria2_manager()
success, error_msg, gid = manager.download_file(
url=download_url,
dest_path=save_path,
cookies=cookie_string,
referer="https://disk.lezhin.com:5001/",
progress_callback=file_progress_callback
)
if not success:
raise RuntimeError(f"aria2 download failed: {error_msg}")
except Exception as e:
import traceback
traceback.print_exc()
success = False
# Update status after download
if success:
files_status[idx]["status"] = "completed"
files_status[idx]["progress"] = 100
success_count += 1
else:
files_status[idx]["status"] = "failed"
results.append({
'name': file_name,
'path': file_path,
'success': success,
'destination': save_path if success else None
})
if success_count == len(files_info):
return ("success", results, f"Downloaded {success_count}/{len(files_info)} files")
elif success_count > 0:
return ("partial", results, f"Downloaded {success_count}/{len(files_info)} files")
else:
return ("failed", results, "All downloads failed")
except Exception as e:
import traceback
traceback.print_exc()
return ("error", [], str(e))