You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flask Flash消息不显示:S3文件上传进度无法在前端展示

问题根源与修复方案

核心问题分析

你当前的实现无法实时显示进度,本质是三个关键错误导致:

  1. 同步请求阻塞响应:dashboard_post是同步处理上传,整个请求会等到上传完成才返回页面响应。Flash消息存储在用户session中,只有请求结束、页面重新渲染时才能读取,上传过程中的消息根本无法被前端获取。
  2. 错误的请求上下文:回调函数里用app.test_request_context()创建的是模拟请求上下文,和用户提交表单的真实请求上下文完全无关,调用flash()时消息不会存入用户真实的session,前端自然拿不到。
  3. 前端无实时更新机制:表单提交后页面处于等待状态,不会主动拉取新的进度数据,就算后端能正确存储进度,前端也无法实时展示。

修复方案(两种可选)

要实现实时进度显示,必须放弃「同步请求+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>

额外注意事项

  1. 生产环境中,内存存储进度会有进程重启丢失的风险,建议用Redis等持久化存储替代。
  2. 异步上传完成后,记得删除服务器上临时保存的文件,避免磁盘占用。
  3. 要处理用户会话与任务ID的关联,避免不同用户看到彼此的上传进度。

内容的提问来源于stack exchange,提问作者jfst

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 05:10:39