fix: Handle res.sendError TypeError and improve SFTP connection stability
- Modified `getSession` to throw errors instead of using `res.sendError`, ensuring compatibility with HTTP and WebSocket contexts - Updated HTTP routes to use `initApi` middleware for consistent error handling with `res.sendError` - Enhanced WebSocket routes to catch and send errors via WebSocket messages - Improved SFTP error handling with better logging and session cleanup - Added input validation and try-catch blocks to prevent unhandled errors - Fixed `Error: No response from server` by ensuring proper session management
This commit is contained in:
248
sftp-browser.js
248
sftp-browser.js
@ -6,7 +6,7 @@ const logger = require('cyber-express-logger');
|
|||||||
const sftp = require('ssh2-sftp-client');
|
const sftp = require('ssh2-sftp-client');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const cors = require('cors'); // Added CORS package
|
const cors = require('cors');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const archiver = require('archiver');
|
const archiver = require('archiver');
|
||||||
const rawBodyParser = bodyParser.raw({
|
const rawBodyParser = bodyParser.raw({
|
||||||
@ -29,19 +29,20 @@ const normalizeRemotePath = remotePath => {
|
|||||||
|
|
||||||
const sessions = {};
|
const sessions = {};
|
||||||
const sessionActivity = {};
|
const sessionActivity = {};
|
||||||
const connections = {}; // Store connection details
|
const connections = {};
|
||||||
|
|
||||||
const getObjectHash = obj => {
|
const getObjectHash = obj => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
hash.update(JSON.stringify(obj));
|
hash.update(JSON.stringify(obj));
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {sftp.ConnectOptions} opts
|
* @param {sftp.ConnectOptions} opts
|
||||||
* @returns {Promise<sftp>|null}
|
* @returns {Promise<sftp>}
|
||||||
* */
|
* @throws {Error} If connection fails
|
||||||
const getSession = async (res, opts) => {
|
*/
|
||||||
|
const getSession = async (opts) => {
|
||||||
const hash = getObjectHash(opts);
|
const hash = getObjectHash(opts);
|
||||||
const address = `${opts.username}@${opts.host}:${opts.port}`;
|
const address = `${opts.username}@${opts.host}:${opts.port}`;
|
||||||
if (sessions[hash]) {
|
if (sessions[hash]) {
|
||||||
@ -52,17 +53,25 @@ const getSession = async (res, opts) => {
|
|||||||
console.log(`Creating new connection to ${address}`);
|
console.log(`Creating new connection to ${address}`);
|
||||||
const session = new sftp();
|
const session = new sftp();
|
||||||
sessions[hash] = session;
|
sessions[hash] = session;
|
||||||
session.on('end', () => delete sessions[hash]);
|
session.on('end', () => {
|
||||||
session.on('close', () => delete sessions[hash]);
|
console.log(`Session ended for ${address}`);
|
||||||
|
delete sessions[hash];
|
||||||
|
delete sessionActivity[hash];
|
||||||
|
});
|
||||||
|
session.on('close', () => {
|
||||||
|
console.log(`Session closed for ${address}`);
|
||||||
|
delete sessions[hash];
|
||||||
|
delete sessionActivity[hash];
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await session.connect(opts);
|
await session.connect(opts);
|
||||||
sessionActivity[hash] = Date.now();
|
sessionActivity[hash] = Date.now();
|
||||||
|
return session;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
delete sessions[hash];
|
delete sessions[hash];
|
||||||
console.log(`Connection to ${address} failed`);
|
console.error(`Connection to ${address} failed: ${error.message}`);
|
||||||
return res ? res.sendError(error) : null;
|
throw error;
|
||||||
}
|
}
|
||||||
return session;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const srv = express();
|
const srv = express();
|
||||||
@ -72,12 +81,11 @@ expressWs(srv, undefined, {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable CORS for all routes
|
|
||||||
srv.use(cors({
|
srv.use(cors({
|
||||||
origin: '*', // Allow all origins; can be restricted to specific origins if needed
|
origin: '*',
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Allow necessary methods
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'sftp-host', 'sftp-port', 'sftp-username', 'sftp-password', 'sftp-key'], // Allow relevant headers
|
allowedHeaders: ['Content-Type', 'sftp-host', 'sftp-port', 'sftp-username', 'sftp-password', 'sftp-key'],
|
||||||
credentials: false // Set to true if cookies or auth headers are needed
|
credentials: false
|
||||||
}));
|
}));
|
||||||
srv.use(logger());
|
srv.use(logger());
|
||||||
const staticDir = path.join(__dirname, 'web');
|
const staticDir = path.join(__dirname, 'web');
|
||||||
@ -90,7 +98,7 @@ const initApi = asyncHandler(async (req, res, next) => {
|
|||||||
res.data.success = false;
|
res.data.success = false;
|
||||||
res.data.error = `${error}`.replace('Error: ', '');
|
res.data.error = `${error}`.replace('Error: ', '');
|
||||||
res.sendData(status);
|
res.sendData(status);
|
||||||
}
|
};
|
||||||
res.data = {
|
res.data = {
|
||||||
success: true
|
success: true
|
||||||
};
|
};
|
||||||
@ -107,12 +115,14 @@ const initApi = asyncHandler(async (req, res, next) => {
|
|||||||
return res.sendError('Missing username header');
|
return res.sendError('Missing username header');
|
||||||
if (!req.connectionOpts.password && !req.connectionOpts.privateKey)
|
if (!req.connectionOpts.password && !req.connectionOpts.privateKey)
|
||||||
return res.sendError('Missing password or key header');
|
return res.sendError('Missing password or key header');
|
||||||
req.session = await getSession(res, req.connectionOpts);
|
try {
|
||||||
if (!req.session) return;
|
req.session = await getSession(req.connectionOpts);
|
||||||
next();
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
res.sendError(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// API endpoint to fetch connection details by connectionId
|
|
||||||
srv.get('/api/connect/:connectionId', asyncHandler(async (req, res) => {
|
srv.get('/api/connect/:connectionId', asyncHandler(async (req, res) => {
|
||||||
const connectionId = req.params.connectionId;
|
const connectionId = req.params.connectionId;
|
||||||
const connection = connections[connectionId];
|
const connection = connections[connectionId];
|
||||||
@ -131,7 +141,6 @@ srv.get('/api/connect/:connectionId', asyncHandler(async (req, res) => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Auto-connection endpoint
|
|
||||||
srv.post('/auto-connection', bodyParser.json(), asyncHandler(async (req, res) => {
|
srv.post('/auto-connection', bodyParser.json(), asyncHandler(async (req, res) => {
|
||||||
const connectionDetails = {
|
const connectionDetails = {
|
||||||
host: req.body.host,
|
host: req.body.host,
|
||||||
@ -141,7 +150,6 @@ srv.post('/auto-connection', bodyParser.json(), asyncHandler(async (req, res) =>
|
|||||||
privateKey: req.body.privateKey || undefined
|
privateKey: req.body.privateKey || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!connectionDetails.host) {
|
if (!connectionDetails.host) {
|
||||||
return res.status(400).json({ success: false, error: 'Missing host' });
|
return res.status(400).json({ success: false, error: 'Missing host' });
|
||||||
}
|
}
|
||||||
@ -152,39 +160,32 @@ srv.post('/auto-connection', bodyParser.json(), asyncHandler(async (req, res) =>
|
|||||||
return res.status(400).json({ success: false, error: 'Missing password or key' });
|
return res.status(400).json({ success: false, error: 'Missing password or key' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate connection ID
|
|
||||||
const connectionId = utils.randomHex(32);
|
const connectionId = utils.randomHex(32);
|
||||||
|
|
||||||
// Store connection details
|
|
||||||
connections[connectionId] = {
|
connections[connectionId] = {
|
||||||
...connectionDetails,
|
...connectionDetails,
|
||||||
created: Date.now()
|
created: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test connection
|
try {
|
||||||
const session = await getSession(res, connectionDetails);
|
const session = await getSession(connectionDetails);
|
||||||
if (!session) {
|
session.end();
|
||||||
|
const connectionUrl = `https://${req.get('host')}/connect/${connectionId}`;
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
connectionId,
|
||||||
|
connectionUrl
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
delete connections[connectionId];
|
delete connections[connectionId];
|
||||||
return res.status(400).json({ success: false, error: 'Failed to establish connection' });
|
res.status(400).json({ success: false, error: `Failed to establish connection: ${error.message}` });
|
||||||
}
|
}
|
||||||
session.end();
|
|
||||||
|
|
||||||
// Return connection URL
|
|
||||||
const connectionUrl = `https://${req.get('host')}/connect/${connectionId}`;
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
connectionId,
|
|
||||||
connectionUrl
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Handle connection URL access
|
|
||||||
srv.get('/connect/:connectionId', asyncHandler(async (req, res) => {
|
srv.get('/connect/:connectionId', asyncHandler(async (req, res) => {
|
||||||
const connectionId = req.params.connectionId;
|
const connectionId = req.params.connectionId;
|
||||||
if (!connections[connectionId]) {
|
if (!connections[connectionId]) {
|
||||||
return res.status(404).send('Connection not found');
|
return res.status(404).send('Connection not found');
|
||||||
}
|
}
|
||||||
// Serve the main application page (index.html)
|
|
||||||
res.sendFile(path.join(staticDir, 'index.html'));
|
res.sendFile(path.join(staticDir, 'index.html'));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -194,8 +195,8 @@ srv.get('/api/sftp/key', initApi, async (req, res) => {
|
|||||||
keyedRequests[res.data.key] = req;
|
keyedRequests[res.data.key] = req;
|
||||||
res.sendData();
|
res.sendData();
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.get('/api/sftp/directories/list', initApi, async (req, res) => {
|
srv.get('/api/sftp/directories/list', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
res.data.includesFiles = req.query.dirsOnly === 'true' ? false : true;
|
res.data.includesFiles = req.query.dirsOnly === 'true' ? false : true;
|
||||||
@ -210,25 +211,23 @@ srv.get('/api/sftp/directories/list', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
||||||
if (!wsReq.query.key) return ws.close();
|
if (!wsReq.query.key) return ws.close();
|
||||||
const req = keyedRequests[wsReq.query.key];
|
const req = keyedRequests[wsReq.query.key];
|
||||||
if (!req) return ws.close();
|
if (!req) return ws.close();
|
||||||
// Add uniqueness to the connection opts
|
|
||||||
// This forces a new connection to be created
|
|
||||||
req.connectionOpts.ts = Date.now();
|
req.connectionOpts.ts = Date.now();
|
||||||
// Create the session and throw an error if it fails
|
let session;
|
||||||
/** @type {sftp} */
|
try {
|
||||||
const session = await getSession(null, req.connectionOpts);
|
session = await getSession(req.connectionOpts);
|
||||||
const sessionHash = getObjectHash(req.connectionOpts);
|
} catch (error) {
|
||||||
if (!session) {
|
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to create session!'
|
error: `Failed to create session: ${error.message}`
|
||||||
}));
|
}));
|
||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
// Normalize the file path or throw an error if it's missing
|
const sessionHash = getObjectHash(req.connectionOpts);
|
||||||
const filePath = normalizeRemotePath(wsReq.query.path);
|
const filePath = normalizeRemotePath(wsReq.query.path);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@ -237,7 +236,6 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
}));
|
}));
|
||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
// Get the query
|
|
||||||
const query = wsReq.query.query;
|
const query = wsReq.query.query;
|
||||||
if (!query) {
|
if (!query) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@ -246,13 +244,11 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
}));
|
}));
|
||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
// Update the session activity periodically
|
|
||||||
let interval;
|
let interval;
|
||||||
const updateActivity = () => {
|
const updateActivity = () => {
|
||||||
sessionActivity[sessionHash] = Date.now();
|
sessionActivity[sessionHash] = Date.now();
|
||||||
};
|
};
|
||||||
interval = setInterval(updateActivity, 1000 * 1);
|
interval = setInterval(updateActivity, 1000 * 1);
|
||||||
// Handle websocket closure
|
|
||||||
let isClosed = false;
|
let isClosed = false;
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
console.log(`Directory search websocket closed`);
|
console.log(`Directory search websocket closed`);
|
||||||
@ -261,14 +257,11 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
delete sessionActivity[sessionHash];
|
delete sessionActivity[sessionHash];
|
||||||
isClosed = true;
|
isClosed = true;
|
||||||
});
|
});
|
||||||
// Listen for messages
|
|
||||||
console.log(`Websocket opened to search directory ${req.connectionOpts.username}@${req.connectionOpts.host}:${req.connectionOpts.port} ${filePath}`);
|
console.log(`Websocket opened to search directory ${req.connectionOpts.username}@${req.connectionOpts.host}:${req.connectionOpts.port} ${filePath}`);
|
||||||
// Function to get a directory listing
|
|
||||||
const scanDir = async (dirPath) => {
|
const scanDir = async (dirPath) => {
|
||||||
try {
|
try {
|
||||||
const list = await session.list(dirPath);
|
const list = await session.list(dirPath);
|
||||||
return [...list].sort((a, b) => {
|
return [...list].sort((a, b) => {
|
||||||
// Sort by name
|
|
||||||
if (a.name < b.name) return -1;
|
if (a.name < b.name) return -1;
|
||||||
if (a.name > b.name) return 1;
|
if (a.name > b.name) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
@ -277,7 +270,6 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Function to send a list when there are enough files
|
|
||||||
let matchedFiles = [];
|
let matchedFiles = [];
|
||||||
let lastSend = 0;
|
let lastSend = 0;
|
||||||
const sendList = () => {
|
const sendList = () => {
|
||||||
@ -291,8 +283,7 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
lastSend = Date.now();
|
lastSend = Date.now();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Function to recursively search a directory
|
const recurse = async (dirPath) => {
|
||||||
const recurse = async dirPath => {
|
|
||||||
if (isClosed) return;
|
if (isClosed) return;
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
@ -314,22 +305,19 @@ srv.ws('/api/sftp/directories/search', async (ws, wsReq) => {
|
|||||||
matchedFiles.push(file);
|
matchedFiles.push(file);
|
||||||
}
|
}
|
||||||
if ((Date.now() - lastSend) > 1000) sendList();
|
if ((Date.now() - lastSend) > 1000) sendList();
|
||||||
if (file.type == 'd') {
|
if (file.type === 'd') {
|
||||||
await recurse(file.path);
|
await recurse(file.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Start the search
|
|
||||||
await recurse(filePath);
|
await recurse(filePath);
|
||||||
if (isClosed) return;
|
if (isClosed) return;
|
||||||
sendList();
|
sendList();
|
||||||
// Send a complete message
|
|
||||||
ws.send(JSON.stringify({ success: true, status: 'complete' }));
|
ws.send(JSON.stringify({ success: true, status: 'complete' }));
|
||||||
// Close the websocket
|
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.post('/api/sftp/directories/create', initApi, async (req, res) => {
|
srv.post('/api/sftp/directories/create', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -340,8 +328,8 @@ srv.post('/api/sftp/directories/create', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.delete('/api/sftp/directories/delete', initApi, async (req, res) => {
|
srv.delete('/api/sftp/directories/delete', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -352,8 +340,8 @@ srv.delete('/api/sftp/directories/delete', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.get('/api/sftp/files/exists', initApi, async (req, res) => {
|
srv.get('/api/sftp/files/exists', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -366,8 +354,8 @@ srv.get('/api/sftp/files/exists', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.post('/api/sftp/files/create', initApi, rawBodyParser, async (req, res) => {
|
srv.post('/api/sftp/files/create', initApi, rawBodyParser, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -378,8 +366,8 @@ srv.post('/api/sftp/files/create', initApi, rawBodyParser, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.put('/api/sftp/files/append', initApi, rawBodyParser, async (req, res) => {
|
srv.put('/api/sftp/files/append', initApi, rawBodyParser, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -390,25 +378,23 @@ srv.put('/api/sftp/files/append', initApi, rawBodyParser, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.ws('/api/sftp/files/append', async (ws, wsReq) => {
|
srv.ws('/api/sftp/files/append', async (ws, wsReq) => {
|
||||||
if (!wsReq.query.key) return ws.close();
|
if (!wsReq.query.key) return ws.close();
|
||||||
const req = keyedRequests[wsReq.query.key];
|
const req = keyedRequests[wsReq.query.key];
|
||||||
if (!req) return ws.close();
|
if (!req) return ws.close();
|
||||||
// Add uniqueness to the connection opts
|
|
||||||
// This forces a new connection to be created
|
|
||||||
req.connectionOpts.ts = Date.now();
|
req.connectionOpts.ts = Date.now();
|
||||||
// Create the session and throw an error if it fails
|
let session;
|
||||||
/** @type {sftp} */
|
try {
|
||||||
const session = await getSession(null, req.connectionOpts);
|
session = await getSession(req.connectionOpts);
|
||||||
const sessionHash = getObjectHash(req.connectionOpts);
|
} catch (error) {
|
||||||
if (!session) {
|
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to create session!'
|
error: `Failed to create session: ${error.message}`
|
||||||
}));
|
}));
|
||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
// Normalize the file path or throw an error if it's missing
|
const sessionHash = getObjectHash(req.connectionOpts);
|
||||||
const filePath = normalizeRemotePath(wsReq.query.path);
|
const filePath = normalizeRemotePath(wsReq.query.path);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@ -417,17 +403,14 @@ srv.ws('/api/sftp/files/append', async (ws, wsReq) => {
|
|||||||
}));
|
}));
|
||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
// Handle websocket closure
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
console.log(`File append websocket closed`);
|
console.log(`File append websocket closed`);
|
||||||
session.end();
|
session.end();
|
||||||
delete sessionActivity[sessionHash];
|
delete sessionActivity[sessionHash];
|
||||||
});
|
});
|
||||||
// Listen for messages
|
|
||||||
console.log(`Websocket opened to append to ${req.connectionOpts.username}@${req.connectionOpts.host}:${req.connectionOpts.port} ${filePath}`);
|
console.log(`Websocket opened to append to ${req.connectionOpts.username}@${req.connectionOpts.host}:${req.connectionOpts.port} ${filePath}`);
|
||||||
let isWriting = false;
|
let isWriting = false;
|
||||||
ws.on('message', async (data) => {
|
ws.on('message', async (data) => {
|
||||||
// If we're already writing, send an error
|
|
||||||
if (isWriting) {
|
if (isWriting) {
|
||||||
return ws.send(JSON.stringify({
|
return ws.send(JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
@ -435,7 +418,6 @@ srv.ws('/api/sftp/files/append', async (ws, wsReq) => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Append the data to the file
|
|
||||||
isWriting = true;
|
isWriting = true;
|
||||||
await session.append(data, filePath);
|
await session.append(data, filePath);
|
||||||
ws.send(JSON.stringify({ success: true }));
|
ws.send(JSON.stringify({ success: true }));
|
||||||
@ -447,14 +429,12 @@ srv.ws('/api/sftp/files/append', async (ws, wsReq) => {
|
|||||||
return ws.close();
|
return ws.close();
|
||||||
}
|
}
|
||||||
isWriting = false;
|
isWriting = false;
|
||||||
// Update the session activity
|
|
||||||
sessionActivity[sessionHash] = Date.now();
|
sessionActivity[sessionHash] = Date.now();
|
||||||
});
|
});
|
||||||
// Send a ready message
|
|
||||||
ws.send(JSON.stringify({ success: true, status: 'ready' }));
|
ws.send(JSON.stringify({ success: true, status: 'ready' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.delete('/api/sftp/files/delete', initApi, async (req, res) => {
|
srv.delete('/api/sftp/files/delete', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -465,8 +445,8 @@ srv.delete('/api/sftp/files/delete', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.put('/api/sftp/files/move', initApi, async (req, res) => {
|
srv.put('/api/sftp/files/move', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.pathOld = normalizeRemotePath(req.query.pathOld);
|
res.data.pathOld = normalizeRemotePath(req.query.pathOld);
|
||||||
res.data.pathNew = normalizeRemotePath(req.query.pathNew);
|
res.data.pathNew = normalizeRemotePath(req.query.pathNew);
|
||||||
@ -479,8 +459,8 @@ srv.put('/api/sftp/files/move', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.put('/api/sftp/files/copy', initApi, async (req, res) => {
|
srv.put('/api/sftp/files/copy', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.pathSrc = normalizeRemotePath(req.query.pathSrc);
|
res.data.pathSrc = normalizeRemotePath(req.query.pathSrc);
|
||||||
res.data.pathDest = normalizeRemotePath(req.query.pathDest);
|
res.data.pathDest = normalizeRemotePath(req.query.pathDest);
|
||||||
@ -493,8 +473,8 @@ srv.put('/api/sftp/files/copy', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.put('/api/sftp/files/chmod', initApi, async (req, res) => {
|
srv.put('/api/sftp/files/chmod', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
@ -506,38 +486,30 @@ srv.put('/api/sftp/files/chmod', initApi, async (req, res) => {
|
|||||||
res.sendError(error);
|
res.sendError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.get('/api/sftp/files/stat', initApi, async (req, res) => {
|
srv.get('/api/sftp/files/stat', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
let stats = null;
|
|
||||||
try {
|
try {
|
||||||
stats = await session.stat(res.data.path);
|
const stats = await session.stat(res.data.path);
|
||||||
|
res.data.stats = stats;
|
||||||
|
res.sendData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.sendError(error, 404);
|
res.sendError(error, 404);
|
||||||
}
|
}
|
||||||
res.data.stats = stats;
|
|
||||||
res.sendData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadSingleFileHandler = async (connectionOpts, res, remotePath, stats) => {
|
const downloadSingleFileHandler = async (connectionOpts, res, remotePath, stats) => {
|
||||||
let interval;
|
let interval;
|
||||||
// Gracefully handle any errors
|
|
||||||
try {
|
try {
|
||||||
// Throw an error if it's not a file
|
|
||||||
if (!stats.isFile) throw new Error('Not a file');
|
if (!stats.isFile) throw new Error('Not a file');
|
||||||
// Add uniqueness to the connection opts
|
|
||||||
// This forces a new connection to be created
|
|
||||||
connectionOpts.ts = Date.now();
|
connectionOpts.ts = Date.now();
|
||||||
// Create the session and throw an error if it fails
|
const session = await getSession(connectionOpts);
|
||||||
const session = await getSession(res, connectionOpts);
|
|
||||||
if (!session) throw new Error('Failed to create session');
|
|
||||||
// Continuously update the session activity
|
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
const hash = getObjectHash(connectionOpts);
|
const hash = getObjectHash(connectionOpts);
|
||||||
sessionActivity[hash] = Date.now();
|
sessionActivity[hash] = Date.now();
|
||||||
}, 1000 * 1);
|
}, 1000 * 1);
|
||||||
// When the response closes, end the session
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
session.end();
|
session.end();
|
||||||
@ -545,46 +517,35 @@ const downloadSingleFileHandler = async (connectionOpts, res, remotePath, stats)
|
|||||||
res.on('end', handleClose);
|
res.on('end', handleClose);
|
||||||
res.on('close', handleClose);
|
res.on('close', handleClose);
|
||||||
res.on('error', handleClose);
|
res.on('error', handleClose);
|
||||||
// Set response headers
|
|
||||||
res.setHeader('Content-Type', mime.getType(remotePath) || 'application/octet-stream');
|
res.setHeader('Content-Type', mime.getType(remotePath) || 'application/octet-stream');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(remotePath)}"`);
|
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(remotePath)}"`);
|
||||||
res.setHeader('Content-Length', stats.size);
|
res.setHeader('Content-Length', stats.size);
|
||||||
// Start the download
|
|
||||||
console.log(`Starting download: ${connectionOpts.username}@${connectionOpts.host}:${connectionOpts.port} ${remotePath}`);
|
console.log(`Starting download: ${connectionOpts.username}@${connectionOpts.host}:${connectionOpts.port} ${remotePath}`);
|
||||||
await session.get(remotePath, res);
|
await session.get(remotePath, res);
|
||||||
// Force-end the response
|
|
||||||
res.end();
|
res.end();
|
||||||
// On error, clear the interval and send a 400 response
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(`Download failed: ${error.message}`);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadMultiFileHandler = async (connectionOpts, res, remotePaths, rootPath = '/') => {
|
const downloadMultiFileHandler = async (connectionOpts, res, remotePaths, rootPath = '/') => {
|
||||||
rootPath = normalizeRemotePath(rootPath);
|
rootPath = normalizeRemotePath(rootPath);
|
||||||
let interval;
|
let interval;
|
||||||
// Gracefully handle any errors
|
|
||||||
try {
|
try {
|
||||||
// Add uniqueness to the connection opts
|
|
||||||
// This forces a new connection to be created
|
|
||||||
connectionOpts.ts = Date.now();
|
connectionOpts.ts = Date.now();
|
||||||
// Create the session and throw an error if it fails
|
const session = await getSession(connectionOpts);
|
||||||
const session = await getSession(res, connectionOpts);
|
interval = setInterval(() => {
|
||||||
if (!session) throw new Error('Failed to create session');
|
|
||||||
// Continuously update the session activity
|
|
||||||
setInterval(() => {
|
|
||||||
const hash = getObjectHash(connectionOpts);
|
const hash = getObjectHash(connectionOpts);
|
||||||
sessionActivity[hash] = Date.now();
|
sessionActivity[hash] = Date.now();
|
||||||
}, 1000 * 1);
|
}, 1000 * 1);
|
||||||
// Set response headers
|
|
||||||
let fileName = `Files (${path.basename(rootPath) || 'Root'})`;
|
let fileName = `Files (${path.basename(rootPath) || 'Root'})`;
|
||||||
if (remotePaths.length == 1)
|
if (remotePaths.length === 1)
|
||||||
fileName = path.basename(remotePaths[0]);
|
fileName = path.basename(remotePaths[0]);
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}.zip"`);
|
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}.zip"`);
|
||||||
// Create the archive and start piping to the response
|
|
||||||
const archive = archiver('zip');
|
const archive = archiver('zip');
|
||||||
archive.pipe(res);
|
archive.pipe(res);
|
||||||
// When the response closes, end the session
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
archive.end();
|
archive.end();
|
||||||
@ -593,23 +554,18 @@ const downloadMultiFileHandler = async (connectionOpts, res, remotePaths, rootPa
|
|||||||
res.on('end', handleClose);
|
res.on('end', handleClose);
|
||||||
res.on('close', handleClose);
|
res.on('close', handleClose);
|
||||||
res.on('error', handleClose);
|
res.on('error', handleClose);
|
||||||
// Add file to the archive
|
|
||||||
const addToArchive = async (remotePath) => {
|
const addToArchive = async (remotePath) => {
|
||||||
const archivePath = normalizeRemotePath(remotePath.replace(rootPath, ''));
|
const archivePath = normalizeRemotePath(remotePath.replace(rootPath, ''));
|
||||||
console.log(`Zipping: ${connectionOpts.username}@${connectionOpts.host}:${connectionOpts.port} ${remotePath}`);
|
console.log(`Zipping: ${connectionOpts.username}@${connectionOpts.host}:${connectionOpts.port} ${remotePath}`);
|
||||||
// Get file read stream
|
|
||||||
const stream = session.createReadStream(remotePath);
|
const stream = session.createReadStream(remotePath);
|
||||||
const waitToEnd = new Promise(resolve => {
|
const waitToEnd = new Promise(resolve => {
|
||||||
stream.on('end', resolve);
|
stream.on('end', resolve);
|
||||||
});
|
});
|
||||||
// Add file to archive
|
|
||||||
archive.append(stream, {
|
archive.append(stream, {
|
||||||
name: archivePath
|
name: archivePath
|
||||||
});
|
});
|
||||||
// Wait for the stream to end
|
|
||||||
await waitToEnd;
|
await waitToEnd;
|
||||||
};
|
};
|
||||||
// Recurse through directories and archive files
|
|
||||||
const recurse = async (remotePath) => {
|
const recurse = async (remotePath) => {
|
||||||
try {
|
try {
|
||||||
const stats = await session.stat(remotePath);
|
const stats = await session.stat(remotePath);
|
||||||
@ -626,93 +582,82 @@ const downloadMultiFileHandler = async (connectionOpts, res, remotePaths, rootPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) {
|
||||||
|
console.error(`Error processing ${remotePath}: ${error.message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
for (const remotePath of remotePaths) {
|
for (const remotePath of remotePaths) {
|
||||||
await recurse(remotePath);
|
await recurse(remotePath);
|
||||||
}
|
}
|
||||||
// Finalize the archive
|
|
||||||
archive.on('close', () => res.end());
|
archive.on('close', () => res.end());
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
// On error, clear the interval and send a 400 response
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(`Multi-file download failed: ${error.message}`);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
srv.get('/api/sftp/files/get/single', initApi, async (req, res) => {
|
srv.get('/api/sftp/files/get/single', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
// Get the normalized path and throw an error if it's missing
|
|
||||||
const remotePath = normalizeRemotePath(req.query.path);
|
const remotePath = normalizeRemotePath(req.query.path);
|
||||||
if (!remotePath) return res.sendError('Missing path', 400);
|
if (!remotePath) return res.sendError('Missing path', 400);
|
||||||
try {
|
try {
|
||||||
const stats = await session.stat(remotePath);
|
const stats = await session.stat(remotePath);
|
||||||
// Handle the download
|
|
||||||
await downloadSingleFileHandler(req.connectionOpts, res, remotePath, stats);
|
await downloadSingleFileHandler(req.connectionOpts, res, remotePath, stats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).end();
|
res.sendError(error, 400);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawDownloads = {};
|
const rawDownloads = {};
|
||||||
srv.get('/api/sftp/files/get/single/url', initApi, async (req, res) => {
|
srv.get('/api/sftp/files/get/single/url', initApi, async (req, res) => {
|
||||||
/** @type {sftp} */
|
|
||||||
const session = req.session;
|
const session = req.session;
|
||||||
// Get the normalized path and throw an error if it's missing
|
|
||||||
res.data.path = normalizeRemotePath(req.query.path);
|
res.data.path = normalizeRemotePath(req.query.path);
|
||||||
if (!res.data.path) return res.sendError('Missing path', 400);
|
if (!res.data.path) return res.sendError('Missing path', 400);
|
||||||
// Get path stats and throw an error if it's not a file
|
let stats;
|
||||||
let stats = null;
|
|
||||||
try {
|
try {
|
||||||
stats = await session.stat(res.data.path);
|
stats = await session.stat(res.data.path);
|
||||||
if (!stats?.isFile) throw new Error('Not a file');
|
if (!stats?.isFile) throw new Error('Not a file');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.sendError(error);
|
return res.sendError(error);
|
||||||
}
|
}
|
||||||
// Generate download URL
|
|
||||||
const id = utils.randomHex(8);
|
const id = utils.randomHex(8);
|
||||||
res.data.download_url = `https://${req.get('host')}/dl/${id}`;
|
res.data.download_url = `https://${req.get('host')}/dl/${id}`;
|
||||||
// Create download handler
|
|
||||||
rawDownloads[id] = {
|
rawDownloads[id] = {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
paths: [res.data.path],
|
paths: [res.data.path],
|
||||||
handler: async (req2, res2) => {
|
handler: async (req2, res2) => {
|
||||||
// Handle the download
|
|
||||||
await downloadSingleFileHandler(req.connectionOpts, res2, res.data.path, stats);
|
await downloadSingleFileHandler(req.connectionOpts, res2, res.data.path, stats);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
res.sendData();
|
res.sendData();
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.get('/api/sftp/files/get/multi/url', initApi, async (req, res) => {
|
srv.get('/api/sftp/files/get/multi/url', initApi, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Get the normalized path and throw an error if it's missing
|
|
||||||
res.data.paths = JSON.parse(req.query.paths);
|
res.data.paths = JSON.parse(req.query.paths);
|
||||||
if (!res.data.paths) throw new Error('Missing path(s)');
|
if (!res.data.paths) throw new Error('Missing path(s)');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.sendError(error);
|
return res.sendError(error);
|
||||||
}
|
}
|
||||||
// Generate download URL
|
|
||||||
const id = utils.randomHex(8);
|
const id = utils.randomHex(8);
|
||||||
res.data.download_url = `https://${req.get('host')}/dl/${id}`;
|
res.data.download_url = `https://${req.get('host')}/dl/${id}`;
|
||||||
// Create download handler
|
|
||||||
rawDownloads[id] = {
|
rawDownloads[id] = {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
paths: res.data.paths,
|
paths: res.data.paths,
|
||||||
isZip: true,
|
isZip: true,
|
||||||
handler: async (req2, res2) => {
|
handler: async (req2, res2) => {
|
||||||
// Handle the download
|
|
||||||
await downloadMultiFileHandler(req.connectionOpts, res2, res.data.paths, req.query.rootPath);
|
await downloadMultiFileHandler(req.connectionOpts, res2, res.data.paths, req.query.rootPath);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
res.sendData();
|
res.sendData();
|
||||||
});
|
});
|
||||||
|
|
||||||
srv.get('/dl/:id', async (req, res) => {
|
srv.get('/dl/:id', async (req, res) => {
|
||||||
// Get the download handler
|
|
||||||
const entry = rawDownloads[req.params.id];
|
const entry = rawDownloads[req.params.id];
|
||||||
if (!entry) return res.status(404).end();
|
if (!entry) return res.status(404).end();
|
||||||
// If the user agent looks like a bot
|
|
||||||
if (req.get('user-agent').match(/(bot|scrape)/)) {
|
if (req.get('user-agent').match(/(bot|scrape)/)) {
|
||||||
// Send some HTML
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
const html = /*html*/`
|
const html = /*html*/`
|
||||||
<html>
|
<html>
|
||||||
@ -738,7 +683,6 @@ srv.get('/dl/:id', async (req, res) => {
|
|||||||
srv.use((req, res) => res.status(404).end());
|
srv.use((req, res) => res.status(404).end());
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
// Delete inactive sessions
|
|
||||||
for (const hash in sessions) {
|
for (const hash in sessions) {
|
||||||
const lastActive = sessionActivity[hash];
|
const lastActive = sessionActivity[hash];
|
||||||
if (!lastActive) continue;
|
if (!lastActive) continue;
|
||||||
@ -749,7 +693,6 @@ setInterval(() => {
|
|||||||
delete sessionActivity[hash];
|
delete sessionActivity[hash];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete unused downloads
|
|
||||||
for (const id in rawDownloads) {
|
for (const id in rawDownloads) {
|
||||||
const download = rawDownloads[id];
|
const download = rawDownloads[id];
|
||||||
if ((Date.now() - download.created) > 1000 * 60 * 60 * 12) {
|
if ((Date.now() - download.created) > 1000 * 60 * 60 * 12) {
|
||||||
@ -757,7 +700,6 @@ setInterval(() => {
|
|||||||
delete rawDownloads[id];
|
delete rawDownloads[id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete expired connections
|
|
||||||
for (const id in connections) {
|
for (const id in connections) {
|
||||||
const connection = connections[id];
|
const connection = connections[id];
|
||||||
if ((Date.now() - connection.created) > 1000 * 60 * 60 * 12) {
|
if ((Date.now() - connection.created) > 1000 * 60 * 60 * 12) {
|
||||||
@ -769,15 +711,13 @@ setInterval(() => {
|
|||||||
|
|
||||||
if (Electron.app) {
|
if (Electron.app) {
|
||||||
Electron.app.whenReady().then(async () => {
|
Electron.app.whenReady().then(async () => {
|
||||||
// Start the server
|
|
||||||
let port = 8001 + Math.floor(Math.random() * 999);
|
let port = 8001 + Math.floor(Math.random() * 999);
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
srv.listen(port, () => {
|
srv.listen(port, () => {
|
||||||
console.log(`App server listening on port ${port}`)
|
console.log(`App server listening on port ${port}`);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Open the window
|
|
||||||
const window = new Electron.BrowserWindow({
|
const window = new Electron.BrowserWindow({
|
||||||
width: 1100,
|
width: 1100,
|
||||||
height: 720,
|
height: 720,
|
||||||
@ -786,8 +726,6 @@ if (Electron.app) {
|
|||||||
minHeight: 200
|
minHeight: 200
|
||||||
});
|
});
|
||||||
window.loadURL(`http://localhost:${port}`);
|
window.loadURL(`http://localhost:${port}`);
|
||||||
// Quit the app when all windows are closed
|
|
||||||
// unless we're on macOS
|
|
||||||
Electron.app.on('window-all-closed', () => {
|
Electron.app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') Electron.app.quit();
|
if (process.platform !== 'darwin') Electron.app.quit();
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user