一、为什么需要文件上传下载功能

在日常开发中,文件上传下载是最常见的需求之一。比如用户头像上传、Excel报表导出、图片分享等功能,都离不开文件操作。Flask作为轻量级Web框架,提供了简单灵活的方式来实现这些功能。

传统实现方式往往直接把文件存在服务器本地,但随着业务发展,这种方式会遇到存储空间不足、文件管理混乱等问题。我们需要更高效、更可靠的技术方案。

二、Flask文件上传的基础实现

让我们先看一个最简单的文件上传示例。这个例子展示了如何接收用户上传的文件并保存到服务器。

技术栈:Python + Flask

from flask import Flask, request, redirect, url_for
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'  # 设置上传文件保存目录

# 允许上传的文件类型
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    """检查文件扩展名是否合法"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # 检查是否有文件被上传
        if 'file' not in request.files:
            return redirect(request.url)
        
        file = request.files['file']
        
        # 如果用户没有选择文件,浏览器可能会提交一个空文件
        if file.filename == '':
            return redirect(request.url)
        
        # 检查文件类型和文件名安全性
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return '文件上传成功!'
    
    # GET请求时返回上传表单
    return '''
    <!doctype html>
    <title>上传文件</title>
    <h1>上传文件</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=上传>
    </form>
    '''

这个例子中有几个关键点需要注意:

  1. 使用secure_filename确保文件名安全,防止路径遍历攻击
  2. 限制允许上传的文件类型,避免危险文件上传
  3. 检查是否有文件实际被上传
  4. 设置专门的上传目录,便于管理

三、提升文件上传的性能与可靠性

基础实现虽然简单,但在实际生产环境中可能会遇到性能问题。下面我们来看几个优化方案。

3.1 使用流式处理大文件

对于大文件上传,直接读取整个文件到内存会消耗大量资源。Flask提供了流式处理的方式:

@app.route('/upload-stream', methods=['POST'])
def upload_stream():
    def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
        # 自定义文件保存路径
        save_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(filename))
        return open(save_path, 'wb')  # 以二进制写入模式打开文件
    
    # 使用流式处理
    storage = request.files['file'].stream
    with custom_stream_factory(None, request.files['file'].filename, None) as f:
        while True:
            chunk = storage.read(8192)  # 每次读取8KB
            if not chunk:
                break
            f.write(chunk)
    return '大文件上传成功!'

3.2 文件分块上传

对于超大文件,可以考虑分块上传技术:

@app.route('/upload-chunk', methods=['POST'])
def upload_chunk():
    file = request.files['file']
    chunk_index = request.form.get('chunk_index')
    total_chunks = request.form.get('total_chunks')
    file_id = request.form.get('file_id')
    
    # 为每个文件创建临时目录
    temp_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'temp', file_id)
    os.makedirs(temp_dir, exist_ok=True)
    
    # 保存当前分块
    chunk_path = os.path.join(temp_dir, f'chunk_{chunk_index}')
    file.save(chunk_path)
    
    # 检查是否所有分块都已上传
    if int(chunk_index) == int(total_chunks) - 1:
        # 合并所有分块
        final_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(file_id))
        with open(final_path, 'wb') as f:
            for i in range(int(total_chunks)):
                chunk_path = os.path.join(temp_dir, f'chunk_{i}')
                with open(chunk_path, 'rb') as cf:
                    f.write(cf.read())
        # 清理临时文件
        shutil.rmtree(temp_dir)
        return '文件上传并合并完成!'
    
    return f'分块 {chunk_index} 上传成功!'

四、高效的文件下载方案

文件下载同样需要考虑性能和安全性。下面介绍几种常见的下载方式。

4.1 基本文件下载

from flask import send_from_directory

@app.route('/download/<filename>')
def download_file(filename):
    # 确保文件名安全
    safe_filename = secure_filename(filename)
    # 从上传目录发送文件
    return send_from_directory(app.config['UPLOAD_FOLDER'], safe_filename, as_attachment=True)

4.2 大文件下载优化

对于大文件下载,可以使用生成器实现流式下载:

@app.route('/download-large/<filename>')
def download_large_file(filename):
    safe_filename = secure_filename(filename)
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
    
    def generate():
        with open(file_path, 'rb') as f:
            while True:
                chunk = f.read(8192)  # 每次读取8KB
                if not chunk:
                    break
                yield chunk
                
    response = Response(generate())
    response.headers['Content-Disposition'] = f'attachment; filename={safe_filename}'
    return response

4.3 文件下载限速

为了防止服务器带宽被占满,可以限制下载速度:

@app.route('/download-limited/<filename>')
def download_limited(filename):
    safe_filename = secure_filename(filename)
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
    
    def generate():
        with open(file_path, 'rb') as f:
            while True:
                chunk = f.read(4096)  # 每次读取4KB
                if not chunk:
                    break
                yield chunk
                time.sleep(0.1)  # 通过sleep限制速度
    
    response = Response(generate())
    response.headers['Content-Disposition'] = f'attachment; filename={safe_filename}'
    return response

五、进阶技巧与最佳实践

5.1 使用云存储服务

对于生产环境,建议使用云存储服务如AWS S3、阿里云OSS等:

import boto3
from botocore.exceptions import NoCredentialsError

@app.route('/upload-s3', methods=['POST'])
def upload_to_s3():
    if 'file' not in request.files:
        return '没有文件上传', 400
    
    file = request.files['file']
    if file.filename == '':
        return '没有选择文件', 400
    
    s3 = boto3.client('s3',
                      aws_access_key_id='YOUR_ACCESS_KEY',
                      aws_secret_access_key='YOUR_SECRET_KEY')
    
    try:
        s3.upload_fileobj(file, 'your-bucket-name', secure_filename(file.filename))
        return '文件上传到S3成功!'
    except NoCredentialsError:
        return '凭证错误', 500

5.2 文件上传进度显示

前端可以通过JavaScript配合后端API实现上传进度显示:

@app.route('/upload-with-progress', methods=['POST'])
def upload_with_progress():
    if 'file' not in request.files:
        return jsonify({'error': '没有文件上传'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': '没有选择文件'}), 400
    
    # 在实际应用中,这里可以保存进度到数据库或Redis
    # 前端可以通过轮询另一个API获取进度
    
    filename = secure_filename(file.filename)
    file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    
    return jsonify({'status': 'complete', 'filename': filename})

六、安全注意事项

  1. 始终使用secure_filename处理用户提供的文件名
  2. 限制上传文件类型,避免可执行文件上传
  3. 为上传文件设置合理的权限
  4. 考虑对上传文件进行病毒扫描
  5. 对敏感文件下载进行身份验证和权限检查
  6. 设置文件大小限制,防止DoS攻击

七、总结与建议

Flask提供了灵活的文件上传下载功能,但在实际应用中需要考虑更多因素:

  1. 对于小文件,直接使用Flask内置功能即可
  2. 大文件应考虑流式处理和分块上传
  3. 生产环境建议使用云存储服务
  4. 始终把安全性放在首位
  5. 根据业务需求选择合适的方案

通过合理的技术选型和优化,可以构建出高效可靠的文件处理系统,满足各种业务场景需求。