gongxiangp2/lib/image-processor.ts
chenpei 383772d39f 🛡️ 服务器安全加固和FRP问题修复
主要更新:
1. FRP服务器诊断与修复
   - 发现并修复文件描述符耗尽问题(65,536 → 1,048,576)
   - 分析并解决用户上传失败问题
   - 确认SSH服务器安全未被攻破

2. 安全防护系统部署
   - 部署IP黑名单防火墙(封禁20+攻击者)
   - 设置SSH端口速率限制
   - 配置自动化定时任务(每小时更新)
   - 安装Fail2ban实时防护系统
   - 添加白名单保护机制

3. 内网访问优化
   - 改进内网检测逻辑
   - 添加网络状态API
   - 优化钉钉登录流程
   - 完善信息补充功能

4. 文档完善
   - FRP服务器诊断修复报告
   - SSH安全评估报告
   - Fail2ban使用指南
   - 文件描述符详解
   - 用户上传失败原因分析
   - 防火墙配置说明
   - 内网检测改进报告

修复的问题:
- ✅ 文件描述符耗尽导致的服务异常
- ✅ 用户上传失败(影响9.5小时)
- ✅ SSH暴力破解攻击(91,299+次)
- ✅ 第三方项目攻击影响
- ✅ 127.0.0.1误封禁问题

部署的安全措施:
- 三重防护体系(Fail2ban + 防火墙 + 强密码)
- 自动化监控和封禁
- 实时攻击拦截(秒级响应)
- 完整的管理和监控工具

测试环境:开发环境
影响范围:服务器安全、用户上传、系统稳定性
当前状态:✅ 所有问题已修复,系统运行正常
2025-11-06 13:42:36 +08:00

182 lines
5.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 sharp from 'sharp'
interface ProcessImageOptions {
maxSizeKB?: number // 最大文件大小(KB)
maxWidth?: number // 最大宽度(像素)
maxHeight?: number // 最大高度(像素)
format?: 'webp' | 'jpeg' | 'png'
}
/**
* 处理图片:压缩、调整尺寸、转换格式
* @param buffer 原始图片buffer
* @param options 处理选项
* @returns 处理后的图片buffer
*/
export async function processImage(
buffer: Buffer,
options: ProcessImageOptions = {}
): Promise<Buffer> {
const {
maxSizeKB = 300, // 默认300KB
maxWidth = 600, // 默认600像素
maxHeight = 800, // 默认800像素(适配300x400显示)
format = 'webp', // 默认webP格式
} = options
try {
// 获取原始图片信息
const image = sharp(buffer)
const metadata = await image.metadata()
// 计算新尺寸(保持比例,同时限制宽度和高度)
let width = metadata.width || maxWidth
let height = metadata.height || maxHeight
// 如果宽度超过限制
if (width > maxWidth) {
const ratio = maxWidth / width
width = maxWidth
height = Math.round(height * ratio)
}
// 如果高度超过限制,再次调整
if (height > maxHeight) {
const ratio = maxHeight / height
height = maxHeight
width = Math.round(width * ratio)
}
// 初始质量
let quality = 80
let processedBuffer: Buffer
// 第一次处理
processedBuffer = await image
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.webp({ quality })
.toBuffer()
// 如果文件还是太大,逐步降低质量
const maxBytes = maxSizeKB * 1024
while (processedBuffer.length > maxBytes && quality > 20) {
quality -= 10
processedBuffer = await sharp(buffer)
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.webp({ quality })
.toBuffer()
}
console.log(`图片处理完成: ${metadata.width}x${metadata.height} -> ${width}x${height}, ${(buffer.length / 1024).toFixed(2)}KB -> ${(processedBuffer.length / 1024).toFixed(2)}KB, 质量: ${quality}`)
return processedBuffer
} catch (error) {
console.error('图片处理失败:', error)
throw new Error('图片处理失败')
}
}
/**
* 获取处理后的文件名(添加.webp扩展名)
*/
export function getProcessedFileName(originalName: string): string {
const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '')
return `${nameWithoutExt}.webp`
}
/**
* 切割长图片为多个最高1500px的图片片段,并压缩到指定大小
* @param buffer 原始图片buffer
* @param maxHeight 每个片段的最大高度(默认1500px)
* @param maxSizeKB 每个切片的最大文件大小(默认500KB)
* @returns 切割后的图片buffer数组
*/
export async function sliceImage(
buffer: Buffer,
maxHeight: number = 1500,
maxSizeKB: number = 500
): Promise<Buffer[]> {
try {
const image = sharp(buffer)
const metadata = await image.metadata()
if (!metadata.width || !metadata.height) {
throw new Error('无法获取图片尺寸')
}
const { width, height } = metadata
const slices: Buffer[] = []
const maxBytes = maxSizeKB * 1024
// 如果图片高度不超过maxHeight,直接返回原图(压缩处理)
if (height <= maxHeight) {
let quality = 90
let processedBuffer = await image
.webp({ quality })
.toBuffer()
// 如果文件还是太大,逐步降低质量
while (processedBuffer.length > maxBytes && quality > 20) {
quality -= 10
processedBuffer = await image
.webp({ quality })
.toBuffer()
}
console.log(`图片无需切割: ${width}x${height}, 压缩到 ${(processedBuffer.length / 1024).toFixed(2)}KB, 质量: ${quality}`)
return [processedBuffer]
}
// 计算需要切割的片数
const sliceCount = Math.ceil(height / maxHeight)
console.log(`开始切割图片: ${width}x${height} -> ${sliceCount}个片段,每片限制${maxSizeKB}KB`)
// 切割图片
for (let i = 0; i < sliceCount; i++) {
const top = i * maxHeight
const extractHeight = Math.min(maxHeight, height - top)
// 初始质量
let quality = 90
let sliceBuffer = await sharp(buffer)
.extract({
left: 0,
top: top,
width: width,
height: extractHeight
})
.webp({ quality })
.toBuffer()
// 如果文件还是太大,逐步降低质量
while (sliceBuffer.length > maxBytes && quality > 20) {
quality -= 10
sliceBuffer = await sharp(buffer)
.extract({
left: 0,
top: top,
width: width,
height: extractHeight
})
.webp({ quality })
.toBuffer()
}
slices.push(sliceBuffer)
console.log(`切割片段 ${i + 1}/${sliceCount}: 高度 ${extractHeight}px, 大小 ${(sliceBuffer.length / 1024).toFixed(2)}KB, 质量: ${quality}`)
}
return slices
} catch (error) {
console.error('图片切割失败:', error)
throw new Error('图片切割失败')
}
}