mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
refactor(main): extract file ops to src/main/files/ (search, git, binary)
This commit is contained in:
+10
-195
@@ -6,6 +6,7 @@ const WordTemplateExporter = require('./wordTemplateExporter');
|
||||
const PDFOperations = require('./main/PDFOperations');
|
||||
const GitOperations = require('./main/GitOperations');
|
||||
const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./main/utils/paths');
|
||||
const fileOps = require('./main/files');
|
||||
|
||||
// Add MiKTeX to PATH for LaTeX support
|
||||
if (process.platform === 'win32') {
|
||||
@@ -2936,55 +2937,6 @@ ipcMain.on('select-folder', (event, type) => {
|
||||
}
|
||||
});
|
||||
|
||||
// List directory contents as FileEntry[]
|
||||
ipcMain.handle('list-directory', async (event, dirPath) => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
return entries.map((dirent) => {
|
||||
const fullPath = path.join(dirPath, dirent.name);
|
||||
let size;
|
||||
let modifiedAt;
|
||||
if (!dirent.isDirectory()) {
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
size = stat.size;
|
||||
modifiedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// ignore stat errors
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: dirent.name,
|
||||
path: fullPath,
|
||||
isDirectory: dirent.isDirectory(),
|
||||
size,
|
||||
modifiedAt,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Show folder picker dialog and return selected path or null
|
||||
ipcMain.handle('pick-folder', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) return null;
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
// Show file picker dialog for markdown files and return selected path or null
|
||||
ipcMain.handle('pick-file', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'mdown', 'mkd'] }],
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) return null;
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||
@@ -3663,6 +3615,15 @@ app.whenReady().then(() => {
|
||||
|
||||
createWindow();
|
||||
|
||||
// Register file ops IPC handlers
|
||||
fileOps.register({
|
||||
validatePath,
|
||||
resolveWritablePath,
|
||||
isPathAccessible,
|
||||
currentFileRef: { get current() { return currentFile; } },
|
||||
mainWindow,
|
||||
});
|
||||
|
||||
// Handle file association on app startup
|
||||
// In packaged apps, process.argv structure is different:
|
||||
// Development: ['electron', 'app.js', 'file.md'] - need slice(2)
|
||||
@@ -3994,152 +3955,6 @@ ipcMain.handle('select-custom-css', async (event) => {
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('read-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
return fs.readFileSync(validation.resolved, 'utf-8');
|
||||
});
|
||||
|
||||
ipcMain.handle('write-file', async (event, payload) => {
|
||||
const validation = resolveWritablePath(payload?.path);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(validation.resolved), { recursive: true });
|
||||
fs.writeFileSync(validation.resolved, payload?.content ?? '', 'utf-8');
|
||||
return { path: validation.resolved };
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
|
||||
fs.rmSync(validation.resolved, { recursive: true, force: false });
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('ensure-directory', async (event, dirPath) => {
|
||||
const validation = resolveWritablePath(dirPath);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid directory path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(validation.resolved, { recursive: true });
|
||||
return validation.resolved;
|
||||
});
|
||||
|
||||
ipcMain.handle('path-exists', async (event, filePath) => {
|
||||
const validation = resolveWritablePath(filePath);
|
||||
return validation.valid ? fs.existsSync(validation.resolved) : false;
|
||||
});
|
||||
|
||||
ipcMain.handle('is-directory', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fs.statSync(validation.resolved).isDirectory();
|
||||
});
|
||||
|
||||
ipcMain.handle('copy-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
ipcMain.handle('move-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
|
||||
try {
|
||||
fs.renameSync(sourceValidation.resolved, destinationValidation.resolved);
|
||||
} catch (error) {
|
||||
if (error.code !== 'EXDEV') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
fs.rmSync(sourceValidation.resolved, { recursive: true, force: false });
|
||||
}
|
||||
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
// Open a file by path (from explorer panel)
|
||||
ipcMain.on('open-file-path', (event, filePath) => {
|
||||
try {
|
||||
// Validate path to prevent traversal attacks
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid) {
|
||||
console.error('[SECURITY] Invalid file path:', validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPathAccessible(validation.resolved)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(validation.resolved);
|
||||
if (stat.size > MAX_FILE_SIZE) return;
|
||||
currentFile = validation.resolved;
|
||||
const content = fs.readFileSync(validation.resolved, 'utf-8');
|
||||
mainWindow.webContents.send('file-opened', { path: validation.resolved, content });
|
||||
} catch (err) {
|
||||
console.error('open-file-path error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Git IPC Handlers
|
||||
// ============================================
|
||||
ipcMain.handle('git-status', async () => {
|
||||
const dir = currentFile ? path.dirname(currentFile) : process.cwd();
|
||||
return GitOperations.getStatus(dir);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-stage', async (event, { files }) => {
|
||||
const dir = currentFile ? path.dirname(currentFile) : process.cwd();
|
||||
return GitOperations.stage(dir, files);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-commit', async (event, { message }) => {
|
||||
const dir = currentFile ? path.dirname(currentFile) : process.cwd();
|
||||
return GitOperations.commit(dir, message);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-log', async () => {
|
||||
const dir = currentFile ? path.dirname(currentFile) : process.cwd();
|
||||
return GitOperations.log(dir);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Snippets IPC Handlers
|
||||
// ============================================
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// src/main/files/binary.js
|
||||
// Binary file write handler (used by Word .docx export)
|
||||
const { ipcMain } = require('electron');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
function register() {
|
||||
ipcMain.handle('write-buffer', async (_event, { path: filePath, buffer }) => {
|
||||
await fs.writeFile(filePath, Buffer.from(buffer));
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { register };
|
||||
@@ -0,0 +1,28 @@
|
||||
// src/main/files/git.js
|
||||
// Git IPC handlers — thin wrapper over GitOperations
|
||||
const { ipcMain } = require('electron');
|
||||
const GitOperations = require('../GitOperations');
|
||||
|
||||
function register(currentFileRef) {
|
||||
ipcMain.handle('git-status', async () => {
|
||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
||||
return GitOperations.getStatus(dir);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-stage', async (_event, { files }) => {
|
||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
||||
return GitOperations.stage(dir, files);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-commit', async (_event, { message }) => {
|
||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
||||
return GitOperations.commit(dir, message);
|
||||
});
|
||||
|
||||
ipcMain.handle('git-log', async () => {
|
||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
||||
return GitOperations.log(dir);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { register };
|
||||
@@ -0,0 +1,161 @@
|
||||
// src/main/files/index.js
|
||||
// File ops facade — registers all file-related IPC handlers
|
||||
const { ipcMain, dialog, shell } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { listDirectory } = require('./search');
|
||||
const { register: registerGit } = require('./git');
|
||||
const { register: registerBinary } = require('./binary');
|
||||
|
||||
function register({ validatePath, resolveWritablePath, isPathAccessible, currentFileRef, mainWindow }) {
|
||||
// list-directory (simple version)
|
||||
ipcMain.handle('list-directory', async (event, dirPath) => listDirectory(dirPath));
|
||||
|
||||
// pick-folder
|
||||
ipcMain.handle('pick-folder', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) return null;
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
// pick-file
|
||||
ipcMain.handle('pick-file', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Markdown', extensions: ['md', 'markdown', 'mdown', 'mkd'] }],
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) return null;
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
// read-file
|
||||
ipcMain.handle('read-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
return fs.readFileSync(validation.resolved, 'utf-8');
|
||||
});
|
||||
|
||||
// write-file
|
||||
ipcMain.handle('write-file', async (event, payload) => {
|
||||
const validation = resolveWritablePath(payload?.path);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
fs.mkdirSync(path.dirname(validation.resolved), { recursive: true });
|
||||
fs.writeFileSync(validation.resolved, payload?.content ?? '', 'utf-8');
|
||||
return { path: validation.resolved };
|
||||
});
|
||||
|
||||
// delete-file
|
||||
ipcMain.handle('delete-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
throw new Error(validation.error || 'Invalid file path');
|
||||
}
|
||||
fs.rmSync(validation.resolved, { recursive: true, force: false });
|
||||
return true;
|
||||
});
|
||||
|
||||
// ensure-directory
|
||||
ipcMain.handle('ensure-directory', async (event, dirPath) => {
|
||||
const validation = resolveWritablePath(dirPath);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error || 'Invalid directory path');
|
||||
}
|
||||
fs.mkdirSync(validation.resolved, { recursive: true });
|
||||
return validation.resolved;
|
||||
});
|
||||
|
||||
// path-exists
|
||||
ipcMain.handle('path-exists', async (event, filePath) => {
|
||||
const validation = resolveWritablePath(filePath);
|
||||
return validation.valid ? fs.existsSync(validation.resolved) : false;
|
||||
});
|
||||
|
||||
// is-directory
|
||||
ipcMain.handle('is-directory', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
return false;
|
||||
}
|
||||
return fs.statSync(validation.resolved).isDirectory();
|
||||
});
|
||||
|
||||
// copy-path
|
||||
ipcMain.handle('copy-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
// move-path
|
||||
ipcMain.handle('move-path', async (event, payload) => {
|
||||
const sourceValidation = validatePath(payload?.source);
|
||||
const destinationValidation = resolveWritablePath(payload?.destination);
|
||||
|
||||
if (!sourceValidation.valid || !isPathAccessible(sourceValidation.resolved)) {
|
||||
throw new Error(sourceValidation.error || 'Invalid source path');
|
||||
}
|
||||
if (!destinationValidation.valid) {
|
||||
throw new Error(destinationValidation.error || 'Invalid destination path');
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destinationValidation.resolved), { recursive: true });
|
||||
|
||||
try {
|
||||
fs.renameSync(sourceValidation.resolved, destinationValidation.resolved);
|
||||
} catch (error) {
|
||||
if (error.code !== 'EXDEV') {
|
||||
throw error;
|
||||
}
|
||||
fs.cpSync(sourceValidation.resolved, destinationValidation.resolved, { recursive: true });
|
||||
fs.rmSync(sourceValidation.resolved, { recursive: true, force: false });
|
||||
}
|
||||
|
||||
return { source: sourceValidation.resolved, destination: destinationValidation.resolved };
|
||||
});
|
||||
|
||||
// open-file-path
|
||||
ipcMain.on('open-file-path', (event, filePath) => {
|
||||
try {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid) {
|
||||
console.error('[SECURITY] Invalid file path:', validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPathAccessible(validation.resolved)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = fs.statSync(validation.resolved);
|
||||
if (stat.size > 50 * 1024 * 1024) return;
|
||||
const content = fs.readFileSync(validation.resolved, 'utf-8');
|
||||
// Emit set-current-file back to main process so it updates its currentFile variable
|
||||
mainWindow.webContents.send('set-current-file', validation.resolved);
|
||||
mainWindow.webContents.send('file-opened', { path: validation.resolved, content });
|
||||
} catch (err) {
|
||||
console.error('open-file-path error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Register sub-modules
|
||||
registerGit(currentFileRef);
|
||||
registerBinary();
|
||||
}
|
||||
|
||||
module.exports = { register };
|
||||
@@ -0,0 +1,35 @@
|
||||
// src/main/files/search.js
|
||||
// File search + basic list ops
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function listDirectory(dirPath) {
|
||||
try {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
return entries.map((dirent) => {
|
||||
const fullPath = path.join(dirPath, dirent.name);
|
||||
let size;
|
||||
let modifiedAt;
|
||||
if (!dirent.isDirectory()) {
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
size = stat.size;
|
||||
modifiedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// ignore stat errors
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: dirent.name,
|
||||
path: fullPath,
|
||||
isDirectory: dirent.isDirectory(),
|
||||
size,
|
||||
modifiedAt,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { listDirectory };
|
||||
Reference in New Issue
Block a user