You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

249 lines
8.8 KiB

// 1. 모듈 불러오기
const express = require('express');
const path = require('path');
const { spawn } = require('child_process');
const multer = require('multer'); // 파일 업로드 처리를 위한 multer
const fs = require('fs'); // 파일 시스템 접근을 위한 fs
// 2. Express 앱 생성
const app = express();
const port = 3000;
// 업로드 경로 설정
const uploadPath = '/mnt/user_data/applications/misc/networks/cuuva';
// 업로드 경로가 없으면 생성
fs.mkdirSync(uploadPath, { recursive: true });
// Multer 저장소 설정
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadPath);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
// Multer 파일 필터 설정 (.aiwbin 확장자만 허용)
const fileFilter = (req, file, cb) => {
if (!file.originalname.endsWith('.aiwbin')) {
// 허용되지 않는 파일 형식
return cb(new Error('Invalid file type: Only .aiwbin files are allowed'), false);
}
// 허용되는 파일 형식
cb(null, true);
};
// Multer 업로드 인스턴스 생성
const upload = multer({
storage: storage,
fileFilter: fileFilter
});
// 3. 'public' 폴더의 파일들을 정적 파일로 제공하도록 설정합니다.
app.use(express.static(path.join(__dirname, 'public')));
// POST 요청의 JSON 본문(body)을 파싱하기 위한 미들웨어
app.use(express.json());
// 4. 루트 경로('/')로 접속하면 'index.html'(로그인 페이지)를 보냅니다.
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 5. '/dashboard' 경로로 접속하면 'dashboard.html'(메인 페이지)를 보냅니다.
app.get('/dashboard', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
});
// 6. 모델 변경 API 엔드포인트 (*** 수정된 부분 ***)
app.post('/set-model', (req, res) => {
const { model } = req.body; // app.js에서 보낸 { model: "OBJDET" } 등
if (!model) {
return res.status(400).json({ status: 'error', message: '모델 값이 없습니다.' });
}
console.log(`[서버] 모델 변경 명령 수신: ${model}`);
// --- 요청하신 쉘 스크립트 실행으로 변경 ---
const scriptCommand = '/mnt/user_data/feat_control/feat_on.sh';
const args = [model]; // OBJDET, ABNORM, CROWD 등
console.log(`[서버] 실행: ${scriptCommand} ${args.join(' ')}`);
// 스크립트 파일에 실행 권한(chmod +x)이 있다고 가정합니다.
const scriptProcess = spawn(scriptCommand, args);
let stdoutData = '';
let stderrData = '';
scriptProcess.stdout.on('data', (data) => {
console.log(`Script stdout: ${data}`);
stdoutData += data.toString();
});
scriptProcess.stderr.on('data', (data) => {
console.error(`Script stderr: ${data}`);
stderrData += data.toString();
});
scriptProcess.on('close', (code) => {
console.log(`Script process exited with code ${code}`);
if (code === 0) {
res.json({ status: 'success', message: `모델이 ${model}(으)로 변경됨`, output: stdoutData });
} else {
res.status(500).json({ status: 'error', message: '쉘 스크립트 실행 실패', error: stderrData });
}
});
scriptProcess.on('error', (err) => {
console.error('[서버] 쉘 프로세스 시작 실패:', err);
// "ENOENT" 오류는
// 1) 스크립트 경로가 잘못되었거나,
// 2) 스크립트 파일에 실행 권한이 없을 때 자주 발생합니다.
res.status(500).json({ status: 'error', message: '프로세스 시작 실패', error: err.message });
});
// --- 스크립트 실행 끝 ---
});
// 7. 모델 파일 업로드 엔드포인트
app.post('/upload-model', (req, res) => {
upload.single('modelFile')(req, res, function (err) {
if (err instanceof multer.MulterError) {
console.error('[Multer Error]:', err.message);
return res.status(500).json({ status: 'error', message: `업로드 오류: ${err.message}` });
}
else if (err) {
console.error('[File Filter Error]:', err.message);
return res.status(400).json({ status: 'error', message: err.message });
}
if (!req.file) {
return res.status(400).json({ status: 'error', message: '업로드할 파일을 찾을 수 없습니다.' });
}
console.log(`[서버] 파일 업로드 성공:`);
console.log(` - 원본 파일명: ${req.file.originalname}`);
console.log(` - 저장 경로: ${req.file.path}`);
res.json({
status: 'success',
message: `파일 '${req.file.originalname}'이(가) 성공적으로 업로드되었습니다.`,
filePath: req.file.path
});
});
});
// ========== 모델 목록 조회 로직 시작 ==========
/**
* 버전 문자열을 비교합니다. (예: "v1.1" > "v1.0")
*/
function isVersionGreater(v1, v2) {
if (!v2) return true;
const parts1 = v1.replace('v', '').split('.').map(Number);
const parts2 = v2.replace('v', '').split('.').map(Number);
if (parts1[0] > parts2[0]) return true;
if (parts1[0] < parts2[0]) return false;
if (parts1[1] > parts2[1]) return true;
return false;
}
// 7-1. 모델 목록 조회 엔드포인트
app.get('/list-models', (req, res) => {
const modelRegex = /^CUUVA_([A-Z]+)_v(\d+\.\d+)\.aiwbin$/;
const latestModels = {};
fs.readdir(uploadPath, (err, files) => {
if (err) {
console.error('[서버] 모델 디렉토리 읽기 실패:', err);
return res.status(500).json({ status: 'error', message: '서버에서 파일 목록을 읽을 수 없습니다.' });
}
files.forEach(file => {
const match = file.match(modelRegex);
if (match) {
const role = match[1];
const version = `v${match[2]}`;
const existing = latestModels[role];
if (!existing || isVersionGreater(version, existing.version)) {
latestModels[role] = {
file: file,
version: version
};
}
}
});
console.log('[서버] 최신 모델 목록 조회:', latestModels);
res.json(latestModels);
});
});
// ========== 모델 목록 조회 로직 끝 ==========
// ========== [추가됨] 모델 파일 삭제 엔드포인트 ==========
app.post('/delete-model', (req, res) => {
const { filename } = req.body; // { "filename": "CUUVA_..." }
if (!filename) {
return res.status(400).json({ status: 'error', message: '파일 이름이 제공되지 않았습니다.' });
}
// [보안] Directory Traversal 공격 방지
// path.basename은 파일명에서 상위 디렉토리(.., /) 등을 제거합니다.
const safeFilename = path.basename(filename);
// 만약 ".." 같은 것이 포함되어 safeFilename이 원래 filename과 다르다면, 비정상 요청
if (safeFilename !== filename) {
return res.status(400).json({ status: 'error', message: '잘못된 파일 이름 형식입니다.' });
}
// [보안] .aiwbin 파일만 삭제하도록 다시 한번 확인
if (!safeFilename.endsWith('.aiwbin')) {
return res.status(400).json({ status: 'error', message: '.aiwbin 파일만 삭제할 수 있습니다.' });
}
// 최종 삭제할 파일 경로
const filePath = path.join(uploadPath, safeFilename);
console.log(`[서버] 파일 삭제 시도: ${filePath}`);
// fs.unlink로 파일 삭제
fs.unlink(filePath, (err) => {
if (err) {
// 파일이 존재하지 않는 경우 (이미 삭제되었거나, 잘못된 요청)
if (err.code === 'ENOENT') {
console.error(`[서버] 삭제 실패: 파일 없음 ${filePath}`, err);
return res.status(404).json({ status: 'error', message: '삭제할 파일을 서버에서 찾을 수 없습니다.' });
}
// 기타 오류 (예: 권한 문제)
console.error(`[서버] 파일 삭제 오류 ${filePath}:`, err);
return res.status(500).json({ status: 'error', message: '파일 삭제 중 서버 오류가 발생했습니다.' });
}
// 삭제 성공
console.log(`[서버] 파일 삭제 성공: ${filePath}`);
res.json({ status: 'success', message: `파일 '${safeFilename}'(이)가 성공적으로 삭제되었습니다.` });
});
});
// ========== 모델 파일 삭제 엔드포인트 끝 ==========
// 8. 설정한 3000번 포트에서 서버가 요청을 기다리도록 실행합니다.
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
//