refactor: 重构文件存储路径并清理冗余代码
- 将项目文件存储从 public/uploads 迁移至项目根目录专用目录 - 更新所有前端页面的路径提示文本与API请求路径 - 移除冗余的列车详情展开收起功能及相关代码 - 重构服务器端文件路径处理逻辑,增强路径访问安全性 - 清理旧注释和未使用的变量与函数
This commit is contained in:
+65
-112
@@ -1,11 +1,4 @@
|
||||
/*
|
||||
* @Author: HenryDu8133 813367384@qq.com
|
||||
* @Date: 2026-02-24 14:40:25
|
||||
* @LastEditors: HenryDu8133 813367384@qq.com
|
||||
* @LastEditTime: 2026-07-31 19:29:12
|
||||
* @FilePath: \StationBroadcastDisplayCN\cloudserver\server.js
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
@@ -35,6 +28,7 @@ const STABROADCAST_HLS_SUFFIX = process.env.STABROADCAST_HLS_SUFFIX || '.m3u8';
|
||||
const HLS_TAIL_SILENCE_SEC = Number(process.env.HLS_TAIL_SILENCE_SEC || '0');
|
||||
const HLS_PREBUILD = process.env.HLS_PREBUILD !== '0';
|
||||
|
||||
const appRootDir = path.resolve(__dirname, '..');
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
||||
const silence1sWavPath = path.join(tmpDir, 'silence-1s.wav');
|
||||
@@ -42,13 +36,15 @@ const silence1sWavPath = path.join(tmpDir, 'silence-1s.wav');
|
||||
const localHlsDir = path.join(__dirname, 'public', 'hls');
|
||||
if (!fs.existsSync(localHlsDir)) fs.mkdirSync(localHlsDir, { recursive: true });
|
||||
|
||||
// Ensure upload directories exist
|
||||
const uploadDir = path.join(__dirname, 'public', 'uploads');
|
||||
const audioGeneralDir = path.join(uploadDir, 'audio', 'general');
|
||||
const audioStationDir = path.join(uploadDir, 'audio', 'station');
|
||||
const tempBroadcastDir = path.join(uploadDir, 'temp-broadcast');
|
||||
const audioRootDir = path.join(appRootDir, 'Audio');
|
||||
const audioGeneralDir = path.join(audioRootDir, 'general');
|
||||
const audioStationDir = path.join(audioRootDir, 'station');
|
||||
const listFilePath = path.join(appRootDir, 'list.txt');
|
||||
const tempBroadcastDir = path.join(appRootDir, 'temp-broadcast');
|
||||
|
||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
||||
if (!fs.existsSync(audioRootDir)) fs.mkdirSync(audioRootDir, { recursive: true });
|
||||
if (!fs.existsSync(audioGeneralDir)) fs.mkdirSync(audioGeneralDir, { recursive: true });
|
||||
if (!fs.existsSync(audioStationDir)) fs.mkdirSync(audioStationDir, { recursive: true });
|
||||
if (!fs.existsSync(tempBroadcastDir)) fs.mkdirSync(tempBroadcastDir, { recursive: true });
|
||||
@@ -57,7 +53,7 @@ if (!fs.existsSync(tempBroadcastDir)) fs.mkdirSync(tempBroadcastDir, { recursive
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
if (file.fieldname === 'listFile') {
|
||||
cb(null, uploadDir);
|
||||
cb(null, appRootDir);
|
||||
} else if (file.fieldname === 'generalAudioFiles') {
|
||||
cb(null, audioGeneralDir);
|
||||
} else if (file.fieldname === 'stationAudioFiles') {
|
||||
@@ -91,7 +87,6 @@ app.set('view engine', 'ejs');
|
||||
|
||||
// Data Store (In-memory)
|
||||
let stations = {};
|
||||
const stationRouteIndex = new Map();
|
||||
let activeTrains = [];
|
||||
let stationLogs = [];
|
||||
let audioVersion = String(Date.now());
|
||||
@@ -479,15 +474,42 @@ function normalizeVersionString(input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
function resolveManagedPath(inputPath) {
|
||||
const normalized = String(inputPath || '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
if (normalized === 'list.txt' || normalized === 'uploads/list.txt') {
|
||||
return { logicalPath: 'list.txt', fullPath: listFilePath };
|
||||
}
|
||||
|
||||
const prefixes = [
|
||||
{ prefix: 'Audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
|
||||
{ prefix: 'uploads/audio/general/', baseDir: audioGeneralDir, logicalPrefix: 'Audio/general/' },
|
||||
{ prefix: 'Audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' },
|
||||
{ prefix: 'uploads/audio/station/', baseDir: audioStationDir, logicalPrefix: 'Audio/station/' }
|
||||
];
|
||||
|
||||
for (const entry of prefixes) {
|
||||
if (!normalized.startsWith(entry.prefix)) continue;
|
||||
const relativeName = normalized.slice(entry.prefix.length).trim();
|
||||
if (!relativeName || relativeName.includes('/') || relativeName.includes('\\')) return null;
|
||||
return {
|
||||
logicalPath: `${entry.logicalPrefix}${relativeName}`,
|
||||
fullPath: path.join(entry.baseDir, relativeName)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getManagedFiles() {
|
||||
const files = [];
|
||||
|
||||
const configPath = path.join(uploadDir, 'list.txt');
|
||||
if (fs.existsSync(configPath)) {
|
||||
const stats = fs.statSync(configPath);
|
||||
if (fs.existsSync(listFilePath)) {
|
||||
const stats = fs.statSync(listFilePath);
|
||||
files.push({
|
||||
name: 'list.txt',
|
||||
path: 'uploads/list.txt',
|
||||
path: 'list.txt',
|
||||
type: 'config',
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs
|
||||
@@ -504,7 +526,7 @@ function getManagedFiles() {
|
||||
const stats = fs.statSync(filePath);
|
||||
files.push({
|
||||
name: file,
|
||||
path: `uploads/audio/general/${file}`,
|
||||
path: `Audio/general/${file}`,
|
||||
type: 'general',
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs
|
||||
@@ -522,7 +544,7 @@ function getManagedFiles() {
|
||||
const stats = fs.statSync(filePath);
|
||||
files.push({
|
||||
name: file,
|
||||
path: `uploads/audio/station/${file}`,
|
||||
path: `Audio/station/${file}`,
|
||||
type: 'station',
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs
|
||||
@@ -630,50 +652,15 @@ function normalizeStationPlatform(platform) {
|
||||
return value || '1';
|
||||
}
|
||||
|
||||
function getStationRouteKeys(name, code, platform) {
|
||||
const keys = [];
|
||||
const normalizedCode = normalizeStationCode(code || getStationCodeFromName(name));
|
||||
const normalizedPlatform = normalizeStationPlatform(platform || getPlatformNumberFromName(name));
|
||||
const baseName = getBaseName(name);
|
||||
|
||||
if (normalizedCode) {
|
||||
keys.push(`code:${normalizedCode}|platform:${normalizedPlatform}`);
|
||||
keys.push(`code:${normalizedCode}`);
|
||||
}
|
||||
if (baseName) {
|
||||
keys.push(`base:${baseName}|platform:${normalizedPlatform}`);
|
||||
keys.push(`base:${baseName}`);
|
||||
}
|
||||
return [...new Set(keys)];
|
||||
}
|
||||
|
||||
function unindexStation(station) {
|
||||
if (!station) return;
|
||||
const keys = Array.isArray(station.routeKeys) ? station.routeKeys : [];
|
||||
for (const key of keys) {
|
||||
const current = stationRouteIndex.get(key);
|
||||
if (current && current.name === station.name) {
|
||||
stationRouteIndex.delete(key);
|
||||
}
|
||||
}
|
||||
station.routeKeys = [];
|
||||
}
|
||||
|
||||
function indexStation(station) {
|
||||
function normalizeStationRecord(station) {
|
||||
if (!station || !station.name) return;
|
||||
unindexStation(station);
|
||||
station.platform = normalizeStationPlatform(station.platform || getPlatformNumberFromName(station.name));
|
||||
station.code = normalizeStationCode(station.code || getStationCodeFromName(station.name)) || station.code || '';
|
||||
station.routeKeys = getStationRouteKeys(station.name, station.code, station.platform);
|
||||
for (const key of station.routeKeys) {
|
||||
stationRouteIndex.set(key, station);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStationIndex() {
|
||||
stationRouteIndex.clear();
|
||||
for (const station of Object.values(stations)) {
|
||||
indexStation(station);
|
||||
normalizeStationRecord(station);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1270,7 +1257,7 @@ function handleHeartbeat(ws, data) {
|
||||
lastSeen: Date.now(),
|
||||
ws: ws
|
||||
};
|
||||
indexStation(stations[cleanName]);
|
||||
normalizeStationRecord(stations[cleanName]);
|
||||
log('INFO', `Station auto-registered via heartbeat: "${cleanName}"`);
|
||||
broadcastUpdate();
|
||||
} else {
|
||||
@@ -1282,7 +1269,7 @@ function handleHeartbeat(ws, data) {
|
||||
log('INFO', `Station WebSocket updated via heartbeat: "${cleanName}"`);
|
||||
broadcastUpdate();
|
||||
}
|
||||
indexStation(stations[cleanName]);
|
||||
normalizeStationRecord(stations[cleanName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1298,7 +1285,7 @@ function handleStationRegister(ws, data) {
|
||||
lastSeen: Date.now(),
|
||||
ws: ws // Keep reference if we need to send back
|
||||
};
|
||||
indexStation(stations[cleanName]);
|
||||
normalizeStationRecord(stations[cleanName]);
|
||||
|
||||
log('SUCCESS', `Station registered: "${cleanName}" (${stationCode})`);
|
||||
|
||||
@@ -1328,24 +1315,15 @@ function handleTrainArrival(data) {
|
||||
const existingIndex = activeTrains.findIndex(t => getBaseTrainId(t.trainId) === baseId);
|
||||
|
||||
// Create history entry
|
||||
const historyEntry = {
|
||||
station: stationName,
|
||||
action: 'Arrival',
|
||||
time: Date.now()
|
||||
};
|
||||
|
||||
let trainData = {
|
||||
trainId,
|
||||
currentStation: stationName,
|
||||
destination: destination || (existingIndex >= 0 ? activeTrains[existingIndex].destination : "Unknown"),
|
||||
status: "At Station",
|
||||
lastUpdate: Date.now(),
|
||||
schedule: schedule || (existingIndex >= 0 ? activeTrains[existingIndex].schedule : null),
|
||||
tripLogs: existingIndex >= 0 ? (activeTrains[existingIndex].tripLogs || []) : []
|
||||
schedule: schedule || (existingIndex >= 0 ? activeTrains[existingIndex].schedule : null)
|
||||
};
|
||||
|
||||
trainData.tripLogs.push(historyEntry);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
activeTrains[existingIndex] = { ...activeTrains[existingIndex], ...trainData };
|
||||
} else {
|
||||
@@ -1420,12 +1398,6 @@ function isFuzzyMatch(str, pattern) {
|
||||
function findStationByFlexibleName(targetName) {
|
||||
if (!targetName) return null;
|
||||
|
||||
const directKeys = getStationRouteKeys(targetName);
|
||||
for (const key of directKeys) {
|
||||
const station = stationRouteIndex.get(key);
|
||||
if (station) return station;
|
||||
}
|
||||
|
||||
// 1. Try exact match
|
||||
if (stations[targetName]) return stations[targetName];
|
||||
|
||||
@@ -1458,20 +1430,13 @@ function handleTrainDeparture(data) {
|
||||
const existingIndex = activeTrains.findIndex(t => getBaseTrainId(t.trainId) === baseId);
|
||||
if (existingIndex >= 0) {
|
||||
const train = activeTrains[existingIndex];
|
||||
train.status = "In Transit";
|
||||
train.currentStation = "Transit (" + stationName + " -> " + (destination || "?") + ")";
|
||||
train.status = "Departed";
|
||||
train.currentStation = stationName;
|
||||
train.lastUpdate = Date.now();
|
||||
train.lastStation = stationName;
|
||||
train.departureTime = Date.now();
|
||||
train.destination = destination || train.destination;
|
||||
train.schedule = schedule || train.schedule;
|
||||
|
||||
if (!train.tripLogs) train.tripLogs = [];
|
||||
train.tripLogs.push({
|
||||
station: stationName,
|
||||
action: 'Departure',
|
||||
time: Date.now()
|
||||
});
|
||||
}
|
||||
broadcastUpdate();
|
||||
}
|
||||
@@ -1779,24 +1744,20 @@ app.delete('/api/files', (req, res) => {
|
||||
const filePath = req.query.path;
|
||||
if (!filePath) return res.status(400).json({ success: false, message: 'Path required' });
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
const safePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '');
|
||||
const fullPath = path.join(__dirname, 'public', safePath);
|
||||
|
||||
if (!fullPath.startsWith(uploadDir)) {
|
||||
return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
}
|
||||
const resolved = resolveManagedPath(filePath);
|
||||
if (!resolved) return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
const { fullPath, logicalPath } = resolved;
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
fs.unlinkSync(fullPath);
|
||||
log('INFO', `Deleted file: ${safePath}`);
|
||||
log('INFO', `Deleted file: ${logicalPath}`);
|
||||
|
||||
// Also try to delete counterpart (.dfpwm if .wav deleted, or vice versa)
|
||||
if (safePath.endsWith('.wav')) {
|
||||
if (logicalPath.endsWith('.wav')) {
|
||||
const dfpwmPath = fullPath.replace(/\.wav$/, '.dfpwm');
|
||||
if (fs.existsSync(dfpwmPath)) fs.unlinkSync(dfpwmPath);
|
||||
} else if (safePath.endsWith('.dfpwm')) {
|
||||
} else if (logicalPath.endsWith('.dfpwm')) {
|
||||
const wavPath = fullPath.replace(/\.dfpwm$/, '.wav');
|
||||
if (fs.existsSync(wavPath)) fs.unlinkSync(wavPath);
|
||||
}
|
||||
@@ -1828,7 +1789,7 @@ app.post('/api/stations', express.json(), (req, res) => {
|
||||
platform: getPlatformNumberFromName(name),
|
||||
lastSeen: Date.now()
|
||||
};
|
||||
indexStation(stations[name]);
|
||||
normalizeStationRecord(stations[name]);
|
||||
saveData();
|
||||
broadcastUpdate();
|
||||
res.json({ success: true, station: { name, code, lastSeen: stations[name].lastSeen } });
|
||||
@@ -1846,7 +1807,6 @@ app.put('/api/stations/:name', express.json(), (req, res) => {
|
||||
if (newName && newName !== name) {
|
||||
if (stations[newName]) return res.status(409).json({ success: false, message: 'New station name already exists' });
|
||||
stations[newName] = { ...st, name: newName };
|
||||
unindexStation(stations[name]);
|
||||
delete stations[name];
|
||||
targetName = newName;
|
||||
}
|
||||
@@ -1854,7 +1814,7 @@ app.put('/api/stations/:name', express.json(), (req, res) => {
|
||||
if (nextCode !== undefined) stations[targetName].code = nextCode;
|
||||
stations[targetName].platform = getPlatformNumberFromName(stations[targetName].name);
|
||||
stations[targetName].lastSeen = stations[targetName].lastSeen || Date.now();
|
||||
indexStation(stations[targetName]);
|
||||
normalizeStationRecord(stations[targetName]);
|
||||
|
||||
saveData();
|
||||
broadcastUpdate();
|
||||
@@ -1864,7 +1824,6 @@ app.put('/api/stations/:name', express.json(), (req, res) => {
|
||||
app.delete('/api/stations/:name', (req, res) => {
|
||||
const name = req.params.name;
|
||||
if (stations[name]) {
|
||||
unindexStation(stations[name]);
|
||||
delete stations[name];
|
||||
log('INFO', `Deleted station: ${name}`);
|
||||
saveData();
|
||||
@@ -1906,12 +1865,9 @@ app.get('/api/files/content', (req, res) => {
|
||||
const filePath = req.query.path;
|
||||
if (!filePath) return res.status(400).json({ success: false, message: 'Path required' });
|
||||
|
||||
const safePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '');
|
||||
const fullPath = path.join(__dirname, 'public', safePath);
|
||||
|
||||
if (!fullPath.startsWith(uploadDir)) {
|
||||
return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
}
|
||||
const resolved = resolveManagedPath(filePath);
|
||||
if (!resolved) return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
const { fullPath } = resolved;
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
@@ -1929,16 +1885,13 @@ app.post('/api/files/content', express.json(), (req, res) => {
|
||||
const { path: filePath, content } = req.body;
|
||||
if (!filePath || content === undefined) return res.status(400).json({ success: false, message: 'Missing parameters' });
|
||||
|
||||
const safePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '');
|
||||
const fullPath = path.join(__dirname, 'public', safePath);
|
||||
|
||||
if (!fullPath.startsWith(uploadDir)) {
|
||||
return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
}
|
||||
const resolved = resolveManagedPath(filePath);
|
||||
if (!resolved) return res.status(403).json({ success: false, message: 'Invalid path' });
|
||||
const { fullPath, logicalPath } = resolved;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
log('INFO', `Updated file: ${safePath}`);
|
||||
log('INFO', `Updated file: ${logicalPath}`);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ success: false, message: e.message });
|
||||
|
||||
Reference in New Issue
Block a user