news 2026/11/5 10:31:02

Flet + FastAPI 文件上传下载系统自定义模板实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flet + FastAPI 文件上传下载系统自定义模板实战

简介:这是一套面向 Python 全栈初学者与工具开发者的文件上传下载系统自定义模板,基于 Flet 构建前端界面、FastAPI 提供后端接口,帮助读者快速搭建带进度反馈的批量文件传输应用。资源包共 8 个文件,包含 5 张 png 界面截图、1 个 xlsx 示例表格、1 个 txt 说明文档与 1 个 py 主程序,压缩包约 273KB,体积轻巧便于直接运行与二次修改。模板核心覆盖多文件选择上传、逐文件进度条实时刷新、上传完成后自动清空列表,以及通过特定 URL 下载指定文件、一键清理上传目录等实用功能,并对上传下载路径做了明确配置,规避路径注入风险,下载链接还带有时间限制以增强安全性。目前已有 113 人学习下载,适合希望理解 Flet 与 FastAPI 协同开发模式、需要现成文件传输脚手架或课程设计参考的读者,可据此快速扩展出属于自己的上传下载服务。

1. 从一次文件上传翻车说起:Flet + FastAPI 这套模板到底解决什么

上周帮朋友看一个内部工具,需求很朴素:几个同事往一个页面拖文件,后台存下来,再能按列表下载回去。他一开始用纯 FastAPI 写了个 HTML 表单,能跑,但界面丑到没人愿意用;换成前端框架吧,又要单独起一个 Node 工程,部署链路一下变长。最后我给他换成了 Flet 做界面、FastAPI 做后端,一个 Python 工程全包,界面还是桌面级的。这套「Flet 结合 FastAPI 构建的文件上传和下载系统自定义模板」,核心就是把这两件事缝在一起:Flet 负责渲染上传区、文件列表和下载按钮,FastAPI 负责接收 multipart 请求、落盘、回传文件流,模板里还留了自定义的入口,方便你改成自己的业务样式。

它适合谁?适合手上有 Python 基础、想快速搭一个带图形界面的文件收发工具的人——比如内网资料收集、测试产物归档、给非技术同事用的上传入口。不适合谁?不适合要做公网大流量、需要分片续传和对象存储的正式生产系统,那是另一套架构。这篇笔记就按「这是什么 → 怎么跑起来 → 怎么改 → 坑在哪」的顺序拆一遍,参数和目录结构我都会落到具体位置,你照着复现基本不会卡住。

2. 环境搭建与最小可运行骨架:把 Flet 和 FastAPI 装进同一个进程

2.1 为什么是 Flet 而不是纯前端

先说选型理由,不然你改到一半会怀疑人生。FastAPI 本身只负责 HTTP 层,它不产出界面。传统做法是配一套 Vue 或 React,好处是生态全,代价是你得维护两套语言、两套依赖、两套构建。Flet 的思路不一样:它用 Python 写 UI,底层是 Flutter 渲染,跑起来可以是桌面窗口,也可以是浏览器里的 Web 页面。对内部工具来说,这个折中非常划算——界面代码和后端代码在同一个仓库、同一门语言里,改一个字段不用来回切工程。

常见做法是让 Flet 以 Web 模式启动,这样同事直接开浏览器就能用,不用装客户端。Flet 的ft.app()支持view=ft.AppView.WEB_BROWSER,配合一个端口就能对外提供页面。而 FastAPI 用 uvicorn 起在另一个端口,两者通过 HTTP 通信。这里有个关键点:Flet 前端发请求时,地址不能写127.0.0.1,因为浏览器里的127.0.0.1指的是用户自己的机器,不是服务器。这个坑后面避坑章节会专门讲。

2.2 依赖安装与目录结构

我一般会把工程拆成下面这样,职责清晰,改起来不互相污染:

file-hub/ ├── main.py # 启动入口,同时拉起 Flet 和 FastAPI ├── backend/ │ ├── app.py # FastAPI 实例与路由 │ └── storage.py # 落盘、列表、读取的封装 ├── frontend/ │ └── ui.py # Flet 页面与交互逻辑 ├── uploads/ # 文件实际存放目录 └── requirements.txt

依赖就三个核心包,版本按你环境里能装上的最新稳定版走:

pip install flet fastapi uvicorn python-multipart

python-multipart这个包容易被漏掉。FastAPI 处理multipart/form-data表单上传时依赖它,不装的话启动不报错,但一上传就抛AssertionError: The python-multipart library is not installed,属于典型的血泪经验。装完可以用pip show确认一下。

2.3 后端:接收上传与提供下载

先写backend/app.py,把上传和下载两个接口立起来:

# backend/app.py from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.responses import FileResponse from pathlib import Path import shutil app = FastAPI() UPLOAD_DIR = Path("uploads") UPLOAD_DIR.mkdir(exist_ok=True) @app.post("/upload") async def upload(file: UploadFile = File(...)): # 只取文件名,防止路径穿越 safe_name = Path(file.filename).name target = UPLOAD_DIR / safe_name # 分块写入,避免大文件一次性读进内存 with target.open("wb") as f: while chunk := await file.read(1024 * 1024): f.write(chunk) return {"ok": True, "name": safe_name, "size": target.stat().st_size} @app.get("/files") async def list_files(): items = [] for p in sorted(UPLOAD_DIR.iterdir()): if p.is_file(): items.append({"name": p.name, "size": p.stat().st_size}) return {"files": items} @app.get("/download/{name}") async def download(name: str): safe_name = Path(name).name target = UPLOAD_DIR / safe_name if not target.exists(): raise HTTPException(status_code=404, detail="file not found") return FileResponse(target, filename=safe_name)

逻辑说明:upload里用Path(file.filename).name剥掉任何目录前缀,这是防路径穿越的第一道闸;await file.read(1024*1024)每次读 1MB,是分块写盘的标准写法,1MB 这个值在内存占用和系统调用次数之间比较平衡,你要传几百 MB 的文件也不会把内存吃爆。list_files返回名字和大小,前端拿来做列表。download用FileResponse,它会自动带上Content-Disposition头,浏览器点下载就能弹出保存框。

参数上你需要注意UPLOAD_DIR是相对路径,实际运行时取决于工作目录。稳妥做法是改成基于__file__的绝对路径,避免从别的目录启动时文件散落到奇怪的地方。

2.4 前端:Flet 的上传区与文件列表

frontend/ui.py负责界面。Flet 里上传文件靠FilePicker,它拿到的是本地路径,需要我们自己读出来再 POST 给后端:

# frontend/ui.py import flet as ft import requests API = "http://127.0.0.1:8000" # 桌面模式可用;Web 模式要换成服务器地址 def build_page(page: ft.Page): page.title = "文件收发台" file_list = ft.Column() def refresh(_=None): resp = requests.get(f"{API}/files", timeout=10) file_list.controls.clear() for item in resp.json()["files"]: file_list.controls.append( ft.Row([ ft.Text(f'{item["name"]} ({item["size"]} B)'), ft.TextButton("下载", on_click=lambda e, n=item["name"]: download(n)), ]) ) page.update() def download(name): url = f"{API}/download/{name}" page.launch_url(url) # 交给浏览器处理下载 def on_pick(e: ft.FilePickerResultEvent): if not e.files: return for f in e.files: with open(f.path, "rb") as fh: requests.post(f"{API}/upload", files={"file": (f.name, fh)}, timeout=60) refresh() picker = ft.FilePicker(on_result=on_pick) page.overlay.append(picker) page.add( ft.ElevatedButton("选择文件上传", on_click=lambda _: picker.pick_files(allow_multiple=True)), ft.Divider(), file_list, ) refresh() def main(): ft.app(target=build_page, view=ft.AppView.WEB_BROWSER, port=8550)

逻辑说明:FilePicker必须挂到page.overlay上才会生效,这是 Flet 的一个固定套路,忘了加就点了没反应。on_pick里遍历选中的文件,逐个 POST,files={"file": (f.name, fh)}这个元组结构对应后端UploadFile的字段名file,名字对不上后端就收不到。refresh在每次上传后重新拉列表,保证界面和后端一致。

参数上,API这个常量是整套模板最需要你改的地方。桌面模式跑,127.0.0.1没问题;一旦用 Web 模式给别的机器访问,必须换成服务器的实际 IP 或域名,否则前端请求会打到用户自己的机器上,直接连接失败。

2.5 启动入口:一个进程拉起两个服务

main.py用线程把 uvicorn 和 Flet 一起跑起来:

# main.py import threading import uvicorn from frontend.ui import main as run_ui def run_api(): uvicorn.run("backend.app:app", host="0.0.0.0", port=8000) if __name__ == "__main__": threading.Thread(target=run_api, daemon=True).start() run_ui()

daemon=True保证主线程退出时后端线程跟着结束,不会留下孤儿进程。host="0.0.0.0"让后端监听所有网卡,这样局域网内其他机器也能访问到接口。跑起来后,浏览器开http://<你的IP>:8550就是界面,接口在 8000 端口。

3. 自定义模板怎么改:样式、字段与业务逻辑的落点

3.1 界面自定义:从配色到布局

模板之所以叫「自定义模板」,是因为它没把界面写死。Flet 的控件都支持theme和样式参数,改配色最省事的做法是在build_page开头设page.theme_mode和page.theme:

page.theme_mode = ft.ThemeMode.LIGHT page.theme = ft.Theme(color_scheme_seed=ft.Colors.INDIGO) page.padding = 24

color_scheme_seed给一个种子色,Flet 会自动推导出一整套协调的配色,比一个个手调颜色靠谱。布局上,如果你想把上传区做成大号拖拽框,可以把ElevatedButton换成Container加on_click,再配ft.icons.UPLOAD_FILE图标。列表部分想加删除按钮,就在Row里再塞一个TextButton,后端补一个DELETE /files/{name}接口即可。

这里要提醒一句:Flet 的控件属性名在不同版本间偶有调整,比如早期版本里ft.Colors写作ft.colors。你如果照抄报AttributeError,先去官方文档确认当前版本的写法,别硬改。

3.2 后端自定义:加字段、加校验、加存储策略

业务一变,接口就得跟着变。最常见的三个改动方向:

第一,上传时附带元数据。比如给文件打标签,前端在 POST 时多带一个表单字段,后端把UploadFile换成显式声明:

from fastapi import Form @app.post("/upload") async def upload(file: UploadFile = File(...), tag: str = Form("default")): ...

Form和File可以混用,但前提还是那个python-multipart已安装。

第二,限制文件类型和大小。类型校验看file.content_type,大小校验在分块写入时累加计数,超过阈值就中断并删除半截文件。常见做法是设一个MAX_SIZE = 100 * 1024 * 1024,写盘循环里判断total > MAX_SIZE就raise HTTPException(413)。

第三,换存储位置。模板默认存本地uploads/,你要接对象存储,就把storage.py里的写盘逻辑换成对应 SDK 的上传调用,接口层不用动。这种分层的好处就在这里——存储策略和 HTTP 层解耦,改一处不影响另一处。

3.3 下载体验优化:文件名编码与大文件

下载这块有个中文文件名的老问题。FileResponse的filename参数如果直接传中文,某些浏览器会显示成乱码。稳妥做法是让 Starlette 自己处理编码,它内部会用 RFC 5987 的filename*形式,现代浏览器都认。你如果自己手写Content-Disposition头,记得同时给filename和filename*两个值。

大文件下载方面,FileResponse默认是流式的,不会把整个文件读进内存,这点可以放心。但如果你在下载前要做权限校验或计数,注意别在校验里把文件读一遍,那等于白白多一次 IO。

4. 避坑与排查:这几个问题我几乎每次都遇到

4.1 上传报 422,后端说字段缺失

现象:前端点了上传,浏览器控制台或后端日志返回422 Unprocessable Entity,detail 里写着field required。

原因:前端 POST 的字段名和后端File(...)声明的参数名不一致。后端写的是file,前端却传了upload或files,FastAPI 找不到对应字段就报 422。

解决:把两边名字对齐。前端files={"file": (...)}的键,必须等于后端函数参数名。改完重启两个服务再试。

4.2 Web 模式下上传一直转圈或连接被拒

现象:桌面模式一切正常,切到 Web 模式后,别人访问页面能打开,但一上传就失败,或者列表一直空。

原因:API常量还写着127.0.0.1。在 Web 模式里,这段代码跑在用户浏览器里,127.0.0.1指向用户自己的电脑,那里根本没有你的后端。

解决:把API改成服务器的局域网 IP 或域名,比如http://192.168.1.20:8000。同时确认后端host="0.0.0.0",防火墙放行 8000 端口。

4.3 大文件上传内存飙升甚至被系统杀掉

现象:传一个几百 MB 的文件,进程内存一路涨,最后 OOM。

原因:用了content = await file.read()一次性读完,整个文件进了内存。

解决:改成分块读写的循环,就是 2.3 节里那个while chunk := await file.read(1024*1024)的写法。块大小 1MB 是个经验值,你可以调到 4MB 减少系统调用,但别调到几十 MB,那就失去分块的意义了。

4.4 文件名带路径导致文件写到目录外

现象:上传一个名为../../etc/passwd的文件,结果真写到了上传目录之外。

原因:直接用了file.filename拼路径,没做净化。这是文件上传攻击里最经典的一类。

解决:永远用Path(file.filename).name只取最后一段文件名。如果业务允许重名覆盖有风险,再加一层 UUID 前缀或时间戳,把原始名存到数据库里做映射。

4.5 下载中文名乱码

现象:下载下来的文件名是一串百分号编码或者问号。

原因:Content-Disposition头里的文件名编码方式不被浏览器识别。

解决:交给FileResponse的filename参数处理,别自己拼头。如果确实要手动控制,同时提供filename和filename*=UTF-8''<编码后的名字>两个字段。

5. 进阶:把模板变成可验证、可交付的工具

5.1 用 curl 做接口级验证

界面能点通不代表接口健壮。我习惯在交付前用 curl 把三个接口各打一遍,确认返回结构稳定:

# 上传 curl -F "file=@./test.pdf" http://127.0.0.1:8000/upload # 列表 curl http://127.0.0.1:8000/files # 下载,-o 指定保存名 curl -o saved.pdf http://127.0.0.1:8000/download/test.pdf

-F表示以 multipart 表单提交,@后面跟本地文件路径。这三条命令能跑通,说明后端逻辑没问题,剩下的问题就都在前端。这个习惯帮我省过很多次「到底是前端还是后端」的扯皮。

5.2 加一层简单的访问控制

内部工具也怕被人乱传。最轻量的做法是加一个固定 token 校验,用 FastAPI 的依赖注入:

from fastapi import Header, Depends def check_token(x_token: str = Header(...)): if x_token != "your-secret": raise HTTPException(status_code=401, detail="unauthorized") @app.post("/upload", dependencies=[Depends(check_token)]) async def upload(...): ...

前端请求时带上headers={"x-token": "your-secret"}。这不是什么强安全方案,但能挡住随手乱试的人。真要上生产,换成正经的鉴权体系。

5.3 打包与部署的注意点

想把整套东西打成 exe 发给同事,PyInstaller 是常见选择,但 Flet 的打包有自己的命令flet pack,它会处理 Flutter 运行时的依赖,比手动配 PyInstaller 省心。注意打包后uploads/目录的路径会变,要用sys._MEIPASS或可执行文件所在目录来定位,否则文件会写到临时目录里,一关就没了。

部署到服务器时,uvicorn 单进程够用;并发上来了再加--workers,但要注意多进程下uploads/是共享目录,别用进程内内存做状态。

从那以后我每次改完这套模板,都会先用 curl 把上传、列表、下载三条链路各走一遍,再切 Web 模式让另一台机器实际点一次,确认API地址和防火墙都没问题才交付。这套流程看着笨,但能挡掉九成的「在我这好好的」问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 12:20:41

CARLA 0.9.16核心概念详解:世界/地图/车辆/传感器入门指南

一、CARLA是什么架构&#xff1f;Client-Server模式首先要搞懂CARLA的工作模式&#xff1a;它是客户端-服务器&#xff08;Client-Server&#xff09;架构。简单理解就是&#xff1a;CarlaUE4.exe 服务器端&#xff0c;负责渲染画面、跑物理引擎你的Python脚本 客户端&#x…

作者头像 李华
网站建设 2026/10/23 6:28:10

PyTorch LSTM影评情感分类:NLP工程闭环实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华