""" Upload Blueprint Handles file upload and processing initiation endpoints. """ import uuid import threading from pathlib import Path from flask import Blueprint, request, current_app from werkzeug.utils import secure_filename import yaml from io import BytesIO from ..app import process_data_async from ..shared import _format_response, log_performance, logger, ALLOWED_DATA_EXTENSIONS, ALLOWED_CONFIG_EXTENSIONS, allowed_file, update_task_status, TASK_STATUS_PENDING, TASK_STATUS_FAILED from ..auth import require_api_key # Create blueprint upload_bp = Blueprint('upload', __name__, url_prefix='/upload') @upload_bp.route('', methods=['POST']) @require_api_key @log_performance def upload_file(): logger.info("Received upload request") logger.info(f"Request content length: {request.content_length} bytes") # Check if data file is present if 'file' not in request.files: logger.warning("Upload failed: No data file part in request") return _format_response(400, "未找到数据文件部分") data_file = request.files['file'] config_file = request.files.get('config') # Log file details logger.info(f"Data file: {data_file.filename} (size: {getattr(data_file, 'content_length', 'unknown')} bytes)") if config_file: logger.info(f"Config file: {config_file.filename} (size: {getattr(config_file, 'content_length', 'unknown')} bytes)") else: logger.info("No custom config file provided, will use default") if data_file.filename == '': logger.warning("Upload failed: No data file selected (empty filename)") return _format_response(400, "未选择数据文件") if not allowed_file(data_file.filename, ALLOWED_DATA_EXTENSIONS): logger.warning(f"Upload failed: Invalid data file type {data_file.filename} - allowed: {ALLOWED_DATA_EXTENSIONS}") return _format_response(400, "无效的数据文件类型。只允许 .xlsx 和 .xls 格式。") # Generate unique job ID task_id = str(uuid.uuid4()) logger.info(f"Generated job ID: {task_id}") # 1) Parse config content (parse in memory without saving first) if config_file and config_file.filename != '': if not allowed_file(config_file.filename, ALLOWED_CONFIG_EXTENSIONS): return _format_response(400, "无效的配置文件类型。只允许 .yaml 和 .yml 格式。") config_file.stream.seek(0) config_text = config_file.read().decode('utf-8', errors='ignore') try: active_config = yaml.safe_load(config_text) except Exception: return _format_response(400, "配置文件解析失败") # Reset stream for saving config_file.stream = BytesIO(config_text.encode('utf-8')) else: default_config_path = Path(__file__).parent.parent / "gasflux_config.yaml" with open(default_config_path, 'r', encoding='utf-8') as f: active_config = yaml.safe_load(f) # 2) Create job directories based on INI configuration upload_base = Path(current_app.config['UPLOAD_FOLDER']) output_base = Path(current_app.config['OUTPUT_FOLDER']) job_upload_dir = upload_base / task_id job_output_dir = output_base / task_id job_upload_dir.mkdir(parents=True, exist_ok=True) job_output_dir.mkdir(parents=True, exist_ok=True) logger.info(f"Job {task_id}: Created directories - Upload: {job_upload_dir}, Output: {job_output_dir}") # 3) Save data file to job_upload_dir data_filename = secure_filename(data_file.filename) data_path = job_upload_dir / data_filename try: data_file.seek(0) data_file.save(str(data_path)) logger.info(f"Job {task_id}: Data file saved successfully - Path: {data_path}") except Exception as e: logger.error(f"Job {task_id}: Failed to save data file {data_filename}: {str(e)}") return _format_response(500, "保存数据文件失败") # 4) Save config file to job_upload_dir if config_file and config_file.filename != '': config_filename = secure_filename(config_file.filename) config_path = job_upload_dir / config_filename try: config_file.seek(0) config_file.save(str(config_path)) active_config_path = config_path logger.info(f"Job {task_id}: Custom config saved successfully - Path: {config_path}") except Exception as e: logger.error(f"Job {task_id}: Failed to save config file {config_filename}: {str(e)}") return _format_response(500, "保存配置文件失败") else: # Copy default config for record keeping config_path = job_upload_dir / "config.yaml" with open(config_path, 'w', encoding='utf-8') as f: yaml.safe_dump(active_config, f, allow_unicode=True) active_config_path = config_path logger.info(f"Job {task_id}: Default config saved for record - Path: {config_path}") # Initialize task status update_task_status(task_id, TASK_STATUS_PENDING, "任务已加入处理队列", output_dir=str(job_output_dir)) logger.info(f"Job {task_id}: Task status initialized as PENDING") # Start background processing try: thread = threading.Thread( target=process_data_async, args=(task_id, data_path, active_config_path, job_output_dir) ) thread.daemon = True thread.start() logger.info(f"Job {task_id}: Background processing thread started successfully") except Exception as e: logger.error(f"Job {task_id}: Failed to start background processing thread: {str(e)}") update_task_status(task_id, TASK_STATUS_FAILED, error=str(e)) return _format_response(500, "启动处理失败") logger.info(f"Job {task_id}: Upload process completed successfully, returning job ID to client") return _format_response(202, "任务已接受并加入处理队列", { "status": "accepted", "task_id": task_id, "task_status_url": f"/task/{task_id}" })