文件上传源码

app.py

import os
import uuid
import magic
import logging
from typing import Optional
from flask import Flask, request, flash, redirect, url_for, render_template
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.utils import secure_filename


class Config:
    UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "uploads")
    MAX_CONTENT_LENGTH = int(os.environ.get("MAX_CONTENT_LENGTH", 16*1024*1024))
    SECRET_KEY = os.environ.get("SECRET_KEY", "5bb02fb755fa57b729f016e20c9c2cfe7e30a585334cc073")
    ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
    ALLOWED_MIMETYPES = {'text/plain', 'image/jpeg', 'image/png', 'image/gif', 'application/pdf'}
    EXTENSION_MIME_TYPE = {
        'txt': 'text/plain',
        'pdf': 'application/pdf',
        'png': 'image/png',
        'jpg': 'image/jpeg',
        'jpeg': 'image/jpeg',
        'gif': 'image/gif'
    }
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"
    # SESSION_COOKIE_SECURE = True

app = Flask(__name__)
app.config.from_object(Config)

logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@app.errorhandler(413)
@app.errorhandler(RequestEntityTooLarge)
def handle_file_too_large(e):
    flash('File is to large!MAXIMUM SIZE is 16MB.')
    return redirect(url_for('upload_file'))


def allowed_file(filename : str) -> bool:
    return ('.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'])

def detect_mime(file_storage : FileStorage) -> Optional[str]:
    try:
        file_head = file_storage.stream.read(2048)
        file_storage.stream.seek(0)
        return magic.from_buffer(file_head, mime=True)
    except Exception as e:
        logger.warning("MIME检测失败:%s",e)
        return None


@app.route('/', methods=['GET', 'POST'])
def hello():
    return redirect(url_for('upload_file'))


@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(url_for('upload_file'))

        file :FileStorage = request.files.get('file')

        if not file or file.filename == '':
            flash('No selected file')
            return redirect(url_for('upload_file'))

        if not allowed_file(file.filename):
            flash('File type not allowed')
            return redirect(url_for('upload_file'))

        ext = file.filename.rsplit('.',1)[1].lower()

        actual_mimetype = detect_mime(file)
        if not actual_mimetype:
            flash('无法识别文件类型')
            return redirect(url_for('upload_file'))

        if actual_mimetype not in app.config['ALLOWED_MIMETYPES']:
            flash('file type not allowed')
            logger.info("拒绝上传,文件名:%s,MIME:%s.",file.filename,actual_mimetype)
            return redirect(url_for('upload_file'))

        expected_mimetype = app.config['EXTENSION_MIME_TYPE'].get(ext)
        if expected_mimetype is not None and expected_mimetype != actual_mimetype:
            flash('扩展名和真实类型不同')
            logger.info("拒绝上传,文件名:%s,ext:%s,实际:%s",file.filename,ext,actual_mimetype)
            return redirect(url_for('upload_file'))
        _, original_ext = os.path.splitext(file.filename)
        safe_ext =original_ext.lower()
        filename = secure_filename(f'{uuid.uuid4().hex}{safe_ext}')
        upload_folder = app.config['UPLOAD_FOLDER']
        save_path = os.path.join(upload_folder, filename)
        os.makedirs(upload_folder, exist_ok=True)
        file.save(save_path)
        flash(f'File uploaded successfully.It is saved as "{filename}"')
        logger.info("文件名为%s,后缀名为%s",filename,actual_mimetype)
        return redirect(url_for('upload_file'))

    return render_template('index.html')


if __name__ == '__main__':
    app.run()

index.html

<!DOCTYPE html>
<html>
<head>
    <title>
        简单的文件上传
    </title>
</head>
<body>
    <h1>
        在这里上传一个文件
    </h1>
    {% with messages = get_flashed_messages() %}
    {% if messages %}
    <ul class="flashes">
        {% for message in messages %}
        <li>{{message}}</li>
        {% endfor %}
    </ul>
    {% endif %}
    {% endwith %}

    <form action="{{url_for('upload_file')}}" method="post" enctype="multipart/form-data">
        <input type="file" name="file"/>
        <input type="submit" name="文件上传"/>
    </form>
</body>
</html>

dockerfile

FROM python:3.11-slim

# 环境设置(防止 Python 缓冲 / 生成 pyc)
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# 安装系统依赖:python-magic 需要 libmagic
RUN apt-get update && \
    apt-get install -y --no-install-recommends libmagic1 && \
    rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制项目文件
COPY . .

# 创建上传目录
RUN mkdir -p uploads

EXPOSE 8000

# 生产环境不使用 python app.py,而用 gunicorn
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]

requirements.txt

Flask==3.0.0
python-magic==0.4.27
gunicorn==21.2.0