多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

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

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()支持viewft.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-multipartpython-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_okTrue) 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_code404, detailfile not found) return FileResponse(target, filenamesafe_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, timeout10) 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_clicklambda e, nitem[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)}, timeout60) refresh() picker ft.FilePicker(on_resulton_pick) page.overlay.append(picker) page.add( ft.ElevatedButton(选择文件上传, on_clicklambda _: picker.pick_files(allow_multipleTrue)), ft.Divider(), file_list, ) refresh() def main(): ft.app(targetbuild_page, viewft.AppView.WEB_BROWSER, port8550)逻辑说明FilePicker必须挂到page.overlay上才会生效这是 Flet 的一个固定套路忘了加就点了没反应。on_pick里遍历选中的文件逐个 POSTfiles{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, host0.0.0.0, port8000) if __name__ __main__: threading.Thread(targetrun_api, daemonTrue).start() run_ui()daemonTrue保证主线程退出时后端线程跟着结束不会留下孤儿进程。host0.0.0.0让后端监听所有网卡这样局域网内其他机器也能访问到接口。跑起来后浏览器开http://你的IP:8550就是界面接口在 8000 端口。3. 自定义模板怎么改样式、字段与业务逻辑的落点3.1 界面自定义从配色到布局模板之所以叫「自定义模板」是因为它没把界面写死。Flet 的控件都支持theme和样式参数改配色最省事的做法是在build_page开头设page.theme_mode和page.themepage.theme_mode ft.ThemeMode.LIGHT page.theme ft.Theme(color_scheme_seedft.Colors.INDIGO) page.padding 24color_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 Entitydetail 里写着field required。原因前端 POST 的字段名和后端File(...)声明的参数名不一致。后端写的是file前端却传了upload或filesFastAPI 找不到对应字段就报 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。同时确认后端host0.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_code401, detailunauthorized) 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地址和防火墙都没问题才交付。这套流程看着笨但能挡掉九成的「在我这好好的」问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表