✨feature: Initial functional build
This commit is contained in:
220
src/main.js
Normal file
220
src/main.js
Normal file
@@ -0,0 +1,220 @@
|
||||
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const state = {
|
||||
hasWget: false,
|
||||
};
|
||||
|
||||
function detectWget() {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
||||
execFile(cmd, ['wget'], (err, stdout) => {
|
||||
if (err) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(Boolean(stdout && stdout.trim()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
win.loadFile(path.join(__dirname, 'index.html'));
|
||||
}
|
||||
|
||||
function logToRenderer(message) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
if (!windows.length) return;
|
||||
windows[0].webContents.send('log', message);
|
||||
}
|
||||
|
||||
function listLocalDir(dirPath) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
return entries.map((entry) => {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
let size = 0;
|
||||
if (!entry.isDirectory()) {
|
||||
try {
|
||||
size = fs.statSync(fullPath).size;
|
||||
} catch (err) {
|
||||
size = 0;
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: entry.name,
|
||||
path: fullPath,
|
||||
isDir: entry.isDirectory(),
|
||||
size,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function fetchUrl(url) {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = client.get(url, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => resolve(data));
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function parseApacheIndex(html, baseUrl) {
|
||||
const results = [];
|
||||
const preMatch = html.match(/<pre>[\s\S]*?<\/pre>/i);
|
||||
const block = preMatch ? preMatch[0] : html;
|
||||
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/gi;
|
||||
let match;
|
||||
while ((match = linkRegex.exec(block)) !== null) {
|
||||
const href = match[1];
|
||||
const anchorText = match[2];
|
||||
if (href === '../' || anchorText === '../') continue;
|
||||
const isDir = href.endsWith('/');
|
||||
const fullUrl = new URL(href, baseUrl).toString();
|
||||
let name = anchorText;
|
||||
try {
|
||||
const urlObj = new URL(href, baseUrl);
|
||||
let pathname = decodeURIComponent(urlObj.pathname);
|
||||
if (pathname.endsWith('/')) {
|
||||
pathname = pathname.slice(0, -1);
|
||||
}
|
||||
const derived = pathname.split('/').pop();
|
||||
if (derived) {
|
||||
name = isDir ? `${derived}/` : derived;
|
||||
}
|
||||
} catch (err) {
|
||||
name = anchorText;
|
||||
}
|
||||
results.push({ name, href: fullUrl, isDir });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function downloadFile(url, destPath) {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileStream = fs.createWriteStream(destPath);
|
||||
const req = client.get(url, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
res.pipe(fileStream);
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.close(resolve);
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
fs.unlink(destPath, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFolderRecursive(baseUrl, destDir) {
|
||||
const html = await fetchUrl(baseUrl);
|
||||
const items = parseApacheIndex(html, baseUrl);
|
||||
for (const item of items) {
|
||||
const targetPath = path.join(destDir, item.name.replace(/\/$/, ''));
|
||||
if (item.isDir) {
|
||||
fs.mkdirSync(targetPath, { recursive: true });
|
||||
logToRenderer(`Entering ${item.href}`);
|
||||
await downloadFolderRecursive(item.href, targetPath);
|
||||
} else {
|
||||
logToRenderer(`Downloading ${item.href}`);
|
||||
await downloadFile(item.href, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadWithWget(url, destDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ['-r', '-np', '-nH', '--cut-dirs=0', '-P', destDir, url];
|
||||
const proc = execFile('wget', args, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
proc.stdout.on('data', (data) => logToRenderer(data.toString()));
|
||||
proc.stderr.on('data', (data) => logToRenderer(data.toString()));
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
state.hasWget = await detectWget();
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-capabilities', () => {
|
||||
return { hasWget: state.hasWget };
|
||||
});
|
||||
|
||||
ipcMain.handle('select-local-dir', async () => {
|
||||
const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
|
||||
if (result.canceled || result.filePaths.length === 0) return null;
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle('list-local-dir', (_event, dirPath) => {
|
||||
return listLocalDir(dirPath);
|
||||
});
|
||||
|
||||
ipcMain.handle('list-remote-dir', async (_event, url) => {
|
||||
const html = await fetchUrl(url);
|
||||
return parseApacheIndex(html, url);
|
||||
});
|
||||
|
||||
ipcMain.handle('download-item', async (_event, payload) => {
|
||||
const { url, destDir, isDir, useWget } = payload;
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
if (isDir) {
|
||||
if (useWget && state.hasWget) {
|
||||
logToRenderer(`wget: ${url}`);
|
||||
await downloadWithWget(url, destDir);
|
||||
return;
|
||||
}
|
||||
await downloadFolderRecursive(url, destDir);
|
||||
return;
|
||||
}
|
||||
const fileName = path.basename(new URL(url).pathname);
|
||||
const destPath = path.join(destDir, fileName);
|
||||
logToRenderer(`Downloading ${url}`);
|
||||
await downloadFile(url, destPath);
|
||||
});
|
||||
Reference in New Issue
Block a user