add
This commit is contained in:
parent
8938337944
commit
047e1fb80a
|
@ -0,0 +1 @@
|
||||||
|
export * from './storage-adapter';
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { prisma } from '@repo/db';
|
||||||
|
import type { Resource } from '@repo/db';
|
||||||
|
import type { DatabaseAdapter, StorageType, ResourceData, CreateResourceData } from '@repo/storage';
|
||||||
|
|
||||||
|
// 将 Prisma Resource 转换为 ResourceData 的辅助函数
|
||||||
|
function transformResource(resource: Resource): 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrismaDatabaseAdapter implements DatabaseAdapter {
|
||||||
|
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: transformResource(resource),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteResource(id: string): Promise<ResourceData> {
|
||||||
|
const resource = await prisma.resource.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return transformResource(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFailedUploadingResource(expirationPeriod: number): Promise<{ count: number }> {
|
||||||
|
const deletedResources = await prisma.resource.deleteMany({
|
||||||
|
where: {
|
||||||
|
createdAt: {
|
||||||
|
lt: new Date(Date.now() - expirationPeriod),
|
||||||
|
},
|
||||||
|
status: 'UPLOADING',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return deletedResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateResource(id: string, data: any): Promise<ResourceData> {
|
||||||
|
const resource = await prisma.resource.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return transformResource(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrateResourcesStorageType(
|
||||||
|
fromStorageType: StorageType,
|
||||||
|
toStorageType: StorageType,
|
||||||
|
): Promise<{ count: number }> {
|
||||||
|
const result = await prisma.resource.updateMany({
|
||||||
|
where: {
|
||||||
|
storageType: fromStorageType,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
storageType: toStorageType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { count: result.count };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 transformResource(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateResourceStatus(fileId: string, status: string, additionalData?: any): Promise<ResourceData> {
|
||||||
|
const resource = await prisma.resource.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: {
|
||||||
|
status,
|
||||||
|
...additionalData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return transformResource(resource);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,12 @@ import { wsHandler, wsConfig } from './socket';
|
||||||
// 导入新的路由
|
// 导入新的路由
|
||||||
import userRest from './user/user.rest';
|
import userRest from './user/user.rest';
|
||||||
// 使用新的 @repo/storage 包
|
// 使用新的 @repo/storage 包
|
||||||
import { createStorageApp, startCleanupScheduler } from '@repo/storage';
|
import { createStorageApp, startCleanupScheduler, adapterRegistry } from '@repo/storage';
|
||||||
|
// 导入 Prisma 适配器实现
|
||||||
|
import { PrismaDatabaseAdapter } from './adapters/storage-adapter';
|
||||||
|
|
||||||
|
// 注册数据库适配器
|
||||||
|
adapterRegistry.setDatabaseAdapter(new PrismaDatabaseAdapter());
|
||||||
|
|
||||||
type Env = {
|
type Env = {
|
||||||
Variables: {
|
Variables: {
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
- 🗄️ **数据库集成**: 与 Prisma 深度集成
|
- 🗄️ **数据库集成**: 与 Prisma 深度集成
|
||||||
- ⏰ **自动清理**: 支持过期文件自动清理
|
- ⏰ **自动清理**: 支持过期文件自动清理
|
||||||
- 🔄 **存储迁移**: 支持不同存储类型间的数据迁移
|
- 🔄 **存储迁移**: 支持不同存储类型间的数据迁移
|
||||||
|
- 🔌 **适配器模式** - 通过适配器与任何数据库后端集成
|
||||||
|
- 📁 **多存储后端** - 支持本地存储和 S3 兼容存储
|
||||||
|
- 🚀 **TUS 协议** - 支持可恢复文件上传
|
||||||
|
- 🔄 **自动清理** - 自动清理失败的上传
|
||||||
|
- 🛡️ **类型安全** - 完整的 TypeScript 支持
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
|
@ -112,91 +117,163 @@ S3_FORCE_PATH_STYLE=false
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. 基础使用
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @repo/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 实现数据库适配器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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. 注册适配器
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { adapterRegistry } from '@repo/storage';
|
||||||
|
import { MyDatabaseAdapter } from './my-database-adapter';
|
||||||
|
|
||||||
|
// 在应用启动时注册适配器
|
||||||
|
adapterRegistry.setDatabaseAdapter(new MyDatabaseAdapter());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用存储功能
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createStorageApp, startCleanupScheduler } from '@repo/storage';
|
import { createStorageApp, startCleanupScheduler } from '@repo/storage';
|
||||||
import { Hono } from 'hono';
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
// 创建存储应用
|
// 创建存储应用
|
||||||
const storageApp = createStorageApp({
|
const storageApp = createStorageApp({
|
||||||
apiBasePath: '/api/storage', // API 路径
|
apiBasePath: '/api/storage',
|
||||||
uploadPath: '/upload', // 上传路径
|
uploadPath: '/upload',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 挂载存储应用
|
// 启动清理任务
|
||||||
app.route('/', storageApp);
|
|
||||||
|
|
||||||
// 启动清理调度器
|
|
||||||
startCleanupScheduler();
|
startCleanupScheduler();
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 分别使用 API 和上传功能
|
## Prisma 适配器示例
|
||||||
|
|
||||||
|
如果您使用 Prisma,可以参考以下实现:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createStorageRoutes, createTusUploadRoutes } from '@repo/storage';
|
import { prisma } from '@your/db-package';
|
||||||
|
import { DatabaseAdapter, ResourceData, CreateResourceData, StorageType } from '@repo/storage';
|
||||||
|
|
||||||
const app = new Hono();
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 只添加存储管理 API
|
async getResourceByFileId(fileId: string): Promise<{ status: string; resource?: ResourceData }> {
|
||||||
app.route('/api/storage', createStorageRoutes());
|
const resource = await prisma.resource.findFirst({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
// 只添加文件上传功能
|
if (!resource) {
|
||||||
app.route('/upload', createTusUploadRoutes());
|
return { status: 'pending' };
|
||||||
```
|
}
|
||||||
|
|
||||||
### 3. 使用存储管理器
|
return {
|
||||||
|
status: resource.status || 'unknown',
|
||||||
|
resource: this.transformResource(resource),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
```typescript
|
async createResource(data: CreateResourceData): Promise<ResourceData> {
|
||||||
import { StorageManager, StorageUtils } from '@repo/storage';
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取存储管理器实例
|
// ... 实现其他方法
|
||||||
const storageManager = StorageManager.getInstance();
|
|
||||||
|
|
||||||
// 获取存储信息
|
|
||||||
const storageInfo = storageManager.getStorageInfo();
|
|
||||||
console.log('当前存储类型:', storageInfo.type);
|
|
||||||
|
|
||||||
// 使用存储工具
|
|
||||||
const storageUtils = StorageUtils.getInstance();
|
|
||||||
|
|
||||||
// 生成文件访问 URL(统一使用下载接口)
|
|
||||||
const fileUrl = storageUtils.generateFileUrl('2024/01/01/abc123/file.jpg');
|
|
||||||
// 结果: http://localhost:3000/download/2024/01/01/abc123/file.jpg
|
|
||||||
|
|
||||||
// 生成完整的公开访问 URL
|
|
||||||
const publicUrl = storageUtils.generateFileUrl('2024/01/01/abc123/file.jpg', 'https://yourdomain.com');
|
|
||||||
// 结果: https://yourdomain.com/download/2024/01/01/abc123/file.jpg
|
|
||||||
|
|
||||||
// 生成 S3 直接访问 URL(仅 S3 存储)
|
|
||||||
try {
|
|
||||||
const directUrl = storageUtils.generateDirectUrl('2024/01/01/abc123/file.jpg');
|
|
||||||
// S3 存储: https://bucket.s3.region.amazonaws.com/2024/01/01/abc123/file.jpg
|
|
||||||
} catch (error) {
|
|
||||||
// 本地存储会抛出错误
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
const exists = await storageUtils.fileExists('file-id');
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 分别配置不同功能
|
## 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)` - 迁移存储类型
|
||||||
|
|
||||||
|
### 适配器注册器
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createStorageRoutes, createTusUploadRoutes, createFileDownloadRoutes } from '@repo/storage';
|
import { adapterRegistry } from '@repo/storage';
|
||||||
|
|
||||||
const app = new Hono();
|
// 注册适配器
|
||||||
|
adapterRegistry.setDatabaseAdapter(adapter);
|
||||||
|
|
||||||
// 只添加存储管理 API
|
// 获取当前适配器
|
||||||
app.route('/api/storage', createStorageRoutes());
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
|
|
||||||
// 只添加文件上传功能
|
// 检查是否已注册适配器
|
||||||
app.route('/upload', createTusUploadRoutes());
|
const hasAdapter = adapterRegistry.hasAdapter();
|
||||||
|
|
||||||
// 只添加文件下载功能(所有存储类型)
|
|
||||||
app.route('/download', createFileDownloadRoutes());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 端点
|
## API 端点
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { DatabaseAdapter } from './database-adapter';
|
||||||
|
|
||||||
|
class AdapterRegistry {
|
||||||
|
private _adapter: DatabaseAdapter | null = null;
|
||||||
|
|
||||||
|
// 注册数据库适配器
|
||||||
|
setDatabaseAdapter(adapter: DatabaseAdapter): void {
|
||||||
|
this._adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库适配器
|
||||||
|
getDatabaseAdapter(): DatabaseAdapter {
|
||||||
|
if (!this._adapter) {
|
||||||
|
throw new Error('数据库适配器未注册。请在使用存储功能前调用 setDatabaseAdapter() 注册适配器。');
|
||||||
|
}
|
||||||
|
return this._adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已注册适配器
|
||||||
|
hasAdapter(): boolean {
|
||||||
|
return this._adapter !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const adapterRegistry = new AdapterRegistry();
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { StorageType, ResourceData, CreateResourceData } from '../types';
|
||||||
|
|
||||||
|
// 数据库适配器接口 - 基于 operations.ts 中的函数
|
||||||
|
export interface DatabaseAdapter {
|
||||||
|
getResourceByFileId(fileId: string): Promise<{ status: string; resource?: ResourceData }>;
|
||||||
|
deleteResource(id: string): Promise<ResourceData>;
|
||||||
|
deleteFailedUploadingResource(expirationPeriod: number): Promise<{ count: number }>;
|
||||||
|
updateResource(id: string, data: any): Promise<ResourceData>;
|
||||||
|
migrateResourcesStorageType(
|
||||||
|
fromStorageType: StorageType,
|
||||||
|
toStorageType: StorageType,
|
||||||
|
): Promise<{ count: number }>;
|
||||||
|
createResource(data: CreateResourceData): Promise<ResourceData>;
|
||||||
|
updateResourceStatus(fileId: string, status: string, additionalData?: any): Promise<ResourceData>;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// 数据库适配器接口
|
||||||
|
export * from './database-adapter';
|
||||||
|
|
||||||
|
// 适配器注册器
|
||||||
|
export * from './adapter-registry';
|
|
@ -1,153 +1,44 @@
|
||||||
import { prisma } from '@repo/db';
|
import { adapterRegistry } from '../adapters/adapter-registry';
|
||||||
import type { Resource } from '@repo/db';
|
import type { StorageType, ResourceData, CreateResourceData } from '../types';
|
||||||
import { StorageType } from '../types';
|
|
||||||
|
|
||||||
export async function getResourceByFileId(fileId: string): Promise<{ status: string; resource?: Resource }> {
|
export async function getResourceByFileId(fileId: string): Promise<{ status: string; resource?: ResourceData }> {
|
||||||
const resource = await prisma.resource.findFirst({
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
where: { fileId },
|
return adapter.getResourceByFileId(fileId);
|
||||||
});
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return { status: 'pending' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: resource.status || 'unknown',
|
|
||||||
resource,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllResources(): Promise<Resource[]> {
|
export async function deleteResource(id: string): Promise<ResourceData> {
|
||||||
return prisma.resource.findMany({
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
orderBy: { createdAt: 'desc' },
|
return adapter.deleteResource(id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResourcesByStorageType(storageType: StorageType): Promise<Resource[]> {
|
export async function deleteFailedUploadingResource(expirationPeriod: number): Promise<{ count: number }> {
|
||||||
return prisma.resource.findMany({
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
where: {
|
return adapter.deleteFailedUploadingResource(expirationPeriod);
|
||||||
storageType: storageType,
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResourcesByStatus(status: string): Promise<Resource[]> {
|
export async function updateResource(id: string, data: any): Promise<ResourceData> {
|
||||||
return prisma.resource.findMany({
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
where: { status },
|
return adapter.updateResource(id, data);
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUploadingResources(): Promise<Resource[]> {
|
|
||||||
return prisma.resource.findMany({
|
|
||||||
where: {
|
|
||||||
status: 'UPLOADING',
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getResourceStats(): Promise<{
|
|
||||||
total: number;
|
|
||||||
byStatus: Record<string, number>;
|
|
||||||
byStorageType: Record<string, number>;
|
|
||||||
}> {
|
|
||||||
const [total, statusStats, storageStats] = await Promise.all([
|
|
||||||
prisma.resource.count(),
|
|
||||||
prisma.resource.groupBy({
|
|
||||||
by: ['status'],
|
|
||||||
_count: true,
|
|
||||||
}),
|
|
||||||
prisma.resource.groupBy({
|
|
||||||
by: ['storageType'],
|
|
||||||
_count: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const byStatus = statusStats.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
acc[item.status || 'unknown'] = item._count || 0;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, number>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const byStorageType = storageStats.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const key = (item.storageType as string) || 'unknown';
|
|
||||||
acc[key] = item._count;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, number>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
byStatus,
|
|
||||||
byStorageType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteResource(id: string): Promise<Resource> {
|
|
||||||
return prisma.resource.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateResource(id: string, data: any): Promise<Resource> {
|
|
||||||
return prisma.resource.update({
|
|
||||||
where: { id },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function migrateResourcesStorageType(
|
export async function migrateResourcesStorageType(
|
||||||
fromStorageType: StorageType,
|
fromStorageType: StorageType,
|
||||||
toStorageType: StorageType,
|
toStorageType: StorageType,
|
||||||
): Promise<{ count: number }> {
|
): Promise<{ count: number }> {
|
||||||
const result = await prisma.resource.updateMany({
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
where: {
|
return adapter.migrateResourcesStorageType(fromStorageType, toStorageType);
|
||||||
storageType: fromStorageType,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
storageType: toStorageType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { count: result.count };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createResource(data: {
|
export async function createResource(data: CreateResourceData): Promise<ResourceData> {
|
||||||
fileId: string;
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
filename: string;
|
return adapter.createResource(data);
|
||||||
size: number;
|
|
||||||
mimeType?: string | null;
|
|
||||||
storageType: StorageType;
|
|
||||||
status?: string;
|
|
||||||
hash?: string;
|
|
||||||
}): Promise<Resource> {
|
|
||||||
return 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateResourceStatus(fileId: string, status: string, additionalData?: any): Promise<Resource> {
|
export async function updateResourceStatus(
|
||||||
return prisma.resource.update({
|
fileId: string,
|
||||||
where: { fileId },
|
status: string,
|
||||||
data: {
|
additionalData?: any,
|
||||||
status,
|
): Promise<ResourceData> {
|
||||||
...additionalData,
|
const adapter = adapterRegistry.getDatabaseAdapter();
|
||||||
},
|
return adapter.updateResourceStatus(fileId, status, additionalData);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export enum QueueJobType {
|
||||||
|
UPDATE_STATS = 'update_stats',
|
||||||
|
FILE_PROCESS = 'file_process',
|
||||||
|
UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount',
|
||||||
|
UPDATE_POST_STATE = 'updatePostState',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ResourceStatus {
|
||||||
|
UPLOADING = 'UPLOADING',
|
||||||
|
UPLOADED = 'UPLOADED',
|
||||||
|
PROCESS_PENDING = 'PROCESS_PENDING',
|
||||||
|
PROCESSING = 'PROCESSING',
|
||||||
|
PROCESSED = 'PROCESSED',
|
||||||
|
PROCESS_FAILED = 'PROCESS_FAILED',
|
||||||
|
}
|
|
@ -12,6 +12,10 @@ export * from './services';
|
||||||
|
|
||||||
// Hono 中间件
|
// Hono 中间件
|
||||||
export * from './middleware';
|
export * from './middleware';
|
||||||
|
export * from './enum';
|
||||||
|
|
||||||
|
// 适配器系统
|
||||||
|
export * from './adapters';
|
||||||
|
|
||||||
// TUS 协议支持 (已集成)
|
// TUS 协议支持 (已集成)
|
||||||
// TUS 相关功能通过 services 层提供,如需直接访问 TUS 类,可使用:
|
// TUS 相关功能通过 services 层提供,如需直接访问 TUS 类,可使用:
|
||||||
|
@ -24,3 +28,4 @@ export { StorageUtils } from './services';
|
||||||
export { getTusServer, handleTusRequest } from './services';
|
export { getTusServer, handleTusRequest } from './services';
|
||||||
export { startCleanupScheduler, triggerCleanup } from './services';
|
export { startCleanupScheduler, triggerCleanup } from './services';
|
||||||
export { createStorageApp, createStorageRoutes, createTusUploadRoutes, createFileDownloadRoutes } from './middleware';
|
export { createStorageApp, createStorageRoutes, createTusUploadRoutes, createFileDownloadRoutes } from './middleware';
|
||||||
|
export { adapterRegistry } from './adapters/adapter-registry';
|
||||||
|
|
|
@ -2,18 +2,12 @@ import { Hono } from 'hono';
|
||||||
import { handleTusRequest, cleanupExpiredUploads, getStorageInfo } from '../services/tus';
|
import { handleTusRequest, cleanupExpiredUploads, getStorageInfo } from '../services/tus';
|
||||||
import {
|
import {
|
||||||
getResourceByFileId,
|
getResourceByFileId,
|
||||||
getAllResources,
|
|
||||||
deleteResource,
|
deleteResource,
|
||||||
updateResource,
|
updateResource,
|
||||||
getResourcesByStorageType,
|
|
||||||
getResourcesByStatus,
|
|
||||||
getUploadingResources,
|
|
||||||
getResourceStats,
|
|
||||||
migrateResourcesStorageType,
|
migrateResourcesStorageType,
|
||||||
} from '../database/operations';
|
} from '../database/operations';
|
||||||
import { StorageManager, validateStorageConfig } from '../core/adapter';
|
import { StorageManager, validateStorageConfig } from '../core/adapter';
|
||||||
import { StorageType, type StorageConfig } from '../types';
|
import { StorageType, type StorageConfig } from '../types';
|
||||||
import { prisma } from '@repo/db';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建存储相关的 Hono 路由
|
* 创建存储相关的 Hono 路由
|
||||||
|
@ -33,37 +27,6 @@ export function createStorageRoutes(basePath: string = '/api/storage') {
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取所有资源
|
|
||||||
app.get('/resources', async (c) => {
|
|
||||||
const resources = await getAllResources();
|
|
||||||
return c.json(resources);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根据存储类型获取资源
|
|
||||||
app.get('/resources/storage/:storageType', async (c) => {
|
|
||||||
const storageType = c.req.param('storageType') as StorageType;
|
|
||||||
const resources = await getResourcesByStorageType(storageType);
|
|
||||||
return c.json(resources);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根据状态获取资源
|
|
||||||
app.get('/resources/status/:status', async (c) => {
|
|
||||||
const status = c.req.param('status');
|
|
||||||
const resources = await getResourcesByStatus(status);
|
|
||||||
return c.json(resources);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取正在上传的资源
|
|
||||||
app.get('/resources/uploading', async (c) => {
|
|
||||||
const resources = await getUploadingResources();
|
|
||||||
return c.json(resources);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取资源统计信息
|
|
||||||
app.get('/stats', async (c) => {
|
|
||||||
const stats = await getResourceStats();
|
|
||||||
return c.json(stats);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 删除资源
|
// 删除资源
|
||||||
app.delete('/resource/:id', async (c) => {
|
app.delete('/resource/:id', async (c) => {
|
||||||
|
@ -108,39 +71,6 @@ export function createStorageRoutes(basePath: string = '/api/storage') {
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 手动清理指定状态的资源
|
|
||||||
app.post('/cleanup/by-status', async (c) => {
|
|
||||||
try {
|
|
||||||
const { status, olderThanDays } = await c.req.json();
|
|
||||||
const cutoffDate = new Date();
|
|
||||||
cutoffDate.setDate(cutoffDate.getDate() - (olderThanDays || 30));
|
|
||||||
|
|
||||||
const deletedResources = await prisma.resource.deleteMany({
|
|
||||||
where: {
|
|
||||||
status,
|
|
||||||
createdAt: {
|
|
||||||
lt: cutoffDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
success: true,
|
|
||||||
message: `Deleted ${deletedResources.count} resources with status ${status}`,
|
|
||||||
count: deletedResources.count,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to cleanup by status:', error);
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取存储信息
|
// 获取存储信息
|
||||||
app.get('/storage/info', async (c) => {
|
app.get('/storage/info', async (c) => {
|
||||||
const storageInfo = getStorageInfo();
|
const storageInfo = getStorageInfo();
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
import { Server, Upload } from '../tus';
|
import { Server, Upload } from '../tus';
|
||||||
import { prisma } from '@repo/db';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { slugify } from 'transliteration';
|
import { slugify } from 'transliteration';
|
||||||
import { StorageManager } from '../core/adapter';
|
import { StorageManager } from '../core/adapter';
|
||||||
import { createResource, updateResourceStatus } from '../database/operations';
|
import { createResource, deleteFailedUploadingResource, updateResourceStatus } from '../database/operations';
|
||||||
|
import { ResourceStatus } from '../enum';
|
||||||
|
|
||||||
const FILE_UPLOAD_CONFIG = {
|
const FILE_UPLOAD_CONFIG = {
|
||||||
maxSizeBytes: 20_000_000_000, // 20GB
|
maxSizeBytes: 20_000_000_000, // 20GB
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum QueueJobType {
|
|
||||||
UPDATE_STATS = 'update_stats',
|
|
||||||
FILE_PROCESS = 'file_process',
|
|
||||||
UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount',
|
|
||||||
UPDATE_POST_STATE = 'updatePostState',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ResourceStatus {
|
|
||||||
UPLOADING = 'UPLOADING',
|
|
||||||
UPLOADED = 'UPLOADED',
|
|
||||||
PROCESS_PENDING = 'PROCESS_PENDING',
|
|
||||||
PROCESSING = 'PROCESSING',
|
|
||||||
PROCESSED = 'PROCESSED',
|
|
||||||
PROCESS_FAILED = 'PROCESS_FAILED',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局 TUS 服务器实例
|
// 全局 TUS 服务器实例
|
||||||
let tusServer: Server | null = null;
|
let tusServer: Server | null = null;
|
||||||
|
|
||||||
|
@ -149,14 +133,7 @@ export async function cleanupExpiredUploads() {
|
||||||
const expirationPeriod: number = 24 * 60 * 60 * 1000;
|
const expirationPeriod: number = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
// Delete incomplete uploads older than expiration period
|
// Delete incomplete uploads older than expiration period
|
||||||
const deletedResources = await prisma.resource.deleteMany({
|
const deletedResources = await deleteFailedUploadingResource(expirationPeriod);
|
||||||
where: {
|
|
||||||
createdAt: {
|
|
||||||
lt: new Date(Date.now() - expirationPeriod),
|
|
||||||
},
|
|
||||||
status: ResourceStatus.UPLOADING,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = getTusServer();
|
const server = getTusServer();
|
||||||
const expiredUploadCount = await server.cleanUpExpiredUploads();
|
const expiredUploadCount = await server.cleanUpExpiredUploads();
|
||||||
|
|
|
@ -49,3 +49,27 @@ export interface StorageConfig {
|
||||||
expirationPeriodInMilliseconds?: number;
|
expirationPeriodInMilliseconds?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 资源数据接口
|
||||||
|
export interface ResourceData {
|
||||||
|
id: string;
|
||||||
|
fileId: string;
|
||||||
|
title: string;
|
||||||
|
type?: string | null;
|
||||||
|
storageType: StorageType;
|
||||||
|
status: string;
|
||||||
|
meta?: any;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建资源数据接口
|
||||||
|
export interface CreateResourceData {
|
||||||
|
fileId: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
mimeType?: string | null;
|
||||||
|
storageType: StorageType;
|
||||||
|
status?: string;
|
||||||
|
hash?: string;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue