11 KiB
fileUpload.py 技术文档
文件上传与临时文件管理模块
概述
fileUpload.py 是一个基于异步 I/O 的文件存储与上传处理模块,主要用于:
- 接收 Base64 编码的文件并保存为本地文件;
- 支持流式下载远程文件;
- 管理临时文件生命周期(自动清理过期文件);
- 提供安全的文件路径映射与访问控制;
- 支持分片读取大文件以支持 HTTP 范围请求(Range Request),适用于视频/音频流媒体场景。
该模块广泛用于 Web 后端服务中处理用户上传、临时缓存和资源代理等场景。
依赖说明
第三方库
import asyncio
import os
import time
import tempfile
import aiofiles
import json
import base64
自定义模块(来自 appPublic 包)
| 模块 | 功能 |
|---|---|
folderUtils._mkdir |
递归创建目录 |
base64_to_file.base64_to_file, getFilenameFromBase64 |
将 Base64 字符串转为文件,并提取原始文件名 |
jsonConfig.getConfig |
获取全局配置对象 |
Singleton.SingletonDecorator |
单例装饰器 |
log.info, debug, warning, exception, critical |
日志输出工具 |
streamhttpclient.StreamHttpClient |
异步流式 HTTP 客户端 |
核心类与功能
1. TmpFileRecord 类:临时文件记录器(单例)
说明
使用单例模式管理所有临时文件的创建时间,定期检查并删除超时文件。通过 JSON 文件持久化记录状态。
装饰器
@SingletonDecorator
class TmpFileRecord:
确保整个应用中仅存在一个实例。
初始化参数
| 参数 | 默认值 | 说明 |
|---|---|---|
timeout |
3600 秒(1小时) |
文件最大存活时间 |
time_period |
10 秒 |
清理任务执行周期 |
属性
| 属性 | 类型 | 描述 |
|---|---|---|
filetime |
dict[str, float] |
文件路径 → 创建时间戳(time.time()) |
changed_flg |
bool |
是否有未保存的状态变更 |
filename |
str |
存储记录的 JSON 文件路径,格式:{config.filesroot}/tmpfile_rec_{pid}.json |
loop |
asyncio.EventLoop |
当前事件循环引用 |
方法
__init__(self, timeout=3600)
初始化记录器,并在事件循环中调度加载与定时清理任务。
savefilename(self)
生成用于保存临时文件记录的 JSON 文件路径:
root = config.filesroot or tempfile.gettempdir()
return f"{root}/tmpfile_rec_{os.getpid()}.json"
newtmpfile(path: str)
注册一个新的临时文件路径及其当前时间。
- 设置
changed_flg = True触发后续持久化。
async save()
将内存中的 filetime 字典异步写入 JSON 文件。
- 若无更改则跳过。
- 使用
aiofiles异步写入 UTF-8 编码内容。
async load()
从磁盘加载已有的文件记录。
- 若文件不存在则忽略。
- 加载后立即调用
remove()执行一次清理。
file_useful(self, fpath)
标记某个文件仍在使用(即被访问),将其从待清理列表中移除。
- 使用
try-except避免键不存在时报错。
async remove()
遍历所有记录的文件,删除超过 timeout 时间的文件。
- 调用
rmfile(k)删除物理文件; - 从
filetime中删除条目; - 异步保存更新后的记录;
- 自动递归调度下一次清理(每
time_period秒执行一次)。
⚠️ 注意:此方法由
loop.call_later()循环调用,构成后台守护任务。
rmfile(name: str)
根据配置根目录拼接完整路径后删除文件。
os.remove(config.fileroot + name)
❗ 存在潜在错误:应为
config.filesroot而非config.fileroot(代码拼写错误)
2. FileStorage 类:文件存储管理器
提供统一接口进行文件的保存、读取、删除及路径转换。
初始化
def __init__(self):
config = getConfig()
self.root = os.path.abspath(config.filesroot or tempfile.gettempdir())
self.tfr = TmpFileRecord() # 共享单例
属性
| 属性 | 类型 | 描述 |
|---|---|---|
root |
str |
文件系统根目录(绝对路径) |
tfr |
TmpFileRecord |
临时文件记录器单例 |
核心方法
realPath(path)
将相对或绝对 Web 路径转换为安全的本地文件系统路径。
- 若路径以
/开头,则去除; - 使用
os.path.join(self.root, ...)拼接; - 返回规范化后的绝对路径。
✅ 安全性:防止路径穿越攻击(如
../../etc/passwd)
webpath(path)
将本地文件路径转换为 Web 可访问路径(相对于 self.root)。
- 成功时返回去根前缀的子路径;
- 失败时返回
None。
_name2path(name, userid=None)
根据文件名生成唯一且分布均匀的存储路径,避免单目录下文件过多。
算法逻辑
- 取当前微秒级时间戳:
int(time.time() * 1000000) - 对四个质数
[191, 193, 197, 97]取模,生成四级子目录; - 若指定
userid,则加入用户隔离路径/userid/... - 最终结构示例:
/filesroot/tmp/123/45/67/89/avatar.png
✅ 优点:高并发下分散 IO 压力;天然防重名。
save_base64_file(b64str)
将 Base64 编码字符串保存为文件。
- 自动提取文件名(
getFilenameFromBase64); - 使用
_name2path生成路径; - 调用
base64_to_file写入; - 返回文件系统路径。
remove(path)
删除指定路径的文件。
- 支持带
/前缀的路径; - 捕获异常并记录日志;
- 实际路径为
os.path.join(self.root, path.lstrip('/'))
async streaming_read(request, webpath, buf_size=8096)
支持 HTTP Range 请求的大文件流式读取,适用于视频播放等场景。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
request |
HTTPRequest |
包含 headers 的请求对象 |
webpath |
str |
Web 上下文路径(如 /uploads/test.mp4) |
buf_size |
int |
每次读取缓冲大小,默认 8KB |
功能
- 解析
Range: bytes=0-1023请求头; - 计算起始和结束位置;
- 使用
aiofiles.open(..., 'rb')异步打开文件; - 支持
seek()跳转; - 分块
yield数据,可用于 ASGI 响应体;
日志输出
debug(f'filesize={stats.st_size}, startpos=..., endpos=...')
async save(name, read_data, userid=None)
通用文件保存方法,支持多种输入类型。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
name |
str |
原始文件名 |
read_data |
str, bytes, 或 callable async () -> bytes |
数据源 |
userid |
str |
用户 ID(用于路径隔离) |
行为
- 生成唯一路径
_name2path(...) - 创建父目录(
_mkdir) - 判断数据类型:
- 若为
str或bytes:直接写入; - 否则视为异步生成器函数,循环读取直到返回空;
- 若为
- 保存成功后调用
tfr.newtmpfile(fpath)注册为临时文件; - 返回 Web 路径(相对于 root)
✅ 支持同步数据和异步流两种模式。
工具函数
file_realpath(path)
获取给定路径对应的实际文件系统路径。
fs = FileStorage()
return fs.realPath(path)
主要用于外部调用快速解析路径。
async downloadfile(url, headers=None, params=None, data={})
从指定 URL 异步下载文件并保存到本地。
流程
- 提取 URL 文件名;
- 使用
FileStorage._name2path(..., userid='tmp')生成临时路径; - 使用
StreamHttpClient发起 GET 请求; - 流式接收 chunk 并写入文件;
- 返回保存后的本地路径;
- 出错时记录异常并重新抛出。
✅ 支持自定义 headers、query 参数、POST 数据。
async base642file(b64str)
将 Base64 字符串解码并保存为二进制文件。
特性
- 自动剥离 Data URL 头部(如
data:image/png;base64,...); - 使用
base64.b64decode解码; - 保存至
userid='tmp'目录; - 返回文件路径。
配置要求
需在全局配置中定义以下字段(通过 getConfig() 获取):
| 配置项 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
filesroot |
str |
否 | 文件存储根目录;若未设置则使用系统临时目录 |
fileroot |
str |
否(但注意 bug) | ❗ 在 rmfile() 中被误用,建议统一为 filesroot |
使用示例
示例 1:保存 Base64 图片
b64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhE..."
fpath = await base642file(b64)
print(f"Saved to: {fpath}")
示例 2:流式下载远程文件
url = "https://example.com/file.zip"
try:
local_path = await downloadfile(url)
print("Download complete:", local_path)
except Exception as e:
print("Failed:", e)
示例 3:处理上传流(FastAPI 示例)
@app.post("/upload")
async def upload_file(file: UploadFile):
fs = FileStorage()
def reader():
return file.read(8192)
web_path = await fs.save(file.filename, reader)
return {"web_path": web_path}
示例 4:流式响应视频(Starlette/FastAPI)
@app.get("/video/{path}")
async def stream_video(request, path: str):
fs = FileStorage()
return StreamingResponse(
fs.streaming_read(request, f"/{path}"),
media_type="video/mp4"
)
注意事项与改进建议
🔴 已知问题
-
变量名拼写错误:
del self.tiletime[k] # 应为 self.filetime➤ 导致 KeyError,无法正确清理过期文件。
-
属性名不一致:
self.change_flg = True # 正确 self.changed_flg = False # 初始化时使用了不同名称!➤ 应统一为
changed_flg。 -
rmfile()中配置项错误:os.remove(config.fileroot + name) # 应为 filesroot -
循环语法错误:
for k,v in ft: # ft 是 dict,不能直接迭代 tuple➤ 应改为:
for k, v in ft.items(): -
webpath()缺少返回值:if path.startswith(self.root): return path[len(self.root):] # else 缺失 return➤ 应补上
return None或默认路径。
✅ 改进建议
| 项目 | 建议 |
|---|---|
| 错误修复 | 修正上述拼写与逻辑错误 |
| 单元测试 | 添加对路径生成、清理机制、流读写的测试 |
| 配置校验 | 初始化时验证 filesroot 是否可写 |
| 更灵活的 TTL | 支持按文件类型设置不同过期时间 |
| 监控指标 | 增加文件数量、总大小、清理统计等 |
总结
fileUpload.py 是一个功能完整、设计合理的异步文件处理模块,具备以下优势:
✅ 异步高性能
✅ 支持流式操作
✅ 自动清理临时文件
✅ 安全路径处理
✅ 易于集成
只要修复文中指出的几处关键 Bug,即可稳定运用于生产环境。
📝 文档版本:v1.0
💬 维护者:开发者团队
📅 更新日期:2025年4月5日