import React, { useState } from 'react';
import {
S3Client,
InitiateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
AbortMultipartUploadCommand,
} from '@aws-sdk/client-s3';
interface FileUploadProps {
bucketName: string;
region: string;
accessKeyId: string;
secretAccessKey: string;
}
const FileUploadComponent: React.FC<FileUploadProps> = ({
bucketName,
region,
accessKeyId,
secretAccessKey,
}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const uploadIdRef = React.useRef<string | null>(null);
const s3Client = new S3Client({
region,
credentials: {
accessKeyId,
secretAccessKey,
},
});
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setSelectedFile(file);
setUploadError(null);
}
};
const handleUpload = async () => {
if (!selectedFile) return;
setIsUploading(true);
setUploadProgress(0);
setUploadError(null);
try {
const objectKey = selectedFile.name;
const FILE_SIZE = selectedFile.size;
const PART_SIZE = 5 * 1024 * 1024;
const isSmallFile = FILE_SIZE < PART_SIZE;
const initCommand = new InitiateMultipartUploadCommand({
Bucket: bucketName,
Key: objectKey,
});
const initResponse = await s3Client.send(initCommand);
uploadIdRef.current = initResponse.UploadId;
if (!uploadIdRef.current) throw new Error('初始化分段上传失败');
if (isSmallFile) {
await uploadSinglePartAsLast(objectKey, selectedFile);
} else {
await uploadMultipleParts(objectKey, selectedFile, PART_SIZE);
}
await completeUpload(objectKey);
setUploadProgress(100);
} catch (error: any) {
setUploadError(error.message || '上传失败');
if (uploadIdRef.current) {
await abortUpload(uploadIdRef.current, selectedFile?.name || '');
}
} finally {
setIsUploading(false);
}
};
const uploadSinglePartAsLast = async (
objectKey: string,
file: File
) => {
const uploadCommand = new UploadPartCommand({
Bucket: bucketName,
Key: objectKey,
UploadId: uploadIdRef.current!,
PartNumber: 1,
Body: file,
ContentLength: file.size,
});
await s3Client.send(uploadCommand);
setUploadProgress(100);
};
const uploadMultipleParts = async (
objectKey: string,
file: File,
partSize: number
) => {
const partCount = Math.ceil(file.size / partSize);
const uploadedParts: { ETag: string; PartNumber: number }[] = [];
for (let i = 0; i < partCount; i++) {
const partNumber = i + 1;
const start = i * partSize;
const end = Math.min(start + partSize, file.size);
const chunk = file.slice(start, end);
const uploadCommand = new UploadPartCommand({
Bucket: bucketName,
Key: objectKey,
UploadId: uploadIdRef.current!,
PartNumber: partNumber,
Body: chunk,
ContentLength: chunk.size,
});
const response = await s3Client.send(uploadCommand);
uploadedParts.push({
ETag: response.ETag!,
PartNumber: partNumber,
});
setUploadProgress((prev) => prev + (100 / partCount));
}
};
const completeUpload = async (objectKey: string) => {
if (!uploadIdRef.current) return;
const completeCommand = new CompleteMultipartUploadCommand({
Bucket: bucketName,
Key: objectKey,
UploadId: uploadIdRef.current!,
MultipartUpload: { Parts: [] },
});
await s3Client.send(completeCommand);
};
const abortUpload = async (uploadId: string, objectKey: string) => {
const abortCommand = new AbortMultipartUploadCommand({
Bucket: bucketName,
Key: objectKey,
UploadId: uploadId,
});
await s3Client.send(abortCommand);
};
return (
<div className="p-6 max-w-md mx-auto bg-white rounded-lg shadow-lg">
<h2 className="text-xl font-bold mb-4">S3 文件上传</h2>
<div className="mb-4">
<input
type="file"
onChange={handleFileSelect}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
/>
</div>
{selectedFile && (
<div className="mb-4">
<p>文件名: {selectedFile.name}</p>
<p>文件大小: {formatSize(selectedFile.size)}</p>
</div>
)}
<button
onClick={handleUpload}
disabled={isUploading || !selectedFile}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg disabled:opacity-50"
>
{isUploading ? '上传中...' : '开始上传'}
</button>
{uploadProgress > 0 && (
<div className="progress mt-4">
<div
className="progress-bar"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{uploadError && (
<div className="text-red-500 mt-4">{uploadError}</div>
)}
</div>
);
};
const formatSize = (bytes: number) => {
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${bytes} B`;
};
export default FileUploadComponent;