fenghuo/packages/storage/README.md

11 KiB
Raw Blame History

@repo/storage

一个完全兼容 Hono 的存储解决方案,支持本地存储和 S3 兼容存储,提供 TUS 协议上传、文件管理和 REST API。

特性

  • 🚀 多存储支持: 支持本地文件系统和 S3 兼容存储
  • 📤 TUS 协议: 支持可恢复的文件上传
  • 🔧 Hono 集成: 提供开箱即用的 Hono 中间件
  • 📊 文件管理: 完整的文件生命周期管理
  • 🗄️ 数据库集成: 与 Prisma 深度集成
  • 自动清理: 支持过期文件自动清理
  • 🔄 存储迁移: 支持不同存储类型间的数据迁移
  • 🔌 适配器模式 - 通过适配器与任何数据库后端集成
  • 📁 多存储后端 - 支持本地存储和 S3 兼容存储
  • 🚀 TUS 协议 - 支持可恢复文件上传
  • 🔄 自动清理 - 自动清理失败的上传
  • 🛡️ 类型安全 - 完整的 TypeScript 支持

安装

npm install @repo/storage

环境变量配置

基础配置

变量名 类型 默认值 描述
STORAGE_TYPE local | s3 local 存储类型选择
UPLOAD_EXPIRATION_MS number 0 上传文件过期时间毫秒0表示不过期

本地存储配置

STORAGE_TYPE=local 时需要配置:

变量名 类型 默认值 描述
UPLOAD_DIR string ./uploads 本地存储目录路径

S3 存储配置

STORAGE_TYPE=s3 时需要配置:

变量名 类型 默认值 描述 必需
S3_BUCKET string - S3 存储桶名称
S3_REGION string us-east-1 S3 区域
S3_ACCESS_KEY_ID string - S3 访问密钥 ID
S3_SECRET_ACCESS_KEY string - S3 访问密钥
S3_ENDPOINT string - 自定义 S3 端点(用于兼容其他服务)
S3_FORCE_PATH_STYLE boolean false 是否强制使用路径样式
S3_PART_SIZE number 8388608 分片上传大小8MB
S3_MAX_CONCURRENT_UPLOADS number 60 最大并发上传数

配置示例

本地存储配置

# .env
STORAGE_TYPE=local
UPLOAD_DIR=./uploads

AWS S3 配置

# .env
STORAGE_TYPE=s3
S3_BUCKET=my-app-uploads
S3_REGION=us-west-2
S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

MinIO 配置

# .env
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 配置

# .env
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 配置

# .env
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

快速开始

1. 安装依赖

npm install @repo/storage

2. 实现数据库适配器

import { DatabaseAdapter, ResourceData, CreateResourceData, StorageType } from '@repo/storage';

export class MyDatabaseAdapter implements DatabaseAdapter {
	async getResourceByFileId(fileId: string): Promise<{ status: string; resource?: ResourceData }> {
		// 实现从数据库获取资源的逻辑
	}

	async createResource(data: CreateResourceData): Promise<ResourceData> {
		// 实现创建资源的逻辑
	}

	async updateResource(id: string, data: any): Promise<ResourceData> {
		// 实现更新资源的逻辑
	}

	async deleteResource(id: string): Promise<ResourceData> {
		// 实现删除资源的逻辑
	}

	async updateResourceStatus(fileId: string, status: string, additionalData?: any): Promise<ResourceData> {
		// 实现更新资源状态的逻辑
	}

	async deleteFailedUploadingResource(expirationPeriod: number): Promise<{ count: number }> {
		// 实现清理失败上传的逻辑
	}

	async migrateResourcesStorageType(
		fromStorageType: StorageType,
		toStorageType: StorageType,
	): Promise<{ count: number }> {
		// 实现存储类型迁移的逻辑
	}
}

3. 注册适配器

import { adapterRegistry } from '@repo/storage';
import { MyDatabaseAdapter } from './my-database-adapter';

// 在应用启动时注册适配器
adapterRegistry.setDatabaseAdapter(new MyDatabaseAdapter());

4. 使用存储功能

import { createStorageApp, startCleanupScheduler } from '@repo/storage';

// 创建存储应用
const storageApp = createStorageApp({
	apiBasePath: '/api/storage',
	uploadPath: '/upload',
});

// 启动清理任务
startCleanupScheduler();

Prisma 适配器示例

如果您使用 Prisma可以参考以下实现

import { prisma } from '@your/db-package';
import { DatabaseAdapter, ResourceData, CreateResourceData, StorageType } from '@repo/storage';

export class PrismaDatabaseAdapter implements DatabaseAdapter {
	// 将 Prisma Resource 转换为 ResourceData
	private transformResource(resource: any): ResourceData {
		return {
			id: resource.id,
			fileId: resource.fileId,
			title: resource.title,
			type: resource.type,
			storageType: resource.storageType as StorageType,
			status: resource.status || 'unknown',
			meta: resource.meta,
			createdAt: resource.createdAt,
			updatedAt: resource.updatedAt,
		};
	}

	async getResourceByFileId(fileId: string): Promise<{ status: string; resource?: ResourceData }> {
		const resource = await prisma.resource.findFirst({
			where: { fileId },
		});

		if (!resource) {
			return { status: 'pending' };
		}

		return {
			status: resource.status || 'unknown',
			resource: this.transformResource(resource),
		};
	}

	async createResource(data: CreateResourceData): Promise<ResourceData> {
		const resource = await prisma.resource.create({
			data: {
				fileId: data.fileId,
				title: data.filename,
				type: data.mimeType,
				storageType: data.storageType,
				status: data.status || 'UPLOADING',
				meta: {
					size: data.size,
					hash: data.hash,
				},
			},
		});
		return this.transformResource(resource);
	}

	// ... 实现其他方法
}

API 参考

DatabaseAdapter 接口

所有数据库适配器都必须实现 DatabaseAdapter 接口:

  • getResourceByFileId(fileId: string) - 根据文件ID获取资源
  • createResource(data: CreateResourceData) - 创建新资源
  • updateResource(id: string, data: any) - 更新资源
  • deleteResource(id: string) - 删除资源
  • updateResourceStatus(fileId: string, status: string, additionalData?: any) - 更新资源状态
  • deleteFailedUploadingResource(expirationPeriod: number) - 清理失败的上传
  • migrateResourcesStorageType(from: StorageType, to: StorageType) - 迁移存储类型

适配器注册器

import { adapterRegistry } from '@repo/storage';

// 注册适配器
adapterRegistry.setDatabaseAdapter(adapter);

// 获取当前适配器
const adapter = adapterRegistry.getDatabaseAdapter();

// 检查是否已注册适配器
const hasAdapter = adapterRegistry.hasAdapter();

API 端点

文件资源管理

  • GET /api/storage/resource/:fileId - 获取文件资源信息
  • GET /api/storage/resources - 获取所有资源
  • GET /api/storage/resources/storage/:storageType - 按存储类型获取资源
  • GET /api/storage/resources/status/:status - 按状态获取资源
  • GET /api/storage/resources/uploading - 获取正在上传的资源
  • DELETE /api/storage/resource/:id - 删除资源
  • PATCH /api/storage/resource/:id - 更新资源

文件访问和下载

  • GET /download/:fileId - 文件下载和访问(支持所有存储类型)

统计和管理

  • GET /api/storage/stats - 获取资源统计信息
  • POST /api/storage/cleanup - 手动清理过期上传
  • POST /api/storage/cleanup/by-status - 按状态清理资源
  • POST /api/storage/migrate-storage - 迁移存储类型

存储配置

  • GET /api/storage/storage/info - 获取存储信息
  • POST /api/storage/storage/switch - 切换存储配置
  • POST /api/storage/storage/validate - 验证存储配置

文件上传

  • POST /upload - TUS 协议文件上传
  • PATCH /upload/:id - 续传文件
  • HEAD /upload/:id - 获取上传状态

数据库操作

import {
	getAllResources,
	getResourceByFileId,
	createResource,
	updateResourceStatus,
	deleteResource,
} from '@repo/storage';

// 获取所有资源
const resources = await getAllResources();

// 根据文件ID获取资源
const { status, resource } = await getResourceByFileId('file-id');

// 创建新资源
const newResource = await createResource({
	fileId: 'unique-file-id',
	filename: 'example.jpg',
	size: 1024000,
	mimeType: 'image/jpeg',
	storageType: 'local',
});

文件生命周期

  1. 上传开始: 创建资源记录,状态为 UPLOADING
  2. 上传完成: 状态更新为 UPLOADED
  3. 处理中: 状态可更新为 PROCESSING
  4. 处理完成: 状态更新为 PROCESSED
  5. 清理: 过期文件自动清理

存储迁移

支持在不同存储类型之间迁移数据:

# API 调用示例
curl -X POST http://localhost:3000/api/storage/migrate-storage \
  -H "Content-Type: application/json" \
  -d '{"from": "local", "to": "s3"}'

安全考虑

  1. 环境变量: 敏感信息(如 S3 密钥)应存储在环境变量中
  2. 访问控制: 建议在生产环境中添加适当的身份验证
  3. CORS 配置: 根据需要配置跨域访问策略
  4. 文件验证: 建议添加文件类型和大小验证

故障排除

常见问题

  1. 找不到模块错误: 确保已正确安装依赖包
  2. S3 连接失败: 检查网络连接和凭据配置
  3. 本地存储权限: 确保应用有写入本地目录的权限
  4. 上传失败: 检查文件大小限制和存储空间

调试模式

启用详细日志:

DEBUG=storage:* npm start

许可证

MIT License

贡献

欢迎提交 Issue 和 Pull Request

更新日志

v2.0.0

  • 重构为模块化架构
  • 添加完整的 TypeScript 支持
  • 支持多种 S3 兼容服务
  • 改进的错误处理和日志记录