mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
591 lines
23 KiB
JavaScript
591 lines
23 KiB
JavaScript
/**
|
||
* 系统相关路由 - 使用SQLite数据库
|
||
*/
|
||
const express = require('express');
|
||
const router = express.Router();
|
||
const os = require('os'); // 确保导入 os 模块
|
||
const util = require('util'); // 导入 util 模块
|
||
const { exec } = require('child_process');
|
||
const execPromise = util.promisify(exec); // 只在这里定义一次
|
||
const logger = require('../logger');
|
||
const { requireLogin } = require('../middleware/auth');
|
||
const configServiceDB = require('../services/configServiceDB');
|
||
const { execCommand, getSystemInfo } = require('../server-utils');
|
||
const dockerService = require('../services/dockerService');
|
||
const path = require('path');
|
||
const fs = require('fs').promises;
|
||
|
||
// 获取系统状态
|
||
async function getSystemStats(req, res) {
|
||
try {
|
||
let dockerAvailable = false;
|
||
let containerCount = '0';
|
||
let memoryUsage = '0%';
|
||
let cpuLoad = '0%';
|
||
let diskSpace = '0%';
|
||
let recentActivities = [];
|
||
|
||
// 尝试获取系统信息
|
||
try {
|
||
const systemInfo = await getSystemInfo();
|
||
memoryUsage = `${systemInfo.memory.percent}%`;
|
||
cpuLoad = systemInfo.cpu.load1;
|
||
diskSpace = systemInfo.disk.percent;
|
||
} catch (sysError) {
|
||
logger.error('获取系统信息失败:', sysError);
|
||
}
|
||
|
||
// 尝试从Docker获取状态信息
|
||
try {
|
||
const docker = await dockerService.getDockerConnection();
|
||
if (docker) {
|
||
dockerAvailable = true;
|
||
|
||
// 获取容器统计
|
||
const containers = await docker.listContainers({ all: true });
|
||
containerCount = containers.length.toString();
|
||
|
||
// 获取最近的容器活动
|
||
const runningContainers = containers.filter(c => c.State === 'running');
|
||
for (let i = 0; i < Math.min(3, runningContainers.length); i++) {
|
||
recentActivities.push({
|
||
time: new Date(runningContainers[i].Created * 1000).toLocaleString(),
|
||
action: '运行中',
|
||
container: runningContainers[i].Names[0].replace(/^\//, ''),
|
||
status: '正常'
|
||
});
|
||
}
|
||
|
||
// 获取最近的Docker事件
|
||
const events = await dockerService.getRecentEvents();
|
||
if (events && events.length > 0) {
|
||
recentActivities = [...events.map(event => ({
|
||
time: new Date(event.time * 1000).toLocaleString(),
|
||
action: event.Action,
|
||
container: event.Actor?.Attributes?.name || '未知容器',
|
||
status: event.status || '完成'
|
||
})), ...recentActivities].slice(0, 10);
|
||
}
|
||
}
|
||
} catch (containerError) {
|
||
logger.error('获取容器信息失败:', containerError);
|
||
}
|
||
|
||
// 如果没有活动记录,添加一个默认记录
|
||
if (recentActivities.length === 0) {
|
||
recentActivities.push({
|
||
time: new Date().toLocaleString(),
|
||
action: '系统检查',
|
||
container: '监控服务',
|
||
status: dockerAvailable ? '正常' : 'Docker服务不可用'
|
||
});
|
||
}
|
||
|
||
// 返回收集到的所有数据,即使部分数据可能不完整
|
||
res.json({
|
||
dockerAvailable,
|
||
containerCount,
|
||
memoryUsage,
|
||
cpuLoad,
|
||
diskSpace,
|
||
recentActivities
|
||
});
|
||
} catch (error) {
|
||
logger.error('获取系统统计数据失败:', error);
|
||
|
||
// 即使出错,仍然尝试返回一些基本数据
|
||
res.status(200).json({
|
||
dockerAvailable: false,
|
||
containerCount: '0',
|
||
memoryUsage: '未知',
|
||
cpuLoad: '未知',
|
||
diskSpace: '未知',
|
||
recentActivities: [{
|
||
time: new Date().toLocaleString(),
|
||
action: '系统错误',
|
||
container: '监控服务',
|
||
status: '数据获取失败'
|
||
}],
|
||
error: '获取系统统计数据失败',
|
||
errorDetails: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
// 获取系统配置 - 修改版本,避免与其他路由冲突
|
||
router.get('/system-config', async (req, res) => {
|
||
try {
|
||
const config = await configServiceDB.getConfig();
|
||
res.json(config);
|
||
} catch (error) {
|
||
logger.error('读取配置失败:', error);
|
||
res.status(500).json({
|
||
error: '读取配置失败',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// 保存系统配置 - 修改版本,避免与其他路由冲突
|
||
router.post('/system-config', requireLogin, async (req, res) => {
|
||
try {
|
||
const currentConfig = await configServiceDB.getConfig();
|
||
const newConfig = { ...currentConfig, ...req.body };
|
||
await configServiceDB.saveConfigs(newConfig);
|
||
logger.info('系统配置已更新');
|
||
res.json({ success: true });
|
||
} catch (error) {
|
||
logger.error('保存配置失败:', error);
|
||
res.status(500).json({
|
||
error: '保存配置失败',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// 获取系统状态
|
||
router.get('/stats', requireLogin, async (req, res) => {
|
||
return await getSystemStats(req, res);
|
||
});
|
||
|
||
// 获取磁盘空间信息
|
||
router.get('/disk-space', requireLogin, async (req, res) => {
|
||
try {
|
||
const systemInfo = await getSystemInfo();
|
||
res.json({
|
||
diskSpace: `${systemInfo.disk.used}/${systemInfo.disk.size}`,
|
||
usagePercent: parseInt(systemInfo.disk.percent)
|
||
});
|
||
} catch (error) {
|
||
logger.error('获取磁盘空间信息失败:', error);
|
||
res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message });
|
||
}
|
||
});
|
||
|
||
// 网络测试
|
||
router.post('/network-test', requireLogin, async (req, res) => {
|
||
const { type, domain } = req.body;
|
||
|
||
// 验证输入
|
||
function validateInput(input, type) {
|
||
if (type === 'domain') {
|
||
return /^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(input);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (!validateInput(domain, 'domain')) {
|
||
return res.status(400).json({ error: '无效的域名格式' });
|
||
}
|
||
|
||
try {
|
||
const result = await execCommand(`${type === 'ping' ? 'ping -c 4' : 'traceroute -m 10'} ${domain}`, { timeout: 30000 });
|
||
res.send(result);
|
||
} catch (error) {
|
||
if (error.killed) {
|
||
return res.status(408).send('测试超时');
|
||
}
|
||
logger.error(`执行网络测试命令错误:`, error);
|
||
res.status(500).send('测试执行失败: ' + error.message);
|
||
}
|
||
});
|
||
|
||
// 获取用户统计信息
|
||
router.get('/user-stats', requireLogin, async (req, res) => {
|
||
try {
|
||
const userService = require('../services/userService');
|
||
const username = req.session.user.username;
|
||
const userStats = await userService.getUserStats(username);
|
||
|
||
res.json(userStats);
|
||
} catch (error) {
|
||
logger.error('获取用户统计信息失败:', error);
|
||
res.status(500).json({
|
||
loginCount: '0',
|
||
lastLogin: '未知',
|
||
accountAge: '0'
|
||
});
|
||
}
|
||
});
|
||
|
||
// 获取系统状态信息 (旧版,可能与 getSystemStats 重复,可以考虑移除)
|
||
router.get('/system-status', requireLogin, async (req, res) => {
|
||
logger.warn('Accessing potentially deprecated /api/system-status route.');
|
||
try {
|
||
// 检查 Docker 可用性
|
||
let dockerAvailable = true;
|
||
let containerCount = 0;
|
||
try {
|
||
// 避免直接执行命令计算,依赖 dockerService
|
||
const docker = await dockerService.getDockerConnection();
|
||
if (docker) {
|
||
const containers = await docker.listContainers({ all: true });
|
||
containerCount = containers.length;
|
||
} else {
|
||
dockerAvailable = false;
|
||
}
|
||
} catch (dockerError) {
|
||
dockerAvailable = false;
|
||
containerCount = 0;
|
||
logger.warn('Docker可能未运行或无法访问 (in /system-status):', dockerError.message);
|
||
}
|
||
|
||
// 获取内存使用信息
|
||
const totalMem = os.totalmem();
|
||
const freeMem = os.freemem();
|
||
const usedMem = totalMem - freeMem;
|
||
const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`;
|
||
|
||
// 获取CPU负载
|
||
const [load1] = os.loadavg();
|
||
const cpuCount = os.cpus().length || 1; // 避免除以0
|
||
const cpuLoad = `${(load1 / cpuCount * 100).toFixed(1)}%`;
|
||
|
||
// 获取磁盘空间 - 简单版
|
||
let diskSpace = '未知';
|
||
try {
|
||
if (os.platform() === 'darwin' || os.platform() === 'linux') {
|
||
const { stdout } = await execPromise('df -h / | tail -n 1'); // 使用 -n 1
|
||
const parts = stdout.trim().split(/\s+/);
|
||
if (parts.length >= 5) diskSpace = parts[4];
|
||
} else if (os.platform() === 'win32') {
|
||
const { stdout } = await execPromise('wmic logicaldisk get size,freespace,caption | findstr /B /L /V "Caption" ');
|
||
const lines = stdout.trim().split(/\r?\n/);
|
||
if (lines.length > 0) {
|
||
const parts = lines[0].trim().split(/\s+/);
|
||
if (parts.length >= 2) {
|
||
const free = parseInt(parts[0], 10);
|
||
const total = parseInt(parts[1], 10);
|
||
if (!isNaN(total) && !isNaN(free) && total > 0) {
|
||
diskSpace = `${Math.round(((total - free) / total) * 100)}%`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (diskError) {
|
||
logger.warn('获取磁盘空间失败 (in /system-status):', diskError.message);
|
||
diskSpace = '未知';
|
||
}
|
||
|
||
// 格式化系统运行时间
|
||
const uptime = formatUptime(os.uptime());
|
||
|
||
res.json({
|
||
dockerAvailable,
|
||
containerCount,
|
||
memoryUsage,
|
||
cpuLoad,
|
||
diskSpace,
|
||
systemUptime: uptime
|
||
});
|
||
} catch (error) {
|
||
logger.error('获取系统状态失败 (in /system-status):', error);
|
||
res.status(500).json({
|
||
error: '获取系统状态失败',
|
||
message: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// 添加新的API端点,提供完整系统资源信息
|
||
router.get('/system-resources', requireLogin, async (req, res) => {
|
||
logger.info('Received request for /api/system-resources');
|
||
let cpuInfoData = null, memoryData = null, diskInfoData = null, systemData = null;
|
||
|
||
// --- 获取 CPU 信息 (独立 try...catch) ---
|
||
try {
|
||
const cpuInfo = os.cpus();
|
||
const [load1, load5, load15] = os.loadavg();
|
||
const cpuCount = cpuInfo.length || 1;
|
||
const cpuUsage = (load1 / cpuCount * 100).toFixed(1);
|
||
cpuInfoData = {
|
||
cores: cpuCount,
|
||
model: cpuInfo[0]?.model || '未知',
|
||
speed: `${cpuInfo[0]?.speed || '未知'} MHz`,
|
||
loadAvg: {
|
||
'1min': load1.toFixed(2),
|
||
'5min': load5.toFixed(2),
|
||
'15min': load15.toFixed(2)
|
||
},
|
||
usage: parseFloat(cpuUsage)
|
||
};
|
||
logger.info('Successfully retrieved CPU info.');
|
||
} catch (cpuError) {
|
||
logger.error('Error getting CPU info:', cpuError.message);
|
||
cpuInfoData = { error: '获取 CPU 信息失败', message: cpuError.message }; // 返回错误信息
|
||
}
|
||
|
||
// --- 获取内存信息 (独立 try...catch) ---
|
||
try {
|
||
const totalMem = os.totalmem();
|
||
const freeMem = os.freemem();
|
||
const usedMem = totalMem - freeMem;
|
||
const memoryUsagePercent = totalMem > 0 ? Math.round(usedMem / totalMem * 100) : 0;
|
||
memoryData = {
|
||
total: formatBytes(totalMem), // 可能出错
|
||
free: formatBytes(freeMem), // 可能出错
|
||
used: formatBytes(usedMem), // 可能出错
|
||
usedPercentage: memoryUsagePercent
|
||
};
|
||
logger.info('Successfully retrieved Memory info.');
|
||
} catch (memError) {
|
||
logger.error('Error getting Memory info:', memError.message);
|
||
memoryData = { error: '获取内存信息失败', message: memError.message }; // 返回错误信息
|
||
}
|
||
|
||
// --- 获取磁盘信息 (独立 try...catch) ---
|
||
try {
|
||
let diskResult = { total: '未知', free: '未知', used: '未知', usedPercentage: '未知' };
|
||
logger.info(`Getting disk info for platform: ${os.platform()}`);
|
||
if (os.platform() === 'darwin' || os.platform() === 'linux') {
|
||
try {
|
||
// 使用 -k 获取 KB 单位,方便计算
|
||
const { stdout } = await execPromise('df -k / | tail -n 1', { timeout: 5000 });
|
||
logger.info(`'df -k' command output: ${stdout}`);
|
||
const parts = stdout.trim().split(/\s+/);
|
||
// 索引通常是 1=Total, 2=Used, 3=Available, 4=Use%
|
||
if (parts.length >= 4) {
|
||
const total = parseInt(parts[1], 10) * 1024; // KB to Bytes
|
||
const used = parseInt(parts[2], 10) * 1024; // KB to Bytes
|
||
const free = parseInt(parts[3], 10) * 1024; // KB to Bytes
|
||
// 优先使用命令输出的百分比,更准确
|
||
let usedPercentage = parseInt(parts[4].replace('%', ''), 10);
|
||
|
||
// 如果解析失败或百分比无效,则尝试计算
|
||
if (isNaN(usedPercentage) && !isNaN(total) && !isNaN(used) && total > 0) {
|
||
usedPercentage = Math.round((used / total) * 100);
|
||
}
|
||
|
||
if (!isNaN(total) && !isNaN(used) && !isNaN(free) && !isNaN(usedPercentage)) {
|
||
diskResult = {
|
||
total: formatBytes(total), // 可能出错
|
||
free: formatBytes(free), // 可能出错
|
||
used: formatBytes(used), // 可能出错
|
||
usedPercentage: usedPercentage
|
||
};
|
||
logger.info('Successfully parsed disk info (Linux/Darwin).');
|
||
} else {
|
||
logger.warn('Failed to parse numbers from df output:', parts);
|
||
diskResult = { ...diskResult, error: '解析 df 输出失败' }; // 添加错误标记
|
||
}
|
||
} else {
|
||
logger.warn('Unexpected output format from df:', stdout);
|
||
diskResult = { ...diskResult, error: 'df 输出格式不符合预期' }; // 添加错误标记
|
||
}
|
||
} catch (dfError) {
|
||
logger.error(`Error executing or parsing 'df -k': ${dfError.message}`);
|
||
if (dfError.killed) logger.error("'df -k' command timed out.");
|
||
diskResult = { error: '获取磁盘信息失败 (df)', message: dfError.message }; // 标记错误
|
||
}
|
||
} else if (os.platform() === 'win32') {
|
||
try {
|
||
// 获取 C 盘信息 (可以修改为获取所有盘符或特定盘符)
|
||
const { stdout } = await execPromise(`wmic logicaldisk where "DeviceID='C:'" get size,freespace /value`, { timeout: 5000 });
|
||
logger.info(`'wmic' command output: ${stdout}`);
|
||
const lines = stdout.trim().split(/\r?\n/);
|
||
let free = NaN, total = NaN;
|
||
lines.forEach(line => {
|
||
if (line.startsWith('FreeSpace=')) {
|
||
free = parseInt(line.split('=')[1], 10);
|
||
} else if (line.startsWith('Size=')) {
|
||
total = parseInt(line.split('=')[1], 10);
|
||
}
|
||
});
|
||
|
||
if (!isNaN(total) && !isNaN(free) && total > 0) {
|
||
const used = total - free;
|
||
const usedPercentage = Math.round((used / total) * 100);
|
||
diskResult = {
|
||
total: formatBytes(total), // 可能出错
|
||
free: formatBytes(free), // 可能出错
|
||
used: formatBytes(used), // 可能出错
|
||
usedPercentage: usedPercentage
|
||
};
|
||
logger.info('Successfully parsed disk info (Windows - C:).');
|
||
} else {
|
||
logger.warn('Failed to parse numbers from wmic output:', stdout);
|
||
diskResult = { ...diskResult, error: '解析 wmic 输出失败' }; // 添加错误标记
|
||
}
|
||
} catch (wmicError) {
|
||
logger.error(`Error executing or parsing 'wmic': ${wmicError.message}`);
|
||
if (wmicError.killed) logger.error("'wmic' command timed out.");
|
||
diskResult = { error: '获取磁盘信息失败 (wmic)', message: wmicError.message }; // 标记错误
|
||
}
|
||
}
|
||
diskInfoData = diskResult;
|
||
} catch (diskErrorOuter) {
|
||
logger.error('Unexpected error during disk info gathering:', diskErrorOuter.message);
|
||
diskInfoData = { error: '获取磁盘信息时发生意外错误', message: diskErrorOuter.message }; // 返回错误信息
|
||
}
|
||
|
||
// --- 获取其他系统信息 (独立 try...catch) ---
|
||
try {
|
||
systemData = {
|
||
platform: os.platform(),
|
||
release: os.release(),
|
||
hostname: os.hostname(),
|
||
uptime: formatUptime(os.uptime()) // 可能出错
|
||
};
|
||
logger.info('Successfully retrieved general system info.');
|
||
} catch (sysInfoError) {
|
||
logger.error('Error getting general system info:', sysInfoError.message);
|
||
systemData = { error: '获取常规系统信息失败', message: sysInfoError.message }; // 返回错误信息
|
||
}
|
||
|
||
// --- 包装 Helper 函数调用以捕获潜在错误 ---
|
||
const safeFormatBytes = (bytes) => {
|
||
try {
|
||
return formatBytes(bytes);
|
||
} catch (e) {
|
||
logger.error(`formatBytes failed for value ${bytes}:`, e.message);
|
||
return '格式化错误';
|
||
}
|
||
};
|
||
const safeFormatUptime = (seconds) => {
|
||
try {
|
||
return formatUptime(seconds);
|
||
} catch (e) {
|
||
logger.error(`formatUptime failed for value ${seconds}:`, e.message);
|
||
return '格式化错误';
|
||
}
|
||
};
|
||
|
||
// --- 构建最终响应数据,使用安全的 Helper 函数 ---
|
||
const finalCpuData = cpuInfoData?.error ? cpuInfoData : {
|
||
...cpuInfoData
|
||
// CPU 不需要格式化
|
||
};
|
||
const finalMemoryData = memoryData?.error ? memoryData : {
|
||
...memoryData,
|
||
total: safeFormatBytes(os.totalmem()),
|
||
free: safeFormatBytes(os.freemem()),
|
||
used: safeFormatBytes(os.totalmem() - os.freemem())
|
||
};
|
||
const finalDiskData = diskInfoData?.error ? diskInfoData : {
|
||
...diskInfoData,
|
||
// 如果 diskInfoData 内部有 total/free/used (字节数),则格式化
|
||
// 否则保持 '未知' 或已格式化的字符串
|
||
total: (diskInfoData?.total && typeof diskInfoData.total === 'number') ? safeFormatBytes(diskInfoData.total) : diskInfoData?.total || '未知',
|
||
free: (diskInfoData?.free && typeof diskInfoData.free === 'number') ? safeFormatBytes(diskInfoData.free) : diskInfoData?.free || '未知',
|
||
used: (diskInfoData?.used && typeof diskInfoData.used === 'number') ? safeFormatBytes(diskInfoData.used) : diskInfoData?.used || '未知'
|
||
};
|
||
const finalSystemData = systemData?.error ? systemData : {
|
||
...systemData,
|
||
uptime: safeFormatUptime(os.uptime())
|
||
};
|
||
|
||
const responseData = {
|
||
cpu: finalCpuData,
|
||
memory: finalMemoryData,
|
||
diskSpace: finalDiskData,
|
||
system: finalSystemData
|
||
};
|
||
|
||
logger.info('Sending response for /api/system-resources:', JSON.stringify(responseData));
|
||
res.status(200).json(responseData);
|
||
});
|
||
|
||
// 格式化系统运行时间
|
||
function formatUptime(seconds) {
|
||
const days = Math.floor(seconds / 86400);
|
||
seconds %= 86400;
|
||
const hours = Math.floor(seconds / 3600);
|
||
seconds %= 3600;
|
||
const minutes = Math.floor(seconds / 60);
|
||
seconds = Math.floor(seconds % 60);
|
||
|
||
let result = '';
|
||
if (days > 0) result += `${days}天 `;
|
||
if (hours > 0 || days > 0) result += `${hours}小时 `;
|
||
if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}分钟 `;
|
||
result += `${seconds}秒`;
|
||
|
||
return result;
|
||
}
|
||
|
||
// 获取系统资源详情
|
||
router.get('/system-resource-details', requireLogin, async (req, res) => {
|
||
try {
|
||
const { type } = req.query;
|
||
|
||
let data = {};
|
||
|
||
switch (type) {
|
||
case 'memory':
|
||
const totalMem = os.totalmem();
|
||
const freeMem = os.freemem();
|
||
const usedMem = totalMem - freeMem;
|
||
|
||
data = {
|
||
totalMemory: formatBytes(totalMem),
|
||
usedMemory: formatBytes(usedMem),
|
||
freeMemory: formatBytes(freeMem),
|
||
memoryUsage: `${Math.round(usedMem / totalMem * 100)}%`
|
||
};
|
||
break;
|
||
|
||
case 'cpu':
|
||
const cpuInfo = os.cpus();
|
||
const [load1, load5, load15] = os.loadavg();
|
||
|
||
data = {
|
||
cpuCores: cpuInfo.length,
|
||
cpuModel: cpuInfo[0].model,
|
||
cpuSpeed: `${cpuInfo[0].speed} MHz`,
|
||
loadAvg1: load1.toFixed(2),
|
||
loadAvg5: load5.toFixed(2),
|
||
loadAvg15: load15.toFixed(2),
|
||
cpuLoad: `${(load1 / cpuInfo.length * 100).toFixed(1)}%`
|
||
};
|
||
break;
|
||
|
||
case 'disk':
|
||
try {
|
||
const { stdout: dfOutput } = await execPromise('df -h / | tail -n 1');
|
||
const parts = dfOutput.trim().split(/\s+/);
|
||
|
||
if (parts.length >= 5) {
|
||
data = {
|
||
totalSpace: parts[1],
|
||
usedSpace: parts[2],
|
||
freeSpace: parts[3],
|
||
diskUsage: parts[4]
|
||
};
|
||
} else {
|
||
throw new Error('解析磁盘信息失败');
|
||
}
|
||
} catch (diskError) {
|
||
logger.warn('获取磁盘信息失败:', diskError.message);
|
||
data = {
|
||
error: '获取磁盘信息失败',
|
||
message: diskError.message
|
||
};
|
||
}
|
||
break;
|
||
|
||
default:
|
||
return res.status(400).json({ error: '无效的资源类型' });
|
||
}
|
||
|
||
res.json(data);
|
||
} catch (error) {
|
||
logger.error('获取系统资源详情失败:', error);
|
||
res.status(500).json({ error: '获取系统资源详情失败', message: error.message });
|
||
}
|
||
});
|
||
|
||
// 格式化字节数为可读格式
|
||
function formatBytes(bytes, decimals = 2) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
|
||
const k = 1024;
|
||
const dm = decimals < 0 ? 0 : decimals;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||
}
|
||
|
||
module.exports = router; // 只导出 router
|