耳机频响文件改为s3 存储,修改了x9 升级包的存储路径

This commit is contained in:
eafonyang
2026-06-09 19:44:51 +08:00
parent e4e6529577
commit 316852274a
8 changed files with 263 additions and 43 deletions
+9 -6
View File
@@ -7,7 +7,7 @@ DATABASE_PASSWORD=root123
# Application Settings
APP_NAME=Audio Dashboard API
DEBUG=True
APP_ENV=development
PORT=8083
# JWT
@@ -20,19 +20,22 @@ MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=
MEILISEARCH_INDEX=models
# AWS S3 (OTA X8)
# AWS S3
AWS_REGION=eu-central-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_OTA_BUCKET=luxsin-app-bucket
AWS_S3_MEASUREMENT_BUCKET=luxsin-app-bucket
# OTA URLs
OTA_X8_PUBLIC_BASE=http://am.luxsinaudio.com
OTA_X9_URL_BASE=http://source.luxsin.net
# OTA Upload Directories
OTA_UPLOAD_DIR_DEV=
OTA_UPLOAD_DIR_PROD=/data/project/dashboard/upload
# X9 升级包存储根目录(正式环境默认 /data/projects/source,路径规则:{root}/ota/{YYYYMM}/x9/{md5前5位}/LUXSIN.PKG
# 开发环境自动使用系统临时目录,无需配置
OTA_UPLOAD_DIR=/data/projects/source
# File Upload
UPLOAD_FOLDER=/data/project/autoeq/measurements
# File Upload (S3)
# 频响文件上传到 S3: autoeq/measurements/{source}/data/{form}/{brandChar}/{brand model}.csv
# 正式环境通过 IAM 角色访问 S3,无需配置 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
+2 -1
View File
@@ -1,4 +1,5 @@
const { Sequelize } = require('sequelize');
const { isDevelopment } = require('./env');
const sequelize = new Sequelize(
process.env.DATABASE_NAME || 'audio',
@@ -11,7 +12,7 @@ const sequelize = new Sequelize(
dialectOptions: {
charset: 'utf8mb4',
},
logging: process.env.DEBUG === 'True' ? (msg) => console.log(msg) : false,
logging: isDevelopment ? (msg) => console.log(msg) : false,
define: {
timestamps: false,
freezeTableName: true,
+12
View File
@@ -0,0 +1,12 @@
/**
* 统一环境判断工具
* 通过 APP_ENV 环境变量区分开发/正式环境
* - development: 本地开发
* - production: 正式环境(Docker 部署)
*/
const APP_ENV = (process.env.APP_ENV || 'development').trim().toLowerCase();
const isDevelopment = APP_ENV === 'development';
const isProduction = APP_ENV === 'production';
module.exports = { APP_ENV, isDevelopment, isProduction };
+17 -14
View File
@@ -5,13 +5,13 @@ const router = require('express').Router();
const { Op } = require('sequelize');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const axios = require('axios');
const Model = require('../models/Model');
const logger = require('../config/logger');
const { ApiResponse } = require('../utils/response');
const { authMiddleware } = require('../middleware/auth');
const { fetchAndValidateCurve } = require('../services/curveClient');
const { uploadMeasurementToS3 } = require('../services/measurementStorage');
/**
* 将 TXT 频响文件内容转换为 CSV 格式
@@ -78,7 +78,6 @@ const MEILISEARCH_API_KEY = process.env.MEILISEARCH_API_KEY || '';
const MEILISEARCH_INDEX = process.env.MEILISEARCH_INDEX || 'models';
// 文件上传配置
const UPLOAD_FOLDER = process.env.UPLOAD_FOLDER || '/data/project/autoeq/measurements';
const ALLOWED_EXTENSIONS = ['.csv', '.txt', '.json'];
// GET /api/models/
@@ -178,12 +177,14 @@ router.post('/api/models/', upload.single('measurement_file'), async (req, res)
return res.json(ApiResponse.error(`不支持的文件格式:${fileExt}`));
}
const { buffer: fileBuffer, filename: savedFilename } = processUploadedFile(req.file.buffer, req.file.originalname);
const saveDir = path.join(UPLOAD_FOLDER, source || '', 'data', form || '');
fs.mkdirSync(saveDir, { recursive: true });
const filePath = path.join(saveDir, savedFilename);
fs.writeFileSync(filePath, fileBuffer);
logger.info(`File saved: ${filePath}`);
const { buffer: fileBuffer } = processUploadedFile(req.file.buffer, req.file.originalname);
const s3Key = await uploadMeasurementToS3(fileBuffer, {
source: source || 'Eafonyoung',
form: form || 'in-ear',
brandName: brand_name,
modelName: name,
});
logger.info(`Measurement file uploaded to S3: ${s3Key}`);
}
const dbModel = await Model.create({
@@ -251,12 +252,14 @@ router.put('/api/models/:model_id', upload.single('measurement_file'), async (re
if (!effSource || !effForm) {
return res.json(ApiResponse.error('上传频响文件需要来源与形式字段'));
}
const { buffer: fileBuffer, filename: savedFilename } = processUploadedFile(req.file.buffer, req.file.originalname);
const saveDir = path.join(UPLOAD_FOLDER, effSource, 'data', effForm);
fs.mkdirSync(saveDir, { recursive: true });
const filePath = path.join(saveDir, savedFilename);
fs.writeFileSync(filePath, fileBuffer);
logger.info(`File saved: ${filePath}`);
const { buffer: fileBuffer } = processUploadedFile(req.file.buffer, req.file.originalname);
const s3Key = await uploadMeasurementToS3(fileBuffer, {
source: effSource,
form: effForm,
brandName: newBrandName,
modelName: newName,
});
logger.info(`Measurement file uploaded to S3: ${s3Key}`);
}
if (brand_name !== undefined && brand_name !== 'null') dbModel.brand_name = brand_name;
@@ -0,0 +1,85 @@
/**
* 频响文件 S3 存储服务
* 本地开发环境:使用 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
* 正式环境:通过 IAM 角色访问 S3(无需配置密钥)
*/
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const logger = require('../config/logger');
const AWS_REGION = process.env.AWS_REGION || 'eu-central-1';
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
const S3_BUCKET = process.env.AWS_S3_MEASUREMENT_BUCKET || process.env.AWS_S3_OTA_BUCKET || 'luxsin-app-bucket';
/**
* 获取 S3 客户端
* 有显式凭证则用凭证,否则走 IAM 角色
*/
function getS3Client() {
const options = { region: AWS_REGION };
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) {
options.credentials = {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
};
}
return new S3Client(options);
}
/**
* 获取品牌首字符(字母则转大写)
*/
function getBrandFirstChar(brandName) {
if (!brandName || !brandName.length) return '_';
const first = brandName[0];
return /[a-zA-Z]/.test(first) ? first.toUpperCase() : first;
}
/**
* 构建 S3 Key
* 路径:autoeq/measurements/{source}/data/{form}/{brandFirstChar}/{brand model}.csv
* @param {object} params
* @param {string} params.source - 来源,默认 Eafonyoung
* @param {string} params.form - 佩戴方式 (in-ear / over-ear / earbud)
* @param {string} params.brandName - 品牌名称
* @param {string} params.modelName - 型号名称
* @returns {string} S3 Key
*/
function buildMeasurementKey({ source, form, brandName, modelName }) {
const src = source || 'Eafonyoung';
const brandChar = getBrandFirstChar(brandName);
const fileName = `${brandName} ${modelName}.csv`;
return `autoeq/measurements/${src}/data/${form}/${brandChar}/${fileName}`;
}
/**
* 上传频响文件到 S3
* @param {Buffer} buffer - 文件内容(已转换为 CSV)
* @param {object} params - 路径参数
* @returns {Promise<string>} S3 Key
*/
async function uploadMeasurementToS3(buffer, params) {
const key = buildMeasurementKey(params);
const client = getS3Client();
try {
await client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: key,
Body: buffer,
ContentType: 'text/csv',
})
);
logger.info(`Measurement uploaded to s3://${S3_BUCKET}/${key}`);
return key;
} catch (e) {
logger.error(`S3 measurement upload failed: ${e.message}`);
throw new Error(`S3 上传失败:${e.message}`);
}
}
module.exports = {
uploadMeasurementToS3,
buildMeasurementKey,
};
+15 -19
View File
@@ -1,19 +1,20 @@
/**
* OTA 升级包存储:Luxsin-X8 -> S3Luxsin-X9 -> 本地目录
* 对应 Python: ota_storage.py
* S3 客户端:有显式凭证用凭证(本地开发),无凭证走 IAM 角色(正式环境)
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const logger = require('../config/logger');
const { isDevelopment } = require('../config/env');
const OTA_MODEL_X8 = 'Luxsin-X8';
const OTA_MODEL_X9 = 'Luxsin-X9';
const OTA_UPLOAD_MODELS = new Set([OTA_MODEL_X8, OTA_MODEL_X9]);
const _OTA_UPLOAD_DIR_DEV_DEFAULT = 'H:/soft/projects/luxsin/dashboard/ota';
const _OTA_UPLOAD_DIR_PROD_DEFAULT = '/data/project/dashboard/upload';
const OTA_UPLOAD_DIR = process.env.OTA_UPLOAD_DIR
|| (isDevelopment ? path.join(require('os').tmpdir(), 'dashboard') : '/data/projects/source');
const AWS_REGION = process.env.AWS_REGION || 'eu-central-1';
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
@@ -27,11 +28,7 @@ const OTA_FILENAME_X8 = 'LUXSIN_X8.PKG';
const OTA_FILENAME_X9 = 'LUXSIN.PKG';
function getOtaUploadDir() {
const debugVal = (process.env.DEBUG || 'false').trim().toLowerCase();
const isDev = ['true', '1', 'yes', 'on'].includes(debugVal);
const dev = process.env.OTA_UPLOAD_DIR_DEV || _OTA_UPLOAD_DIR_DEV_DEFAULT;
const prod = process.env.OTA_UPLOAD_DIR_PROD || _OTA_UPLOAD_DIR_PROD_DEFAULT;
return isDev ? dev : prod;
return OTA_UPLOAD_DIR;
}
function md5Prefix5(md5Hex) {
@@ -52,10 +49,12 @@ async function readUploadContentAndMd5(fileBuffer) {
}
function saveX9PackageLocal(content, md5Hex) {
const root = getOtaUploadDir();
const prefix = md5Prefix5(md5Hex);
const ym = new Date().toISOString().slice(0, 7).replace('-', '');
const destDir = path.join(root, prefix);
// 正式环境: /data/projects/source/ota/{YYYYMM}/x9/{md5前5位}/LUXSIN.PKG
// 开发环境: {tmpdir}/dashboard/ota/{YYYYMM}/x9/{md5前5位}/LUXSIN.PKG
const destDir = path.join(OTA_UPLOAD_DIR, 'ota', ym, 'x9', prefix);
fs.mkdirSync(destDir, { recursive: true });
const finalPath = path.join(destDir, OTA_FILENAME_X9);
@@ -71,21 +70,18 @@ function saveX9PackageLocal(content, md5Hex) {
}
async function uploadX8PackageToS3(content, md5Hex) {
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
throw new Error('未配置 AWS 访问密钥(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY');
}
const ym = new Date().toISOString().slice(0, 7).replace('-', '');
const prefix = md5Prefix5(md5Hex);
const s3Key = `ota/${ym}/x8/${prefix}/${OTA_FILENAME_X8}`;
const client = new S3Client({
region: AWS_REGION,
credentials: {
const options = { region: AWS_REGION };
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) {
options.credentials = {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
});
};
}
const client = new S3Client(options);
try {
await client.send(