270 lines
8.2 KiB
TypeScript
270 lines
8.2 KiB
TypeScript
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(`新设备 ${decodeURI(hostname)} 上线`)
|
||
} else if (lastUpdate.getTime() < new Date().getTime() - 1000 * 60) {
|
||
push(`设备 ${decodeURI(hostname)} 上线`)
|
||
}
|
||
|
||
// Clear existing timeout and set new one
|
||
if (timeoutMap.has(hostname)) {
|
||
clearTimeout(timeoutMap.get(hostname)!)
|
||
}
|
||
timeoutMap.set(hostname, setTimeout(() => {
|
||
push(`设备 ${decodeURI(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)
|