TUS 文件上传功能文档
目录
功能概述
本次新增了基于 TUS 协议的大文件上传功能,支持以下特性:
- ✅ 客户端分块上传:将大文件分成多个小块上传,提高上传效率
- ✅ 断点续传:网络中断后可以从中断点继续上传,无需重新开始
- ✅ 自动重试:上传失败时自动重试,提高上传成功率
- ✅ 上传进度监控:实时显示上传进度
- ✅ 大文件支持:支持 GB 级别的大文件上传
- ✅ 并发控制:支持多个文件并发上传
技术选型
- 前端 :
@uppy/core+@uppy/tus(基于 Uppy 的 TUS 插件) - 后端 :
@tus/server+@tus/file-store(TUS 协议服务器实现) - 协议:TUS (Resumable Upload Protocol) v1.0.0
依赖包版本
前端依赖(packages/ui/package.json)
| 包名 | 版本 | 作用 |
|---|---|---|
@uppy/core |
^5.2.0 |
Uppy 核心库,提供文件管理和插件架构 |
@uppy/react |
^5.1.1 |
Uppy React 组件(本项目未直接使用) |
@uppy/tus |
^5.1.0 |
TUS 协议插件,实现分块上传和断点续传 |
后端依赖(packages/server/package.json)
| 包名 | 版本 | 作用 |
|---|---|---|
@tus/server |
^1.10.2 |
TUS 协议服务器核心实现 |
@tus/file-store |
^1.5.1 |
文件存储插件,将文件存储到本地文件系统 |
包之间的关系
前端:
@uppy/core (核心)
└── @uppy/tus (TUS 插件,依赖 @uppy/core)
└── tus-js-client (底层 TUS 客户端,@uppy/tus 内部使用)
后端:
@tus/server (TUS 服务器核心)
└── @tus/file-store (存储插件,依赖 @tus/server)
说明:
@uppy/tus内部使用tus-js-client作为底层 TUS 客户端@tus/file-store是@tus/server的存储插件,可以替换为其他存储实现(如 S3、GCS)- 前端和后端通过 TUS 协议(HTTP)通信,无需直接依赖关系
技术架构
TUS 协议工作流程
1. 客户端发起 POST 请求创建上传会话
↓
2. 服务器返回 Location 头(包含上传 URL)
↓
3. 客户端分块上传(PATCH 请求)
↓
4. 服务器返回 upload-offset(已上传字节数)
↓
5. 重复步骤 3-4 直到上传完成
↓
6. 客户端可随时查询上传状态(HEAD 请求)
文件存储结构
~/.agentfactory/uploads/tus/
├── {upload-id} # 实际文件内容
└── {upload-id}.json # 文件元数据
前端实现
依赖安装
前端依赖已包含在 packages/ui/package.json 中:
json
{
"@uppy/core": "^5.2.0", // Uppy 核心库,提供文件管理和插件架构
"@uppy/react": "^5.1.1", // Uppy React 组件(可选,本项目未使用)
"@uppy/tus": "^5.1.0" // TUS 协议插件,实现分块上传和断点续传
}
安装命令:
bash
cd packages/ui
pnpm install @uppy/core @uppy/tus
核心组件
项目中有两个文件上传组件:
-
FileUploadButton:基础版本,仅文件选择,不上传
- 文件位置 :
packages/ui/FileUploadButton.jsx - 功能 :使用
@uppy/core实现无头样式的文件选择组件
- 文件位置 :
-
FileUploadButtonEnhanced:增强版本,支持 TUS 协议上传
- 文件位置 :
packages/ui/FileUploadButtonEnhanced.jsx - 功能 :基于
@uppy/core+@uppy/tus实现大文件分块上传
- 文件位置 :
FileUploadButton 实现详解
文件位置 :packages/ui/FileUploadButton.jsx
这是一个基础的文件选择组件,使用 @uppy/core 实现,不包含上传功能:
jsx
import Uppy from '@uppy/core'
const FileUploadButton = ({ onFilesAdded, disabled, maxFiles = 5, allowedFileTypes, className = '' }) => {
const uppyRef = useRef(null)
const inputRef = useRef(null)
const [files, setFiles] = useState([])
// 初始化 Uppy 实例(仅用于文件管理,不上传)
useEffect(() => {
const uppy = new Uppy({
restrictions: {
maxNumberOfFiles: maxFiles,
allowedFileTypes: allowedFileTypes || null
},
autoProceed: false // 不自动处理
})
// 监听文件添加
uppy.on('file-added', (file) => {
const currentFiles = uppy.getFiles()
setFiles(currentFiles)
if (onFilesAdded) {
onFilesAdded(currentFiles)
}
})
// 监听文件移除
uppy.on('file-removed', (file) => {
const currentFiles = uppy.getFiles()
setFiles(currentFiles)
if (onFilesAdded) {
onFilesAdded(currentFiles)
}
})
uppyRef.current = uppy
return () => {
uppy.close()
}
}, [maxFiles, allowedFileTypes, onFilesAdded])
// 处理文件选择
const handleFileSelect = useCallback((event) => {
const selectedFiles = Array.from(event.target.files || [])
if (uppyRef.current && selectedFiles.length > 0) {
selectedFiles.forEach((file) => {
uppyRef.current.addFile({
source: 'Local',
name: file.name,
type: file.type,
data: file
})
})
}
// 重置 input 值,允许重复选择同一文件
if (inputRef.current) {
inputRef.current.value = ''
}
}, [])
// UI 渲染:显示已选文件列表和上传按钮
return (
<div className={`relative ${className}`}>
<input ref={inputRef} type='file' multiple style={{ display: 'none' }} onChange={handleFileSelect} />
{/* 文件列表和上传按钮 UI */}
</div>
)
}
特点:
- ✅ 使用
@uppy/core进行文件管理 - ✅ 无头样式设计,UI 完全自定义
- ✅ 仅负责文件选择,不处理上传
- ✅ 支持多文件选择
- ✅ 支持文件类型限制
FileUploadButtonEnhanced 实现详解
文件位置 :packages/ui/FileUploadButtonEnhanced.jsx
这是基于 TUS 协议的增强版上传组件,完整实现代码解析:
1. 导入依赖
jsx
import Uppy from '@uppy/core' // Uppy 核心库
import Tus from '@uppy/tus' // TUS 协议插件
import { useRef, useEffect, useState, useCallback, useMemo } from 'react'
2. 初始化 Uppy 实例
jsx
useEffect(() => {
// 创建 Uppy 实例
const uppy = new Uppy({
restrictions: {
maxNumberOfFiles: maxFiles, // 最大文件数
maxFileSize: maxFileSize, // 最大文件大小
allowedFileTypes: stableAllowedFileTypes || null // 允许的文件类型
},
autoProceed: false, // 手动控制上传
allowMultipleUploadBatches: true // 允许多批次上传
})
// 配置 TUS 插件
uppy.use(Tus, {
endpoint: uploadUrl, // TUS 服务器端点
chunkSize: chunkSize, // 分块大小(建议值)
retryDelays: [0, 1000, 3000, 5000], // 重试延迟(毫秒)
maxConcurrentUploads: 2, // 最大并发上传数
removeFingerprintOnSuccess: true, // 成功后清理缓存
storage: window.localStorage, // 存储上传 URL(支持断点续传)
storageName: 'uppy-tus-urls' // 存储键名
})
// 事件监听...
}, [maxFiles, maxFileSize, uploadUrl, chunkSize, stableAllowedFileTypes])
3. 事件监听
jsx
// 文件添加事件 - 自动开始上传
uppy.on('file-added', (_file) => {
const currentFiles = uppy.getFiles()
setFiles(currentFiles)
if (callbacksRef.current.onFilesAdded) {
callbacksRef.current.onFilesAdded(currentFiles)
}
// 文件添加后自动开始上传
if (uppyRef.current) {
setTimeout(() => {
uppyRef.current.upload()
}, 100)
}
})
// 上传进度事件
uppy.on('upload-progress', (file, progress) => {
setUploadingFiles((prev) => {
const next = new Map(prev)
next.set(file.id, {
progress: progress.bytesUploaded / progress.bytesTotal,
bytesUploaded: progress.bytesUploaded,
bytesTotal: progress.bytesTotal
})
return next
})
if (callbacksRef.current.onUploadProgress) {
callbacksRef.current.onUploadProgress(file, progress)
}
})
// 上传成功事件
uppy.on('upload-success', (file, response) => {
setUploadingFiles((prev) => {
const next = new Map(prev)
next.set(file.id, { ...next.get(file.id), status: 'success' })
return next
})
if (callbacksRef.current.onUploadComplete) {
callbacksRef.current.onUploadComplete(file, response)
}
})
// 上传错误事件
uppy.on('upload-error', (file, error, response) => {
setUploadingFiles((prev) => {
const next = new Map(prev)
next.set(file.id, { ...next.get(file.id), status: 'error', error: error.message })
return next
})
if (callbacksRef.current.onUploadError) {
callbacksRef.current.onUploadError(file, error, response)
}
})
4. 性能优化策略
jsx
// 使用 ref 存储回调函数,避免在依赖数组中包含它们
const callbacksRef = useRef({ onFilesAdded, onUploadProgress, onUploadComplete, onUploadError })
useEffect(() => {
callbacksRef.current = { onFilesAdded, onUploadProgress, onUploadComplete, onUploadError }
}, [onFilesAdded, onUploadProgress, onUploadComplete, onUploadError])
// 稳定 allowedFileTypes,避免数组引用变化导致重新创建 uppy 实例
const allowedFileTypesStr = allowedFileTypes ? allowedFileTypes.join(',') : ''
const stableAllowedFileTypes = useMemo(() => allowedFileTypes, [allowedFileTypesStr])
优化说明:
- 使用
useRef存储回调函数,避免 Uppy 实例因回调函数引用变化而重新创建 - 使用
useMemo稳定allowedFileTypes数组引用,减少不必要的重新渲染
5. UI 渲染
组件提供完整的 UI,包括:
- 文件列表显示
- 上传进度条
- 成功/错误状态图标
- 重试按钮
- 移除文件按钮
jsx
{files.map((file) => {
const uploadStatus = uploadingFiles.get(file.id)
return (
<div key={file.id} className={/* 根据状态设置样式 */}>
<span>{file.name}</span>
{uploadStatus?.status === 'uploading' && (
<div className='progress-bar'>
<div style={{ width: `${uploadStatus.progress * 100}%` }} />
</div>
)}
{uploadStatus?.status === 'success' && <CheckCircle2 />}
{uploadStatus?.status === 'error' && (
<button onClick={() => handleUpload(file.id)}>重试</button>
)}
</div>
)
})}
组件 API
jsx
<FileUploadButtonEnhanced
onFilesAdded={(files) => {}}
onUploadProgress={(file, progress) => {}}
onUploadComplete={(file, response) => {}}
onUploadError={(file, error) => {}}
disabled={false}
maxFiles={5}
maxFileSize={100 * 1024 * 1024} // 100MB
allowedFileTypes={['.json', '.txt']}
uploadUrl='/api/v1/tus/upload'
chunkSize={50 * 1024} // 50KB 分块
className=''
/>
主要配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
uploadUrl |
string | /api/v1/tus/upload |
TUS 上传端点 |
chunkSize |
number | 50 * 1024 |
分块大小(字节),建议值,实际分块取决于文件大小 |
maxFiles |
number | 5 |
最大文件数量 |
maxFileSize |
number | 100 * 1024 * 1024 |
最大文件大小(100MB) |
allowedFileTypes |
string[] | null |
允许的文件类型(MIME 类型或扩展名) |
disabled |
boolean | false |
是否禁用上传 |
onFilesAdded |
function | - | 文件添加回调 |
onUploadProgress |
function | - | 上传进度回调 |
onUploadComplete |
function | - | 上传完成回调 |
onUploadError |
function | - | 上传错误回调 |
TUS 插件配置
javascript
uppy.use(Tus, {
endpoint: uploadUrl, // TUS 服务器端点
chunkSize: chunkSize, // 分块大小(建议值)
retryDelays: [0, 1000, 3000, 5000], // 重试延迟(毫秒)
maxConcurrentUploads: 2, // 最大并发上传数
removeFingerprintOnSuccess: true, // 成功后清理缓存
storage: window.localStorage, // 存储上传 URL(支持断点续传)
storageName: 'uppy-tus-urls' // 存储键名
})
性能优化
组件使用了以下优化策略:
- 使用
useRef存储回调函数 :避免在useEffect依赖数组中包含回调,减少不必要的重新创建 - 使用
useMemo稳定allowedFileTypes:避免数组引用变化导致 Uppy 实例重新创建 - 动态导入优化:TUS 插件按需加载(已改为直接导入)
后端实现
依赖安装
后端依赖已包含在 packages/server/package.json 中:
json
{
"@tus/file-store": "^1.5.1", // TUS 文件存储插件,将文件存储到本地文件系统
"@tus/server": "^1.10.2" // TUS 协议服务器核心实现
}
安装命令:
bash
cd packages/server
pnpm install @tus/server @tus/file-store
路由注册
文件位置 :packages/server/src/routes/index.ts
TUS 路由在主路由文件中注册:
typescript
import tusRouter from './tus'
// 注册 TUS 路由
router.use('/tus', tusRouter)
完整路由路径 :/api/v1/tus/upload 和 /api/v1/tus/upload/*
核心实现详解
文件位置 :packages/server/src/routes/tus/index.ts
1. 导入依赖
typescript
import express, { Request, Response } from 'express'
import { Server } from '@tus/server' // TUS 协议服务器
import { FileStore } from '@tus/file-store' // 文件存储实现
import path from 'path'
import { getUploadPath } from '../../utils' // 获取上传路径的工具函数
import fs from 'fs'
2. 创建上传目录
typescript
const router = express.Router()
// 确保上传目录存在
const uploadDir = path.join(getUploadPath(), 'tus')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
说明:
getUploadPath()返回上传根目录(默认:~/.agentfactory/uploads)- TUS 文件存储在
{uploadPath}/tus/子目录中 - 使用
recursive: true确保父目录不存在时也能创建
3. 创建文件存储(DataStore)
typescript
// 创建 TUS 数据存储(使用 FileStore 存储到本地文件系统)
const datastore = new FileStore({
directory: uploadDir // 存储目录路径
})
FileStore 说明:
@tus/file-store是@tus/server的存储插件- 将上传的文件存储到本地文件系统
- 每个上传会生成两个文件:
{upload-id}:实际文件内容{upload-id}.json:文件元数据(文件名、大小、MIME 类型等)
4. 创建 TUS 服务器实例
typescript
const tusServer = new Server({
path: '/api/v1/tus', // TUS 协议路径前缀
datastore: datastore, // 文件存储实例
respectForwardedHeaders: true, // 允许跨域,尊重代理头
generateUrl: (req, { id, host, proto }) => {
// 生成客户端可访问的上传 URL
// 策略:优先使用 Origin 头(前端请求会带这个)
const origin = req.headers?.['origin']
if (origin && typeof origin === 'string') {
// Origin 头已经是完整的 URL(如 http://localhost:8081)
// 直接拼接路径即可
return `${origin}/api/v1/tus/upload/${id}`
}
// 如果 Origin 头不存在,使用传入的 host 和 proto(最后的回退)
return `${proto}://${host}/api/v1/tus/upload/${id}`
}
})
Server 配置说明:
| 配置项 | 说明 |
|---|---|
path |
TUS 协议路径前缀,所有 TUS 请求都会匹配这个路径 |
datastore |
文件存储实例,负责文件的保存和读取 |
respectForwardedHeaders |
是否尊重代理头(x-forwarded-*),用于支持反向代理场景 |
generateUrl |
自定义 URL 生成函数,用于生成客户端可访问的上传 URL |
generateUrl 函数详解:
- 作用 :TUS 协议要求服务器在创建上传会话时返回
Location头,包含后续 PATCH/HEAD 请求的 URL - 策略 :
- 优先使用请求的
Origin头(前端请求会自动携带,如http://localhost:8081) - 如果
Origin不存在,使用传入的host和proto参数
- 优先使用请求的
- 为什么重要:确保返回的 URL 是前端可以访问的地址,而不是后端内部地址
5. 注册路由处理器
typescript
// TUS 协议端点 - 处理所有 TUS 相关请求
router.all('/upload', (req: Request, res: Response) => {
tusServer.handle(req, res)
})
router.all('/upload/*', (req: Request, res: Response) => {
tusServer.handle(req, res)
})
路由说明:
router.all():处理所有 HTTP 方法(GET、POST、PATCH、HEAD、DELETE、OPTIONS)/upload:创建上传会话的端点(POST 请求)/upload/*:处理具体上传操作的端点(PATCH、HEAD 请求)tusServer.handle():将请求交给 TUS 服务器处理
TUS 协议请求类型:
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/tus/upload |
创建上传会话,返回 Location 头 |
| PATCH | /api/v1/tus/upload/{id} |
上传文件块,包含 Upload-Offset 和 Content-Length 头 |
| HEAD | /api/v1/tus/upload/{id} |
查询上传状态,返回 Upload-Offset 和 Upload-Length 头 |
| DELETE | /api/v1/tus/upload/{id} |
删除上传(可选) |
| OPTIONS | /api/v1/tus/upload |
CORS 预检请求 |
6. 导出路由
typescript
export default router
使用的包详解
@tus/server
版本 :^1.10.2
作用:TUS 协议的核心服务器实现
主要功能:
- 实现 TUS 协议规范(v1.0.0)
- 处理 POST、PATCH、HEAD、DELETE、OPTIONS 请求
- 管理上传会话和状态
- 支持扩展(如
generateUrl)
官方文档:https://github.com/tus/tus-node-server
@tus/file-store
版本 :^1.5.1
作用 :@tus/server 的文件存储插件,将文件存储到本地文件系统
主要功能:
- 将上传的文件块写入本地文件
- 保存文件元数据(JSON 格式)
- 支持查询上传状态
- 支持删除上传
存储结构:
{tus-upload-dir}/
├── {upload-id} # 实际文件内容(二进制)
└── {upload-id}.json # 文件元数据(JSON)
元数据示例:
json
{
"id": "aeea1f39fe5b69ae86c5351072559ac2",
"upload_length": 134486,
"upload_metadata": "filename dGVzdC50eHQ=",
"upload_offset": 134486
}
文件存储路径
默认路径:
~/.agentfactory/uploads/tus/
自定义路径 :
如果设置了 BLOB_STORAGE_PATH 环境变量:
${BLOB_STORAGE_PATH}/uploads/tus/
获取路径的函数:
typescript
// packages/server/src/utils/index.ts
export const getUploadPath = (): string => {
return process.env.BLOB_STORAGE_PATH
? path.join(process.env.BLOB_STORAGE_PATH, 'uploads')
: path.join(getUserHome(), '.agentfactory', 'uploads')
}
URL 生成策略
generateUrl 函数用于生成客户端可访问的上传 URL,策略如下:
- 优先使用
Origin头 :前端请求会自动携带Origin头(如http://localhost:8081),直接使用确保返回前端可访问的地址 - 回退到传入参数 :如果
Origin头不存在,使用@tus/server传入的host和proto参数
白名单配置
TUS 上传接口已加入白名单,无需权限验证:
文件位置 :packages/server/src/utils/constants.ts
typescript
export const WHITELIST_URLS = [
// ...
'/api/v1/tus/upload' // TUS 文件上传接口(不需要权限验证)
]
文件存储路径
文件存储在以下路径:
- 默认路径 :
~/.agentfactory/uploads/tus/ - 自定义路径 :如果设置了
BLOB_STORAGE_PATH环境变量,则为${BLOB_STORAGE_PATH}/uploads/tus/
每个上传的文件会生成两个文件:
{upload-id}:实际文件内容{upload-id}.json:文件元数据(文件名、大小等)
配置说明
环境变量
后端环境变量
| 变量名 | 说明 | 默认值 |
|---|---|---|
BLOB_STORAGE_PATH |
文件存储根路径 | ~/.agentfactory/uploads |
FRONTEND_URL |
前端地址(可选,用于 URL 生成) | - |
分块大小配置
注意 :chunkSize 是建议值,不是强制值。TUS 客户端会根据以下因素决定实际分块:
- 文件大小 :小文件(<
chunkSize * 2)可能会一次性上传 - 网络状况:网络良好时可能会合并多个小块
- 客户端优化 :
tus-js-client内部会优化分块策略
示例:
- 设置
chunkSize = 50KB,上传 131KB 文件 → 可能一次性上传(优化行为) - 设置
chunkSize = 50KB,上传 10MB 文件 → 会按 50KB 分块上传
使用示例
基础使用
jsx
import FileUploadButtonEnhanced from './FileUploadButtonEnhanced'
function MyComponent() {
const [uploadedFiles, setUploadedFiles] = useState([])
return (
<FileUploadButtonEnhanced
onFilesAdded={setUploadedFiles}
onUploadProgress={(file, progress) => {
console.log(`上传进度: ${progress.bytesUploaded}/${progress.bytesTotal}`)
}}
onUploadComplete={(file, response) => {
console.log('上传完成:', file.name)
}}
onUploadError={(file, error) => {
console.error('上传失败:', error.message)
}}
maxFiles={5}
maxFileSize={100 * 1024 * 1024}
uploadUrl='/api/v1/tus/upload'
chunkSize={5 * 1024 * 1024} // 5MB 分块
/>
)
}
在 NatureWorkFlowDialog 中使用
jsx
<FileUploadButtonEnhanced
onFilesAdded={setUploadedFiles}
onUploadProgress={(_file, _progress) => {
// 上传进度处理(可选)
}}
onUploadComplete={(_file, _response) => {
console.log('上传完成:', _file, _response)
}}
onUploadError={(file, error) => {
enqueueSnackbar({
message: `文件 ${file.name} 上传失败: ${error.message}`,
options: { key: Date.now() + Math.random(), variant: 'error', autoHideDuration: 3000 }
})
}}
disabled={loading}
maxFiles={5}
maxFileSize={100 * 1024 * 1024} // 100MB
uploadUrl='/api/v1/tus/upload'
chunkSize={5 * 1024 * 1024} // 5MB 分块
/>
前后端联调指南
1. 启动服务
启动后端服务
bash
cd packages/server
pnpm dev
# 或
pnpm start
后端服务默认运行在 http://localhost:3098
启动前端服务
bash
cd packages/ui
pnpm dev
# 或
pnpm start
前端服务默认运行在 http://localhost:8081
2. 验证后端 TUS 服务
检查路由注册
确保 TUS 路由已注册:
typescript
// packages/server/src/routes/index.ts
import tusRouter from './tus'
router.use('/tus', tusRouter)
测试 TUS 端点
使用 curl 测试 TUS 服务:
bash
# 创建上传会话(POST)
curl -X POST http://localhost:3098/api/v1/tus/upload \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 1024" \
-H "Upload-Metadata: filename dGVzdC50eHQ=" \
-v
# 预期响应:
# HTTP/1.1 201 Created
# Location: http://localhost:8081/api/v1/tus/upload/{upload-id}
# Upload-Offset: 0
3. 验证前端组件
检查依赖
确保前端依赖已安装:
bash
cd packages/ui
pnpm install
检查组件导入
确保 FileUploadButtonEnhanced 组件可以正常导入和使用。
4. 联调测试步骤
步骤 1:上传小文件(< 100KB)
- 打开前端页面
- 点击上传按钮
- 选择一个小于 100KB 的文件
- 观察上传行为:
- 可能一次性上传(优化行为)
- 或按
chunkSize分块上传
预期结果:
- 文件成功上传
- 控制台显示上传进度
- 文件存储在
~/.agentfactory/uploads/tus/
步骤 2:上传大文件(> 5MB)
- 选择一个大于 5MB 的文件
- 开始上传
- 观察网络请求:
- 应该看到多个 PATCH 请求
- 每个请求的
content-length约为chunkSize upload-offset递增
预期结果:
- 文件按
chunkSize分块上传 - 每个分块请求的
content-length约为配置的分块大小 - 上传完成后文件完整
步骤 3:测试断点续传
- 开始上传一个大文件
- 在上传过程中断开网络(或关闭浏览器标签页)
- 重新连接网络(或重新打开页面)
- 重新选择同一个文件上传
预期结果:
- 上传从中断点继续
- 不会重新上传已上传的部分
- 最终文件完整
步骤 4:测试错误处理
- 上传一个文件
- 在上传过程中停止后端服务
- 观察前端行为
预期结果:
- 前端显示错误信息
- 自动重试(根据
retryDelays配置) - 重试失败后调用
onUploadError回调
5. 调试技巧
查看网络请求
打开浏览器开发者工具 → Network 标签:
-
POST 请求:创建上传会话
- URL:
/api/v1/tus/upload - Headers:
Tus-Resumable: 1.0.0,Upload-Length: {文件大小} - Response:
Location头包含上传 URL
- URL:
-
PATCH 请求:分块上传
- URL:
/api/v1/tus/upload/{upload-id} - Headers:
Upload-Offset: {已上传字节数},Content-Length: {当前块大小} - Response:
Upload-Offset: {新的已上传字节数}
- URL:
-
HEAD 请求:查询上传状态
- URL:
/api/v1/tus/upload/{upload-id} - Response:
Upload-Offset: {已上传字节数},Upload-Length: {总大小}
- URL:
查看服务器日志
后端日志会显示 TUS 请求信息:
[info] PATCH /api/v1/tus/upload/{upload-id}
[info] request: { headers: { 'upload-offset': '102400', 'content-length': '5242880' } }
查看存储文件
bash
# 查看上传目录
ls -lh ~/.agentfactory/uploads/tus/
# 查看文件元数据
cat ~/.agentfactory/uploads/tus/{upload-id}.json
常见问题
Q1: 为什么小文件没有分块上传?
A : chunkSize 是建议值,不是强制值。TUS 客户端会根据文件大小和网络状况优化分块策略。对于小文件(< chunkSize * 2),可能会一次性上传以提高效率。这是正常行为。
Q2: 上传失败后如何重试?
A : TUS 客户端会自动重试,重试延迟由 retryDelays 配置(默认:[0, 1000, 3000, 5000] 毫秒)。如果所有重试都失败,会调用 onUploadError 回调。
Q3: 如何实现断点续传?
A : 断点续传已自动启用。上传 URL 会存储在 localStorage 中(键名:uppy-tus-urls)。重新上传同一文件时,客户端会先发送 HEAD 请求查询已上传的字节数,然后从中断点继续。
Q4: 上传的文件存储在哪里?
A : 默认存储在 ~/.agentfactory/uploads/tus/。可以通过设置 BLOB_STORAGE_PATH 环境变量自定义存储路径。
Q5: 为什么返回的 URL 是前端地址而不是后端地址?
A : 这是为了确保客户端可以访问返回的 URL。generateUrl 函数会优先使用请求的 Origin 头(前端地址),确保返回的 URL 是前端可访问的。
Q6: 如何配置认证?
A: 当前 TUS 路由已加入白名单,无需认证。如果需要认证,可以:
- 从
WHITELIST_URLS中移除/api/v1/tus/upload - 在
FileUploadButtonEnhanced.jsx中取消注释认证相关代码 - 在 TUS 路由中添加认证中间件
Q7: 如何调整分块大小?
A : 在组件中设置 chunkSize 属性:
jsx
<FileUploadButtonEnhanced
chunkSize={5 * 1024 * 1024} // 5MB 分块
/>
注意 :chunkSize 是建议值,实际分块可能因文件大小和网络状况而有所不同。
Q8: 上传进度不准确?
A: 上传进度是基于已上传的字节数计算的。由于 TUS 协议的特性,进度更新可能有延迟。如果进度长时间不更新,检查网络连接和服务器日志。
Q9: 如何限制文件类型?
A : 使用 allowedFileTypes 属性:
jsx
<FileUploadButtonEnhanced allowedFileTypes={['.json', '.txt', '.pdf']} />
Q10: 如何查看上传的文件?
A: 上传的文件存储在服务器本地文件系统。可以通过以下方式查看:
bash
# 列出所有上传的文件
ls -lh ~/.agentfactory/uploads/tus/
# 查看文件元数据
cat ~/.agentfactory/uploads/tus/{upload-id}.json
相关资源
更新日志
2026-01-30
- ✅ 实现基于 TUS 协议的大文件上传功能
- ✅ 支持客户端分块上传和断点续传
- ✅ 优化 URL 生成策略,使用动态 Origin 头
- ✅ 移除硬编码端口,使用动态配置
- ✅ 添加白名单配置,无需权限验证
- ✅ 实现上传进度监控和错误处理
技术支持
如有问题,请查看:
- 浏览器控制台错误信息
- 服务器日志
- 网络请求详情(开发者工具 → Network)
或联系开发团队。