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 PDFOperations = require('./main/PDFOperations');
|
||||||
const GitOperations = require('./main/GitOperations');
|
const GitOperations = require('./main/GitOperations');
|
||||||
const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./main/utils/paths');
|
const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./main/utils/paths');
|
||||||
|
const fileOps = require('./main/files');
|
||||||
|
|
||||||
// Add MiKTeX to PATH for LaTeX support
|
// Add MiKTeX to PATH for LaTeX support
|
||||||
if (process.platform === 'win32') {
|
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 }) => {
|
ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
||||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||||
@@ -3663,6 +3615,15 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
|
// Register file ops IPC handlers
|
||||||
|
fileOps.register({
|
||||||
|
validatePath,
|
||||||
|
resolveWritablePath,
|
||||||
|
isPathAccessible,
|
||||||
|
currentFileRef: { get current() { return currentFile; } },
|
||||||
|
mainWindow,
|
||||||
|
});
|
||||||
|
|
||||||
// Handle file association on app startup
|
// Handle file association on app startup
|
||||||
// In packaged apps, process.argv structure is different:
|
// In packaged apps, process.argv structure is different:
|
||||||
// Development: ['electron', 'app.js', 'file.md'] - need slice(2)
|
// Development: ['electron', 'app.js', 'file.md'] - need slice(2)
|
||||||
@@ -3994,152 +3955,6 @@ ipcMain.handle('select-custom-css', async (event) => {
|
|||||||
return null;
|
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
|
// 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