# TUS 上传 Hook 使用指南

## 概述

`useTusUpload` 是一个自定义 React Hook，提供了基于 TUS 协议的文件上传功能，支持大文件上传、断点续传、进度跟踪等特性。

## 环境变量配置

确保在 `.env` 文件中配置了以下环境变量：

```env
NEXT_PUBLIC_SERVER_PORT=3000
NEXT_PUBLIC_SERVER_IP=http://localhost
```

**注意**：在 Next.js 中，客户端组件只能访问以 `NEXT_PUBLIC_` 开头的环境变量。

## Hook API

### 返回值

```typescript
const {
	uploadProgress, // 上传进度 (0-100)
	isUploading, // 是否正在上传
	uploadError, // 上传错误信息
	handleFileUpload, // 文件上传函数
	getFileUrlByFileId, // 根据文件ID获取访问链接
	getFileInfo, // 获取文件详细信息
	getUploadStatus, // 获取上传状态
	serverUrl, // 服务器地址
} = useTusUpload();
```

### 主要方法

#### `handleFileUpload(file, onSuccess?, onError?)`

上传文件的主要方法。

**参数：**

- `file: File` - 要上传的文件对象
- `onSuccess?: (result: UploadResult) => void` - 成功回调
- `onError?: (error: string) => void` - 失败回调

**返回：** `Promise<UploadResult>`

**UploadResult 接口：**

```typescript
interface UploadResult {
	compressedUrl: string; // 压缩版本URL（当前与原始URL相同）
	url: string; // 文件访问URL
	fileId: string; // 文件唯一标识
	fileName: string; // 文件名
}
```

#### `getFileUrlByFileId(fileId: string)`

根据文件ID生成访问链接。

**参数：**

- `fileId: string` - 文件唯一标识

**返回：** `string` - 文件访问URL

## 使用示例

### 基础使用

```tsx
import React, { useState } from 'react';
import { useTusUpload } from '../hooks/useTusUpload';

function UploadComponent() {
	const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload();
	const [uploadedUrl, setUploadedUrl] = useState<string>('');

	const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files?.[0];
		if (!file) return;

		try {
			const result = await handleFileUpload(
				file,
				(result) => {
					console.log('上传成功！', result);
					setUploadedUrl(result.url);
				},
				(error) => {
					console.error('上传失败：', error);
				},
			);
		} catch (error) {
			console.error('上传出错：', error);
		}
	};

	return (
		<div>
			<input type="file" onChange={handleFileChange} disabled={isUploading} />

			{isUploading && (
				<div>
					<p>上传进度: {uploadProgress}%</p>
					<progress value={uploadProgress} max="100" />
				</div>
			)}

			{uploadError && <p style={{ color: 'red' }}>{uploadError}</p>}

			{uploadedUrl && (
				<a href={uploadedUrl} target="_blank" rel="noopener noreferrer">
					查看上传的文件
				</a>
			)}
		</div>
	);
}
```

### 拖拽上传

```tsx
import React, { useCallback, useState } from 'react';
import { useTusUpload } from '../hooks/useTusUpload';

function DragDropUpload() {
	const { handleFileUpload, isUploading, uploadProgress } = useTusUpload();
	const [dragOver, setDragOver] = useState(false);

	const handleDrop = useCallback(
		async (e: React.DragEvent) => {
			e.preventDefault();
			setDragOver(false);

			const files = e.dataTransfer.files;
			if (files.length > 0) {
				await handleFileUpload(files[0]);
			}
		},
		[handleFileUpload],
	);

	const handleDragOver = useCallback((e: React.DragEvent) => {
		e.preventDefault();
		setDragOver(true);
	}, []);

	return (
		<div
			onDrop={handleDrop}
			onDragOver={handleDragOver}
			onDragLeave={() => setDragOver(false)}
			style={{
				border: dragOver ? '2px dashed #0070f3' : '2px dashed #ccc',
				padding: '20px',
				textAlign: 'center',
				backgroundColor: dragOver ? '#f0f8ff' : '#fafafa',
			}}
		>
			{isUploading ? <p>上传中... {uploadProgress}%</p> : <p>拖拽文件到这里上传</p>}
		</div>
	);
}
```

### 多文件上传

```tsx
function MultiFileUpload() {
	const { handleFileUpload } = useTusUpload();
	const [uploadingFiles, setUploadingFiles] = useState<Map<string, number>>(new Map());

	const handleFilesChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
		const files = e.target.files;
		if (!files) return;

		for (let i = 0; i < files.length; i++) {
			const file = files[i];
			const fileId = `${file.name}-${Date.now()}-${i}`;

			setUploadingFiles((prev) => new Map(prev).set(fileId, 0));

			try {
				await handleFileUpload(
					file,
					(result) => {
						console.log(`文件 ${file.name} 上传成功:`, result);
						setUploadingFiles((prev) => {
							const newMap = new Map(prev);
							newMap.delete(fileId);
							return newMap;
						});
					},
					(error) => {
						console.error(`文件 ${file.name} 上传失败:`, error);
						setUploadingFiles((prev) => {
							const newMap = new Map(prev);
							newMap.delete(fileId);
							return newMap;
						});
					},
				);
			} catch (error) {
				console.error(`文件 ${file.name} 上传出错:`, error);
			}
		}
	};

	return (
		<div>
			<input type="file" multiple onChange={handleFilesChange} />

			{uploadingFiles.size > 0 && (
				<div>
					<h4>正在上传的文件:</h4>
					{Array.from(uploadingFiles.entries()).map(([fileId, progress]) => (
						<div key={fileId}>
							{fileId}: {progress}%
						</div>
					))}
				</div>
			)}
		</div>
	);
}
```

## 特性

### 1. 断点续传

TUS 协议支持断点续传，如果上传过程中断，可以从中断的地方继续上传。

### 2. 大文件支持

适合上传大文件，没有文件大小限制（取决于服务器配置）。

### 3. 进度跟踪

实时显示上传进度，提供良好的用户体验。

### 4. 错误处理

提供详细的错误信息和重试机制。

### 5. 自动重试

内置重试机制，网络异常时自动重试。

## 故障排除

### 1. 环境变量获取不到

确保环境变量以 `NEXT_PUBLIC_` 开头，并且 Next.js 应用已重启。

### 2. 上传失败

检查服务器是否正在运行，端口是否正确。

### 3. CORS 错误

确保后端服务器配置了正确的 CORS 设置。

### 4. 文件无法访问

确认文件上传成功后，检查返回的 URL 是否正确。

## 注意事项

1. **Next.js 环境变量**：客户端组件只能访问 `NEXT_PUBLIC_` 前缀的环境变量
2. **服务器配置**：确保后端服务器支持 TUS 协议
3. **文件大小**：虽然支持大文件，但要注意服务器和客户端的内存限制
4. **网络环境**：在网络不稳定的环境下，断点续传功能特别有用

## API 路由

Hook 会访问以下 API 路由：

- `POST /upload` - TUS 上传端点
- `GET /download/:fileId` - 文件下载/访问
- `GET /api/storage/resource/:fileId` - 获取文件信息
- `HEAD /upload/:fileId` - 获取上传状态
