270 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { storeFile } from '@/lib/fileStorage'
import { push } from '@/lib/push'
import { withCors } from '@/lib/middleware'
import ffmpeg from 'fluent-ffmpeg'
import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
interface WindowInfo {
title: string
path: string
memory: number
}
const timeoutMap = new Map<string, NodeJS.Timeout>()
async function handleScreenshotUpload(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const formData = await req.formData()
const files: File[] = []
const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
// Extract files from formData
for (const [key, value] of formData.entries()) {
if (key.startsWith('screenshot_') && value instanceof File) {
files.push(value)
}
}
if (files.length === 0) {
return NextResponse.json({ error: '没有收到文件' }, { status: 400 })
}
// Process screenshots
const processingStartTime = Date.now()
console.log(`[${hostname}] 开始处理 ${files.length} 张截图`)
const screenshots = await Promise.all(files.map(async (file, index) => {
const imageStartTime = Date.now()
// Convert image to AV1 format using FFmpeg
const buffer = Buffer.from(await file.arrayBuffer())
const originalSize = buffer.length
// Create temporary files
const tempDir = mkdtempSync(join(tmpdir(), 'ffmpeg-'))
const inputPath = join(tempDir, `input_${index}.png`)
const outputPath = join(tempDir, `output_${index}.avif`)
try {
// Write input file
writeFileSync(inputPath, buffer)
const conversionStartTime = Date.now()
// Convert using FFmpeg with libsvtav1
await new Promise<void>((resolve, reject) => {
ffmpeg(inputPath)
.videoCodec('libsvtav1')
.outputOptions([
'-crf 30', // 质量控制,值越小质量越好
'-preset 6', // 编码速度预设0-12值越大速度越快但质量可能下降
'-pix_fmt yuv420p' // 像素格式
])
.output(outputPath)
.on('end', () => resolve())
.on('error', (err) => reject(err))
.run()
})
const conversionTime = Date.now() - conversionStartTime
// Read the converted file
const avifBuffer = readFileSync(outputPath)
const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.avif`
const storageStartTime = Date.now()
const storedFile = await storeFile(avifBuffer, filename, 'image/avif', 'screenshot', hostname)
const storageTime = Date.now() - storageStartTime
const totalImageTime = Date.now() - imageStartTime
const compressionRatio = ((originalSize - avifBuffer.length) / originalSize * 100).toFixed(2)
console.log(`[${hostname}] 图片 ${index + 1}: FFmpeg转换 ${conversionTime}ms, 存储 ${storageTime}ms, 总计 ${totalImageTime}ms, 压缩率 ${compressionRatio}% (${originalSize} -> ${avifBuffer.length} bytes)`)
return {
fileId: storedFile.id,
objectName: storedFile.objectName,
filename: storedFile.filename,
contentType: storedFile.contentType,
fileSize: storedFile.size,
monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}`
}
} finally {
// Clean up temporary files
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (err) {
console.warn(`清理临时文件失败: ${err}`)
}
}
}))
const totalProcessingTime = Date.now() - processingStartTime
console.log(`[${hostname}] 完成处理 ${files.length} 张截图,总耗时 ${totalProcessingTime}ms平均每张 ${(totalProcessingTime / files.length).toFixed(2)}ms`)
// Ensure host exists first
await prisma.host.upsert({
where: { hostname },
update: {},
create: { hostname }
})
// Create new record
const newRecord = await prisma.record.create({
data: {
hostname,
timestamp: new Date(),
windows: {
create: windowsInfo
},
screenshots: {
create: screenshots
}
}
})
// Handle host status and notifications
const host = await prisma.host.findUnique({
where: { hostname },
select: { lastUpdate: true }
})
const lastUpdate = host?.lastUpdate || new Date(0)
if (lastUpdate.getTime() === new Date(0).getTime()) {
push(`新设备 ${hostname} 上线`)
} else if (lastUpdate.getTime() < new Date().getTime() - 1000 * 60) {
push(`设备 ${hostname} 上线`)
}
// Clear existing timeout and set new one
if (timeoutMap.has(hostname)) {
clearTimeout(timeoutMap.get(hostname)!)
}
timeoutMap.set(hostname, setTimeout(() => {
push(`设备 ${hostname} 离线`)
timeoutMap.delete(hostname)
}, 1000 * 60))
// Update host last update time
await prisma.host.upsert({
where: { hostname },
update: { lastUpdate: new Date() },
create: { hostname, lastUpdate: new Date() }
})
return NextResponse.json({
message: '上传成功',
hostname,
filesCount: files.length,
windowsCount: windowsInfo.length
})
} catch (error) {
console.error('上传失败:', error)
return NextResponse.json({ error: '上传失败' }, { status: 500 })
}
}
async function handleGetScreenshots(req: NextRequest) {
try {
const pathSegments = req.nextUrl.pathname.split('/')
const hostnameIndex = pathSegments.indexOf('hosts') + 1
const hostname = pathSegments[hostnameIndex]
if (!hostname) {
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
}
const { searchParams } = req.nextUrl
const startTimeParam = searchParams.get('startTime')
const endTimeParam = searchParams.get('endTime')
let startTime: Date | undefined
let endTime: Date | undefined
if (startTimeParam) {
const timestamp = isNaN(Number(startTimeParam)) ?
new Date(startTimeParam) :
new Date(Number(startTimeParam) * 1000)
startTime = timestamp
}
if (endTimeParam) {
const timestamp = isNaN(Number(endTimeParam)) ?
new Date(endTimeParam) :
new Date(Number(endTimeParam) * 1000)
endTime = timestamp
}
// Build query conditions
const whereClause: any = { hostname }
if (startTime || endTime) {
whereClause.timestamp = {}
if (startTime) whereClause.timestamp.gte = startTime
if (endTime) whereClause.timestamp.lte = endTime
}
// Get records
const records = await prisma.record.findMany({
where: whereClause,
include: {
windows: true,
screenshots: true
},
orderBy: {
timestamp: 'desc'
}
})
// Get host info
const host = await prisma.host.findUnique({
where: { hostname },
select: {
hostname: true,
lastUpdate: true
}
})
if (!host) {
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
}
// Convert BigInt to string in windows.memory field
const serializedRecords = records.map(record => ({
...record,
windows: record.windows.map(window => ({
...window,
memory: window.memory.toString()
}))
}))
return NextResponse.json({
hostname,
lastUpdate: host.lastUpdate,
records: serializedRecords,
total: records.length
})
} catch (error) {
console.error('获取记录失败:', error)
return NextResponse.json({ error: '获取记录失败' }, { status: 500 })
}
}
export const POST = withCors(handleScreenshotUpload)
export const GET = withCors(handleGetScreenshots)