58 lines
2.9 KiB
JavaScript
58 lines
2.9 KiB
JavaScript
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const { randomUUID } = require('node:crypto');
|
|
|
|
// Exclusive creation protects originals, existing exports and case-insensitive collisions.
|
|
async function saveUnique(directory, filename, bytes) {
|
|
if (typeof filename !== 'string' || !/^[^<>:"/\\|?*\x00-\x1f]+\.(png|jpg)$/i.test(filename) || filename.length > 220 || /[. ]$/.test(filename) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])\./i.test(filename)) throw new Error('Invalid output filename.');
|
|
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) throw new Error('No encoded image data.');
|
|
const extension = path.extname(filename), stem = filename.slice(0, -extension.length);
|
|
for (let index = 1; index <= 10000; index++) {
|
|
const name = `${stem}${index === 1 ? '' : '-' + index}${extension}`;
|
|
const target = path.join(directory, name);
|
|
let handle;
|
|
try { handle = await fs.open(target, 'wx'); }
|
|
catch (error) { if (error.code === 'EEXIST') continue; throw error; }
|
|
try {
|
|
await handle.writeFile(bytes);
|
|
await handle.close();
|
|
return { filename: name, path: target };
|
|
} catch (error) {
|
|
await handle.close().catch(() => {});
|
|
await fs.unlink(target).catch(() => {});
|
|
throw error;
|
|
}
|
|
}
|
|
throw new Error('Too many files with this name. Choose another output folder.');
|
|
}
|
|
|
|
function registerOutput({ ipcMain, dialog, BrowserWindow }) {
|
|
const folders = new Map();
|
|
function check(event) {
|
|
const url = new URL(event.senderFrame.url);
|
|
if (event.senderFrame !== event.sender.mainFrame || url.protocol !== 'untrace:' || url.host !== 'app') throw new Error('Untrusted sender.');
|
|
}
|
|
ipcMain.handle('output:choose', async event => {
|
|
check(event);
|
|
const selection = await dialog.showOpenDialog(BrowserWindow.fromWebContents(event.sender), { title: 'Choose output folder', properties: ['openDirectory', 'createDirectory'] });
|
|
if (selection.canceled || !selection.filePaths[0]) return null;
|
|
const directory = await fs.realpath(selection.filePaths[0]);
|
|
const token = randomUUID();
|
|
folders.set(event.sender.id, { token, directory });
|
|
return { token, path: directory };
|
|
});
|
|
ipcMain.handle('output:save', async (event, token, filename, bytes) => {
|
|
check(event);
|
|
const folder = folders.get(event.sender.id);
|
|
if (!folder || token !== folder.token) throw new Error('Choose an output folder first.');
|
|
try { return await saveUnique(folder.directory, filename, bytes); }
|
|
catch (error) {
|
|
if (['EACCES', 'EPERM', 'EROFS'].includes(error.code)) throw new Error('The output folder is not writable. Choose another folder.');
|
|
if (error.code === 'ENOSPC') throw new Error('The drive is full. Free space or choose another folder.');
|
|
if (error.code === 'ENOENT') throw new Error('The output folder is unavailable. Choose it again.');
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
module.exports = { saveUnique, registerOutput };
|