Node.js二次请求截图时文件无法打开错误的解决求助
问题描述
我用Node.js做了个服务,能捕获所有显示器截图并合并成一张图。第一次请求正常,但不用重启服务器的第二次请求就会报错,文件打不开。试过先删旧文件再创建新的,结果碰到error eperm operation not permitted unlink错误,只有关了服务器才能删文件,求解决办法。
原代码
const express = require('express'); const app = express(); const port = 3000; const fs = require('fs'); app.get('/', (req, res) => { hello = 'Hello World!'; console.log(hello); res.send(hello); }); app.get('/screenshot', (req, res) => { const screenshot = require('screenshot-desktop'); const sharp = require('sharp'); screenshot.listDisplays().then((displays) => { let screenshots = []; displays.forEach((display, i) => { const screenshotPath = `screenshot${i}.png`; screenshot({ screen: display.id }).then((imgBuffer) => { fs.writeFile(screenshotPath, imgBuffer, (err) => { if (err) throw err; console.log('Screenshot salvo!'); screenshots.push(screenshotPath); if (screenshots.length === displays.length) { // Todas as screenshots foram salvas, agora junte-as Promise.all([ sharp(screenshots[1]).metadata(), sharp(screenshots[0]).metadata() ]).then(([metadata1, metadata2]) => { sharp(screenshots[1]) .extend({ top: 0, bottom: 0, left: 0, right: metadata2.width, background: { r: 0, g: 0, b: 0, alpha: 0 } }) .composite([{ input: screenshots[0], left: metadata1.width, top: 0 }]) .toBuffer() .then((buffer) => { let base64Image = `data:image/png;base64,${buffer.toString('base64')}`; res.send(base64Image); }) .catch((err) => console.error(err)); }).catch(err => console.error(err)); } }); }, (err) => { console.error(err); }); }); }); }); app.listen(port, () => { console.log(`Servidor rodando em http://localhost:${port}`); });
错误信息
Servidor rodando em http://localhost:3000 Screenshot salvo! Screenshot salvo! F:\Andre\sk-access-control-ai\facialRecog\server.js:23 if (err) throw err; ^ [Error: UNKNOWN: unknown error, open 'facialRecog\screenshot0.png'] { errno: -4094, code: 'UNKNOWN', syscall: 'open', path: 'facialRecog\\screenshot0.png' } Node.js v20.12.2
解决办法
核心问题是sharp处理文件后未释放文件句柄,导致系统锁定文件,二次请求时无法写入/删除。另外原代码还存在异步流程混乱、重复导入模块的问题,以下是优化后的方案:
最优方案:全程用Buffer处理(彻底避免文件锁定)
直接在内存中处理截图Buffer,完全跳过本地文件写入,从根源解决文件被占用的问题:
const express = require('express'); const app = express(); const port = 3000; const screenshot = require('screenshot-desktop'); const sharp = require('sharp'); app.get('/', (req, res) => { const hello = 'Hello World!'; console.log(hello); res.send(hello); }); app.get('/screenshot', async (req, res) => { try { // 获取所有显示器列表 const displays = await screenshot.listDisplays(); // 并行捕获所有显示器的截图Buffer const screenshotBuffers = await Promise.all( displays.map(display => screenshot({ screen: display.id })) ); // 获取每个截图的元数据(宽高) const metadataList = await Promise.all( screenshotBuffers.map(buffer => sharp(buffer).metadata()) ); // 计算合并后的总宽度和最大高度 const totalWidth = metadataList.reduce((sum, meta) => sum + meta.width, 0); const maxHeight = Math.max(...metadataList.map(meta => meta.height)); // 初始化基础画布,以第一张图为起点扩展宽度 let combinedBuffer = await sharp(screenshotBuffers[0]) .extend({ top: 0, bottom: maxHeight - metadataList[0].height, left: 0, right: totalWidth - metadataList[0].width, background: { r: 0, g: 0, b: 0, alpha: 0 } }) .toBuffer(); // 依次合并剩余截图,垂直居中对齐 let currentX = metadataList[0].width; for (let i = 1; i < screenshotBuffers.length; i++) { combinedBuffer = await sharp(combinedBuffer) .composite([{ input: screenshotBuffers[i], left: currentX, top: (maxHeight - metadataList[i].height) / 2 }]) .toBuffer(); currentX += metadataList[i].width; } // 返回base64格式图片 const base64Image = `data:image/png;base64,${combinedBuffer.toString('base64')}`; res.send(base64Image); } catch (err) { console.error('截图合并失败:', err); res.status(500).send('截图生成失败'); } }); app.listen(port, () => { console.log(`Servidor rodando em http://localhost:${port}`); });
修改说明
- 把
require移到顶部,避免每次请求重复加载模块 - 用
async/await替代嵌套Promise,代码逻辑更清晰 - 全程基于内存Buffer处理,彻底绕开本地文件操作,解决文件锁定问题
- 用
Promise.all并行处理截图和元数据,提升处理效率 - 增加全局错误捕获,确保请求不会挂起
- 新增垂直居中对齐逻辑(可根据需求调整)
备选方案:使用临时文件并确保清理
如果必须写入本地文件,需使用唯一命名的临时文件,并在处理完成后强制删除:
const express = require('express'); const app = express(); const port = 3000; const fs = require('fs/promises'); const path = require('path'); const screenshot = require('screenshot-desktop'); const sharp = require('sharp'); app.get('/', (req, res) => { const hello = 'Hello World!'; console.log(hello); res.send(hello); }); app.get('/screenshot', async (req, res) => { const tempFiles = []; try { const displays = await screenshot.listDisplays(); // 生成带时间戳的临时文件路径,避免重名 const screenshotPaths = displays.map((_, i) => path.join(__dirname, `temp_${Date.now()}_screenshot_${i}.png`) ); tempFiles.push(...screenshotPaths); // 并行写入临时文件 await Promise.all( displays.map(async (display, i) => { const imgBuffer = await screenshot({ screen: display.id }); await fs.writeFile(screenshotPaths[i], imgBuffer); }) ); // 合并图片逻辑(和原代码类似) const [metadata1, metadata2] = await Promise.all([ sharp(screenshotPaths[1]).metadata(), sharp(screenshotPaths[0]).metadata() ]); const buffer = await sharp(screenshotPaths[1]) .extend({ top: 0, bottom: 0, left: 0, right: metadata2.width, background: { r: 0, g: 0, b: 0, alpha: 0 } }) .composite([{ input: screenshotPaths[0], left: metadata1.width, top: 0 }]) .toBuffer(); const base64Image = `data:image/png;base64,${buffer.toString('base64')}`; res.send(base64Image); } catch (err) { console.error('处理失败:', err); res.status(500).send('截图生成失败'); } finally { // 无论成功失败,都删除临时文件 await Promise.all(tempFiles.map(file => fs.unlink(file).catch(() => {}) )); } }); app.listen(port, () => { console.log(`Servidor rodando em http://localhost:${port}`); });
内容的提问来源于stack exchange,提问作者André Pimentel
相关产品推荐
相关产品推荐

