/** * WebZilla 백엔드 프록시 서버 (Node.js) v1.2 * * 기능: * - WebSocket Proxy (Port: 8090) * - FTP/SFTP 지원 * - 로컬 파일 시스템 탐색 (LOCAL_LIST) * - 설정 저장 (AppData) * - **NEW**: 포트 충돌 자동 감지 및 프로세스 종료 기능 */ const WebSocket = require('ws'); 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(); if (process.platform === 'win32') { return path.join(process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'WebZilla'); } else if (process.platform === 'darwin') { return path.join(homedir, 'Library', 'Application Support', 'WebZilla'); } else { return path.join(homedir, '.config', 'webzilla'); } } const configDir = getConfigDir(); if (!fs.existsSync(configDir)) { try { fs.mkdirSync(configDir, { recursive: true }); } catch (e) { } } const PORT = 8090; // --- 서버 시작 함수 (재시도 로직 포함) --- function startServer() { const wss = new WebSocket.Server({ port: PORT }); wss.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`\n❌ 포트 ${PORT}이(가) 이미 사용 중입니다.`); handlePortConflict(); } else { console.error("❌ 서버 오류:", err); process.exit(1); } }); 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();