115 lines
3.5 KiB
JavaScript
115 lines
3.5 KiB
JavaScript
/**
|
||
* 频响文件 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,
|
||
};
|