Flask Flash消息不显示:S3文件上传进度无法在前端展示
问题根源与修复方案
核心问题分析
你当前的实现无法实时显示进度,本质是三个关键错误导致:
- 同步请求阻塞响应:
dashboard_post是同步处理上传,整个请求会等到上传完成才返回页面响应。Flash消息存储在用户session中,只有请求结束、页面重新渲染时才能读取,上传过程中的消息根本无法被前端获取。 - 错误的请求上下文:回调函数里用
app.test_request_context()创建的是模拟请求上下文,和用户提交表单的真实请求上下文完全无关,调用flash()时消息不会存入用户真实的session,前端自然拿不到。 - 前端无实时更新机制:表单提交后页面处于等待状态,不会主动拉取新的进度数据,就算后端能正确存储进度,前端也无法实时展示。
修复方案(两种可选)
要实现实时进度显示,必须放弃「同步请求+Flash」的思路,改用异步上传配合前端实时获取进度的方式。
方案1:WebSocket实时推送(推荐,低延迟)
后端代码修改
from flask_socketio import SocketIO, emit import threading import sys import os from flask import jsonify, request from werkzeug.utils import secure_filename import boto3 # 初始化SocketIO,解决跨域问题 socketio = SocketIO(app, cors_allowed_origins="*") class ProgressPercentage(object): def __init__(self, filename, session_id): self._filename = filename self._size = float(os.path.getsize(filename)) self._seen_so_far = 0 self._lock = threading.Lock() self.session_id = session_id # 绑定用户的Socket会话ID def __call__(self, bytes_amount): with self._lock: self._seen_so_far += bytes_amount percentage = round((self._seen_so_far / self._size) * 100, 2) # 终端输出进度(保留原有逻辑) sys.stdout.write( "\r%s %s / %s (%.2f%%)" % ( self._filename, self._seen_so_far, self._size, percentage)) sys.stdout.flush() # 通过WebSocket推送进度到前端 socketio.emit('upload_progress', { 'filename': self._filename, 'percentage': percentage }, room=self.session_id) @app.route('/dashboard', methods=['POST']) @login_required def dashboard_post(): s3_client = boto3.client('s3', aws_access_key_id=AWS_KEY, aws_secret_access_key=AWS_SECRET_KEY) file = request.files['input_file'] session_id = request.form.get('session_id') # 获取前端传来的会话ID if file: filename = secure_filename(file.filename) file.save(filename) # 把上传逻辑放到线程,避免阻塞请求响应 threading.Thread(target=lambda: s3_client.upload_file( Bucket='deepstream-input', Filename=filename, Key=filename, Callback=ProgressPercentage(filename, session_id) )).start() # 立即返回响应,不等待上传完成 return jsonify({'status': 'upload_started'})
前端代码修改
<form id="upload-form" method="POST" enctype="multipart/form-data"> <label class="custom-file-upload"> <br><br> <i class="fas fa-cloud-upload-alt"></i> <input id="file-upload" type="file" name="input_file" accept="video/*"/> <p>Zu verfremdende Datei auswählen</p> </label> </form> <section class="progress-area" id="progress-area"> <!-- 进度条将在这里动态生成 --> </section> <!-- 引入SocketIO客户端 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> <script> const socket = io(); const form = document.getElementById('upload-form'); const progressArea = document.getElementById('progress-area'); form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); // 把当前Socket会话ID传给后端 formData.append('session_id', socket.id); // 发送上传请求 await fetch('/dashboard', { method: 'POST', body: formData }); }); // 监听后端推送的进度消息 socket.on('upload_progress', (data) => { let progressItem = document.querySelector(`[data-filename="${data.filename}"]`); if (!progressItem) { // 创建新的进度条元素 progressItem = document.createElement('li'); progressItem.className = 'row'; progressItem.dataset.filename = data.filename; progressItem.innerHTML = ` <i class="fas fa-file-alt"></i> <div class="content"> <div class="details"> <span class="name">${data.filename} • Uploading</span> <span class="percent">${data.percentage}%</span> </div> <div class="progress-bar"> <div class="progress" style="width: ${data.percentage}%"></div> </div> </div> `; progressArea.appendChild(progressItem); } else { // 更新现有进度条 const percentSpan = progressItem.querySelector('.percent'); const progressBar = progressItem.querySelector('.progress'); percentSpan.textContent = `${data.percentage}%`; progressBar.style.width = `${data.percentage}%`; } }); </script>
方案2:前端轮询(无需WebSocket,实现简单)
后端代码修改
import uuid import threading import sys import os from flask import jsonify, request from werkzeug.utils import secure_filename import boto3 # 内存存储进度(生产环境建议用Redis替代,避免进程重启丢失数据) upload_progress = {} progress_lock = threading.Lock() class ProgressPercentage(object): def __init__(self, filename, task_id): self._filename = filename self._size = float(os.path.getsize(filename)) self._seen_so_far = 0 self._lock = threading.Lock() self.task_id = task_id def __call__(self, bytes_amount): with self._lock: self._seen_so_far += bytes_amount percentage = round((self._seen_so_far / self._size) * 100, 2) # 终端输出进度 sys.stdout.write( "\r%s %s / %s (%.2f%%)" % ( self._filename, self._seen_so_far, self._size, percentage)) sys.stdout.flush() # 更新进度存储 with progress_lock: upload_progress[self.task_id] = { 'filename': self._filename, 'percentage': percentage } @app.route('/dashboard', methods=['POST']) @login_required def dashboard_post(): s3_client = boto3.client('s3', aws_access_key_id=AWS_KEY, aws_secret_access_key=AWS_SECRET_KEY) file = request.files['input_file'] task_id = uuid.uuid4().hex # 生成唯一任务ID if file: filename = secure_filename(file.filename) file.save(filename) threading.Thread(target=lambda: s3_client.upload_file( Bucket='deepstream-input', Filename=filename, Key=filename, Callback=ProgressPercentage(filename, task_id) )).start() return jsonify({'status': 'upload_started', 'task_id': task_id}) @app.route('/progress/<task_id>') @login_required def get_progress(task_id): with progress_lock: progress = upload_progress.get(task_id, {'percentage': 0}) return jsonify(progress)
前端代码修改
<form id="upload-form" method="POST" enctype="multipart/form-data"> <label class="custom-file-upload"> <br><br> <i class="fas fa-cloud-upload-alt"></i> <input id="file-upload" type="file" name="input_file" accept="video/*"/> <p>Zu verfremdende Datei auswählen</p> </label> </form> <section class="progress-area" id="progress-area"> </section> <script> const form = document.getElementById('upload-form'); const progressArea = document.getElementById('progress-area'); let pollInterval = null; form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); const response = await fetch('/dashboard', { method: 'POST', body: formData }); const data = await response.json(); // 每500ms查询一次进度 pollInterval = setInterval(async () => { const progressResponse = await fetch(`/progress/${data.task_id}`); const progressData = await progressResponse.json(); let progressItem = document.querySelector(`[data-filename="${progressData.filename}"]`); if (!progressItem) { progressItem = document.createElement('li'); progressItem.className = 'row'; progressItem.dataset.filename = progressData.filename; progressItem.innerHTML = ` <i class="fas fa-file-alt"></i> <div class="content"> <div class="details"> <span class="name">${progressData.filename} • Uploading</span> <span class="percent">${progressData.percentage}%</span> </div> <div class="progress-bar"> <div class="progress" style="width: ${progressData.percentage}%"></div> </div> </div> `; progressArea.appendChild(progressItem); } else { const percentSpan = progressItem.querySelector('.percent'); const progressBar = progressItem.querySelector('.progress'); percentSpan.textContent = `${progressData.percentage}%`; progressBar.style.width = `${progressData.percentage}%`; // 上传完成后停止轮询 if (progressData.percentage >= 100) { clearInterval(pollInterval); percentSpan.textContent = '100% • Completed'; } } }, 500); }); </script>
额外注意事项
- 生产环境中,内存存储进度会有进程重启丢失的风险,建议用Redis等持久化存储替代。
- 异步上传完成后,记得删除服务器上临时保存的文件,避免磁盘占用。
- 要处理用户会话与任务ID的关联,避免不同用户看到彼此的上传进度。
内容的提问来源于stack exchange,提问作者jfst
相关产品推荐
相关产品推荐

