引入minio

This commit is contained in:
2025-09-02 16:50:35 +08:00
parent e4d4bc5b6a
commit e5ace37c68
12 changed files with 28231 additions and 144 deletions

20
.env
View File

@@ -24,3 +24,23 @@ PORT=3000
# 前端地址配置
FRONTEND_URL=https://www.zrbjr.com/frontend
# FRONTEND_URL=http://114.55.111.44:3001/frontend
# MinIO 对象存储配置
# MinIO服务器地址不包含协议
MINIO_ENDPOINT=114.55.111.44
# MinIO服务器端口
MINIO_PORT=9000
# 是否使用SSLtrue/false
MINIO_USE_SSL=false
# MinIO访问密钥
MINIO_ACCESS_KEY=minio
# MinIO秘密密钥
MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE
# MinIO公开访问地址用于生成文件URL
MINIO_PUBLIC_URL=https://minio.zrbjr.com
# MinIO存储桶配置
MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan

View File

@@ -18,3 +18,26 @@ ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054
# 环境配置
NODE_ENV=development
PORT=3000
# 前端地址配置
FRONTEND_URL=http://localhost:3001
# MinIO 对象存储配置
# MinIO服务器地址不包含协议
MINIO_ENDPOINT=localhost
# MinIO服务器端口
MINIO_PORT=9000
# 是否使用SSLtrue/false
MINIO_USE_SSL=false
# MinIO访问密钥
MINIO_ACCESS_KEY=minioadmin
# MinIO秘密密钥
MINIO_SECRET_KEY=minioadmin
# MinIO公开访问地址用于生成文件URL
MINIO_PUBLIC_URL=http://minio.zrbjr.com:9000
# MinIO存储桶配置
MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan

186
MIGRATION-GUIDE.md Normal file
View File

@@ -0,0 +1,186 @@
# 文件迁移到 MinIO 指南
本指南将帮助您将现有的本地静态文件迁移到 MinIO 对象存储。
## 迁移前准备
### 1. 确保 MinIO 配置正确
确保 `.env` 文件中的 MinIO 配置正确:
```env
MINIO_ENDPOINT=your-minio-server
MINIO_PORT=9000
MINIO_ACCESS_KEY=your-access-key
MINIO_SECRET_KEY=your-secret-key
MINIO_USE_SSL=false
MINIO_PUBLIC_URL=https://minio.yourdomain.com
```
### 2. 确保存储桶已创建
确保以下存储桶在 MinIO 中已存在:
- `avatars` - 用户头像
- `products` - 产品图片
- `documents` - 文档文件
### 3. 备份数据库
**重要:在开始迁移前,请务必备份数据库!**
```bash
mysqldump -u username -p database_name > backup_before_migration.sql
```
## 执行迁移
### 1. 运行迁移脚本
```bash
cd jurong_circle_black
node migrate-to-minio.js
```
### 2. 监控迁移过程
脚本会显示详细的迁移进度:
- 扫描本地文件
- 逐个上传到 MinIO
- 更新数据库中的文件路径引用
- 生成迁移报告
## 迁移后验证
### 1. 检查迁移报告
查看生成的 `migration-report.json` 文件:
```json
{
"migrationDate": "2024-01-15T10:30:00.000Z",
"totalFiles": 150,
"successCount": 148,
"failedCount": 2,
"migratedFiles": [...],
"failedFiles": [...]
}
```
### 2. 验证文件访问
- 检查用户头像是否正常显示
- 检查产品图片是否正常显示
- 检查文档下载是否正常
### 3. 验证数据库更新
检查数据库中的文件路径是否已更新:
```sql
-- 检查用户头像路径
SELECT id, username, avatar FROM users WHERE avatar LIKE 'https://minio%' LIMIT 10;
-- 检查产品图片路径
SELECT id, name, image_url FROM products WHERE image_url LIKE 'https://minio%' LIMIT 10;
```
## 文件组织结构
迁移后,文件将按以下结构组织:
```
MinIO 存储桶/
├── avatars/
│ ├── 2024/
│ │ ├── 01/
│ │ │ ├── 15/
│ │ │ │ ├── 1640995200000_a1b2c3d4.jpg
│ │ │ │ └── 1640995300000_b2c3d4e5.png
│ │ │ └── 16/
│ │ └── 02/
│ └── 2023/
├── products/
│ ├── 2024/
│ │ ├── 01/
│ │ │ ├── 15/
│ │ │ └── 16/
│ │ └── 02/
│ └── 2023/
└── documents/
├── 2024/
└── 2023/
```
## 故障排除
### 常见问题
1. **连接 MinIO 失败**
- 检查 MinIO 服务是否运行
- 验证网络连接
- 确认访问密钥正确
2. **存储桶不存在**
- 在 MinIO 控制台创建所需的存储桶
- 确保存储桶名称与配置一致
3. **权限问题**
- 确保 MinIO 用户有读写权限
- 检查存储桶策略设置
4. **部分文件迁移失败**
- 查看迁移报告中的失败文件列表
- 检查文件是否损坏或被占用
- 手动重新上传失败的文件
### 回滚方案
如果迁移出现问题,可以通过以下步骤回滚:
1. **恢复数据库备份**
```bash
mysql -u username -p database_name < backup_before_migration.sql
```
2. **重新配置文件上传路径**
- 修改 `routes/upload.js` 使用本地存储
- 确保 `uploads` 目录存在且有正确权限
## 迁移完成后的清理
### 1. 删除本地文件(可选)
**警告:只有在确认迁移成功且系统运行正常后才执行此操作!**
```bash
# 备份 uploads 目录
mv uploads uploads_backup_$(date +%Y%m%d)
# 或者直接删除(谨慎操作)
# rm -rf uploads
```
### 2. 更新部署脚本
更新生产环境的部署脚本,移除对 `uploads` 目录的依赖。
### 3. 更新备份策略
确保备份策略包含 MinIO 数据的备份。
## 注意事项
1. **迁移时间**:根据文件数量和大小,迁移可能需要较长时间
2. **网络稳定性**:确保网络连接稳定,避免迁移中断
3. **存储空间**:确保 MinIO 有足够的存储空间
4. **并发限制**:脚本已添加延迟避免过快请求,如需调整可修改代码
5. **文件路径**:迁移后的文件路径将包含日期文件夹结构
## 技术支持
如果在迁移过程中遇到问题,请:
1. 查看控制台输出的错误信息
2. 检查 `migration-report.json` 中的详细信息
3. 确保 MinIO 服务正常运行
4. 验证网络连接和权限设置

97
config/minio.js Normal file
View File

@@ -0,0 +1,97 @@
const Minio = require('minio');
require('dotenv').config();
/**
* MinIO 配置
* 用于对象存储服务配置
*/
const minioConfig = {
// MinIO 服务器配置
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
port: parseInt(process.env.MINIO_PORT) || 9000,
useSSL: process.env.MINIO_USE_SSL === 'true' || false,
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
// 存储桶配置
buckets: {
uploads: process.env.MINIO_BUCKET_UPLOADS || 'uploads',
avatars: process.env.MINIO_BUCKET_AVATARS || 'avatars',
products: process.env.MINIO_BUCKET_PRODUCTS || 'products',
documents: process.env.MINIO_BUCKET_DOCUMENTS || 'documents'
},
// 文件访问配置
publicUrl: process.env.MINIO_PUBLIC_URL || `http://localhost:9000`
};
/**
* 创建 MinIO 客户端实例
*/
const createMinioClient = () => {
return new Minio.Client({
endPoint: minioConfig.endPoint,
port: minioConfig.port,
useSSL: minioConfig.useSSL,
accessKey: minioConfig.accessKey,
secretKey: minioConfig.secretKey
});
};
/**
* 初始化存储桶
* 确保所有需要的存储桶都存在
*/
const initializeBuckets = async () => {
const minioClient = createMinioClient();
try {
// 检查并创建存储桶
for (const [key, bucketName] of Object.entries(minioConfig.buckets)) {
const exists = await minioClient.bucketExists(bucketName);
if (!exists) {
await minioClient.makeBucket(bucketName, 'us-east-1');
console.log(`✅ 存储桶 '${bucketName}' 创建成功`);
// 设置存储桶策略为公开读取(可选)
const policy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject'],
Resource: [`arn:aws:s3:::${bucketName}/*`]
}
]
};
try {
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
console.log(`✅ 存储桶 '${bucketName}' 策略设置成功`);
} catch (policyError) {
console.warn(`⚠️ 存储桶 '${bucketName}' 策略设置失败:`, policyError.message);
}
} else {
console.log(`✅ 存储桶 '${bucketName}' 已存在`);
}
}
} catch (error) {
console.error('❌ 初始化存储桶失败:', error);
throw error;
}
};
/**
* 获取文件的公开访问URL
*/
const getPublicUrl = (bucketName, objectName) => {
return `${minioConfig.publicUrl}/${bucketName}/${objectName}`;
};
module.exports = {
minioConfig,
createMinioClient,
initializeBuckets,
getPublicUrl
};

398
migrate-to-minio.js Normal file
View File

@@ -0,0 +1,398 @@
const fs = require('fs');
const path = require('path');
const minioService = require('./services/minioService');
const { getDB, initDB } = require('./database');
/**
* 文件迁移到 MinIO 的脚本
* 将本地 uploads 目录下的文件迁移到 MinIO 存储
*/
class FilesMigration {
constructor() {
this.minioService = minioService;
this.uploadsDir = path.join(__dirname, 'uploads');
this.migratedFiles = [];
this.failedFiles = [];
}
/**
* 递归获取目录下所有文件
* @param {string} dir - 目录路径
* @param {string} baseDir - 基础目录路径
* @returns {Array} 文件列表
*/
getAllFiles(dir, baseDir = dir) {
const files = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
files.push(...this.getAllFiles(fullPath, baseDir));
} else {
const relativePath = path.relative(baseDir, fullPath);
files.push({
fullPath,
relativePath: relativePath.replace(/\\/g, '/'), // 统一使用正斜杠
fileName: item,
size: stat.size,
mtime: stat.mtime
});
}
}
return files;
}
/**
* 生成新的 MinIO 路径(去掉 uploads 前缀)
* @param {Object} fileInfo - 文件信息
* @returns {string} 去掉 uploads 前缀的文件路径
*/
generateMinioPath(fileInfo) {
// 去掉 uploads/ 前缀,因为数据库中存储的是 /uploads/xxx而 MinIO 中应该是 xxx
let minioPath = fileInfo.relativePath;
if (minioPath.startsWith('uploads/')) {
minioPath = minioPath.substring('uploads/'.length);
}
return minioPath;
}
/**
* 上传单个文件到 MinIO
* @param {Object} fileInfo - 文件信息
* @returns {Promise<Object>} 上传结果
*/
async uploadFileToMinio(fileInfo) {
try {
const minioPath = this.generateMinioPath(fileInfo);
// 使用固定的存储桶名称
const bucketName = 'jurongquan';
console.log(`正在上传: ${fileInfo.relativePath} -> ${minioPath}`);
// 读取文件内容
const fileBuffer = fs.readFileSync(fileInfo.fullPath);
// 上传到 MinIO
const uploadResult = await this.minioService.uploadFileForMigration(
bucketName,
minioPath,
fileBuffer,
this.getContentType(fileInfo.fileName)
);
const result = {
url: uploadResult.data.url,
path: uploadResult.data.path
};
return {
success: true,
originalPath: fileInfo.relativePath,
minioPath,
minioUrl: result.url,
fileInfo
};
} catch (error) {
console.error(`上传失败 ${fileInfo.relativePath}:`, error.message);
return {
success: false,
originalPath: fileInfo.relativePath,
error: error.message,
fileInfo
};
}
}
/**
* 根据文件扩展名获取 Content-Type
* @param {string} fileName - 文件名
* @returns {string} Content-Type
*/
getContentType(fileName) {
const ext = path.extname(fileName).toLowerCase();
const contentTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.txt': 'text/plain'
};
return contentTypes[ext] || 'application/octet-stream';
}
/**
* 更新数据库中的文件路径引用
* @param {Array} migratedFiles - 已迁移的文件列表
*/
async updateDatabaseReferences(migratedFiles) {
const db = getDB();
let updatedCount = 0;
console.log('\n开始更新数据库中的文件路径引用...');
for (const file of migratedFiles) {
console.log(file,'file');
try {
// 原路径:/uploads/avatars/xxx.jpg
const oldPath = `/${file.originalPath}`;
// 新路径:/avatars/xxx.jpg (去掉 uploads 前缀)
const newPath = `/${file.minioPath}`;
// 更新用户头像
const [avatarResult] = await db.execute(
'UPDATE users SET avatar = ? WHERE avatar = ?',
[newPath, oldPath]
);
if (avatarResult.affectedRows > 0) {
console.log(`更新用户头像: ${oldPath} -> ${newPath}`);
updatedCount += avatarResult.affectedRows;
}
// 更新用户微信收款码
const [wechatQrResult] = await db.execute(
'UPDATE users SET wechat_qr = ? WHERE wechat_qr = ?',
[newPath, oldPath]
);
if (wechatQrResult.affectedRows > 0) {
console.log(`更新用户微信收款码: ${oldPath} -> ${newPath}`);
updatedCount += wechatQrResult.affectedRows;
}
// 更新用户支付宝收款码
const [alipayQrResult] = await db.execute(
'UPDATE users SET alipay_qr = ? WHERE alipay_qr = ?',
[newPath, oldPath]
);
if (alipayQrResult.affectedRows > 0) {
console.log(`更新用户支付宝收款码: ${oldPath} -> ${newPath}`);
updatedCount += alipayQrResult.affectedRows;
}
// 更新用户云闪付收款码
const [unionpayQrResult] = await db.execute(
'UPDATE users SET unionpay_qr = ? WHERE unionpay_qr = ?',
[newPath, oldPath]
);
if (unionpayQrResult.affectedRows > 0) {
console.log(`更新用户云闪付收款码: ${oldPath} -> ${newPath}`);
updatedCount += unionpayQrResult.affectedRows;
}
// 更新用户营业执照
const [businessLicenseResult] = await db.execute(
'UPDATE users SET business_license = ? WHERE business_license = ?',
[newPath, oldPath]
);
if (businessLicenseResult.affectedRows > 0) {
console.log(`更新用户营业执照: ${oldPath} -> ${newPath}`);
updatedCount += businessLicenseResult.affectedRows;
}
// 更新用户身份证正面
const [idCardFrontResult] = await db.execute(
'UPDATE users SET id_card_front = ? WHERE id_card_front = ?',
[newPath, oldPath]
);
if (idCardFrontResult.affectedRows > 0) {
console.log(`更新用户身份证正面: ${oldPath} -> ${newPath}`);
updatedCount += idCardFrontResult.affectedRows;
}
// 更新用户身份证反面
const [idCardBackResult] = await db.execute(
'UPDATE users SET id_card_back = ? WHERE id_card_back = ?',
[newPath, oldPath]
);
if (idCardBackResult.affectedRows > 0) {
console.log(`更新用户身份证反面: ${oldPath} -> ${newPath}`);
updatedCount += idCardBackResult.affectedRows;
}
// 更新产品图片
const [productResult] = await db.execute(
'UPDATE products SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (productResult.affectedRows > 0) {
console.log(`更新产品图片: ${oldPath} -> ${newPath}`);
updatedCount += productResult.affectedRows;
}
// 更新产品店铺头像
const [shopAvatarResult] = await db.execute(
'UPDATE products SET shop_avatar = ? WHERE shop_avatar = ?',
[newPath, oldPath]
);
if (shopAvatarResult.affectedRows > 0) {
console.log(`更新产品店铺头像: ${oldPath} -> ${newPath}`);
updatedCount += shopAvatarResult.affectedRows;
}
// 更新产品图片JSON 字段中的图片)
const [products] = await db.execute(
'SELECT id, images FROM products WHERE images LIKE ?',
[`%${oldPath}%`]
);
for (const product of products) {
if (product.images) {
const updatedImages = product.images.replace(new RegExp(oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newPath);
await db.execute(
'UPDATE products SET images = ? WHERE id = ?',
[updatedImages, product.id]
);
console.log(`更新产品图片集合: 产品ID ${product.id}`);
updatedCount++;
}
}
// 更新转账凭证
const [transferResult] = await db.execute(
'UPDATE transfers SET voucher_url = ? WHERE voucher_url = ?',
[newPath, oldPath]
);
if (transferResult.affectedRows > 0) {
console.log(`更新转账凭证: ${oldPath} -> ${newPath}`);
updatedCount += transferResult.affectedRows;
}
// 更新区域代理收款码
const [agentQrResult] = await db.execute(
'UPDATE regional_agents SET qr_code_url = ? WHERE qr_code_url = ?',
[newPath, oldPath]
);
if (agentQrResult.affectedRows > 0) {
console.log(`更新区域代理收款码: ${oldPath} -> ${newPath}`);
updatedCount += agentQrResult.affectedRows;
}
// 更新规格图片
const [specResult] = await db.execute(
'UPDATE spec_values SET image_url = ? WHERE image_url = ?',
[newPath, oldPath]
);
if (specResult.affectedRows > 0) {
console.log(`更新规格图片: ${oldPath} -> ${newPath}`);
updatedCount += specResult.affectedRows;
}
} catch (error) {
console.error(`更新数据库引用失败 ${file.originalPath}:`, error.message);
}
}
console.log(`\n数据库更新完成,共更新 ${updatedCount} 条记录`);
}
/**
* 执行迁移
*/
async migrate() {
try {
console.log('开始文件迁移到 MinIO...');
console.log(`源目录: ${this.uploadsDir}`);
// 初始化数据库连接
console.log('初始化数据库连接...');
await initDB();
console.log('数据库连接初始化完成');
// 检查源目录是否存在
if (!fs.existsSync(this.uploadsDir)) {
console.log('uploads 目录不存在,无需迁移');
return;
}
// 获取所有文件
const allFiles = this.getAllFiles(this.uploadsDir);
console.log(`找到 ${allFiles.length} 个文件需要迁移`);
if (allFiles.length === 0) {
console.log('没有文件需要迁移');
return;
}
// 逐个上传文件
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
console.log(`\n进度: ${i + 1}/${allFiles.length}`);
const result = await this.uploadFileToMinio(file);
if (result.success) {
this.migratedFiles.push(result);
} else {
this.failedFiles.push(result);
}
// 添加小延迟避免过快请求
await new Promise(resolve => setTimeout(resolve, 100));
}
// 输出迁移结果
console.log('\n=== 迁移结果 ===');
console.log(`成功迁移: ${this.migratedFiles.length} 个文件`);
console.log(`迁移失败: ${this.failedFiles.length} 个文件`);
if (this.failedFiles.length > 0) {
console.log('\n失败的文件:');
this.failedFiles.forEach(file => {
console.log(`- ${file.originalPath}: ${file.error}`);
});
}
// 更新数据库引用,将 /uploads/xxx 路径更新为 /xxx
if (this.migratedFiles.length > 0) {
await this.updateDatabaseReferences(this.migratedFiles);
}
// 生成迁移报告
await this.generateMigrationReport();
console.log('\n迁移完成');
console.log('\n注意事项:');
console.log('1. 请验证文件访问是否正常');
console.log('2. 确认数据库中的文件路径已正确更新');
console.log('3. 测试完成后可以删除本地 uploads 目录');
console.log('4. 查看 migration-report.json 了解详细迁移信息');
} catch (error) {
console.error('迁移过程中发生错误:', error);
}
}
/**
* 生成迁移报告
*/
async generateMigrationReport() {
const report = {
migrationDate: new Date().toISOString(),
totalFiles: this.migratedFiles.length + this.failedFiles.length,
successCount: this.migratedFiles.length,
failedCount: this.failedFiles.length,
migratedFiles: this.migratedFiles,
failedFiles: this.failedFiles
};
const reportPath = path.join(__dirname, 'migration-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n迁移报告已生成: ${reportPath}`);
}
}
// 如果直接运行此脚本
if (require.main === module) {
const migration = new FilesMigration();
migration.migrate().catch(console.error);
}
module.exports = FilesMigration;

26659
migration-report.json Normal file

File diff suppressed because it is too large Load Diff

453
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"helmet": "^8.1.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"minio": "^8.0.5",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3",
"node-cron": "^4.2.1",
@@ -386,6 +387,13 @@
"@types/node": "*"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -467,6 +475,21 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -512,6 +535,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/block-stream2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
"license": "MIT",
"dependencies": {
"readable-stream": "^3.4.0"
}
},
"node_modules/block-stream2/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -559,6 +605,21 @@
"node": ">=8"
}
},
"node_modules/browser-or-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
"license": "MIT"
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -591,6 +652,24 @@
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -934,6 +1013,32 @@
"node": ">=0.10.0"
}
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1130,6 +1235,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -1204,6 +1315,24 @@
"node": ">= 8.0.0"
}
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -1223,6 +1352,15 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
@@ -1280,6 +1418,21 @@
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -1455,6 +1608,18 @@
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1612,6 +1777,22 @@
"node": ">= 0.10"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -1631,6 +1812,18 @@
"node": ">=8"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1650,6 +1843,24 @@
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1679,6 +1890,24 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -1691,6 +1920,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -2018,6 +2262,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minio": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz",
"integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.4",
"block-stream2": "^2.1.0",
"browser-or-node": "^2.1.1",
"buffer-crc32": "^1.0.0",
"eventemitter3": "^5.0.1",
"fast-xml-parser": "^4.4.1",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"query-string": "^7.1.3",
"stream-json": "^1.8.0",
"through2": "^4.0.2",
"web-encoding": "^1.1.5",
"xml2js": "^0.5.0 || ^0.6.2"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/minio/node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -2374,6 +2652,15 @@
"node": ">=10.13.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -2504,6 +2791,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -2607,6 +2912,23 @@
],
"license": "MIT"
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
@@ -2705,6 +3027,23 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -2830,6 +3169,15 @@
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
"dev": true
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -2857,6 +3205,21 @@
"node": ">= 0.8"
}
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -2865,6 +3228,15 @@
"node": ">=10.0.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -2906,6 +3278,18 @@
"node": ">=8"
}
},
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -2984,6 +3368,29 @@
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
"license": "MIT",
"dependencies": {
"readable-stream": "3"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3083,6 +3490,19 @@
"node": ">= 0.8"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3116,12 +3536,45 @@
"node": ">= 0.8"
}
},
"node_modules/web-encoding": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
"license": "MIT",
"dependencies": {
"util": "^0.12.3"
},
"optionalDependencies": {
"@zxing/text-encoding": "0.9.0"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/winston": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",

View File

@@ -4,14 +4,7 @@
"description": "Vue3 + Node.js 集成系统",
"main": "server.js",
"scripts": {
"dev": "concurrently \"npm run dev:admin\" \"npm run dev:server\"",
"dev:frontend": "cd frontend && npm run dev",
"dev:admin": "cd admin && npm run dev",
"dev:server": "nodemon server.js",
"build": " npm run build:admin",
"build:frontend": "cd frontend && npm run build",
"build:admin": "cd admin && npm run build",
"start": "node server.js"
"dev": "nodemon server.js"
},
"dependencies": {
"@alicloud/dysmsapi20170525": "^4.1.2",
@@ -28,6 +21,7 @@
"helmet": "^8.1.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"minio": "^8.0.5",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3",
"node-cron": "^4.2.1",

View File

@@ -43,7 +43,7 @@ router.get('/', async (req, res) => {
// 获取商品列表
const query = `
SELECT id, name, rongdou_price, category, points_price as points, stock, image_url as image, description, status, payment_methods, created_at, updated_at
SELECT id, name, rongdou_price, category, points_price, stock, image_url as image, description, status, payment_methods, created_at, updated_at
FROM products
${whereClause}
ORDER BY created_at DESC
@@ -57,7 +57,6 @@ router.get('/', async (req, res) => {
products.forEach(item=>{
item.payment_methods = JSON.parse(item.payment_methods)
})
console.log('查询结果:', products);
res.json({
success: true,
data: {

View File

@@ -1,12 +1,16 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { auth } = require('../middleware/auth');
const { authenticateToken } = require('./auth');
const minioService = require('../services/minioService');
const { initializeBuckets } = require('../config/minio');
const router = express.Router();
// 初始化MinIO存储桶
initializeBuckets().catch(console.error);
/**
* @swagger
* tags:
@@ -14,48 +18,8 @@ const router = express.Router();
* description: 文件上传API
*/
// 确保上传目录存在
const uploadDir = path.join(__dirname, '../uploads');
const documentsDir = path.join(uploadDir, 'documents');
const avatarsDir = path.join(uploadDir, 'avatars');
const productsDir = path.join(uploadDir, 'products');
[uploadDir, documentsDir, avatarsDir, productsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// 配置multer存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const type = req.body.type || 'documents';
let uploadPath;
switch (type) {
case 'avatar':
uploadPath = avatarsDir;
break;
case 'product':
uploadPath = productsDir;
break;
case 'document':
default:
uploadPath = documentsDir;
break;
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
// 生成唯一文件名:时间戳 + 随机数 + 原始扩展名
const timestamp = Date.now();
const random = Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const filename = `${timestamp}_${random}${ext}`;
cb(null, filename);
}
});
// 配置multer内存存储用于MinIO上传
const storage = multer.memoryStorage();
// 文件过滤器 - 支持图片和视频
const fileFilter = (req, file, cb) => {
@@ -136,7 +100,7 @@ const multiUpload = multer({
* description: 服务器错误
*/
router.post('/image', authenticateToken, (req, res) => {
upload.single('file')(req, res, (err) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
@@ -168,30 +132,28 @@ router.post('/image', authenticateToken, (req, res) => {
});
}
// 构建文件访问路径
const type = req.body.type || 'documents';
// 确保路径与实际目录结构一致
let folderName = type;
if (type === 'product') {
folderName = 'products'; // 目录是复数形式
} else if (type === 'avatar') {
folderName = 'avatars'; // 目录是复数形式
}
const relativePath = path.join(folderName, req.file.filename).replace(/\\/g, '/');
const fileUrl = `/uploads/${relativePath}`;
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
data: {
filename: req.file.filename,
originalname: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
path: relativePath,
url: fileUrl
}
data: result.data
});
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
@@ -261,7 +223,7 @@ router.post('/image', authenticateToken, (req, res) => {
* @access Private
*/
router.post('/', authenticateToken, (req, res) => {
multiUpload.array('file', 10)(req, res, (err) => {
multiUpload.array('file', 10)(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
@@ -294,52 +256,43 @@ router.post('/', authenticateToken, (req, res) => {
}
try {
// 处理多个文件
const uploadedFiles = req.files.map(file => {
const type = req.body.type || 'documents';
let folderName = type;
if (type === 'product') {
folderName = 'products';
} else if (type === 'avatar') {
folderName = 'avatars';
}
const relativePath = path.join(folderName, file.filename).replace(/\\/g, '/');
const fileUrl = `/uploads/${relativePath}`;
// 使用MinIO服务上传多个文件
const type = req.body.type || 'document';
const files = req.files.map(file => ({
buffer: file.buffer,
originalName: file.originalname,
mimeType: file.mimetype
}));
return {
filename: file.filename,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
path: relativePath,
url: fileUrl
};
});
const result = await minioService.uploadMultipleFiles(files, type);
// 如果只上传了一个文件,返回单文件格式以保持兼容性
if (uploadedFiles.length === 1) {
if (result.data.files.length === 1) {
result.data.files.forEach(element => {
element.path = '/' + element.path
});
res.json({
success: true,
message: '文件上传成功',
data: {
...uploadedFiles[0],
urls: [uploadedFiles[0].url] // 同时提供urls数组格式
...result.data.files[0],
urls: result.data.urls // 同时提供urls数组格式
}
});
} else {
// 多文件返回数组格式
res.json({
success: true,
message: `成功上传${uploadedFiles.length}个文件`,
data: {
files: uploadedFiles,
urls: uploadedFiles.map(file => file.url)
}
message: `成功上传${result.data.files.length}个文件`,
data: result.data
});
}
} catch (error) {
console.error('文件上传错误:', error);
res.status(500).json({ success: false, message: '文件上传失败' });
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
@@ -401,37 +354,51 @@ router.post('/', authenticateToken, (req, res) => {
* 500:
* description: 服务器错误
*/
router.post('/single', auth, upload.single('file'), (req, res) => {
try {
router.post('/single', auth, (req, res) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({ success: false, message: '没有上传文件' });
}
// 返回文件访问URL
const type = req.body.type || 'documents';
// 确保路径与实际目录结构一致
let folderName = type;
if (type === 'product') {
folderName = 'products'; // 目录是复数形式
} else if (type === 'avatar') {
folderName = 'avatars'; // 目录是复数形式
}
const relativePath = path.join(folderName, req.file.filename).replace(/\\/g, '/');
const fileUrl = `/uploads/${relativePath}`;
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
url: fileUrl,
filename: req.file.filename,
originalname: req.file.originalname,
size: req.file.size
url: result.data.url,
filename: result.data.filename,
originalname: result.data.originalname,
size: result.data.size
});
} catch (error) {
console.error('文件上传错误:', error);
res.status(500).json({ success: false, message: '文件上传失败' });
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
// 错误处理中间件
router.use((error, req, res, next) => {

View File

@@ -306,8 +306,6 @@ app.listen(PORT, async () => {
try {
logger.info('Server starting', { port: PORT });
console.log(`服务器运行在 http://localhost:${PORT}`);
console.log(`前端页面: http://localhost:${PORT}/frontend`);
console.log(`后台管理: http://localhost:${PORT}/admin`);
await initDatabase();
// global.sqlReq = mysql.createConnection()

293
services/minioService.js Normal file
View File

@@ -0,0 +1,293 @@
const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio');
const path = require('path');
const crypto = require('crypto');
/**
* MinIO 文件服务
* 提供文件上传、删除、获取等功能
*/
class MinioService {
constructor() {
this.client = createMinioClient();
}
/**
* 生成唯一文件名
* @param {string} originalName - 原始文件名
* @returns {string} 唯一文件名
*/
generateUniqueFileName(originalName) {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const timestamp = Date.now();
const randomString = crypto.randomBytes(8).toString('hex');
const ext = path.extname(originalName);
return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`;
}
/**
* 根据文件类型获取存储桶名称
* @param {string} type - 文件类型 (avatar, product, document)
* @returns {string} 存储桶名称
*/
getBucketName(type = 'document') {
const bucketMap = {
'avatar': minioConfig.buckets.avatars,
'product': minioConfig.buckets.products,
'document': minioConfig.buckets.documents
};
return bucketMap[type] || minioConfig.buckets.documents;
}
/**
* 上传单个文件
* @param {Buffer} fileBuffer - 文件缓冲区
* @param {string} originalName - 原始文件名
* @param {string} mimeType - 文件MIME类型
* @param {string} type - 文件类型
* @returns {Promise<Object>} 上传结果
*/
async uploadFile(fileBuffer, originalName, mimeType, type = 'document') {
try {
const bucketName = this.getBucketName(type);
const fileName = this.generateUniqueFileName(originalName);
// 设置文件元数据
const metaData = {
'Content-Type': mimeType,
'Original-Name': encodeURIComponent(originalName),
'Upload-Time': new Date().toISOString()
};
// 上传文件到MinIO
await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData);
// 生成访问URL
const url = getPublicUrl(bucketName, fileName);
return {
success: true,
data: {
filename: fileName,
originalname: originalName,
mimetype: mimeType,
size: fileBuffer.length,
bucket: bucketName,
path: `${bucketName}/${fileName}`,
url: url
}
};
} catch (error) {
console.error('MinIO文件上传失败:', error);
throw new Error(`文件上传失败: ${error.message}`);
}
}
/**
* 迁移专用:上传文件到指定存储桶和路径
* @param {string} bucketName - 存储桶名称
* @param {string} filePath - 文件路径
* @param {Buffer} fileBuffer - 文件缓冲区
* @param {string} mimeType - 文件MIME类型
* @returns {Promise<Object>} 上传结果
*/
async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) {
try {
// 设置文件元数据
const metaData = {
'Content-Type': mimeType,
'Upload-Time': new Date().toISOString()
};
// 上传文件到MinIO
await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData);
// 生成访问URL
const url = getPublicUrl(bucketName, filePath);
return {
success: true,
data: {
filename: filePath,
mimetype: mimeType,
size: fileBuffer.length,
bucket: bucketName,
path: `${bucketName}/${filePath}`,
url: url
}
};
} catch (error) {
console.error('MinIO文件迁移上传失败:', error);
throw new Error(`文件迁移上传失败: ${error.message}`);
}
}
/**
* 上传多个文件
* @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType}
* @param {string} type - 文件类型
* @returns {Promise<Array>} 上传结果数组
*/
async uploadMultipleFiles(files, type = 'document') {
try {
const uploadPromises = files.map(file =>
this.uploadFile(file.buffer, file.originalName, file.mimeType, type)
);
const results = await Promise.all(uploadPromises);
const uploadedFiles = results.map(result => result.data);
return {
success: true,
data: {
files: uploadedFiles,
urls: uploadedFiles.map(file => file.url),
count: uploadedFiles.length
}
};
} catch (error) {
console.error('MinIO多文件上传失败:', error);
throw new Error(`多文件上传失败: ${error.message}`);
}
}
/**
* 删除文件
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<boolean>} 删除结果
*/
async deleteFile(bucketName, fileName) {
try {
await this.client.removeObject(bucketName, fileName);
console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`);
return true;
} catch (error) {
console.error('MinIO文件删除失败:', error);
throw new Error(`文件删除失败: ${error.message}`);
}
}
/**
* 批量删除文件
* @param {string} bucketName - 存储桶名称
* @param {Array<string>} fileNames - 文件名数组
* @returns {Promise<Object>} 删除结果
*/
async deleteMultipleFiles(bucketName, fileNames) {
try {
const deletePromises = fileNames.map(fileName =>
this.deleteFile(bucketName, fileName)
);
await Promise.all(deletePromises);
return {
success: true,
deletedCount: fileNames.length,
message: `成功删除${fileNames.length}个文件`
};
} catch (error) {
console.error('MinIO批量删除失败:', error);
throw new Error(`批量删除失败: ${error.message}`);
}
}
/**
* 检查文件是否存在
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<boolean>} 文件是否存在
*/
async fileExists(bucketName, fileName) {
try {
await this.client.statObject(bucketName, fileName);
return true;
} catch (error) {
if (error.code === 'NotFound') {
return false;
}
throw error;
}
}
/**
* 获取文件信息
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @returns {Promise<Object>} 文件信息
*/
async getFileInfo(bucketName, fileName) {
try {
const stat = await this.client.statObject(bucketName, fileName);
return {
size: stat.size,
lastModified: stat.lastModified,
etag: stat.etag,
contentType: stat.metaData['content-type'],
originalName: decodeURIComponent(stat.metaData['original-name'] || fileName)
};
} catch (error) {
console.error('获取文件信息失败:', error);
throw new Error(`获取文件信息失败: ${error.message}`);
}
}
/**
* 生成预签名URL用于临时访问
* @param {string} bucketName - 存储桶名称
* @param {string} fileName - 文件名
* @param {number} expiry - 过期时间默认7天
* @returns {Promise<string>} 预签名URL
*/
async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) {
try {
const url = await this.client.presignedGetObject(bucketName, fileName, expiry);
return url;
} catch (error) {
console.error('生成预签名URL失败:', error);
throw new Error(`生成预签名URL失败: ${error.message}`);
}
}
/**
* 列出存储桶中的文件
* @param {string} bucketName - 存储桶名称
* @param {string} prefix - 文件前缀
* @param {number} limit - 限制数量
* @returns {Promise<Array>} 文件列表
*/
async listFiles(bucketName, prefix = '', limit = 100) {
try {
const files = [];
const stream = this.client.listObjects(bucketName, prefix, true);
return new Promise((resolve, reject) => {
stream.on('data', (obj) => {
if (files.length < limit) {
files.push({
name: obj.name,
size: obj.size,
lastModified: obj.lastModified,
etag: obj.etag,
url: getPublicUrl(bucketName, obj.name)
});
}
});
stream.on('end', () => resolve(files));
stream.on('error', reject);
});
} catch (error) {
console.error('列出文件失败:', error);
throw new Error(`列出文件失败: ${error.message}`);
}
}
}
// 创建单例实例
const minioService = new MinioService();
module.exports = minioService;