dootask/electron/lib/renderer.js
kuaifan f0982d7d9a efactor: 拆分 electron 主进程代码为独立模块
将 electron.js 中的 PDF 导出、渲染器辅助函数和工具函数拆分为独立模块:
  - electron/lib/pdf-export.js: PDF 导出相关功能
  - electron/lib/renderer.js: 渲染器辅助函数
  - electron/lib/other.js: 平台检测和 URL 验证常量

  此重构提高了代码可维护性,减少了主文件的复杂度。
2026-01-08 13:54:55 +00:00

676 lines
23 KiB
JavaScript
Vendored

const fs = require("fs");
const {BrowserWindow, shell, app, dialog, clipboard, nativeImage, ipcMain} = require("electron");
const fsProm = require("fs/promises");
const path = require("path");
const {spawn} = require("child_process");
const config = require("../package.json");
const electronDown = require("../electron-down");
const loger = require("electron-log");
const {allowedUrls, isWin} = require("./other");
const {O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY} = fs.constants;
const DRAFT_PREFEX = '.$';
const OLD_DRAFT_PREFEX = '~$';
const DRAFT_EXT = '.dtmp';
const BKP_PREFEX = '.$';
const OLD_BKP_PREFEX = '~$';
const BKP_EXT = '.bkp';
let enableStoreBkp = true,
dialogOpen = false,
enablePlugins = false;
const renderer = {
checkFileContent(body, enc) {
if (body != null) {
let head, headBinay;
if (typeof body === 'string') {
if (enc == 'base64') {
headBinay = Buffer.from(body.substring(0, 22), 'base64');
head = headBinay.toString();
} else {
head = body.substring(0, 16);
headBinay = Buffer.from(head);
}
} else {
head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
headBinay = body;
}
let c1 = head[0],
c2 = head[1],
c3 = head[2],
c4 = head[3],
c5 = head[4],
c6 = head[5],
c7 = head[6],
c8 = head[7],
c9 = head[8],
c10 = head[9],
c11 = head[10],
c12 = head[11],
c13 = head[12],
c14 = head[13],
c15 = head[14],
c16 = head[15];
let cc1 = headBinay[0],
cc2 = headBinay[1],
cc3 = headBinay[2],
cc4 = headBinay[3],
cc5 = headBinay[4],
cc6 = headBinay[5],
cc7 = headBinay[6],
cc8 = headBinay[7],
cc9 = headBinay[8],
cc10 = headBinay[9],
cc11 = headBinay[10],
cc12 = headBinay[11],
cc13 = headBinay[12],
cc14 = headBinay[13],
cc15 = headBinay[14],
cc16 = headBinay[15];
if (c1 == '<') {
// text/html
if (c2 == '!'
|| ((c2 == 'h'
&& (c3 == 't' && c4 == 'm' && c5 == 'l'
|| c3 == 'e' && c4 == 'a' && c5 == 'd')
|| (c2 == 'b' && c3 == 'o' && c4 == 'd'
&& c5 == 'y')))
|| ((c2 == 'H'
&& (c3 == 'T' && c4 == 'M' && c5 == 'L'
|| c3 == 'E' && c4 == 'A' && c5 == 'D')
|| (c2 == 'B' && c3 == 'O' && c4 == 'D'
&& c5 == 'Y')))) {
return true;
}
// application/xml
if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l'
&& c6 == ' ') {
return true;
}
// application/svg+xml
if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ') {
return true;
}
}
// big and little (identical) endian UTF-8 encodings, with BOM
// application/xml
if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf) {
if (c4 == '<' && c5 == '?' && c6 == 'x') {
return true;
}
}
// big and little endian UTF-16 encodings, with byte order mark
// application/xml
if (cc1 == 0xfe && cc2 == 0xff) {
if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0
&& c8 == 'x') {
return true;
}
}
// application/xml
if (cc1 == 0xff && cc2 == 0xfe) {
if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x'
&& cc8 == 0) {
return true;
}
}
// big and little endian UTF-32 encodings, with BOM
// application/xml
if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff) {
if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0
&& cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0
&& cc14 == 0 && cc15 == 0 && c16 == 'x') {
return true;
}
}
// application/xml
if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00) {
if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?'
&& cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x'
&& cc14 == 0 && cc15 == 0 && cc16 == 0) {
return true;
}
}
// application/pdf (%PDF-)
if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45) {
return true;
}
// image/png
if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13
&& cc6 == 10 && cc7 == 26 && cc8 == 10) ||
(cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML
&& cc7 == 10 && cc8 == 26 && cc9 == 10)) {
return true;
}
// image/jpeg
if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF) {
if (cc4 == 0xE0 || cc4 == 0xEE) {
return true;
}
/**
* File format used by digital cameras to store images.
* Exif Format can be read by any application supporting
* JPEG. Exif Spec can be found at:
* http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF
*/
if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i'
&& c10 == 'f' && cc11 == 0)) {
return true;
}
}
// vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar)
if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04) {
return true;
} else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06) {
return true;
}
// mxfile, mxlibrary, mxGraphModel
if (c1 == '<' && c2 == 'm' && c3 == 'x') {
return true;
}
}
return false;
},
isConflict(origStat, stat) {
return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs;
},
getDraftFileName(fileObject) {
let filePath = fileObject.path;
let draftFileName = '', counter = 1, uniquePart = '';
do {
draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
uniquePart = '_' + counter++;
} while (fs.existsSync(draftFileName));
return draftFileName;
},
async getFileDrafts(fileObject) {
let filePath = fileObject.path;
let draftsPaths = [], drafts = [], draftFileName, counter = 1, uniquePart = '';
do {
draftsPaths.push(draftFileName);
draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
uniquePart = '_' + counter++;
} while (fs.existsSync(draftFileName)); //TODO this assume continuous drafts names
//Port old draft files to new prefex
counter = 1;
uniquePart = '';
let draftExists = false;
do {
draftFileName = path.join(path.dirname(filePath), OLD_DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
draftExists = fs.existsSync(draftFileName);
if (draftExists) {
const newDraftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
await fsProm.rename(draftFileName, newDraftFileName);
draftsPaths.push(newDraftFileName);
}
uniquePart = '_' + counter++;
} while (draftExists); //TODO this assume continuous drafts names
//Skip the first null element
for (let i = 1; i < draftsPaths.length; i++) {
try {
let stat = await fsProm.lstat(draftsPaths[i]);
drafts.push({
data: await fsProm.readFile(draftsPaths[i], 'utf8'),
created: stat.ctimeMs,
modified: stat.mtimeMs,
path: draftsPaths[i]
});
} catch (e) {
} // Ignore
}
return drafts;
},
async saveDraft(fileObject, data) {
if (!renderer.checkFileContent(data)) {
throw new Error('Invalid file data');
} else {
let draftFileName = fileObject.draftFileName || renderer.getDraftFileName(fileObject);
await fsProm.writeFile(draftFileName, data, 'utf8');
if (isWin) {
try {
// Add Hidden attribute:
spawn('attrib', ['+h', draftFileName], {shell: true});
} catch (e) {
}
}
return draftFileName;
}
},
async saveFile(fileObject, data, origStat, overwrite, defEnc) {
if (!renderer.checkFileContent(data)) {
throw new Error('Invalid file data');
}
let retryCount = 0;
let backupCreated = false;
let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
const oldBkpPath = path.join(path.dirname(fileObject.path), OLD_BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
let writeEnc = defEnc || fileObject.encoding;
let writeFile = async function () {
let fh;
try {
// O_SYNC is for sync I/O and reduce risk of file corruption
fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
await fsProm.writeFile(fh, data, writeEnc);
} finally {
await fh?.close();
}
let stat2 = await fsProm.stat(fileObject.path);
// Workaround for possible writing errors is to check the written
// contents of the file and retry 3 times before showing an error
let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
if (data != writtenData) {
retryCount++;
if (retryCount < 3) {
return await writeFile();
} else {
throw new Error('all saving trials failed');
}
} else {
//We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
if (backupCreated) {
//fs.unlink(bkpPath, (err) => {}); //Ignore errors!
//Delete old backup file with old prefix
if (fs.existsSync(oldBkpPath)) {
fs.unlink(oldBkpPath, () => {
//Ignore errors
});
}
}
return stat2;
}
};
async function doSaveFile(isNew) {
if (enableStoreBkp && !isNew) {
//Copy file to back up file (after conflict and stat is checked)
let bkpFh;
try {
//Use file read then write to open the backup file direct sync write to reduce the chance of file corruption
let fileContent = await fsProm.readFile(fileObject.path, writeEnc);
bkpFh = await fsProm.open(bkpPath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
await fsProm.writeFile(bkpFh, fileContent, writeEnc);
backupCreated = true;
} catch (e) {
if (__DEV__) {
console.log('Backup file writing failed', e); //Ignore
}
} finally {
await bkpFh?.close();
if (isWin) {
try {
// Add Hidden attribute:
spawn('attrib', ['+h', bkpPath], {shell: true});
} catch (e) {
}
}
}
}
return await writeFile();
}
if (overwrite) {
return await doSaveFile(true);
} else {
let stat = fs.existsSync(fileObject.path) ?
await fsProm.stat(fileObject.path) : null;
if (stat && renderer.isConflict(origStat, stat)) {
throw new Error('conflict');
} else {
return await doSaveFile(stat == null);
}
}
},
async writeFile(path, data, enc) {
if (!renderer.checkFileContent(data, enc)) {
throw new Error('Invalid file data');
} else {
return await fsProm.writeFile(path, data, enc);
}
},
getAppDataFolder() {
try {
let appDataDir = app.getPath('appData');
let drawioDir = appDataDir + '/' + config.name;
if (!fs.existsSync(drawioDir)) //Usually this dir already exists
{
fs.mkdirSync(drawioDir);
}
return drawioDir;
} catch (e) {
}
return '.';
},
getDocumentsFolder() {
//On windows, misconfigured Documents folder cause an exception
try {
return app.getPath('documents');
} catch (e) {
}
return '.';
},
checkFileExists(pathParts) {
let filePath = path.join(...pathParts);
return {exists: fs.existsSync(filePath), path: filePath};
},
async showOpenDialog(defaultPath, filters, properties) {
let win = BrowserWindow.getFocusedWindow();
return dialog.showOpenDialog(win, {
defaultPath: defaultPath,
filters: filters,
properties: properties
});
},
async showSaveDialog(defaultPath, filters) {
let win = BrowserWindow.getFocusedWindow();
return dialog.showSaveDialog(win, {
defaultPath: defaultPath,
filters: filters
});
},
async installPlugin(filePath) {
if (!enablePlugins) return {};
let pluginsDir = path.join(renderer.getAppDataFolder(), '/plugins');
if (!fs.existsSync(pluginsDir)) {
fs.mkdirSync(pluginsDir);
}
let pluginName = path.basename(filePath);
let dstFile = path.join(pluginsDir, pluginName);
if (fs.existsSync(dstFile)) {
throw new Error('fileExists');
} else {
await fsProm.copyFile(filePath, dstFile);
}
return {pluginName: pluginName, selDir: path.dirname(filePath)};
},
getPluginFile(plugin) {
if (!enablePlugins) return null;
const prefix = path.join(renderer.getAppDataFolder(), '/plugins/');
const pluginFile = path.join(prefix, plugin);
if (pluginFile.startsWith(prefix) && fs.existsSync(pluginFile)) {
return pluginFile;
}
return null;
},
async uninstallPlugin(plugin) {
const pluginFile = renderer.getPluginFile(plugin);
if (pluginFile != null) {
fs.unlinkSync(pluginFile);
}
},
dirname(path_p) {
return path.dirname(path_p);
},
async readFile(filename, encoding) {
let data = await fsProm.readFile(filename, encoding);
if (renderer.checkFileContent(data, encoding)) {
return data;
}
throw new Error('Invalid file data');
},
async fileStat(file) {
return await fsProm.stat(file);
},
async isFileWritable(file) {
try {
await fsProm.access(file, fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
},
clipboardAction(method, data) {
if (method == 'writeText') {
clipboard.writeText(data);
} else if (method == 'readText') {
return clipboard.readText();
} else if (method == 'writeImage') {
clipboard.write({
image:
nativeImage.createFromDataURL(data.dataUrl), html: '<img src="' +
data.dataUrl + '" width="' + data.w + '" height="' + data.h + '">'
});
}
},
async deleteFile(file) {
// Reading the header of the file to confirm it is a file we can delete
let fh = await fsProm.open(file, O_RDONLY);
let buffer = Buffer.allocUnsafe(16);
await fh.read(buffer, 0, 16);
await fh.close();
if (renderer.checkFileContent(buffer)) {
await fsProm.unlink(file);
}
},
async windowAction(method) {
let win = BrowserWindow.getFocusedWindow();
if (win) {
if (method == 'minimize') {
win.minimize();
} else if (method == 'maximize') {
win.maximize();
} else if (method == 'unmaximize') {
win.unmaximize();
} else if (method == 'close') {
win.close();
} else if (method == 'isMaximized') {
return win.isMaximized();
} else if (method == 'removeAllListeners') {
win.removeAllListeners();
}
}
},
async openExternal(url) {
//Only open http(s), mailto, tel, and callto links
if (allowedUrls.test(url)) {
await shell.openExternal(url)
}
},
async watchFile(path) {
let win = BrowserWindow.getFocusedWindow();
if (win) {
fs.watchFile(path, (curr, prev) => {
try {
win.webContents.send('fileChanged', {
path: path,
curr: curr,
prev: prev
});
} catch (e) {
// Ignore
}
});
}
},
async unwatchFile(path) {
fs.unwatchFile(path);
},
}
const onRenderer = (mainWindow) => {
ipcMain.on("rendererReq", async (event, args) => {
try {
let ret = null;
switch (args.action) {
case 'saveFile':
ret = await renderer.saveFile(args.fileObject, args.data, args.origStat, args.overwrite, args.defEnc);
break;
case 'writeFile':
ret = await renderer.writeFile(args.path, args.data, args.enc);
break;
case 'saveDraft':
ret = await renderer.saveDraft(args.fileObject, args.data);
break;
case 'getFileDrafts':
ret = await renderer.getFileDrafts(args.fileObject);
break;
case 'getDocumentsFolder':
ret = await renderer.getDocumentsFolder();
break;
case 'checkFileExists':
ret = renderer.checkFileExists(args.pathParts);
break;
case 'showOpenDialog':
dialogOpen = true;
ret = await renderer.showOpenDialog(args.defaultPath, args.filters, args.properties);
ret = ret.filePaths;
dialogOpen = false;
break;
case 'showSaveDialog':
dialogOpen = true;
ret = await renderer.showSaveDialog(args.defaultPath, args.filters);
ret = ret.canceled ? null : ret.filePath;
dialogOpen = false;
break;
case 'installPlugin':
ret = await renderer.installPlugin(args.filePath);
break;
case 'uninstallPlugin':
ret = await renderer.uninstallPlugin(args.plugin);
break;
case 'getPluginFile':
ret = renderer.getPluginFile(args.plugin);
break;
case 'isPluginsEnabled':
ret = enablePlugins;
break;
case 'dirname':
ret = await renderer.dirname(args.path);
break;
case 'readFile':
ret = await renderer.readFile(args.filename, args.encoding);
break;
case 'clipboardAction':
ret = renderer.clipboardAction(args.method, args.data);
break;
case 'deleteFile':
ret = await renderer.deleteFile(args.file);
break;
case 'fileStat':
ret = await renderer.fileStat(args.file);
break;
case 'isFileWritable':
ret = await renderer.isFileWritable(args.file);
break;
case 'windowAction':
ret = await renderer.windowAction(args.method);
break;
case 'openExternal':
ret = await renderer.openExternal(args.url);
break;
case 'openDownloadWindow':
ret = await electronDown.open(args.language || 'zh', args.theme || 'light');
break;
case 'updateDownloadWindow':
ret = await electronDown.updateWindow(args.language, args.theme);
break;
case 'createDownload':
ret = await electronDown.createDownload(mainWindow, args.url, args.options || {});
break;
case 'watchFile':
ret = await renderer.watchFile(args.path);
break;
case 'unwatchFile':
ret = await renderer.unwatchFile(args.path);
break;
case 'getCurDir':
ret = __dirname;
break;
}
event.reply('mainResp', {success: true, data: ret, reqId: args.reqId});
} catch (e) {
event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId});
loger.error('Renderer request error', e.message, e.stack);
}
});
}
module.exports = { renderer, onRenderer };