tus 协议
tus 是一个开放的可恢复文件上传协议。用它实现可在网络中断后继续的可靠上传。
为什么使用 tus?
- 可恢复:在网络故障后继续上传
- 高效:从中断处继续,而非从头开始
- 标准化:定义明确的协议,有许多服务器实现
- 分块:自动处理大文件
安装
npm install @samithahansaka/dropup tus-js-client
tus-js-client 是对等依赖项。
基本用法
import { useDropup } from '@samithahansaka/dropup';
import { createTusUploader } from '@samithahansaka/dropup/tus';
function TusUploader() {
const { files, actions, state, getDropProps, getInputProps } = useDropup({
upload: createTusUploader({
endpoint: 'https://tusd.tusdemo.net/files/',
}),
onUploadComplete: (file) => {
console.log('上传完成:', file.uploadedUrl);
},
});
return (
<div>
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>拖放文件进行可恢复上传</p>
</div>
{files.map(file => (
<div key={file.id} style={styles.fileItem}>
<span>{file.name}</span>
<span>{file.progress}%</span>
<span>{file.status}</span>
</div>
))}
<button onClick={() => actions.upload()}>
上传
</button>
</div>
);
}
配置选项
createTusUploader({
// 必需
endpoint: 'https://your-tus-server.com/files/',
// 分块大小(默认:Infinity = 不分块)
chunkSize: 5 * 1024 * 1024, // 5MB 分块
// 重试配置
retryDelays: [0, 1000, 3000, 5000], // 在 0s、1s、3s、5s 后重试
// 所有请求的标头
headers: {
'Authorization': 'Bearer token',
},
// 发送到服务器的元数据
metadata: {
filename: 'file.name',
filetype: 'file.type',
// 添加自定义元数据
userId: 'user123',
},
// 通过 localStorage 启用可恢复性
storeFingerprintForResuming: true,
// 成功上传后删除指纹
removeFingerprintOnSuccess: true,
// 并行上传
parallelUploads: 3,
});
恢复之前的上传
tus 可以恢复中断的上传:
const { files, actions } = useDropup({
upload: createTusUploader({
endpoint: 'https://your-tus-server.com/files/',
// 存储上传 URL 以供恢复
storeFingerprintForResuming: true,
// 可选:自定义存储键
urlStorageKey: 'my-app-tus-uploads',
onShouldRetry: (error, retryAttempt) => {
// 自定义重试逻辑
if (error.message.includes('network')) {
return retryAttempt < 5;
}
return false;
},
}),
});
// 重新添加时可以自动恢复文件
// 或使用 retry 手动恢复:
actions.retry(['file-id']);
进度跟踪
function TusWithProgress() {
const { files, actions, getDropProps, getInputProps } = useDropup({
upload: createTusUploader({
endpoint: 'https://your-tus-server.com/files/',
}),
onUploadProgress: (file, progress) => {
console.log(`${file.name}: ${progress}%`);
},
onUploadStart: (file) => {
console.log(`开始上传:${file.name}`);
},
onUploadComplete: (file) => {
console.log(`完成:${file.uploadedUrl}`);
},
});
return (
<div>
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>拖放文件到这里</p>
</div>
{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
{file.status === 'uploading' && (
<div style={styles.progressBar}>
<div
style={{
...styles.progress,
width: `${file.progress}%`,
}}
/>
</div>
)}
<span>{file.status}</span>
</div>
))}
</div>
);
}
暂停和恢复
function PausableUploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
upload: createTusUploader({
endpoint: 'https://your-tus-server.com/files/',
storeFingerprintForResuming: true,
}),
});
return (
<div>
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>拖放文件到这里</p>
</div>
{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
<span>{file.progress}%</span>
{file.status === 'uploading' && (
<button onClick={() => actions.cancel(file.id)}>
暂停
</button>
)}
{file.status === 'paused' && (
<button onClick={() => actions.retry([file.id])}>
恢复
</button>
)}
{file.status === 'error' && (
<button onClick={() => actions.retry([file.id])}>
重试
</button>
)}
</div>
))}
<button onClick={() => actions.upload()}>
全部开始
</button>
</div>
);
}
服务器设置
tusd(官方服务器)
# 安装 tusd
go install github.com/tus/tusd/cmd/tusd@latest
# 使用默认选项运行
tusd -upload-dir=/uploads
# 使用 S3 存储运行
tusd -s3-bucket=my-bucket -s3-endpoint=https://s3.amazonaws.com
Node.js (tus-node-server)
const { Server } = require('@tus/server');
const { FileStore } = require('@tus/file-store');
const server = new Server({
path: '/files',
datastore: new FileStore({ directory: './uploads' }),
});
// 与 Express 一起使用
app.all('/files/*', server.handle.bind(server));
自定义元数据处理
// 服务器端:访问元数据
const server = new Server({
path: '/files',
datastore: new FileStore({ directory: './uploads' }),
onUploadCreate: async (req, res, upload) => {
// 访问从客户端发送的元数据
console.log(upload.metadata);
// { filename: 'photo.jpg', filetype: 'image/jpeg', userId: 'user123' }
},
onUploadFinish: async (req, res, upload) => {
// 处理完成的上传
console.log('上传完成:', upload.id);
},
});
tus 扩展
Dropup 的 tus 集成支持这些 tus 扩展:
| 扩展 | 描述 |
|---|---|
| Creation | 创建新上传 |
| Termination | 取消上传 |
| Checksum | 验证完整性 |
| Concatenation | 并行上传 |
| Expiration | 自动清理 |
错误处理
const { files, actions } = useDropup({
upload: createTusUploader({
endpoint: 'https://your-tus-server.com/files/',
retryDelays: [0, 1000, 3000, 5000, 10000],
}),
onUploadError: (file, error) => {
if (error.code === 'TUS_OFFLINE') {
console.log('网络离线,将在连接后恢复');
} else if (error.code === 'TUS_EXPIRED') {
console.log('上传已过期,重新开始');
actions.retry([file.id]);
} else {
console.error('上传失败:', error.message);
}
},
});
大文件上传
tus 非常适合大文件:
function LargeFileUploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
maxSize: 10 * 1024 * 1024 * 1024, // 10GB 限制
upload: createTusUploader({
endpoint: 'https://your-tus-server.com/files/',
chunkSize: 50 * 1024 * 1024, // 50MB 分块
retryDelays: [0, 3000, 5000, 10000, 20000],
storeFingerprintForResuming: true,
}),
onUploadProgress: (file, progress) => {
// 为大文件显示详细进度
const uploadedMB = (file.size * progress / 100 / 1024 / 1024).toFixed(0);
const totalMB = (file.size / 1024 / 1024).toFixed(0);
console.log(`${uploadedMB}MB / ${totalMB}MB`);
},
});
return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>上传大文件(最大 10GB)</p>
</div>
);
}
比较:tus 与分块上传
| 功能 | tus | 自定义分块 |
|---|---|---|
| 可恢复性 | 内置 | 手动 |
| 服务器支持 | 多种选择 | 自定义 |
| 协议开销 | 较高 | 较低 |
| 配置 | 标准化 | 灵活 |
| 最适合 | 大文件、不可靠网络 | 简单上传 |