下载工作台
Flask Web 开发

文件上传与配置分环境

试读上半部分 · 解锁后可读全文

第 8 章 · 文件上传与配置分环境

本章目标:掌握 Flask Config 类多环境配置(Development / Production / Testing);实现商品封面文件上传UPLOAD_FOLDER 管理;理解 .env 环境变量加载;区分开发静态托管与生产 Nginx 职责;在 api-demo 中完成可部署的配置分层。

学时建议:4 小时(含 1.5 小时上传跟练)

前置:本模块 ch01 应用工厂;ch04 WTForms;ch05 Product 模型;ch07 API(本章同时覆盖 Web 表单与 API 上传,文件上传 request.files 为本章新引入)。运维概念见 ops-deploy 相关章节。


8.1 为什么需要分环境配置

同一套代码在开发机测试机生产机上行为不同:调试开关、数据库地址、密钥、上传目录、CORS 白名单均不应硬编码。

                    ┌─────────────────┐
  APP_ENV ─────────►│  config.py      │
                    │  Development    │──► DEBUG=True, SQLite
                    │  Production     │──► DEBUG=False, PostgreSQL
                    │  Testing        │──► 内存库、禁用 CSRF
                    └─────────────────┘
                              ▲
                    .env 文件注入敏感变量
环境典型特征
DevelopmentDEBUG=True,详细错误页,本地 SQLite
ProductionDEBUG=False,PostgreSQL,密钥来自环境变量
Testing快速测试库,TESTING=True
本章基于虚构项目 api-demo;域名示例 https://api.example.com 仅用于说明 CDN/Nginx 地址,不涉及真实业务配置。

8.2 Config 类层次设计

api_demo/config.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent


class Config:
    """所有环境共享的默认项。"""
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me")

    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL",
        f"sqlite:///{BASE_DIR / 'instance' / 'api_demo.db'}",
    )

  # 上传
    MAX_CONTENT_LENGTH = 2 * 1024 * 1024          # 全局请求体上限 2MB
    UPLOAD_FOLDER = BASE_DIR / "uploads"
    ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}

  # CORS(ch07)
    CORS_ORIGINS = []

    @staticmethod
    def init_app(app):
        """子类可覆盖:创建目录、校验配置等。"""
        app.config["UPLOAD_FOLDER"].mkdir(parents=True, exist_ok=True)


class DevelopmentConfig(Config):
    DEBUG = True
    CORS_ORIGINS = ["http://127.0.0.1:5173", "http://localhost:3000"]

    @staticmethod
    def init_app(app):
        Config.init_app(app)
        # 开发期可由 Flask 托管 /uploads(见 8.7)


class ProductionConfig(Config):
    DEBUG = False
    # 生产 SECRET_KEY 必须由环境变量提供
    SECRET_KEY = os.environ["SECRET_KEY"]
    CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")

    @staticmethod
    def init_app(app):
        Config.init_app(app)
        if not os.environ.get("SECRET_KEY"):
            raise RuntimeError("生产环境必须设置 SECRET_KEY 环境变量")


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
    WTF_CSRF_ENABLED = False
    UPLOAD_FOLDER = BASE_DIR / "tests" / "tmp_uploads"


config_map = {
    "development": DevelopmentConfig,
    "production": ProductionConfig,
    "testing": TestingConfig,
    "default": DevelopmentConfig,
}

应用工厂加载

# api_demo/__init__.py
import os
from flask import Flask
from .config import config_map


def create_app(config_name=None):
    if config_name is None:
        config_name = os.getenv("APP_ENV", "development")
    app = Flask(__name__)
    config_class = config_map.get(config_name, config_map["default"])
    app.config.from_object(config_class)
    config_class.init_app(app)
    # 初始化扩展 ...
    return app
配置项作用
MAX_CONTENT_LENGTH超限返回 413
UPLOAD_FOLDER磁盘存储根目录
ALLOWED_EXTENSIONS白名单扩展名

8.3 .env 与环境变量

禁止将生产 SECRET_KEY、数据库密码提交 Git。使用 python-dotenv 在本地加载 .env

pip install python-dotenv

.env.example(提交仓库):

APP_ENV=development
SECRET_KEY=change-me-in-production
DATABASE_URL=sqlite:///instance/api_demo.db
CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:3000
UPLOAD_FOLDER=uploads

.env(本地使用,加入 .gitignore):

APP_ENV=development
SECRET_KEY=local-dev-secret-not-for-prod
DATABASE_URL=postgresql://api_demo:pass@localhost:5432/api_demo

run.pywsgi.py 入口:

from dotenv import load_dotenv

load_dotenv()  # 必须在 create_app 之前

from api_demo import create_app

app = create_app()
变量说明
APP_ENVdevelopment / production / testing(自定义变量;Flask 3.x 已移除内置 FLASK_ENV,故不用旧名)
SECRET_KEY会话签名,泄露需轮换
DATABASE_URLSQLAlchemy 连接串
CORS_ORIGINS逗号分隔的前端 Origin 列表

十二要素:配置与代码分离,部署时由平台(systemd、Docker、K8s Secret)注入环境变量。


8.4 模型字段与文件名策略

api_demo/models/product.py

from api_demo.extensions import db


class Product(db.Model):
    __tablename__ = "products"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False)
    slug = db.Column(db.String(200), unique=True, nullable=False)
    price = db.Column(db.Numeric(10, 2), default=0)
    stock = db.Column(db.Integer, default=0)
    cover_filename = db.Column(db.String(255), nullable=True)  # 仅存文件名
    is_published = db.Column(db.Boolean, default=False)

    @property
    def cover_url(self):
        if not self.cover_filename:
            return None
        return f"/uploads/products/{self.cover_filename}"

存储策略:数据库只存相对文件名,完整 URL 由 cover_url 属性或序列化层拼接,便于切换 CDN 前缀。

def save_cover(file_storage, slug):
    """校验扩展名与大小,保存到 UPLOAD_FOLDER/products/。"""
    from flask import current_app
    from werkzeug.utils import secure_filename
    import uuid

    filename = secure_filename(file_storage.filename or "")
    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
    allowed = current_app.config["ALLOWED_EXTENSIONS"]
    if ext not in allowed:
        raise ValueError(f"不允许的扩展名: {ext}")

    subdir = current_app.config["UPLOAD_FOLDER"] / "products"
    subdir.mkdir(parents=True, exist_ok=True)
    new_name = f"{slug}-{uuid.uuid4().hex[:8]}.{ext}"
    path = subdir / new_name
    file_storage.save(path)
    return new_name

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。