05301132
This commit is contained in:
parent
6aa7af73f6
commit
7c76dda7f3
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client';
|
import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useRef, useState, useEffect } from 'react';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
|
||||||
export default function WebSocketPage() {
|
export default function WebSocketPage() {
|
||||||
|
|
121
debug-minio.js
121
debug-minio.js
|
@ -1,121 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MinIO连接调试脚本
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { S3 } = require('@aws-sdk/client-s3');
|
|
||||||
|
|
||||||
async function debugMinIO() {
|
|
||||||
console.log('🔍 MinIO连接调试开始...\n');
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
endpoint: 'http://localhost:9000',
|
|
||||||
region: 'us-east-1',
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: '7Nt7OyHkwIoo3zvSKdnc',
|
|
||||||
secretAccessKey: 'EZ0cyrjJAsabTLNSqWcU47LURMppBW2kka3LuXzb',
|
|
||||||
},
|
|
||||||
forcePathStyle: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('配置信息:');
|
|
||||||
console.log('- Endpoint:', config.endpoint);
|
|
||||||
console.log('- Region:', config.region);
|
|
||||||
console.log('- Access Key:', config.credentials.accessKeyId);
|
|
||||||
console.log('- Force Path Style:', config.forcePathStyle);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
const s3Client = new S3(config);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 测试基本连接
|
|
||||||
console.log('📡 测试基本连接...');
|
|
||||||
const buckets = await s3Client.listBuckets();
|
|
||||||
console.log('✅ 连接成功!');
|
|
||||||
console.log('📂 现有存储桶:', buckets.Buckets?.map((b) => b.Name) || []);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// 2. 检查test123存储桶
|
|
||||||
const bucketName = 'test123';
|
|
||||||
console.log(`🪣 检查存储桶 "${bucketName}"...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await s3Client.headBucket({ Bucket: bucketName });
|
|
||||||
console.log(`✅ 存储桶 "${bucketName}" 存在`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'NotFound') {
|
|
||||||
console.log(`❌ 存储桶 "${bucketName}" 不存在,正在创建...`);
|
|
||||||
try {
|
|
||||||
await s3Client.createBucket({ Bucket: bucketName });
|
|
||||||
console.log(`✅ 存储桶 "${bucketName}" 创建成功`);
|
|
||||||
} catch (createError) {
|
|
||||||
console.log(`❌ 创建存储桶失败:`, createError.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`❌ 检查存储桶失败:`, error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 测试简单上传
|
|
||||||
console.log('\n📤 测试简单上传...');
|
|
||||||
const testKey = 'test-file.txt';
|
|
||||||
const testContent = 'Hello MinIO!';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await s3Client.putObject({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: testKey,
|
|
||||||
Body: testContent,
|
|
||||||
});
|
|
||||||
console.log(`✅ 简单上传成功: ${testKey}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 简单上传失败:`, error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 测试分片上传初始化
|
|
||||||
console.log('\n🔄 测试分片上传初始化...');
|
|
||||||
const multipartKey = 'test-multipart.txt';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const multipartUpload = await s3Client.createMultipartUpload({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: multipartKey,
|
|
||||||
});
|
|
||||||
console.log(`✅ 分片上传初始化成功: ${multipartUpload.UploadId}`);
|
|
||||||
|
|
||||||
// 立即取消这个分片上传
|
|
||||||
await s3Client.abortMultipartUpload({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: multipartKey,
|
|
||||||
UploadId: multipartUpload.UploadId,
|
|
||||||
});
|
|
||||||
console.log('✅ 分片上传取消成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 分片上传初始化失败:`, error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
if (error.$metadata) {
|
|
||||||
console.log('HTTP状态码:', error.$metadata.httpStatusCode);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 所有测试通过!MinIO配置正确。');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ 连接失败:', error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
|
|
||||||
if (error.message.includes('ECONNREFUSED')) {
|
|
||||||
console.log('\n💡 提示:');
|
|
||||||
console.log('- 确保MinIO正在端口9000运行');
|
|
||||||
console.log('- 检查docker容器状态: docker ps');
|
|
||||||
console.log('- 重启MinIO: docker restart minio-container-name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugMinIO().catch(console.error);
|
|
169
debug-s3.js
169
debug-s3.js
|
@ -1,169 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* S3存储调试脚本
|
|
||||||
* 用于快速诊断S3存储连接问题
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 检查是否有.env文件,如果有就加载
|
|
||||||
try {
|
|
||||||
require('dotenv').config();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('No dotenv found, using environment variables directly');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function debugS3() {
|
|
||||||
console.log('🔍 S3存储调试开始...\n');
|
|
||||||
|
|
||||||
// 1. 检查环境变量
|
|
||||||
console.log('📋 环境变量检查:');
|
|
||||||
const requiredVars = {
|
|
||||||
STORAGE_TYPE: process.env.STORAGE_TYPE,
|
|
||||||
S3_BUCKET: process.env.S3_BUCKET,
|
|
||||||
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
|
||||||
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
|
|
||||||
S3_REGION: process.env.S3_REGION,
|
|
||||||
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(requiredVars)) {
|
|
||||||
if (key.includes('SECRET')) {
|
|
||||||
console.log(` ${key}: ${value ? '✅ 已设置' : '❌ 未设置'}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ${key}: ${value || '❌ 未设置'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.STORAGE_TYPE !== 's3') {
|
|
||||||
console.log('\n❌ STORAGE_TYPE 不是 s3,无法测试S3连接');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingVars = ['S3_BUCKET', 'S3_ACCESS_KEY_ID', 'S3_SECRET_ACCESS_KEY'].filter((key) => !process.env[key]);
|
|
||||||
|
|
||||||
if (missingVars.length > 0) {
|
|
||||||
console.log(`\n❌ 缺少必要的环境变量: ${missingVars.join(', ')}`);
|
|
||||||
console.log('请设置这些环境变量后重试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n✅ 环境变量检查通过\n');
|
|
||||||
|
|
||||||
// 2. 测试AWS SDK加载
|
|
||||||
console.log('📦 加载AWS SDK...');
|
|
||||||
try {
|
|
||||||
const { S3 } = require('@aws-sdk/client-s3');
|
|
||||||
console.log('✅ AWS SDK加载成功\n');
|
|
||||||
|
|
||||||
// 3. 创建S3客户端
|
|
||||||
console.log('🔧 创建S3客户端...');
|
|
||||||
const config = {
|
|
||||||
region: process.env.S3_REGION || 'auto',
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
|
||||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.S3_ENDPOINT) {
|
|
||||||
config.endpoint = process.env.S3_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.S3_FORCE_PATH_STYLE === 'true') {
|
|
||||||
config.forcePathStyle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('S3客户端配置:', {
|
|
||||||
region: config.region,
|
|
||||||
endpoint: config.endpoint || '默认AWS端点',
|
|
||||||
forcePathStyle: config.forcePathStyle || false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const s3Client = new S3(config);
|
|
||||||
console.log('✅ S3客户端创建成功\n');
|
|
||||||
|
|
||||||
// 4. 测试bucket访问
|
|
||||||
console.log('🪣 测试bucket访问...');
|
|
||||||
try {
|
|
||||||
await s3Client.headBucket({ Bucket: process.env.S3_BUCKET });
|
|
||||||
console.log('✅ Bucket访问成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Bucket访问失败: ${error.message}`);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
|
|
||||||
if (error.name === 'NotFound') {
|
|
||||||
console.log(' 💡 提示: Bucket不存在,请检查bucket名称');
|
|
||||||
} else if (error.name === 'Forbidden') {
|
|
||||||
console.log(' 💡 提示: 访问被拒绝,请检查访问密钥权限');
|
|
||||||
} else if (error.message.includes('getaddrinfo ENOTFOUND')) {
|
|
||||||
console.log(' 💡 提示: DNS解析失败,请检查endpoint设置');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 测试列出对象
|
|
||||||
console.log('\n📂 测试列出对象...');
|
|
||||||
try {
|
|
||||||
const result = await s3Client.listObjectsV2({
|
|
||||||
Bucket: process.env.S3_BUCKET,
|
|
||||||
MaxKeys: 5,
|
|
||||||
});
|
|
||||||
console.log(`✅ 列出对象成功,共有 ${result.KeyCount || 0} 个对象`);
|
|
||||||
|
|
||||||
if (result.Contents && result.Contents.length > 0) {
|
|
||||||
console.log(' 前几个对象:');
|
|
||||||
result.Contents.slice(0, 3).forEach((obj, index) => {
|
|
||||||
console.log(` ${index + 1}. ${obj.Key} (${obj.Size} bytes)`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 列出对象失败: ${error.message}`);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 测试创建multipart upload
|
|
||||||
console.log('\n🚀 测试创建multipart upload...');
|
|
||||||
const testKey = `test-multipart-${Date.now()}`;
|
|
||||||
let uploadId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const createResult = await s3Client.createMultipartUpload({
|
|
||||||
Bucket: process.env.S3_BUCKET,
|
|
||||||
Key: testKey,
|
|
||||||
Metadata: { test: 'debug-script' },
|
|
||||||
});
|
|
||||||
uploadId = createResult.UploadId;
|
|
||||||
console.log(`✅ Multipart upload创建成功,UploadId: ${uploadId}`);
|
|
||||||
|
|
||||||
// 清理测试upload
|
|
||||||
await s3Client.abortMultipartUpload({
|
|
||||||
Bucket: process.env.S3_BUCKET,
|
|
||||||
Key: testKey,
|
|
||||||
UploadId: uploadId,
|
|
||||||
});
|
|
||||||
console.log('✅ 测试upload已清理');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Multipart upload创建失败: ${error.message}`);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 S3连接测试全部通过!S3存储应该可以正常工作。');
|
|
||||||
console.log('\n💡 如果上传仍然失败,请检查:');
|
|
||||||
console.log('1. 网络连接是否稳定');
|
|
||||||
console.log('2. 防火墙是否阻止了连接');
|
|
||||||
console.log('3. S3服务是否有临时问题');
|
|
||||||
console.log('4. 查看应用日志中的详细错误信息');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ AWS SDK加载失败: ${error.message}`);
|
|
||||||
console.log('请确保已安装 @aws-sdk/client-s3 包:');
|
|
||||||
console.log('npm install @aws-sdk/client-s3');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行调试
|
|
||||||
debugS3().catch((error) => {
|
|
||||||
console.error('调试脚本出错:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
|
@ -1,235 +0,0 @@
|
||||||
# 环境变量配置指南
|
|
||||||
|
|
||||||
本文档详细说明了项目中所有环境变量的配置方法和用途。
|
|
||||||
|
|
||||||
## 存储配置 (@repo/storage)
|
|
||||||
|
|
||||||
### 基础配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 存储类型选择
|
|
||||||
STORAGE_TYPE=local # 可选值: local | s3
|
|
||||||
|
|
||||||
# 上传文件过期时间(毫秒),0表示不过期
|
|
||||||
UPLOAD_EXPIRATION_MS=0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本地存储配置
|
|
||||||
|
|
||||||
当 `STORAGE_TYPE=local` 时需要配置:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 本地存储目录路径
|
|
||||||
UPLOAD_DIR=./uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
### S3 存储配置
|
|
||||||
|
|
||||||
当 `STORAGE_TYPE=s3` 时需要配置:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# S3 存储桶名称 (必需)
|
|
||||||
S3_BUCKET=my-app-uploads
|
|
||||||
|
|
||||||
# S3 区域 (必需)
|
|
||||||
S3_REGION=us-east-1
|
|
||||||
|
|
||||||
# S3 访问密钥 ID (必需)
|
|
||||||
S3_ACCESS_KEY_ID=your-access-key-id
|
|
||||||
|
|
||||||
# S3 访问密钥 (必需)
|
|
||||||
S3_SECRET_ACCESS_KEY=your-secret-access-key
|
|
||||||
|
|
||||||
# 自定义 S3 端点 (可选,用于 MinIO、阿里云 OSS 等)
|
|
||||||
S3_ENDPOINT=
|
|
||||||
|
|
||||||
# 是否强制使用路径样式 (可选)
|
|
||||||
S3_FORCE_PATH_STYLE=false
|
|
||||||
|
|
||||||
# 分片上传大小,单位字节 (可选,默认 8MB)
|
|
||||||
S3_PART_SIZE=8388608
|
|
||||||
|
|
||||||
# 最大并发上传数 (可选)
|
|
||||||
S3_MAX_CONCURRENT_UPLOADS=60
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置示例
|
|
||||||
|
|
||||||
### 开发环境 - 本地存储
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.development
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
UPLOAD_DIR=./uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
### 生产环境 - AWS S3
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.production
|
|
||||||
STORAGE_TYPE=s3
|
|
||||||
S3_BUCKET=prod-app-uploads
|
|
||||||
S3_REGION=us-west-2
|
|
||||||
S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
|
||||||
S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
||||||
```
|
|
||||||
|
|
||||||
### MinIO 本地开发
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.local
|
|
||||||
STORAGE_TYPE=s3
|
|
||||||
S3_BUCKET=uploads
|
|
||||||
S3_REGION=us-east-1
|
|
||||||
S3_ACCESS_KEY_ID=minioadmin
|
|
||||||
S3_SECRET_ACCESS_KEY=minioadmin
|
|
||||||
S3_ENDPOINT=http://localhost:9000
|
|
||||||
S3_FORCE_PATH_STYLE=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 阿里云 OSS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.aliyun
|
|
||||||
STORAGE_TYPE=s3
|
|
||||||
S3_BUCKET=my-oss-bucket
|
|
||||||
S3_REGION=oss-cn-hangzhou
|
|
||||||
S3_ACCESS_KEY_ID=your-access-key-id
|
|
||||||
S3_SECRET_ACCESS_KEY=your-access-key-secret
|
|
||||||
S3_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
||||||
S3_FORCE_PATH_STYLE=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### 腾讯云 COS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env.tencent
|
|
||||||
STORAGE_TYPE=s3
|
|
||||||
S3_BUCKET=my-cos-bucket-1234567890
|
|
||||||
S3_REGION=ap-beijing
|
|
||||||
S3_ACCESS_KEY_ID=your-secret-id
|
|
||||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
|
||||||
S3_ENDPOINT=https://cos.ap-beijing.myqcloud.com
|
|
||||||
S3_FORCE_PATH_STYLE=false
|
|
||||||
```
|
|
||||||
|
|
||||||
## 其他配置
|
|
||||||
|
|
||||||
### 数据库配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# PostgreSQL 数据库连接字符串
|
|
||||||
DATABASE_URL="postgresql://username:password@localhost:5432/database"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redis 配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redis 连接字符串
|
|
||||||
REDIS_URL="redis://localhost:6379"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 应用配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 应用端口
|
|
||||||
PORT=3000
|
|
||||||
|
|
||||||
# 应用环境
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# CORS 允许的源
|
|
||||||
CORS_ORIGIN=http://localhost:3001
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全注意事项
|
|
||||||
|
|
||||||
1. **敏感信息保护**:
|
|
||||||
|
|
||||||
- 永远不要将包含敏感信息的 `.env` 文件提交到版本控制系统
|
|
||||||
- 使用 `.env.example` 文件作为模板
|
|
||||||
|
|
||||||
2. **生产环境**:
|
|
||||||
|
|
||||||
- 使用环境变量管理服务(如 AWS Secrets Manager、Azure Key Vault)
|
|
||||||
- 定期轮换访问密钥
|
|
||||||
|
|
||||||
3. **权限控制**:
|
|
||||||
- S3 存储桶应配置适当的访问策略
|
|
||||||
- 使用最小权限原则
|
|
||||||
|
|
||||||
## 验证配置
|
|
||||||
|
|
||||||
可以使用以下 API 端点验证存储配置:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 验证存储配置
|
|
||||||
curl -X POST http://localhost:3000/api/storage/storage/validate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"type": "s3",
|
|
||||||
"s3": {
|
|
||||||
"bucket": "my-bucket",
|
|
||||||
"region": "us-east-1",
|
|
||||||
"accessKeyId": "your-key",
|
|
||||||
"secretAccessKey": "your-secret"
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
|
|
||||||
# 获取当前存储信息
|
|
||||||
curl http://localhost:3000/api/storage/storage/info
|
|
||||||
```
|
|
||||||
|
|
||||||
## 文件访问
|
|
||||||
|
|
||||||
### 统一下载接口
|
|
||||||
|
|
||||||
无论使用哪种存储类型,都通过统一的下载接口访问文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 统一下载接口(推荐)
|
|
||||||
GET http://localhost:3000/download/2024/01/01/abc123/example.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本地存储
|
|
||||||
|
|
||||||
当使用本地存储时:
|
|
||||||
|
|
||||||
- 下载接口会直接读取本地文件并返回
|
|
||||||
- 支持内联显示(图片、PDF等)和下载
|
|
||||||
|
|
||||||
### S3 存储
|
|
||||||
|
|
||||||
当使用 S3 存储时:
|
|
||||||
|
|
||||||
- 下载接口会重定向到 S3 URL
|
|
||||||
- 也可以直接访问 S3 URL(如果存储桶是公开的)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 直接访问 S3 URL
|
|
||||||
GET https://bucket.s3.region.amazonaws.com/2024/01/01/abc123/example.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件 URL 生成
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { StorageUtils } from '@repo/storage';
|
|
||||||
|
|
||||||
const storageUtils = StorageUtils.getInstance();
|
|
||||||
|
|
||||||
// 生成下载 URL(推荐方式)
|
|
||||||
const fileUrl = storageUtils.generateFileUrl('file-id');
|
|
||||||
// 结果: http://localhost:3000/download/file-id
|
|
||||||
|
|
||||||
// 生成完整的公开访问 URL
|
|
||||||
const publicUrl = storageUtils.generateFileUrl('file-id', 'https://yourdomain.com');
|
|
||||||
// 结果: https://yourdomain.com/download/file-id
|
|
||||||
|
|
||||||
// 生成 S3 直接访问 URL(仅 S3 存储)
|
|
||||||
try {
|
|
||||||
const directUrl = storageUtils.generateDirectUrl('file-id');
|
|
||||||
// 结果: https://bucket.s3.region.amazonaws.com/file-id
|
|
||||||
} catch (error) {
|
|
||||||
// 本地存储会抛出错误
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,279 +0,0 @@
|
||||||
# 文件访问使用指南
|
|
||||||
|
|
||||||
本文档说明如何使用 `@repo/storage` 包提供的文件访问功能。
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
存储包提供统一的文件访问接口:
|
|
||||||
|
|
||||||
- **统一下载接口** (`/download/:fileId`) - 适用于所有存储类型,提供统一的文件访问
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 基础配置
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createStorageApp } from '@repo/storage';
|
|
||||||
|
|
||||||
// 创建包含所有功能的存储应用
|
|
||||||
const storageApp = createStorageApp({
|
|
||||||
apiBasePath: '/api/storage', // API 管理接口
|
|
||||||
uploadPath: '/upload', // TUS 上传接口
|
|
||||||
downloadPath: '/download', // 文件下载接口
|
|
||||||
});
|
|
||||||
|
|
||||||
app.route('/', storageApp);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 分别配置功能
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createStorageRoutes, createTusUploadRoutes, createFileDownloadRoutes } from '@repo/storage';
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
// 存储管理 API
|
|
||||||
app.route('/api/storage', createStorageRoutes());
|
|
||||||
|
|
||||||
// 文件上传
|
|
||||||
app.route('/upload', createTusUploadRoutes());
|
|
||||||
|
|
||||||
// 文件下载(所有存储类型)
|
|
||||||
app.route('/download', createFileDownloadRoutes());
|
|
||||||
```
|
|
||||||
|
|
||||||
## 文件访问方式
|
|
||||||
|
|
||||||
### 统一下载接口
|
|
||||||
|
|
||||||
无论使用哪种存储类型,都通过统一的下载接口访问文件:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 访问文件(支持内联显示和下载)
|
|
||||||
GET http://localhost:3000/download/2024/01/01/abc123/image.jpg
|
|
||||||
GET http://localhost:3000/download/2024/01/01/abc123/document.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本地存储
|
|
||||||
|
|
||||||
当 `STORAGE_TYPE=local` 时:
|
|
||||||
|
|
||||||
- 下载接口直接读取本地文件
|
|
||||||
- 自动设置正确的 Content-Type
|
|
||||||
- 支持内联显示(`Content-Disposition: inline`)
|
|
||||||
|
|
||||||
### S3 存储
|
|
||||||
|
|
||||||
当 `STORAGE_TYPE=s3` 时:
|
|
||||||
|
|
||||||
- 下载接口重定向到 S3 URL
|
|
||||||
- 也可以直接访问 S3 URL(如果存储桶是公开的)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 直接访问 S3 URL(如果存储桶是公开的)
|
|
||||||
GET https://bucket.s3.region.amazonaws.com/2024/01/01/abc123/file.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
## 代码示例
|
|
||||||
|
|
||||||
### 生成文件访问 URL
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { StorageUtils } from '@repo/storage';
|
|
||||||
|
|
||||||
const storageUtils = StorageUtils.getInstance();
|
|
||||||
|
|
||||||
// 生成文件访问 URL
|
|
||||||
function getFileUrl(fileId: string) {
|
|
||||||
// 结果: http://localhost:3000/download/2024/01/01/abc123/file.jpg
|
|
||||||
return storageUtils.generateFileUrl(fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成完整的公开访问 URL
|
|
||||||
function getPublicFileUrl(fileId: string) {
|
|
||||||
// 结果: https://yourdomain.com/download/2024/01/01/abc123/file.jpg
|
|
||||||
return storageUtils.generateFileUrl(fileId, 'https://yourdomain.com');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 S3 直接访问 URL(仅 S3 存储)
|
|
||||||
function getDirectUrl(fileId: string) {
|
|
||||||
try {
|
|
||||||
// S3 存储: https://bucket.s3.region.amazonaws.com/2024/01/01/abc123/file.jpg
|
|
||||||
return storageUtils.generateDirectUrl(fileId);
|
|
||||||
} catch (error) {
|
|
||||||
// 本地存储会抛出错误,使用下载接口
|
|
||||||
return storageUtils.generateFileUrl(fileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 在 React 组件中使用
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
function FileDisplay({ fileId }: { fileId: string }) {
|
|
||||||
const [fileUrl, setFileUrl] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 获取文件访问 URL
|
|
||||||
fetch(`/api/storage/resource/${fileId}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.status === 'ready' && data.resource) {
|
|
||||||
// 生成文件访问 URL
|
|
||||||
const url = `/download/${fileId}`;
|
|
||||||
setFileUrl(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [fileId]);
|
|
||||||
|
|
||||||
if (!fileUrl) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* 图片会内联显示 */}
|
|
||||||
<img src={fileUrl} alt="Uploaded file" />
|
|
||||||
|
|
||||||
{/* 下载链接 */}
|
|
||||||
<a href={fileUrl} download>
|
|
||||||
下载文件
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* PDF 等文档可以在新窗口打开 */}
|
|
||||||
<a href={fileUrl} target="_blank" rel="noopener noreferrer">
|
|
||||||
在新窗口打开
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件类型处理
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function getFileDisplayUrl(fileId: string, mimeType: string) {
|
|
||||||
const baseUrl = `/download/${fileId}`;
|
|
||||||
|
|
||||||
// 根据文件类型决定显示方式
|
|
||||||
if (mimeType.startsWith('image/')) {
|
|
||||||
// 图片直接显示
|
|
||||||
return baseUrl;
|
|
||||||
} else if (mimeType === 'application/pdf') {
|
|
||||||
// PDF 可以内联显示
|
|
||||||
return baseUrl;
|
|
||||||
} else {
|
|
||||||
// 其他文件类型强制下载
|
|
||||||
return `${baseUrl}?download=true`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
### 1. 访问控制
|
|
||||||
|
|
||||||
如需要权限验证,可以添加认证中间件:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createFileDownloadRoutes } from '@repo/storage';
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
// 添加认证中间件
|
|
||||||
app.use('/download/*', async (c, next) => {
|
|
||||||
// 检查用户权限
|
|
||||||
const token = c.req.header('Authorization');
|
|
||||||
if (!isValidToken(token)) {
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401);
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加文件下载服务
|
|
||||||
app.route('/download', createFileDownloadRoutes());
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 文件类型限制
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
app.use('/download/*', async (c, next) => {
|
|
||||||
const fileId = c.req.param('fileId');
|
|
||||||
|
|
||||||
// 从数据库获取文件信息
|
|
||||||
const { resource } = await getResourceByFileId(fileId);
|
|
||||||
if (!resource) {
|
|
||||||
return c.json({ error: 'File not found' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查文件类型
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
||||||
if (!allowedTypes.includes(resource.mimeType)) {
|
|
||||||
return c.json({ error: 'File type not allowed' }, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
### 1. 缓存设置
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
app.use('/download/*', async (c, next) => {
|
|
||||||
await next();
|
|
||||||
|
|
||||||
// 设置缓存头
|
|
||||||
c.header('Cache-Control', 'public, max-age=31536000'); // 1年
|
|
||||||
c.header('ETag', generateETag(c.req.path));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. CDN 配置
|
|
||||||
|
|
||||||
对于生产环境,建议使用 CDN:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { StorageUtils } from '@repo/storage';
|
|
||||||
|
|
||||||
const storageUtils = StorageUtils.getInstance();
|
|
||||||
|
|
||||||
// 使用 CDN 域名
|
|
||||||
const cdnUrl = 'https://cdn.yourdomain.com';
|
|
||||||
const fileUrl = storageUtils.generateFileUrl(fileId, cdnUrl);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 常见问题
|
|
||||||
|
|
||||||
1. **404 文件未找到**
|
|
||||||
|
|
||||||
- 检查文件是否存在于数据库
|
|
||||||
- 确认文件路径是否正确
|
|
||||||
- 检查文件权限(本地存储)
|
|
||||||
|
|
||||||
2. **下载接口不工作**
|
|
||||||
|
|
||||||
- 检查路由配置
|
|
||||||
- 确认存储配置正确
|
|
||||||
- 查看服务器日志
|
|
||||||
|
|
||||||
3. **S3 文件无法访问**
|
|
||||||
- 检查 S3 存储桶权限
|
|
||||||
- 确认文件是否上传成功
|
|
||||||
- 验证 S3 配置是否正确
|
|
||||||
|
|
||||||
### 调试方法
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 检查文件是否存在
|
|
||||||
curl -I http://localhost:3000/download/2024/01/01/abc123/file.jpg
|
|
||||||
|
|
||||||
# 检查存储配置
|
|
||||||
curl http://localhost:3000/api/storage/storage/info
|
|
||||||
|
|
||||||
# 检查文件信息
|
|
||||||
curl http://localhost:3000/api/storage/resource/2024/01/01/abc123/file.jpg
|
|
||||||
```
|
|
71
env.example
71
env.example
|
@ -1,71 +0,0 @@
|
||||||
# ===========================================
|
|
||||||
# 存储配置 (@repo/storage)
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# 存储类型: local | s3
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
|
|
||||||
# 上传文件过期时间(毫秒),0表示不过期
|
|
||||||
UPLOAD_EXPIRATION_MS=0
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 本地存储配置 (当 STORAGE_TYPE=local 时)
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# 本地存储目录路径
|
|
||||||
UPLOAD_DIR=./uploads
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# S3 存储配置 (当 STORAGE_TYPE=s3 时)
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# S3 存储桶名称 (必需)
|
|
||||||
S3_BUCKET=
|
|
||||||
|
|
||||||
# S3 区域 (必需)
|
|
||||||
S3_REGION=us-east-1
|
|
||||||
|
|
||||||
# S3 访问密钥 ID (必需)
|
|
||||||
S3_ACCESS_KEY_ID=
|
|
||||||
|
|
||||||
# S3 访问密钥 (必需)
|
|
||||||
S3_SECRET_ACCESS_KEY=
|
|
||||||
|
|
||||||
# 自定义 S3 端点 (可选,用于 MinIO、阿里云 OSS 等)
|
|
||||||
S3_ENDPOINT=
|
|
||||||
|
|
||||||
# 是否强制使用路径样式 (可选)
|
|
||||||
S3_FORCE_PATH_STYLE=false
|
|
||||||
|
|
||||||
# 分片上传大小,单位字节 (可选,默认 8MB)
|
|
||||||
S3_PART_SIZE=8388608
|
|
||||||
|
|
||||||
# 最大并发上传数 (可选)
|
|
||||||
S3_MAX_CONCURRENT_UPLOADS=60
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 数据库配置
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# 数据库连接字符串
|
|
||||||
DATABASE_URL="postgresql://username:password@localhost:5432/database"
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Redis 配置
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# Redis 连接字符串
|
|
||||||
REDIS_URL="redis://localhost:6379"
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# 应用配置
|
|
||||||
# ===========================================
|
|
||||||
|
|
||||||
# 应用端口
|
|
||||||
PORT=3000
|
|
||||||
|
|
||||||
# 应用环境
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# CORS 允许的源
|
|
||||||
CORS_ORIGIN=http://localhost:3001
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// 优化后的架构 - 数据中台 v4.0 (完全通用化)
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
||||||
|
@ -9,128 +11,394 @@ datasource db {
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============= 数据源层 =============
|
||||||
|
model DataSource {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
code String @unique
|
||||||
|
type SourceType
|
||||||
|
config Json // 连接配置
|
||||||
|
description String?
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
|
||||||
|
// 元数据版本管理
|
||||||
|
schemaVersion String? @default("1.0")
|
||||||
|
lastSynced DateTime?
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
pipelines Pipeline[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("data_sources")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 统一流水线层 =============
|
||||||
|
model Pipeline {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
type PipelineType // SYNC | TRANSFORM | STREAM | HYBRID
|
||||||
|
schedule String? // Cron表达式
|
||||||
|
config Json // 流水线配置
|
||||||
|
description String?
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
|
||||||
|
// 关联数据源(可选)
|
||||||
|
dataSource DataSource? @relation(fields: [dataSourceId], references: [id])
|
||||||
|
dataSourceId String?
|
||||||
|
|
||||||
|
// 执行记录
|
||||||
|
executions PipelineExecution[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("pipelines")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一执行记录
|
||||||
|
model PipelineExecution {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
executionId String @unique
|
||||||
|
status ExecutionStatus @default(PENDING)
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
inputRecords Int @default(0)
|
||||||
|
outputRecords Int @default(0)
|
||||||
|
errorRecords Int @default(0)
|
||||||
|
|
||||||
|
// 时间信息
|
||||||
|
startedAt DateTime @default(now())
|
||||||
|
completedAt DateTime?
|
||||||
|
duration Int? // 毫秒
|
||||||
|
|
||||||
|
// 元数据
|
||||||
|
metadata Json? // 灵活的元数据存储
|
||||||
|
errorMsg String?
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
pipeline Pipeline @relation(fields: [pipelineId], references: [id])
|
||||||
|
pipelineId String
|
||||||
|
|
||||||
|
// 产生的数据资产
|
||||||
|
dataAssets DataAsset[]
|
||||||
|
|
||||||
|
// 数据血缘
|
||||||
|
lineageRecords LineageRecord[]
|
||||||
|
|
||||||
|
@@map("pipeline_executions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 数据资产层 =============
|
||||||
|
model DataAsset {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
assetId String @unique
|
||||||
|
name String
|
||||||
|
type AssetType @default(BATCH)
|
||||||
|
format DataFormat @default(PARQUET)
|
||||||
|
|
||||||
|
// 存储信息(支持多种存储)
|
||||||
|
storageConfig Json // 统一存储配置
|
||||||
|
|
||||||
|
// 元数据管理
|
||||||
|
schema Json?
|
||||||
|
partitions Json? // 分区信息
|
||||||
|
size BigInt? // 数据大小
|
||||||
|
recordCount BigInt? // 记录数
|
||||||
|
|
||||||
|
// 数据质量
|
||||||
|
qualityScore Float? // 0-1之间的质量分数
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
execution PipelineExecution? @relation(fields: [executionId], references: [id])
|
||||||
|
executionId String?
|
||||||
|
|
||||||
|
// 查询记录
|
||||||
|
queries Query[]
|
||||||
|
|
||||||
|
status AssetStatus @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([type, status])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("data_assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 查询层 =============
|
||||||
|
model Query {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
queryId String @unique
|
||||||
|
sql String
|
||||||
|
engine QueryEngine @default(DUCKDB)
|
||||||
|
|
||||||
|
// 执行信息
|
||||||
|
status QueryStatus @default(PENDING)
|
||||||
|
resultCount BigInt?
|
||||||
|
resultPath String?
|
||||||
|
duration Int? // 毫秒
|
||||||
|
errorMsg String?
|
||||||
|
|
||||||
|
// 查询标签
|
||||||
|
tags Json?
|
||||||
|
|
||||||
|
// 关联资产
|
||||||
|
dataAsset DataAsset @relation(fields: [assetId], references: [id])
|
||||||
|
assetId String
|
||||||
|
|
||||||
|
// 审计信息
|
||||||
|
userId String? // 执行用户
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
completedAt DateTime?
|
||||||
|
|
||||||
|
@@index([status, createdAt])
|
||||||
|
@@map("queries")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 通用实体层 =============
|
||||||
|
|
||||||
|
// 通用实体模型(支持优雅的树形层级)
|
||||||
|
model Entity {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
code String @unique
|
||||||
|
name String
|
||||||
|
type EntityType // PERSON, EQUIPMENT, FACILITY, MATERIAL, ORGANIZATION等
|
||||||
|
attributes Json? // 灵活的属性存储
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
|
||||||
|
// 树形层级关系(直接支持)
|
||||||
|
parentId String?
|
||||||
|
parent Entity? @relation("EntityTree", fields: [parentId], references: [id])
|
||||||
|
children Entity[] @relation("EntityTree")
|
||||||
|
|
||||||
|
// 层级路径(用于快速查询)
|
||||||
|
path String? // 如: "/org1/dept1/team1" 便于层级查询
|
||||||
|
level Int? // 层级深度,根节点为0
|
||||||
|
|
||||||
|
// 作为源实体的关系(非树形关系)
|
||||||
|
sourceRelations EntityRelation[] @relation("SourceEntity")
|
||||||
|
// 作为目标实体的关系
|
||||||
|
targetRelations EntityRelation[] @relation("TargetEntity")
|
||||||
|
|
||||||
|
// 数据血缘
|
||||||
|
lineageRecords LineageRecord[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([type, status])
|
||||||
|
@@index([code])
|
||||||
|
@@index([type, code])
|
||||||
|
@@index([parentId])
|
||||||
|
@@index([path])
|
||||||
|
@@index([type, parentId])
|
||||||
|
@@index([level])
|
||||||
|
@@map("entities")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 通用关系层 =============
|
||||||
|
|
||||||
|
// 通用实体关系表(处理非树形的复杂关系)
|
||||||
|
model EntityRelation {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sourceId String // 源实体ID
|
||||||
|
targetId String // 目标实体ID
|
||||||
|
relationship String // 关系类型
|
||||||
|
|
||||||
|
// 关系属性
|
||||||
|
attributes Json? // 关系属性:权限级别、时间范围等
|
||||||
|
startDate DateTime?
|
||||||
|
endDate DateTime?
|
||||||
|
|
||||||
|
// 关系元数据
|
||||||
|
metadata Json? // 其他元数据
|
||||||
|
|
||||||
|
// 关联实体
|
||||||
|
sourceEntity Entity @relation("SourceEntity", fields: [sourceId], references: [id])
|
||||||
|
targetEntity Entity @relation("TargetEntity", fields: [targetId], references: [id])
|
||||||
|
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([sourceId, targetId, relationship])
|
||||||
|
@@index([sourceId])
|
||||||
|
@@index([targetId])
|
||||||
|
@@index([relationship])
|
||||||
|
@@index([sourceId, relationship])
|
||||||
|
@@index([targetId, relationship])
|
||||||
|
@@index([relationship, status])
|
||||||
|
@@map("entity_relations")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 优化的血缘层 =============
|
||||||
|
model LineageRecord {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sourceType String // 来源类型
|
||||||
|
sourceId String // 来源ID
|
||||||
|
targetType String // 目标类型
|
||||||
|
targetId String // 目标ID
|
||||||
|
relationship String // 关系类型: CREATE, UPDATE, DERIVE
|
||||||
|
|
||||||
|
// 元数据
|
||||||
|
metadata Json? // 血缘元数据
|
||||||
|
|
||||||
|
// 关联执行(可选)
|
||||||
|
execution PipelineExecution? @relation(fields: [executionId], references: [id])
|
||||||
|
executionId String?
|
||||||
|
|
||||||
|
// 关联实体(可选)
|
||||||
|
entity Entity? @relation(fields: [entityId], references: [id])
|
||||||
|
entityId String?
|
||||||
|
|
||||||
|
status LineageStatus @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([sourceType, sourceId, targetType, targetId])
|
||||||
|
@@index([relationship])
|
||||||
|
@@map("lineage_records")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 数据治理层 =============
|
||||||
|
model QualityRule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
rule Json // 质量规则定义
|
||||||
|
threshold Float? // 阈值
|
||||||
|
|
||||||
|
// 应用范围
|
||||||
|
entityType String? // 应用的实体类型
|
||||||
|
entityId String? // 特定实体ID
|
||||||
|
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("quality_rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
model DataCatalog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
tags Json? // 标签
|
||||||
|
owner String? // 数据负责人
|
||||||
|
|
||||||
|
// 分类
|
||||||
|
category String?
|
||||||
|
sensitivity String? // 敏感度级别
|
||||||
|
|
||||||
|
// 关联资产或实体
|
||||||
|
assetType String // 资产类型:DataAsset, Entity等
|
||||||
|
assetId String // 资产ID
|
||||||
|
|
||||||
|
status Status @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([assetType, assetId])
|
||||||
|
@@index([category])
|
||||||
|
@@map("data_catalogs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 精简的权限层 =============
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
username String @unique
|
||||||
password String?
|
email String @unique
|
||||||
salt String?
|
name String
|
||||||
phone String? @unique
|
roles Json? // 简化为JSON存储角色
|
||||||
email String @unique
|
status Status @default(ACTIVE)
|
||||||
avatar String?
|
|
||||||
isSystem Boolean? @map("is_system")
|
createdAt DateTime @default(now())
|
||||||
isAdmin Boolean? @map("is_admin")
|
updatedAt DateTime @updatedAt
|
||||||
lastSignTime DateTime? @map("last_sign_time")
|
|
||||||
deactivatedTime DateTime? @map("deactivated_time")
|
|
||||||
createdTime DateTime @default(now()) @map("created_time")
|
|
||||||
deletedTime DateTime? @map("deleted_time")
|
|
||||||
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
|
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Attachments {
|
// ============= 枚举定义 =============
|
||||||
id String @id @default(cuid())
|
enum Status {
|
||||||
token String @unique
|
ACTIVE
|
||||||
hash String
|
INACTIVE
|
||||||
size Int
|
|
||||||
mimetype String
|
|
||||||
path String
|
|
||||||
width Int?
|
|
||||||
height Int?
|
|
||||||
deletedTime DateTime? @map("deleted_time")
|
|
||||||
createdTime DateTime @default(now()) @map("created_time")
|
|
||||||
createdBy String @map("created_by")
|
|
||||||
lastModifiedBy String? @map("last_modified_by")
|
|
||||||
thumbnailPath String? @map("thumbnail_path")
|
|
||||||
|
|
||||||
@@map("attachments")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Notification {
|
enum SourceType {
|
||||||
id String @id @default(cuid())
|
DATABASE
|
||||||
fromUserId String @map("from_user_id")
|
API
|
||||||
toUserId String @map("to_user_id")
|
FILE
|
||||||
type String @map("type")
|
ERP
|
||||||
message String @map("message")
|
MES
|
||||||
urlPath String? @map("url_path")
|
IOT
|
||||||
isRead Boolean @default(false) @map("is_read")
|
STREAM
|
||||||
createdTime DateTime @default(now()) @map("created_time")
|
|
||||||
createdBy String @map("created_by")
|
|
||||||
|
|
||||||
@@index([toUserId, isRead, createdTime])
|
|
||||||
@@map("notification")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Setting {
|
enum PipelineType {
|
||||||
instanceId String @id @default(cuid()) @map("instance_id")
|
SYNC
|
||||||
disallowSignUp Boolean? @map("disallow_sign_up")
|
TRANSFORM
|
||||||
disallowSpaceCreation Boolean? @map("disallow_space_creation")
|
STREAM
|
||||||
disallowSpaceInvitation Boolean? @map("disallow_space_invitation")
|
HYBRID
|
||||||
enableEmailVerification Boolean? @map("enable_email_verification")
|
|
||||||
aiConfig String? @map("ai_config")
|
|
||||||
brandName String? @map("brand_name")
|
|
||||||
brandLogo String? @map("brand_logo")
|
|
||||||
|
|
||||||
@@map("setting")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Trash {
|
enum ExecutionStatus {
|
||||||
id String @id @default(cuid())
|
PENDING
|
||||||
resourceType String @map("resource_type")
|
RUNNING
|
||||||
resourceId String @map("resource_id")
|
SUCCESS
|
||||||
parentId String? @map("parent_id")
|
FAILED
|
||||||
deletedTime DateTime @default(now()) @map("deleted_time")
|
CANCELLED
|
||||||
deletedBy String @map("deleted_by")
|
|
||||||
|
|
||||||
@@unique([resourceType, resourceId])
|
|
||||||
@@map("trash")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserLastVisit {
|
enum DataFormat {
|
||||||
id String @id @default(cuid())
|
PARQUET
|
||||||
userId String @map("user_id")
|
JSON
|
||||||
resourceType String @map("resource_type")
|
CSV
|
||||||
resourceId String @map("resource_id")
|
AVRO
|
||||||
parentResourceId String @map("parent_resource_id")
|
DELTA
|
||||||
lastVisitTime DateTime @default(now()) @map("last_visit_time")
|
|
||||||
|
|
||||||
@@unique([userId, resourceType, parentResourceId])
|
|
||||||
@@index([userId, resourceType])
|
|
||||||
@@map("user_last_visit")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model OidcClient {
|
enum AssetType {
|
||||||
id String @id @default(cuid())
|
BATCH
|
||||||
clientId String @unique
|
STREAM
|
||||||
clientSecret String
|
TABLE
|
||||||
redirectUris String // 存储为JSON字符串
|
VIEW
|
||||||
grantTypes String // 存储为JSON字符串
|
MODEL
|
||||||
responseTypes String // 存储为JSON字符串
|
}
|
||||||
scope String
|
|
||||||
createdAt DateTime @default(now())
|
enum AssetStatus {
|
||||||
updatedAt DateTime @updatedAt
|
ACTIVE
|
||||||
|
ARCHIVED
|
||||||
@@map("oidc_clients")
|
DEPRECATED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QueryEngine {
|
||||||
|
DUCKDB
|
||||||
|
ATHENA
|
||||||
|
SPARK
|
||||||
|
TRINO
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QueryStatus {
|
||||||
|
PENDING
|
||||||
|
RUNNING
|
||||||
|
SUCCESS
|
||||||
|
FAILED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EntityType {
|
||||||
|
PERSON
|
||||||
|
EQUIPMENT
|
||||||
|
FACILITY
|
||||||
|
MATERIAL
|
||||||
|
ORGANIZATION
|
||||||
|
PROJECT
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LineageStatus {
|
||||||
|
ACTIVE
|
||||||
|
ARCHIVED
|
||||||
}
|
}
|
||||||
model Resource {
|
|
||||||
id String @id @default(cuid()) @map("id")
|
|
||||||
title String? @map("title")
|
|
||||||
description String? @map("description")
|
|
||||||
type String? @map("type")
|
|
||||||
fileId String? @unique
|
|
||||||
url String?
|
|
||||||
meta Json? @map("meta")
|
|
||||||
status String?
|
|
||||||
createdAt DateTime? @default(now()) @map("created_at")
|
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
|
||||||
createdBy String? @map("created_by")
|
|
||||||
updatedBy String? @map("updated_by")
|
|
||||||
deletedAt DateTime? @map("deleted_at")
|
|
||||||
isPublic Boolean? @default(true) @map("is_public")
|
|
||||||
storageType String? @map("storage_type")
|
|
||||||
|
|
||||||
// 索引
|
|
||||||
@@index([type])
|
|
||||||
@@index([createdAt])
|
|
||||||
@@map("resource")
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
# 存储配置
|
|
||||||
STORAGE_TYPE=s3
|
|
||||||
|
|
||||||
# 本地存储配置 (当 STORAGE_TYPE=local 时使用)
|
|
||||||
LOCAL_STORAGE_DIRECTORY=./uploads
|
|
||||||
|
|
||||||
# S3/MinIO 存储配置 (当 STORAGE_TYPE=s3 时使用)
|
|
||||||
S3_ENDPOINT=http://localhost:9000
|
|
||||||
S3_REGION=us-east-1
|
|
||||||
S3_BUCKET=test123
|
|
||||||
# 使用Docker环境变量设置的凭据
|
|
||||||
S3_ACCESS_KEY_ID=nice1234
|
|
||||||
S3_SECRET_ACCESS_KEY=nice1234
|
|
||||||
S3_FORCE_PATH_STYLE=true
|
|
||||||
|
|
||||||
# S3 高级配置
|
|
||||||
S3_PART_SIZE=8388608
|
|
||||||
S3_MAX_CONCURRENT_UPLOADS=6
|
|
||||||
|
|
||||||
# 清理配置
|
|
||||||
CLEANUP_INCOMPLETE_UPLOADS=true
|
|
||||||
CLEANUP_SCHEDULE=0 2 * * *
|
|
||||||
CLEANUP_MAX_AGE_HOURS=24
|
|
|
@ -2,8 +2,9 @@
|
||||||
"name": "@repo/storage",
|
"name": "@repo/storage",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Storage implementation for Hono - 完全兼容 Hono 的 Storage",
|
"description": "Storage implementation for Hono - 完全兼容 Hono 的 Storage",
|
||||||
"main": "dist/index.js",
|
"exports": {
|
||||||
"types": "dist/index.d.ts",
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
|
@ -31,13 +32,6 @@
|
||||||
"hono": "^4.0.0",
|
"hono": "^4.0.0",
|
||||||
"ioredis": "^5.0.0"
|
"ioredis": "^5.0.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.js",
|
|
||||||
"require": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"README.md"
|
"README.md"
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
// 测试不同的凭据组合
|
|
||||||
const credentialsList = [
|
|
||||||
{
|
|
||||||
name: 'Docker环境变量凭据 (nice1234)',
|
|
||||||
accessKey: 'nice1234',
|
|
||||||
secretKey: 'nice1234',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MinIO默认凭据',
|
|
||||||
accessKey: 'minioadmin',
|
|
||||||
secretKey: 'minioadmin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '你创建的新AccessKey',
|
|
||||||
accessKey: '7Nt7OyHkwIoo3zvSKdnc',
|
|
||||||
secretKey: 'EZ0cyrjJAsabTLNSqWcU47LURMppBW2kka3LuXzb',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function testCredentials(accessKey, secretKey) {
|
|
||||||
const options = {
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: 9000,
|
|
||||||
path: '/?list-type=2', // 列出objects
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Host: 'localhost:9000',
|
|
||||||
Authorization: `AWS ${accessKey}:fakesignature`, // 简化测试
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
resolve({
|
|
||||||
statusCode: res.statusCode,
|
|
||||||
data: data,
|
|
||||||
headers: res.headers,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
req.setTimeout(3000, () => {
|
|
||||||
req.destroy();
|
|
||||||
reject(new Error('请求超时'));
|
|
||||||
});
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('🔍 测试所有可能的MinIO凭据...\n');
|
|
||||||
|
|
||||||
for (const { name, accessKey, secretKey } of credentialsList) {
|
|
||||||
console.log(`📱 测试 ${name}:`);
|
|
||||||
console.log(` Access Key: ${accessKey}`);
|
|
||||||
console.log(` Secret Key: ${secretKey.substring(0, 8)}...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await testCredentials(accessKey, secretKey);
|
|
||||||
console.log(` 状态码: ${result.statusCode}`);
|
|
||||||
|
|
||||||
if (result.statusCode === 403) {
|
|
||||||
if (result.data.includes('SignatureDoesNotMatch')) {
|
|
||||||
console.log(' 🔐 签名错误 (但认证方式正确)');
|
|
||||||
} else if (result.data.includes('InvalidAccessKeyId')) {
|
|
||||||
console.log(' ❌ AccessKey无效');
|
|
||||||
} else {
|
|
||||||
console.log(' 🔐 权限被拒绝');
|
|
||||||
}
|
|
||||||
} else if (result.statusCode === 200) {
|
|
||||||
console.log(' ✅ 认证成功!');
|
|
||||||
} else {
|
|
||||||
console.log(` ⚠️ 未知状态: ${result.statusCode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示错误详情
|
|
||||||
if (result.data.includes('<Code>')) {
|
|
||||||
const codeMatch = result.data.match(/<Code>([^<]+)<\/Code>/);
|
|
||||||
const messageMatch = result.data.match(/<Message>([^<]+)<\/Message>/);
|
|
||||||
if (codeMatch && messageMatch) {
|
|
||||||
console.log(` 错误: ${codeMatch[1]} - ${messageMatch[1]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ 连接失败: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(''); // 空行分隔
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('💡 建议:');
|
|
||||||
console.log('1. 如果Docker凭据有效,更新应用配置使用 nice1234/nice1234');
|
|
||||||
console.log('2. 如果新AccessKey有效,确保它有正确的权限');
|
|
||||||
console.log('3. 可以通过MinIO控制台 (http://localhost:9001) 管理用户和权限');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
|
@ -1,127 +0,0 @@
|
||||||
// 在项目内运行,可以使用现有的AWS SDK依赖
|
|
||||||
process.chdir('./packages/storage');
|
|
||||||
|
|
||||||
async function testWithCorrectCreds() {
|
|
||||||
console.log('🔍 使用正确的MinIO凭据测试...\n');
|
|
||||||
|
|
||||||
// 动态导入AWS SDK
|
|
||||||
const { S3 } = await import('@aws-sdk/client-s3');
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
endpoint: 'http://localhost:9000',
|
|
||||||
region: 'us-east-1',
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: 'nice1234', // Docker环境变量设置的凭据
|
|
||||||
secretAccessKey: 'nice1234',
|
|
||||||
},
|
|
||||||
forcePathStyle: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('配置信息:');
|
|
||||||
console.log('- Endpoint:', config.endpoint);
|
|
||||||
console.log('- Region:', config.region);
|
|
||||||
console.log('- Access Key:', config.credentials.accessKeyId);
|
|
||||||
console.log('- Force Path Style:', config.forcePathStyle);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
const s3Client = new S3(config);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 测试基本连接
|
|
||||||
console.log('📡 测试基本连接...');
|
|
||||||
const buckets = await s3Client.listBuckets();
|
|
||||||
console.log('✅ 连接成功!');
|
|
||||||
console.log('📂 现有存储桶:', buckets.Buckets?.map((b) => b.Name) || []);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// 2. 检查test123存储桶
|
|
||||||
const bucketName = 'test123';
|
|
||||||
console.log(`🪣 检查存储桶 "${bucketName}"...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await s3Client.headBucket({ Bucket: bucketName });
|
|
||||||
console.log(`✅ 存储桶 "${bucketName}" 存在`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'NotFound') {
|
|
||||||
console.log(`❌ 存储桶 "${bucketName}" 不存在,正在创建...`);
|
|
||||||
try {
|
|
||||||
await s3Client.createBucket({ Bucket: bucketName });
|
|
||||||
console.log(`✅ 存储桶 "${bucketName}" 创建成功`);
|
|
||||||
} catch (createError) {
|
|
||||||
console.log(`❌ 创建存储桶失败:`, createError.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`❌ 检查存储桶失败:`, error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 测试简单上传
|
|
||||||
console.log('\n📤 测试简单上传...');
|
|
||||||
const testKey = 'test-file.txt';
|
|
||||||
const testContent = 'Hello MinIO from correct credentials!';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await s3Client.putObject({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: testKey,
|
|
||||||
Body: testContent,
|
|
||||||
});
|
|
||||||
console.log(`✅ 简单上传成功: ${testKey}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 简单上传失败:`, error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 测试分片上传初始化
|
|
||||||
console.log('\n🔄 测试分片上传初始化...');
|
|
||||||
const multipartKey = 'test-multipart.txt';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const multipartUpload = await s3Client.createMultipartUpload({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: multipartKey,
|
|
||||||
});
|
|
||||||
console.log(`✅ 分片上传初始化成功: ${multipartUpload.UploadId}`);
|
|
||||||
|
|
||||||
// 立即取消这个分片上传
|
|
||||||
await s3Client.abortMultipartUpload({
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: multipartKey,
|
|
||||||
UploadId: multipartUpload.UploadId,
|
|
||||||
});
|
|
||||||
console.log('✅ 分片上传取消成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 分片上传初始化失败:`, error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
if (error.$metadata) {
|
|
||||||
console.log('HTTP状态码:', error.$metadata.httpStatusCode);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 所有测试通过!MinIO配置正确。');
|
|
||||||
console.log('\n📝 下一步: 更新你的.env文件使用以下配置:');
|
|
||||||
console.log('STORAGE_TYPE=s3');
|
|
||||||
console.log('S3_ENDPOINT=http://localhost:9000');
|
|
||||||
console.log('S3_REGION=us-east-1');
|
|
||||||
console.log('S3_BUCKET=test123');
|
|
||||||
console.log('S3_ACCESS_KEY_ID=nice1234');
|
|
||||||
console.log('S3_SECRET_ACCESS_KEY=nice1234');
|
|
||||||
console.log('S3_FORCE_PATH_STYLE=true');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ 连接失败:', error.message);
|
|
||||||
console.log('错误详情:', error);
|
|
||||||
|
|
||||||
if (error.message.includes('ECONNREFUSED')) {
|
|
||||||
console.log('\n💡 提示:');
|
|
||||||
console.log('- 确保MinIO正在端口9000运行');
|
|
||||||
console.log('- 检查docker容器状态: docker ps');
|
|
||||||
console.log('- 重启MinIO: docker restart minio-container-name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testWithCorrectCreds().catch(console.error);
|
|
|
@ -1,69 +0,0 @@
|
||||||
const { S3 } = require('@aws-sdk/client-s3');
|
|
||||||
|
|
||||||
async function testWithDefaultCreds() {
|
|
||||||
console.log('🔍 测试MinIO默认凭据...\n');
|
|
||||||
|
|
||||||
const configs = [
|
|
||||||
{
|
|
||||||
name: 'MinIO 默认凭据',
|
|
||||||
config: {
|
|
||||||
endpoint: 'http://localhost:9000',
|
|
||||||
region: 'us-east-1',
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: 'minioadmin',
|
|
||||||
secretAccessKey: 'minioadmin',
|
|
||||||
},
|
|
||||||
forcePathStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '你的自定义凭据',
|
|
||||||
config: {
|
|
||||||
endpoint: 'http://localhost:9000',
|
|
||||||
region: 'us-east-1',
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: '7Nt7OyHkwIoo3zvSKdnc',
|
|
||||||
secretAccessKey: 'EZ0cyrjJAsabTLNSqWcU47LURMppBW2kka3LuXzb',
|
|
||||||
},
|
|
||||||
forcePathStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { name, config } of configs) {
|
|
||||||
console.log(`\n📱 测试 ${name}:`);
|
|
||||||
console.log(` Access Key: ${config.credentials.accessKeyId}`);
|
|
||||||
console.log(` Secret Key: ${config.credentials.secretAccessKey.substring(0, 8)}...`);
|
|
||||||
|
|
||||||
const s3Client = new S3(config);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 测试列出buckets
|
|
||||||
const result = await s3Client.listBuckets();
|
|
||||||
console.log(` ✅ 连接成功!`);
|
|
||||||
console.log(` 📂 现有buckets:`, result.Buckets?.map((b) => b.Name) || []);
|
|
||||||
|
|
||||||
// 测试创建bucket
|
|
||||||
const bucketName = 'test123';
|
|
||||||
try {
|
|
||||||
await s3Client.headBucket({ Bucket: bucketName });
|
|
||||||
console.log(` ✅ Bucket "${bucketName}" 已存在`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'NotFound') {
|
|
||||||
console.log(` 📦 创建bucket "${bucketName}"...`);
|
|
||||||
await s3Client.createBucket({ Bucket: bucketName });
|
|
||||||
console.log(` ✅ Bucket "${bucketName}" 创建成功`);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ❌ 连接失败:`, error.message);
|
|
||||||
if (error.$metadata?.httpStatusCode) {
|
|
||||||
console.log(` 📊 HTTP状态码:`, error.$metadata.httpStatusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testWithDefaultCreds().catch(console.error);
|
|
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "🔍 测试MinIO连接..."
|
|
||||||
|
|
||||||
# 测试1: 默认凭据
|
|
||||||
echo -e "\n📱 测试MinIO默认凭据 (minioadmin/minioadmin):"
|
|
||||||
curl -s -w "HTTP状态码: %{http_code}\n" \
|
|
||||||
-H "Host: localhost:9000" \
|
|
||||||
-H "Authorization: AWS minioadmin:signature" \
|
|
||||||
http://localhost:9000/ | head -5
|
|
||||||
|
|
||||||
# 测试2: 无认证访问根路径
|
|
||||||
echo -e "\n🌐 测试无认证访问:"
|
|
||||||
curl -s -w "HTTP状态码: %{http_code}\n" http://localhost:9000/ | head -3
|
|
||||||
|
|
||||||
# 测试3: 检查MinIO管理界面
|
|
||||||
echo -e "\n🖥️ 测试MinIO控制台:"
|
|
||||||
curl -s -w "HTTP状态码: %{http_code}\n" -I http://localhost:9001/ | grep -E "(HTTP|Server|Content-Type)"
|
|
||||||
|
|
||||||
echo -e "\n💡 提示:"
|
|
||||||
echo "1. 如果你使用Docker运行MinIO,检查环境变量MINIO_ROOT_USER和MINIO_ROOT_PASSWORD"
|
|
||||||
echo "2. 默认凭据通常是 minioadmin/minioadmin"
|
|
||||||
echo "3. 如果修改了凭据,请更新配置文件"
|
|
||||||
|
|
||||||
echo -e "\n🐳 Docker命令参考:"
|
|
||||||
echo "查看MinIO容器: docker ps | grep minio"
|
|
||||||
echo "查看容器日志: docker logs <container_name>"
|
|
||||||
echo "检查环境变量: docker inspect <container_name> | grep -A 10 Env"
|
|
|
@ -1,163 +0,0 @@
|
||||||
const https = require('https');
|
|
||||||
const http = require('http');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
// MinIO配置
|
|
||||||
const config = {
|
|
||||||
endpoint: 'localhost:9000',
|
|
||||||
accessKeyId: '7Nt7OyHkwIoo3zvSKdnc',
|
|
||||||
secretAccessKey: 'EZ0cyrjJAsabTLNSqWcU47LURMppBW2kka3LuXzb',
|
|
||||||
bucket: 'test123',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成AWS签名v4
|
|
||||||
function generateSignature(method, path, headers, body, date) {
|
|
||||||
const region = 'us-east-1';
|
|
||||||
const service = 's3';
|
|
||||||
|
|
||||||
// 创建规范请求
|
|
||||||
const canonicalRequest = [
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
'', // query string
|
|
||||||
Object.keys(headers)
|
|
||||||
.sort()
|
|
||||||
.map((key) => `${key.toLowerCase()}:${headers[key]}`)
|
|
||||||
.join('\n'),
|
|
||||||
'',
|
|
||||||
Object.keys(headers)
|
|
||||||
.sort()
|
|
||||||
.map((key) => key.toLowerCase())
|
|
||||||
.join(';'),
|
|
||||||
crypto.createHash('sha256').update(body).digest('hex'),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// 创建字符串待签名
|
|
||||||
const stringToSign = [
|
|
||||||
'AWS4-HMAC-SHA256',
|
|
||||||
date.toISOString().replace(/[:\-]|\.\d{3}/g, ''),
|
|
||||||
date.toISOString().substr(0, 10).replace(/-/g, '') + '/' + region + '/' + service + '/aws4_request',
|
|
||||||
crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
// 计算签名
|
|
||||||
const kDate = crypto
|
|
||||||
.createHmac('sha256', 'AWS4' + config.secretAccessKey)
|
|
||||||
.update(date.toISOString().substr(0, 10).replace(/-/g, ''))
|
|
||||||
.digest();
|
|
||||||
const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
|
|
||||||
const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
|
|
||||||
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
|
|
||||||
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
|
|
||||||
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试基本连接
|
|
||||||
async function testConnection() {
|
|
||||||
console.log('🔍 测试MinIO基本连接...\n');
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: 9000,
|
|
||||||
path: '/',
|
|
||||||
method: 'GET',
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
console.log(`状态码: ${res.statusCode}`);
|
|
||||||
console.log(`响应头:`, res.headers);
|
|
||||||
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
console.log('响应内容:', data);
|
|
||||||
resolve({ statusCode: res.statusCode, data });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试bucket列表
|
|
||||||
async function testListBuckets() {
|
|
||||||
console.log('\n📂 测试列出bucket...\n');
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
const headers = {
|
|
||||||
Host: config.endpoint,
|
|
||||||
'X-Amz-Date': date.toISOString().replace(/[:\-]|\.\d{3}/g, ''),
|
|
||||||
Authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${date.toISOString().substr(0, 10).replace(/-/g, '')}/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=placeholder`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: 9000,
|
|
||||||
path: '/',
|
|
||||||
method: 'GET',
|
|
||||||
headers: headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
console.log(`状态码: ${res.statusCode}`);
|
|
||||||
console.log(`响应头:`, res.headers);
|
|
||||||
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
console.log('响应内容:', data);
|
|
||||||
resolve({ statusCode: res.statusCode, data });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试创建bucket
|
|
||||||
async function testCreateBucket() {
|
|
||||||
console.log(`\n🪣 测试创建bucket: ${config.bucket}...\n`);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: 9000,
|
|
||||||
path: `/${config.bucket}`,
|
|
||||||
method: 'PUT',
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
console.log(`状态码: ${res.statusCode}`);
|
|
||||||
console.log(`响应头:`, res.headers);
|
|
||||||
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
console.log('响应内容:', data);
|
|
||||||
resolve({ statusCode: res.statusCode, data });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
await testConnection();
|
|
||||||
await testListBuckets();
|
|
||||||
await testCreateBucket();
|
|
||||||
|
|
||||||
console.log('\n✅ 测试完成!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 测试失败:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
Loading…
Reference in New Issue