709 lines
22 KiB
TypeScript
709 lines
22 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { S3Client, GetObjectCommand, PutObjectCommand, ListObjectsV2Command, DeleteObjectCommand, ListObjectsV2CommandOutput } from '@aws-sdk/client-s3'
|
||
import { randomBytes } from 'crypto'
|
||
import { detectIsIntranet } from '@/lib/network-detection'
|
||
import { authOptions } from '@/lib/auth'
|
||
import { hasSystemAdminPermission } from '@/lib/department-permission'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
const ALLOWED_TEST_TYPES = new Set(['full', 'concurrent', 'mixed', 'quick'])
|
||
const MAX_CONCURRENCY = 10
|
||
const MAX_REQUEST_COUNT = 50
|
||
const MAX_MIXED_DURATION_MS = 10_000
|
||
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000
|
||
const RATE_LIMIT_MAX_REQUESTS = 2
|
||
const requestCounts = new Map<string, { count: number; resetAt: number }>()
|
||
|
||
function consumeRateLimit(userId: string): number | null {
|
||
const now = Date.now()
|
||
const current = requestCounts.get(userId)
|
||
if (!current || current.resetAt <= now) {
|
||
requestCounts.set(userId, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })
|
||
return null
|
||
}
|
||
if (current.count >= RATE_LIMIT_MAX_REQUESTS) {
|
||
return Math.max(1, Math.ceil((current.resetAt - now) / 1000))
|
||
}
|
||
current.count += 1
|
||
return null
|
||
}
|
||
|
||
// 创建S3客户端
|
||
const s3Client = new S3Client({
|
||
endpoint: process.env.S3_ENDPOINT || '',
|
||
region: process.env.S3_REGION || 'us-east-1',
|
||
credentials: {
|
||
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
||
},
|
||
forcePathStyle: true,
|
||
requestHandler: {
|
||
requestTimeout: 60000, // 增加到60秒以支持大文件
|
||
connectionTimeout: 10000,
|
||
},
|
||
})
|
||
|
||
// 计算统计信息
|
||
function calculateStats(durations: number[]) {
|
||
if (durations.length === 0) {
|
||
return {
|
||
min: 0,
|
||
max: 0,
|
||
avg: 0,
|
||
p50: 0,
|
||
p90: 0,
|
||
p95: 0,
|
||
p99: 0,
|
||
total: 0,
|
||
}
|
||
}
|
||
const sorted = [...durations].sort((a, b) => a - b)
|
||
return {
|
||
min: sorted[0],
|
||
max: sorted[sorted.length - 1],
|
||
avg: durations.reduce((a, b) => a + b, 0) / durations.length,
|
||
p50: sorted[Math.floor(sorted.length * 0.5)],
|
||
p90: sorted[Math.floor(sorted.length * 0.9)],
|
||
p95: sorted[Math.floor(sorted.length * 0.95)],
|
||
p99: sorted[Math.floor(sorted.length * 0.99)],
|
||
total: durations.reduce((a, b) => a + b, 0),
|
||
}
|
||
}
|
||
|
||
// 测试连接速度
|
||
async function testConnectionSpeed(): Promise<number> {
|
||
const startTime = Date.now()
|
||
try {
|
||
const command = new ListObjectsV2Command({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
MaxKeys: 1,
|
||
})
|
||
await s3Client.send(command)
|
||
return Date.now() - startTime
|
||
} catch (error: any) {
|
||
return Date.now() - startTime
|
||
}
|
||
}
|
||
|
||
// 测试上传速度(单个)
|
||
async function testUploadSpeed(sizeKB: number, testKey?: string): Promise<{ duration: number; speed: number; success: boolean; error?: string; key?: string }> {
|
||
const key = testKey || `speed-test-${Date.now()}-${Math.random().toString(36).substring(7)}.bin`
|
||
const testData = randomBytes(sizeKB * 1024)
|
||
|
||
const startTime = Date.now()
|
||
try {
|
||
const command = new PutObjectCommand({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Key: key,
|
||
Body: testData,
|
||
ContentType: 'application/octet-stream',
|
||
})
|
||
await s3Client.send(command)
|
||
const duration = Date.now() - startTime
|
||
const speed = (sizeKB / duration) * 1000 // KB/s
|
||
return { duration, speed, success: true, key }
|
||
} catch (error: any) {
|
||
const duration = Date.now() - startTime
|
||
return { duration, speed: 0, success: false, error: error.message, key }
|
||
}
|
||
}
|
||
|
||
// 测试下载速度(通过HTTP)
|
||
async function testDownloadSpeed(url: string): Promise<{ duration: number; speed: number; ttfb: number; success: boolean; error?: string }> {
|
||
const startTime = Date.now()
|
||
let ttfb = 0
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'HEAD',
|
||
signal: AbortSignal.timeout(30000),
|
||
})
|
||
ttfb = Date.now() - startTime
|
||
|
||
if (response.ok) {
|
||
const contentLength = response.headers.get('content-length')
|
||
if (contentLength) {
|
||
const sizeKB = parseInt(contentLength) / 1024
|
||
|
||
// 如果文件较小,尝试完整下载测试
|
||
if (sizeKB < 5000) { // 增加到5MB
|
||
const downloadStart = Date.now()
|
||
const downloadResponse = await fetch(url, {
|
||
signal: AbortSignal.timeout(60000),
|
||
})
|
||
if (downloadResponse.ok) {
|
||
await downloadResponse.arrayBuffer()
|
||
const downloadDuration = Date.now() - downloadStart
|
||
const speed = (sizeKB / downloadDuration) * 1000 // KB/s
|
||
return { duration: downloadDuration, speed, ttfb, success: true }
|
||
}
|
||
}
|
||
return { duration: ttfb, speed: 0, ttfb, success: true }
|
||
}
|
||
return { duration: ttfb, speed: 0, ttfb, success: true }
|
||
} else {
|
||
return { duration: ttfb, speed: 0, ttfb, success: false, error: `HTTP ${response.status}` }
|
||
}
|
||
} catch (error: any) {
|
||
const duration = Date.now() - startTime
|
||
return { duration, speed: 0, ttfb: duration, success: false, error: error.message }
|
||
}
|
||
}
|
||
|
||
// 并发上传测试
|
||
async function testConcurrentUploads(
|
||
concurrency: number,
|
||
sizeKB: number,
|
||
count: number
|
||
): Promise<{
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
durations: number[]
|
||
speeds: number[]
|
||
stats: ReturnType<typeof calculateStats>
|
||
throughput: number // 总吞吐量 MB/s
|
||
}> {
|
||
const durations: number[] = []
|
||
const speeds: number[] = []
|
||
let success = 0
|
||
let failed = 0
|
||
const keys: string[] = []
|
||
|
||
const startTime = Date.now()
|
||
|
||
// 分批执行,每批concurrency个
|
||
for (let i = 0; i < count; i += concurrency) {
|
||
const batch = []
|
||
for (let j = 0; j < concurrency && i + j < count; j++) {
|
||
batch.push(testUploadSpeed(sizeKB))
|
||
}
|
||
|
||
const results = await Promise.allSettled(batch)
|
||
|
||
for (const result of results) {
|
||
if (result.status === 'fulfilled') {
|
||
const r = result.value
|
||
if (r.success) {
|
||
durations.push(r.duration)
|
||
speeds.push(r.speed)
|
||
success++
|
||
if (r.key) keys.push(r.key)
|
||
} else {
|
||
failed++
|
||
}
|
||
} else {
|
||
failed++
|
||
}
|
||
}
|
||
}
|
||
|
||
const totalTime = Date.now() - startTime
|
||
const totalSizeMB = (success * sizeKB) / 1024
|
||
const throughput = totalTime > 0 ? (totalSizeMB / totalTime) * 1000 : 0 // MB/s
|
||
|
||
// 清理测试文件
|
||
if (keys.length > 0) {
|
||
await Promise.allSettled(
|
||
keys.map(key =>
|
||
s3Client.send(
|
||
new DeleteObjectCommand({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Key: key,
|
||
})
|
||
)
|
||
)
|
||
)
|
||
}
|
||
|
||
return {
|
||
total: count,
|
||
success,
|
||
failed,
|
||
durations,
|
||
speeds,
|
||
stats: calculateStats(durations),
|
||
throughput,
|
||
}
|
||
}
|
||
|
||
// 并发下载测试
|
||
async function testConcurrentDownloads(
|
||
concurrency: number,
|
||
urls: string[],
|
||
count: number
|
||
): Promise<{
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
durations: number[]
|
||
speeds: number[]
|
||
ttfb: number[]
|
||
stats: ReturnType<typeof calculateStats>
|
||
ttfbStats: ReturnType<typeof calculateStats>
|
||
throughput: number // 总吞吐量 MB/s
|
||
}> {
|
||
const durations: number[] = []
|
||
const speeds: number[] = []
|
||
const ttfb: number[] = []
|
||
let success = 0
|
||
let failed = 0
|
||
|
||
const startTime = Date.now()
|
||
|
||
// 循环使用URLs
|
||
for (let i = 0; i < count; i += concurrency) {
|
||
const batch = []
|
||
for (let j = 0; j < concurrency && i + j < count; j++) {
|
||
const url = urls[(i + j) % urls.length]
|
||
batch.push(testDownloadSpeed(url))
|
||
}
|
||
|
||
const results = await Promise.allSettled(batch)
|
||
|
||
for (const result of results) {
|
||
if (result.status === 'fulfilled') {
|
||
const r = result.value
|
||
if (r.success) {
|
||
durations.push(r.duration)
|
||
speeds.push(r.speed)
|
||
ttfb.push(r.ttfb)
|
||
success++
|
||
} else {
|
||
failed++
|
||
}
|
||
} else {
|
||
failed++
|
||
}
|
||
}
|
||
}
|
||
|
||
const totalTime = Date.now() - startTime
|
||
// 估算总大小(假设每个文件平均大小)
|
||
const avgSizeKB = speeds.length > 0 ? (speeds.reduce((a, b) => a + b, 0) / speeds.length) * (durations.reduce((a, b) => a + b, 0) / durations.length) / 1000 : 0
|
||
const totalSizeMB = (success * avgSizeKB) / 1024
|
||
const throughput = totalTime > 0 ? (totalSizeMB / totalTime) * 1000 : 0 // MB/s
|
||
|
||
return {
|
||
total: count,
|
||
success,
|
||
failed,
|
||
durations,
|
||
speeds,
|
||
ttfb,
|
||
stats: calculateStats(durations),
|
||
ttfbStats: calculateStats(ttfb),
|
||
throughput,
|
||
}
|
||
}
|
||
|
||
// 混合负载测试(同时上传和下载)
|
||
async function testMixedLoad(
|
||
uploadConcurrency: number,
|
||
downloadConcurrency: number,
|
||
uploadSizeKB: number,
|
||
downloadUrls: string[],
|
||
duration: number // 测试持续时间(毫秒)
|
||
): Promise<{
|
||
upload: {
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
throughput: number
|
||
}
|
||
download: {
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
throughput: number
|
||
}
|
||
}> {
|
||
let uploadTotal = 0
|
||
let uploadSuccess = 0
|
||
let uploadFailed = 0
|
||
let uploadTotalSizeMB = 0
|
||
let downloadTotal = 0
|
||
let downloadSuccess = 0
|
||
let downloadFailed = 0
|
||
let downloadTotalSizeMB = 0
|
||
|
||
const startTime = Date.now()
|
||
const endTime = startTime + duration
|
||
|
||
// 启动上传任务
|
||
const uploadTask = (async () => {
|
||
while (Date.now() < endTime) {
|
||
const batch = []
|
||
for (let i = 0; i < uploadConcurrency; i++) {
|
||
batch.push(testUploadSpeed(uploadSizeKB))
|
||
}
|
||
const results = await Promise.allSettled(batch)
|
||
|
||
for (const result of results) {
|
||
uploadTotal++
|
||
if (result.status === 'fulfilled') {
|
||
const r = result.value
|
||
if (r.success) {
|
||
uploadSuccess++
|
||
uploadTotalSizeMB += uploadSizeKB / 1024
|
||
// 清理
|
||
if (r.key) {
|
||
s3Client.send(
|
||
new DeleteObjectCommand({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Key: r.key,
|
||
})
|
||
).catch(() => {})
|
||
}
|
||
} else {
|
||
uploadFailed++
|
||
}
|
||
} else {
|
||
uploadFailed++
|
||
}
|
||
}
|
||
}
|
||
})()
|
||
|
||
// 启动下载任务
|
||
const downloadTask = (async () => {
|
||
while (Date.now() < endTime) {
|
||
const batch = []
|
||
for (let i = 0; i < downloadConcurrency; i++) {
|
||
const url = downloadUrls[i % downloadUrls.length]
|
||
if (url) {
|
||
batch.push(testDownloadSpeed(url))
|
||
}
|
||
}
|
||
const results = await Promise.allSettled(batch)
|
||
|
||
for (const result of results) {
|
||
downloadTotal++
|
||
if (result.status === 'fulfilled') {
|
||
const r = result.value
|
||
if (r.success) {
|
||
downloadSuccess++
|
||
// 估算大小
|
||
if (r.speed > 0) {
|
||
downloadTotalSizeMB += (r.speed * r.duration) / (1024 * 1000)
|
||
}
|
||
} else {
|
||
downloadFailed++
|
||
}
|
||
} else {
|
||
downloadFailed++
|
||
}
|
||
}
|
||
}
|
||
})()
|
||
|
||
await Promise.all([uploadTask, downloadTask])
|
||
|
||
const actualDuration = Date.now() - startTime
|
||
|
||
return {
|
||
upload: {
|
||
total: uploadTotal,
|
||
success: uploadSuccess,
|
||
failed: uploadFailed,
|
||
throughput: actualDuration > 0 ? (uploadTotalSizeMB / actualDuration) * 1000 : 0,
|
||
},
|
||
download: {
|
||
total: downloadTotal,
|
||
success: downloadSuccess,
|
||
failed: downloadFailed,
|
||
throughput: actualDuration > 0 ? (downloadTotalSizeMB / actualDuration) * 1000 : 0,
|
||
},
|
||
}
|
||
}
|
||
|
||
// 获取测试文件URLs
|
||
async function getTestFileUrls(count: number = 10): Promise<string[]> {
|
||
const urls: string[] = []
|
||
try {
|
||
const command = new ListObjectsV2Command({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
MaxKeys: count,
|
||
})
|
||
const response = await s3Client.send(command)
|
||
if (response.Contents && response.Contents.length > 0) {
|
||
for (const file of response.Contents) {
|
||
const key = file.Key || ''
|
||
// 跳过测试文件
|
||
if (key && !key.includes('speed-test-')) {
|
||
urls.push(`${process.env.S3_PUBLIC_DOMAIN}/${key}`)
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
return urls
|
||
}
|
||
|
||
// 清理所有测试文件(以防有遗漏)
|
||
async function cleanupAllTestFiles(): Promise<{ deleted: number; failed: number }> {
|
||
let deleted = 0
|
||
let failed = 0
|
||
let continuationToken: string | undefined = undefined
|
||
|
||
try {
|
||
do {
|
||
const listCommand: ListObjectsV2Command = new ListObjectsV2Command({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Prefix: 'speed-test-',
|
||
ContinuationToken: continuationToken,
|
||
MaxKeys: 1000,
|
||
})
|
||
|
||
const listResponse = await s3Client.send(listCommand) as ListObjectsV2CommandOutput
|
||
|
||
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
||
const deletePromises = listResponse.Contents.map((file) => {
|
||
if (file.Key) {
|
||
return s3Client.send(
|
||
new DeleteObjectCommand({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Key: file.Key,
|
||
})
|
||
).then(() => {
|
||
deleted++
|
||
}).catch(() => {
|
||
failed++
|
||
})
|
||
}
|
||
return Promise.resolve()
|
||
})
|
||
|
||
await Promise.allSettled(deletePromises)
|
||
}
|
||
|
||
continuationToken = listResponse.NextContinuationToken
|
||
} while (continuationToken)
|
||
} catch (error) {
|
||
console.warn('[S3 Speed Test] Cleanup error:', error)
|
||
}
|
||
|
||
return { deleted, failed }
|
||
}
|
||
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
if (!session?.user) {
|
||
return NextResponse.json({ error: '请先登录' }, { status: 401 })
|
||
}
|
||
|
||
const user = session.user as any
|
||
if (!hasSystemAdminPermission(user.role, Boolean(user.isSuperAdmin))) {
|
||
return NextResponse.json({ error: '仅系统管理员可执行 S3 测速' }, { status: 403 })
|
||
}
|
||
|
||
const retryAfter = consumeRateLimit(String(user.id || 'unknown'))
|
||
if (retryAfter) {
|
||
return NextResponse.json(
|
||
{ error: 'S3 测速请求过于频繁,请稍后再试' },
|
||
{ status: 429, headers: { 'Retry-After': String(retryAfter) } },
|
||
)
|
||
}
|
||
|
||
const searchParams = request.nextUrl.searchParams
|
||
const testTypeRaw = searchParams.get('type') || 'quick'
|
||
const concurrency = Number(searchParams.get('concurrency') || '5')
|
||
const count = Number(searchParams.get('count') || '20')
|
||
const mixedDurationMs = Number(searchParams.get('durationMs') || '5000')
|
||
if (!ALLOWED_TEST_TYPES.has(testTypeRaw)) {
|
||
return NextResponse.json({ error: 'type 参数无效' }, { status: 400 })
|
||
}
|
||
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
|
||
return NextResponse.json(
|
||
{ error: `concurrency 必须是 1 到 ${MAX_CONCURRENCY} 之间的整数` },
|
||
{ status: 400 },
|
||
)
|
||
}
|
||
if (!Number.isInteger(count) || count < 1 || count > MAX_REQUEST_COUNT) {
|
||
return NextResponse.json(
|
||
{ error: `count 必须是 1 到 ${MAX_REQUEST_COUNT} 之间的整数` },
|
||
{ status: 400 },
|
||
)
|
||
}
|
||
if (!Number.isInteger(mixedDurationMs) || mixedDurationMs < 1000 || mixedDurationMs > MAX_MIXED_DURATION_MS) {
|
||
return NextResponse.json(
|
||
{ error: `durationMs 必须是 1000 到 ${MAX_MIXED_DURATION_MS} 之间的整数` },
|
||
{ status: 400 },
|
||
)
|
||
}
|
||
const testType = testTypeRaw as 'full' | 'concurrent' | 'mixed' | 'quick'
|
||
|
||
const isIntranet = detectIsIntranet(request)
|
||
|
||
console.log('[S3 Speed Test] Starting comprehensive tests...')
|
||
console.log(`[S3 Speed Test] Type: ${testType}, Concurrency: ${concurrency}, Count: ${count}`)
|
||
console.log(`[S3 Speed Test] Is Intranet: ${isIntranet}`)
|
||
|
||
const result: any = {
|
||
success: true,
|
||
isIntranet,
|
||
config: {
|
||
s3Endpoint: process.env.S3_ENDPOINT,
|
||
s3PublicDomain: process.env.S3_PUBLIC_DOMAIN,
|
||
serverIp: process.env.SERVER_IP,
|
||
minioPort: process.env.MINIO_API_PORT || '9000',
|
||
},
|
||
timestamp: new Date().toISOString(),
|
||
}
|
||
|
||
// 1. 基础连接测试
|
||
console.log('[S3 Speed Test] 1. Testing connection...')
|
||
const connectionTime = await testConnectionSpeed()
|
||
result.connection = { duration: connectionTime }
|
||
|
||
// 2. 单文件上传测试(不同大小)
|
||
if (testType === 'full' || testType === 'quick') {
|
||
console.log('[S3 Speed Test] 2. Testing single file uploads (various sizes)...')
|
||
const uploadSizes: number[] = [10, 100, 1024]
|
||
|
||
result.upload = {}
|
||
for (const sizeKB of uploadSizes) {
|
||
const test = await testUploadSpeed(sizeKB)
|
||
result.upload[`${sizeKB}KB`] = {
|
||
sizeKB,
|
||
duration: test.duration,
|
||
speedKBps: test.speed,
|
||
speedMBps: test.speed / 1024,
|
||
success: test.success,
|
||
error: test.error,
|
||
}
|
||
// 清理
|
||
if (test.key) {
|
||
s3Client.send(
|
||
new DeleteObjectCommand({
|
||
Bucket: process.env.S3_BUCKET!,
|
||
Key: test.key,
|
||
})
|
||
).catch(() => {})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 并发上传测试
|
||
if (testType === 'full' || testType === 'concurrent' || testType === 'mixed' || testType === 'quick') {
|
||
console.log('[S3 Speed Test] 3. Testing concurrent uploads...')
|
||
const concurrentSizes = testType === 'quick'
|
||
? [{ size: 100, concurrency: Math.min(5, concurrency), count: Math.min(20, count) }]
|
||
: [
|
||
{ size: 10, concurrency, count },
|
||
{ size: 100, concurrency, count },
|
||
{ size: 1024, concurrency: Math.min(5, concurrency), count: Math.min(20, count) },
|
||
]
|
||
|
||
result.concurrentUpload = {}
|
||
for (const config of concurrentSizes) {
|
||
const test = await testConcurrentUploads(config.concurrency, config.size, config.count)
|
||
result.concurrentUpload[`${config.size}KB_${config.concurrency}并发_${config.count}次`] = {
|
||
...test,
|
||
config,
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 下载测试
|
||
if (testType === 'full' || testType === 'quick' || testType === 'concurrent' || testType === 'mixed') {
|
||
console.log('[S3 Speed Test] 4. Testing downloads...')
|
||
const testFileUrls = await getTestFileUrls(20)
|
||
|
||
if (testFileUrls.length > 0) {
|
||
// 公网下载测试
|
||
const publicDownload = await testDownloadSpeed(testFileUrls[0])
|
||
result.download = {
|
||
public: {
|
||
url: testFileUrls[0],
|
||
...publicDownload,
|
||
speedMBps: publicDownload.speed / 1024,
|
||
},
|
||
}
|
||
|
||
// 内网下载测试
|
||
if (isIntranet && process.env.SERVER_IP) {
|
||
try {
|
||
const url = new URL(testFileUrls[0])
|
||
const intranetUrl = `http://${process.env.SERVER_IP}:${process.env.MINIO_API_PORT || '9000'}${url.pathname}`
|
||
const intranetDownload = await testDownloadSpeed(intranetUrl)
|
||
result.download.intranet = {
|
||
url: intranetUrl,
|
||
...intranetDownload,
|
||
speedMBps: intranetDownload.speed / 1024,
|
||
speedup: publicDownload.speed > 0 ? (intranetDownload.speed / publicDownload.speed) : 0,
|
||
}
|
||
} catch (error) {
|
||
console.warn('[S3 Speed Test] Cannot test intranet download')
|
||
}
|
||
}
|
||
|
||
// 并发下载测试
|
||
if (testType === 'full' || testType === 'concurrent' || testType === 'mixed' || testType === 'quick') {
|
||
console.log('[S3 Speed Test] 5. Testing concurrent downloads...')
|
||
const concurrentConfigs: Array<{ concurrency: number; count: number }> = testType === 'quick'
|
||
? [{ concurrency: Math.min(5, concurrency), count: Math.min(20, count) }]
|
||
: [
|
||
{ concurrency, count },
|
||
]
|
||
|
||
result.concurrentDownload = {}
|
||
for (const config of concurrentConfigs) {
|
||
const test = await testConcurrentDownloads(config.concurrency, testFileUrls, config.count)
|
||
result.concurrentDownload[`${config.concurrency}并发_${config.count}次`] = {
|
||
...test,
|
||
config,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. 混合负载测试
|
||
if (testType === 'full' || testType === 'mixed') {
|
||
console.log('[S3 Speed Test] 6. Testing mixed load (upload + download)...')
|
||
const testFileUrls = await getTestFileUrls(10)
|
||
if (testFileUrls.length > 0) {
|
||
const mixedTest = await testMixedLoad(
|
||
concurrency,
|
||
concurrency,
|
||
100, // 100KB上传
|
||
testFileUrls,
|
||
mixedDurationMs
|
||
)
|
||
result.mixedLoad = mixedTest
|
||
}
|
||
}
|
||
|
||
// 7. 压力测试(高并发)
|
||
if (testType === 'full') {
|
||
console.log('[S3 Speed Test] 7. Testing stress (high concurrency)...')
|
||
const stressTest = await testConcurrentUploads(concurrency, 50, count)
|
||
result.stressTest = stressTest
|
||
}
|
||
|
||
// 8. 最终清理:确保所有测试文件都被删除
|
||
console.log('[S3 Speed Test] 8. Cleaning up all test files...')
|
||
const cleanupResult = await cleanupAllTestFiles()
|
||
result.cleanup = cleanupResult
|
||
console.log(`[S3 Speed Test] Cleanup: ${cleanupResult.deleted} deleted, ${cleanupResult.failed} failed`)
|
||
|
||
console.log('[S3 Speed Test] All tests completed')
|
||
|
||
return NextResponse.json(result)
|
||
} catch (error: any) {
|
||
console.error('[S3 Speed Test] Error:', error)
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
error: error.message || '测试失败',
|
||
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||
},
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|