Files
dashboard/backend/src/services/measurementStorage.js
T

115 lines
3.5 KiB
JavaScript
Raw Normal View History

/**
* 频响文件 S3 存储服务
* 本地开发环境:使用 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
* 正式环境:通过 IAM 角色访问 S3(无需配置密钥)
*/
const { S3Client, PutObjectCommand, GetObjectCommand } = 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}`);
}
}
/**
* 从 S3 读取频响 CSV 文件
* @param {object} params - 路径参数(同 buildMeasurementKey
* @returns {Promise<{ key: string, content: string }>}
*/
async function getMeasurementFromS3(params) {
const key = buildMeasurementKey(params);
const client = getS3Client();
try {
const response = await client.send(
new GetObjectCommand({
Bucket: S3_BUCKET,
Key: key,
})
);
const content = await response.Body.transformToString('utf-8');
logger.info(`Measurement read from s3://${S3_BUCKET}/${key}`);
return { key, content };
} catch (e) {
if (e.name === 'NoSuchKey' || e.$metadata?.httpStatusCode === 404) {
throw new Error('S3 上未找到该型号的频响文件');
}
logger.error(`S3 measurement read failed: ${e.message}`);
throw new Error(`S3 读取失败:${e.message}`);
}
}
module.exports = {
uploadMeasurementToS3,
getMeasurementFromS3,
buildMeasurementKey,
};