05301132
This commit is contained in:
parent
6aa7af73f6
commit
7c76dda7f3
|
@ -1,6 +1,5 @@
|
|||
'use client';
|
||||
import { useHello, useTRPC, useWebSocket, MessageType } from '@repo/client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
||||
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 {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
||||
|
@ -9,128 +11,394 @@ datasource db {
|
|||
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 {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
password String?
|
||||
salt String?
|
||||
phone String? @unique
|
||||
email String @unique
|
||||
avatar String?
|
||||
isSystem Boolean? @map("is_system")
|
||||
isAdmin Boolean? @map("is_admin")
|
||||
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")
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
email String @unique
|
||||
name String
|
||||
roles Json? // 简化为JSON存储角色
|
||||
status Status @default(ACTIVE)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Attachments {
|
||||
id String @id @default(cuid())
|
||||
token String @unique
|
||||
hash String
|
||||
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")
|
||||
// ============= 枚举定义 =============
|
||||
enum Status {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
fromUserId String @map("from_user_id")
|
||||
toUserId String @map("to_user_id")
|
||||
type String @map("type")
|
||||
message String @map("message")
|
||||
urlPath String? @map("url_path")
|
||||
isRead Boolean @default(false) @map("is_read")
|
||||
createdTime DateTime @default(now()) @map("created_time")
|
||||
createdBy String @map("created_by")
|
||||
|
||||
@@index([toUserId, isRead, createdTime])
|
||||
@@map("notification")
|
||||
enum SourceType {
|
||||
DATABASE
|
||||
API
|
||||
FILE
|
||||
ERP
|
||||
MES
|
||||
IOT
|
||||
STREAM
|
||||
}
|
||||
|
||||
model Setting {
|
||||
instanceId String @id @default(cuid()) @map("instance_id")
|
||||
disallowSignUp Boolean? @map("disallow_sign_up")
|
||||
disallowSpaceCreation Boolean? @map("disallow_space_creation")
|
||||
disallowSpaceInvitation Boolean? @map("disallow_space_invitation")
|
||||
enableEmailVerification Boolean? @map("enable_email_verification")
|
||||
aiConfig String? @map("ai_config")
|
||||
brandName String? @map("brand_name")
|
||||
brandLogo String? @map("brand_logo")
|
||||
|
||||
@@map("setting")
|
||||
enum PipelineType {
|
||||
SYNC
|
||||
TRANSFORM
|
||||
STREAM
|
||||
HYBRID
|
||||
}
|
||||
|
||||
model Trash {
|
||||
id String @id @default(cuid())
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
parentId String? @map("parent_id")
|
||||
deletedTime DateTime @default(now()) @map("deleted_time")
|
||||
deletedBy String @map("deleted_by")
|
||||
|
||||
@@unique([resourceType, resourceId])
|
||||
@@map("trash")
|
||||
enum ExecutionStatus {
|
||||
PENDING
|
||||
RUNNING
|
||||
SUCCESS
|
||||
FAILED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model UserLastVisit {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
parentResourceId String @map("parent_resource_id")
|
||||
lastVisitTime DateTime @default(now()) @map("last_visit_time")
|
||||
|
||||
@@unique([userId, resourceType, parentResourceId])
|
||||
@@index([userId, resourceType])
|
||||
@@map("user_last_visit")
|
||||
enum DataFormat {
|
||||
PARQUET
|
||||
JSON
|
||||
CSV
|
||||
AVRO
|
||||
DELTA
|
||||
}
|
||||
|
||||
model OidcClient {
|
||||
id String @id @default(cuid())
|
||||
clientId String @unique
|
||||
clientSecret String
|
||||
redirectUris String // 存储为JSON字符串
|
||||
grantTypes String // 存储为JSON字符串
|
||||
responseTypes String // 存储为JSON字符串
|
||||
scope String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("oidc_clients")
|
||||
enum AssetType {
|
||||
BATCH
|
||||
STREAM
|
||||
TABLE
|
||||
VIEW
|
||||
MODEL
|
||||
}
|
||||
|
||||
enum AssetStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
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",
|
||||
"version": "2.0.0",
|
||||
"description": "Storage implementation for Hono - 完全兼容 Hono 的 Storage",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
|
@ -31,13 +32,6 @@
|
|||
"hono": "^4.0.0",
|
||||
"ioredis": "^5.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"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