Files
TrueGrowth/specs/009-media-library/contracts/asset-storage-service.md

408 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API Contract: Asset Storage Service
**Service**: `asset-storage-service.ts`
**Feature**: 009-media-library
**Date**: 2025-12-11
## Overview
定义素材存储服务的接口契约该服务负责与IndexedDB交互管理素材的持久化存储。
## Service Interface
```typescript
export interface AssetStorageService {
/**
* 初始化存储服务
* 创建IndexedDB实例和必要的stores
*/
initialize(): Promise<void>;
/**
* 添加新素材到存储
* @param data - 素材数据
* @returns 保存的素材对象
* @throws QuotaExceededError - 存储空间不足
* @throws ValidationError - 数据验证失败
*/
addAsset(data: AddAssetData): Promise<Asset>;
/**
* 获取所有素材
* @returns 所有素材的数组
*/
getAllAssets(): Promise<Asset[]>;
/**
* 根据ID获取单个素材
* @param id - 素材ID
* @returns 素材对象不存在则返回null
*/
getAssetById(id: string): Promise<Asset | null>;
/**
* 更新素材名称
* @param id - 素材ID
* @param newName - 新名称
* @throws NotFoundError - 素材不存在
* @throws ValidationError - 名称验证失败
*/
renameAsset(id: string, newName: string): Promise<void>;
/**
* 删除素材
* @param id - 素材ID
* @throws NotFoundError - 素材不存在
*/
removeAsset(id: string): Promise<void>;
/**
* 清空所有素材
* 慎用,不可恢复
*/
clearAll(): Promise<void>;
/**
* 获取存储统计信息
* @returns 存储使用情况
*/
getStorageStats(): Promise<StorageStats>;
/**
* 检查存储配额
* @returns 配额信息
*/
checkQuota(): Promise<StorageQuota>;
/**
* 估算添加新素材后的存储使用量
* @param blobSize - Blob大小字节
* @returns 是否有足够空间
*/
canAddAsset(blobSize: number): Promise<boolean>;
}
```
## Data Types
### AddAssetData
```typescript
export interface AddAssetData {
type: AssetType;
source: AssetSource;
name: string;
blob: Blob;
mimeType: string;
prompt?: string;
modelName?: string;
}
```
### StorageStats
```typescript
export interface StorageStats {
totalAssets: number;
imageCount: number;
videoCount: number;
localCount: number;
aiGeneratedCount: number;
totalSize: number; // 估算的总大小(字节)
}
```
### Error Types
```typescript
export class AssetStorageError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'AssetStorageError';
}
}
export class QuotaExceededError extends AssetStorageError {
constructor() {
super('存储空间不足', 'QUOTA_EXCEEDED');
}
}
export class NotFoundError extends AssetStorageError {
constructor(id: string) {
super(`素材未找到: ${id}`, 'NOT_FOUND');
}
}
export class ValidationError extends AssetStorageError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR');
}
}
```
## Usage Examples
### Initialize Service
```typescript
import { assetStorageService } from './services/asset-storage-service';
// 在应用启动时初始化
await assetStorageService.initialize();
```
### Add Asset
```typescript
// 用户上传文件
const file: File = event.target.files[0];
try {
const asset = await assetStorageService.addAsset({
type: AssetType.IMAGE,
source: AssetSource.LOCAL,
name: file.name,
blob: file,
mimeType: file.type
});
console.log('Asset added:', asset.id);
} catch (error) {
if (error instanceof QuotaExceededError) {
// 显示存储空间不足错误
} else if (error instanceof ValidationError) {
// 显示验证错误
}
}
```
### Load All Assets
```typescript
try {
const assets = await assetStorageService.getAllAssets();
console.log(`Loaded ${assets.length} assets`);
} catch (error) {
console.error('Failed to load assets:', error);
}
```
### Rename Asset
```typescript
try {
await assetStorageService.renameAsset(assetId, '新名称');
console.log('Asset renamed successfully');
} catch (error) {
if (error instanceof NotFoundError) {
// 素材不存在
} else if (error instanceof ValidationError) {
// 名称验证失败
}
}
```
### Delete Asset
```typescript
try {
await assetStorageService.removeAsset(assetId);
console.log('Asset deleted');
} catch (error) {
if (error instanceof NotFoundError) {
// 素材已被删除或不存在
}
}
```
### Check Storage Quota
```typescript
const quota = await assetStorageService.checkQuota();
console.log(`Storage usage: ${quota.percentUsed.toFixed(2)}%`);
if (quota.percentUsed > 80) {
// 显示警告
console.warn('Storage is nearly full!');
}
```
### Check Before Adding
```typescript
const fileSize = file.size;
const canAdd = await assetStorageService.canAddAsset(fileSize);
if (!canAdd) {
alert('存储空间不足,请删除一些旧素材');
return;
}
// 继续添加
await assetStorageService.addAsset({...});
```
## Implementation Notes
### 1. localforage Configuration
```typescript
import localforage from 'localforage';
const assetStore = localforage.createInstance({
name: 'aitu-assets',
storeName: 'assets',
description: 'Media library assets storage'
});
```
### 2. Blob URL Management
- 创建时使用 `URL.createObjectURL(blob)`
- 组件卸载时调用 `URL.revokeObjectURL(url)` 释放内存
- 不直接存储 Blob URL 到 IndexedDB存储 Blob 对象本身)
### 3. Error Handling Strategy
```typescript
async function wrapStorageOperation<T>(
operation: () => Promise<T>,
errorContext: string
): Promise<T> {
try {
return await operation();
} catch (error) {
if (error.name === 'QuotaExceededError') {
throw new QuotaExceededError();
}
if (error.name === 'NotFoundError') {
throw new NotFoundError(errorContext);
}
throw new AssetStorageError(
`Storage operation failed: ${error.message}`,
'UNKNOWN_ERROR'
);
}
}
```
### 4. Performance Considerations
- **批量加载**: 使用 `getAllAssets()` 一次加载所有元数据
- **懒加载Blob**: 网格项只在可见时加载图片
- **索引**: 考虑为 `type`, `source`, `createdAt` 添加索引如使用原生IndexedDB
- **缓存**: 在内存中缓存最近加载的assets减少IndexedDB查询
## Testing Contract
### Unit Tests
```typescript
describe('AssetStorageService', () => {
beforeEach(async () => {
await assetStorageService.initialize();
await assetStorageService.clearAll();
});
it('should add asset successfully', async () => {
const blob = new Blob(['test'], { type: 'image/png' });
const asset = await assetStorageService.addAsset({
type: AssetType.IMAGE,
source: AssetSource.LOCAL,
name: 'test.png',
blob,
mimeType: 'image/png'
});
expect(asset.id).toBeDefined();
expect(asset.type).toBe(AssetType.IMAGE);
});
it('should throw ValidationError for invalid name', async () => {
const blob = new Blob(['test'], { type: 'image/png' });
await expect(
assetStorageService.addAsset({
type: AssetType.IMAGE,
source: AssetSource.LOCAL,
name: '', // Invalid
blob,
mimeType: 'image/png'
})
).rejects.toThrow(ValidationError);
});
it('should get all assets', async () => {
// Add multiple assets
const blob = new Blob(['test'], { type: 'image/png' });
await assetStorageService.addAsset({ /* ... */ });
await assetStorageService.addAsset({ /* ... */ });
const assets = await assetStorageService.getAllAssets();
expect(assets).toHaveLength(2);
});
it('should rename asset', async () => {
const blob = new Blob(['test'], { type: 'image/png' });
const asset = await assetStorageService.addAsset({ /* ... */ });
await assetStorageService.renameAsset(asset.id, 'new-name.png');
const updated = await assetStorageService.getAssetById(asset.id);
expect(updated?.name).toBe('new-name.png');
});
it('should delete asset', async () => {
const blob = new Blob(['test'], { type: 'image/png' });
const asset = await assetStorageService.addAsset({ /* ... */ });
await assetStorageService.removeAsset(asset.id);
const deleted = await assetStorageService.getAssetById(asset.id);
expect(deleted).toBeNull();
});
it('should throw NotFoundError when deleting non-existent asset', async () => {
await expect(
assetStorageService.removeAsset('non-existent-id')
).rejects.toThrow(NotFoundError);
});
});
```
## Security Considerations
1. **Input Validation**: 所有用户输入name, mimeType必须验证
2. **MIME Type Verification**: 验证文件实际内容与声明的MIME类型匹配
3. **Size Limits**: 虽然没有硬性限制,但应警告大文件
4. **Error Messages**: 不暴露内部实现细节给用户
## Migration Strategy
如果将来需要更改存储格式:
```typescript
export interface StorageVersion {
version: number;
migrateFrom(oldVersion: number): Promise<void>;
}
// 例如从v1迁移到v2
async function migrateV1ToV2(): Promise<void> {
const allAssets = await assetStore.keys();
for (const key of allAssets) {
const old = await assetStore.getItem(key);
const migrated = transformV1ToV2(old);
await assetStore.setItem(key, migrated);
}
}
```
---
**契约完成日期**: 2025-12-11
**实现文件**: `packages/drawnix/src/services/asset-storage-service.ts`
**下一步**: 生成 quickstart.md 开发指南