主要更新: 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 + 防火墙 + 强密码) - 自动化监控和封禁 - 实时攻击拦截(秒级响应) - 完整的管理和监控工具 测试环境:开发环境 影响范围:服务器安全、用户上传、系统稳定性 当前状态:✅ 所有问题已修复,系统运行正常
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
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('图片切割失败')
|
||
}
|
||
}
|