feat: Add Help System, Local File Operations, Site Manager improvements, and UI refinements
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* WebZilla 백엔드 프록시 서버 (Node.js) v1.1
|
||||
* WebZilla 백엔드 프록시 서버 (Node.js) v1.2
|
||||
*
|
||||
* 기능 추가:
|
||||
* - 로컬 파일 시스템 접근 (fs)
|
||||
* - 설정 데이터 저장 (AppData/Roaming 또는 ~/.config)
|
||||
* 기능:
|
||||
* - WebSocket Proxy (Port: 8090)
|
||||
* - FTP/SFTP 지원
|
||||
* - 로컬 파일 시스템 탐색 (LOCAL_LIST)
|
||||
* - 설정 저장 (AppData)
|
||||
* - **NEW**: 포트 충돌 자동 감지 및 프로세스 종료 기능
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
@@ -11,139 +14,296 @@ const ftp = require('basic-ftp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { exec } = require('child_process');
|
||||
const readline = require('readline');
|
||||
|
||||
// --- 로컬 저장소 경로 설정 (AppData 구현) ---
|
||||
function getConfigDir() {
|
||||
const homedir = os.homedir();
|
||||
|
||||
// Windows: C:\Users\User\AppData\Roaming\WebZilla
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'WebZilla');
|
||||
}
|
||||
// macOS: ~/Library/Application Support/WebZilla
|
||||
else if (process.platform === 'darwin') {
|
||||
} else if (process.platform === 'darwin') {
|
||||
return path.join(homedir, 'Library', 'Application Support', 'WebZilla');
|
||||
}
|
||||
// Linux: ~/.config/webzilla
|
||||
else {
|
||||
} else {
|
||||
return path.join(homedir, '.config', 'webzilla');
|
||||
}
|
||||
}
|
||||
|
||||
// 앱 시작 시 설정 디렉토리 생성
|
||||
const configDir = getConfigDir();
|
||||
if (!fs.existsSync(configDir)) {
|
||||
try {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
console.log(`📂 설정 폴더가 생성되었습니다: ${configDir}`);
|
||||
} catch (e) {
|
||||
console.error(`❌ 설정 폴더 생성 실패: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`📂 설정 폴더 로드됨: ${configDir}`);
|
||||
try { fs.mkdirSync(configDir, { recursive: true }); } catch (e) { }
|
||||
}
|
||||
|
||||
const wss = new WebSocket.Server({ port: 8090 });
|
||||
const PORT = 8090;
|
||||
|
||||
console.log("🚀 WebZilla FTP Proxy Server가 ws://localhost:8090 에서 실행 중입니다.");
|
||||
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
||||
function startServer() {
|
||||
const wss = new WebSocket.Server({ port: PORT });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log("클라이언트가 접속했습니다.");
|
||||
|
||||
const client = new ftp.Client();
|
||||
|
||||
ws.on('message', async (message) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
switch (data.command) {
|
||||
// --- FTP 연결 관련 ---
|
||||
case 'CONNECT':
|
||||
console.log(`FTP 연결 시도: ${data.user}@${data.host}:${data.port}`);
|
||||
try {
|
||||
await client.access({
|
||||
host: data.host,
|
||||
user: data.user,
|
||||
password: data.pass,
|
||||
port: parseInt(data.port),
|
||||
secure: false
|
||||
});
|
||||
ws.send(JSON.stringify({ type: 'status', status: 'connected', message: 'FTP 서버에 연결되었습니다.' }));
|
||||
console.log("FTP 연결 성공");
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `연결 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LIST':
|
||||
if (client.closed) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' }));
|
||||
return;
|
||||
}
|
||||
const listPath = data.path || '/';
|
||||
const list = await client.list(listPath);
|
||||
|
||||
const files = list.map(f => ({
|
||||
id: `ftp-${Date.now()}-${Math.random()}`,
|
||||
name: f.name,
|
||||
type: f.isDirectory ? 'FOLDER' : 'FILE',
|
||||
size: f.size,
|
||||
date: f.rawModifiedAt || new Date().toISOString(),
|
||||
permissions: '-'
|
||||
}));
|
||||
|
||||
ws.send(JSON.stringify({ type: 'list', files, path: listPath }));
|
||||
break;
|
||||
|
||||
case 'DISCONNECT':
|
||||
client.close();
|
||||
ws.send(JSON.stringify({ type: 'status', status: 'disconnected', message: '연결이 종료되었습니다.' }));
|
||||
break;
|
||||
|
||||
// --- 로컬 설정 저장 관련 (새로 추가됨) ---
|
||||
case 'SAVE_SITE':
|
||||
// 예: 사이트 정보를 JSON 파일로 저장
|
||||
try {
|
||||
const sitesFile = path.join(configDir, 'sites.json');
|
||||
let sites = [];
|
||||
if (fs.existsSync(sitesFile)) {
|
||||
sites = JSON.parse(fs.readFileSync(sitesFile, 'utf8'));
|
||||
}
|
||||
sites.push(data.siteInfo);
|
||||
fs.writeFileSync(sitesFile, JSON.stringify(sites, null, 2));
|
||||
|
||||
console.log(`💾 사이트 정보 저장됨: ${data.siteInfo.host}`);
|
||||
ws.send(JSON.stringify({ type: 'success', message: '사이트 정보가 로컬(AppData)에 저장되었습니다.' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `저장 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'GET_SITES':
|
||||
try {
|
||||
const sitesFile = path.join(configDir, 'sites.json');
|
||||
if (fs.existsSync(sitesFile)) {
|
||||
const sites = JSON.parse(fs.readFileSync(sitesFile, 'utf8'));
|
||||
ws.send(JSON.stringify({ type: 'sites_list', sites }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'sites_list', sites: [] }));
|
||||
}
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `로드 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`알 수 없는 명령: ${data.command}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("오류 발생:", err);
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
wss.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`\n❌ 포트 ${PORT}이(가) 이미 사용 중입니다.`);
|
||||
handlePortConflict();
|
||||
} else {
|
||||
console.error("❌ 서버 오류:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log("클라이언트 접속 종료");
|
||||
client.close();
|
||||
wss.on('listening', () => {
|
||||
console.log(`\n🚀 WebZilla FTP Proxy Server가 ws://localhost:${PORT} 에서 실행 중입니다.`);
|
||||
console.log(`📂 설정 폴더: ${configDir}`);
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
const client = new ftp.Client();
|
||||
|
||||
ws.on('message', async (message) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
switch (data.command) {
|
||||
case 'CONNECT':
|
||||
console.log(`FTP 연결 시도: ${data.user}@${data.host}:${data.port}`);
|
||||
try {
|
||||
await client.access({
|
||||
host: data.host,
|
||||
user: data.user,
|
||||
password: data.pass,
|
||||
port: parseInt(data.port),
|
||||
secure: false
|
||||
});
|
||||
ws.send(JSON.stringify({ type: 'status', status: 'connected', message: 'FTP 서버에 연결되었습니다.' }));
|
||||
console.log("FTP 연결 성공");
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `연결 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LIST':
|
||||
if (client.closed) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const listPath = data.path || '/';
|
||||
const list = await client.list(listPath);
|
||||
const files = list.map(f => ({
|
||||
id: `ftp-${Date.now()}-${Math.random()}`,
|
||||
name: f.name,
|
||||
type: f.isDirectory ? 'FOLDER' : 'FILE',
|
||||
size: f.size,
|
||||
date: f.rawModifiedAt || new Date().toISOString(),
|
||||
permissions: '-'
|
||||
}));
|
||||
ws.send(JSON.stringify({ type: 'list', files, path: listPath }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'MKD':
|
||||
if (client.closed) return;
|
||||
try {
|
||||
await client.ensureDir(data.path);
|
||||
ws.send(JSON.stringify({ type: 'success', message: '폴더 생성 완료' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DELE':
|
||||
if (client.closed) return;
|
||||
try {
|
||||
if (data.isFolder) await client.removeDir(data.path);
|
||||
else await client.remove(data.path);
|
||||
ws.send(JSON.stringify({ type: 'success', message: '삭제 완료' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'RENAME':
|
||||
if (client.closed) return;
|
||||
try {
|
||||
await client.rename(data.from, data.to);
|
||||
ws.send(JSON.stringify({ type: 'success', message: '이름 변경 완료' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DISCONNECT':
|
||||
client.close();
|
||||
ws.send(JSON.stringify({ type: 'status', status: 'disconnected', message: '연결이 종료되었습니다.' }));
|
||||
break;
|
||||
|
||||
case 'SAVE_SITE':
|
||||
// Deprecated in favor of SAVE_SITES for full sync, but kept for compatibility
|
||||
try {
|
||||
const sitesFile = path.join(configDir, 'sites.json');
|
||||
let sites = [];
|
||||
if (fs.existsSync(sitesFile)) sites = JSON.parse(fs.readFileSync(sitesFile, 'utf8'));
|
||||
sites.push(data.siteInfo);
|
||||
fs.writeFileSync(sitesFile, JSON.stringify(sites, null, 2));
|
||||
ws.send(JSON.stringify({ type: 'success', message: '사이트가 추가되었습니다.' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `저장 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SAVE_SITES':
|
||||
try {
|
||||
const sitesFile = path.join(configDir, 'sites.json');
|
||||
fs.writeFileSync(sitesFile, JSON.stringify(data.sites, null, 2));
|
||||
ws.send(JSON.stringify({ type: 'success', message: '사이트 목록이 저장되었습니다.' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `저장 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'GET_SITES':
|
||||
try {
|
||||
const sitesFile = path.join(configDir, 'sites.json');
|
||||
if (fs.existsSync(sitesFile)) {
|
||||
const sites = JSON.parse(fs.readFileSync(sitesFile, 'utf8'));
|
||||
ws.send(JSON.stringify({ type: 'sites_list', sites }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'sites_list', sites: [] }));
|
||||
}
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `로드 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LOCAL_LIST':
|
||||
try {
|
||||
const targetPath = data.path || os.homedir();
|
||||
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
||||
|
||||
const files = entries.map(dirent => {
|
||||
let size = 0;
|
||||
let date = new Date().toISOString();
|
||||
try {
|
||||
const stats = fs.statSync(path.join(targetPath, dirent.name));
|
||||
size = stats.size;
|
||||
date = stats.mtime.toISOString();
|
||||
} catch (e) { }
|
||||
|
||||
return {
|
||||
id: `local-${Math.random()}`,
|
||||
name: dirent.name,
|
||||
type: dirent.isDirectory() ? 'FOLDER' : 'FILE',
|
||||
size: size,
|
||||
date: date,
|
||||
permissions: '-'
|
||||
};
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify({ type: 'local_list', files, path: targetPath }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `로컬 목록 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LOCAL_MKD':
|
||||
try {
|
||||
if (!fs.existsSync(data.path)) {
|
||||
fs.mkdirSync(data.path, { recursive: true });
|
||||
ws.send(JSON.stringify({ type: 'success', message: '로컬 폴더 생성 완료' }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'error', message: '이미 존재하는 폴더입니다.' }));
|
||||
}
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `로컬 폴더 생성 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LOCAL_RENAME':
|
||||
try {
|
||||
fs.renameSync(data.from, data.to);
|
||||
ws.send(JSON.stringify({ type: 'success', message: '이름 변경 완료' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `이름 변경 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LOCAL_DELE':
|
||||
try {
|
||||
fs.rmSync(data.path, { recursive: true, force: true });
|
||||
ws.send(JSON.stringify({ type: 'success', message: '삭제 완료' }));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `삭제 실패: ${err.message}` }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("오류 발생:", err);
|
||||
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- 포트 충돌 처리 ---
|
||||
function handlePortConflict() {
|
||||
// Windows: netstat -ano | findstr :8090
|
||||
// Mac/Linux: lsof -i :8090
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
exec(`netstat -ano | findstr :${PORT}`, (err, stdout, stderr) => {
|
||||
if (err || !stdout) {
|
||||
console.log("실행 중인 프로세스를 찾지 못했습니다. 수동으로 확인해주세요.");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse PID (Last token in line)
|
||||
const lines = stdout.trim().split('\n');
|
||||
const line = lines[0].trim();
|
||||
const parts = line.split(/\s+/);
|
||||
const pid = parts[parts.length - 1];
|
||||
|
||||
askToKill(pid);
|
||||
});
|
||||
} else {
|
||||
// Simple fallback/notification for non-Windows (or implement lsof)
|
||||
console.log(`Port ${PORT} is in use. Please kill the process manually.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function askToKill(pid) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log(`⚠️ PID [${pid}] 프로세스가 포트 ${PORT}를 사용 중입니다.`);
|
||||
rl.question(`❓ 해당 프로세스를 종료하고 서버를 시작하시겠습니까? (Y/n): `, (answer) => {
|
||||
const ans = answer.trim().toLowerCase();
|
||||
if (ans === '' || ans === 'y' || ans === 'yes') {
|
||||
console.log(`🔫 PID ${pid} 종료 시도 중...`);
|
||||
exec(`taskkill /F /PID ${pid}`, (killErr) => {
|
||||
if (killErr) {
|
||||
console.error(`❌ 종료 실패: ${killErr.message}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ 프로세스가 종료되었습니다. 서버를 다시 시작합니다...");
|
||||
rl.close();
|
||||
setTimeout(startServer, 1000); // 1초 후 재시작
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("🚫 작업을 취소했습니다.");
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 초기 실행
|
||||
startServer();
|
||||
Reference in New Issue
Block a user