mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
refactor(picture): optimize storage service
- Replace manual key generation (Date.now + Math.random) with generateUserFileKey() for UUID-based collision-free keys - Add maxSizeBytes validation (50MB images, 5MB thumbnails) - Add cacheControl headers (immutable for images, 7d for thumbnails) - Add upload hooks for structured logging - Simplify constructor (remove OnModuleInit, init storage directly) - Remove unused StorageMode type and getStorageMode() - Add deleteAllUserFiles() for account deletion - Consistent error handling in deleteFile() (warn + swallow) - Board thumbnails use stable path (no timestamp) for overwrite Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
887b60c8c1
commit
69eac206fa
1 changed files with 30 additions and 37 deletions
|
|
@ -1,32 +1,28 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
createPictureStorage,
|
||||
StorageClient,
|
||||
generateUserFileKey,
|
||||
getContentType,
|
||||
} from '@manacore/shared-storage';
|
||||
import { createPictureStorage, StorageClient, generateUserFileKey } from '@manacore/shared-storage';
|
||||
|
||||
export type StorageMode = 's3';
|
||||
const MAX_IMAGE_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
const MAX_THUMBNAIL_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
@Injectable()
|
||||
export class StorageService implements OnModuleInit {
|
||||
export class StorageService {
|
||||
private readonly logger = new Logger(StorageService.name);
|
||||
private storage!: StorageClient;
|
||||
private publicUrl: string;
|
||||
private storage: StorageClient;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
// Get public URL from config
|
||||
this.publicUrl = this.configService.get<string>(
|
||||
const publicUrl = this.configService.get<string>(
|
||||
'STORAGE_PUBLIC_URL',
|
||||
'http://localhost:9000/picture-storage'
|
||||
);
|
||||
}
|
||||
this.storage = createPictureStorage(publicUrl);
|
||||
|
||||
onModuleInit() {
|
||||
// Initialize storage client
|
||||
this.storage = createPictureStorage(this.publicUrl);
|
||||
this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: picture-storage)`);
|
||||
this.storage.hooks.on('upload', ({ key, sizeBytes, contentType }) => {
|
||||
this.logger.debug(`Uploaded ${key} (${sizeBytes} bytes, ${contentType})`);
|
||||
});
|
||||
this.storage.hooks.on('upload:error', ({ key, error }) => {
|
||||
this.logger.error(`Upload failed for ${key}: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
|
|
@ -35,21 +31,16 @@ export class StorageService implements OnModuleInit {
|
|||
filename: string,
|
||||
contentType: string
|
||||
): Promise<{ storagePath: string; publicUrl: string }> {
|
||||
const timestamp = Date.now();
|
||||
const randomId = Math.random().toString(36).substring(2, 10);
|
||||
const ext = filename.split('.').pop() || 'jpg';
|
||||
const storagePath = `${userId}/${timestamp}-${randomId}.${ext}`;
|
||||
const storagePath = generateUserFileKey(userId, filename, 'images');
|
||||
|
||||
const result = await this.storage.upload(storagePath, buffer, {
|
||||
contentType,
|
||||
public: true,
|
||||
maxSizeBytes: MAX_IMAGE_SIZE,
|
||||
cacheControl: 'public, max-age=31536000, immutable',
|
||||
});
|
||||
|
||||
const publicUrl = result.url || this.getPublicUrl(storagePath);
|
||||
|
||||
this.logger.debug(`Uploaded file to ${storagePath}`);
|
||||
|
||||
return { storagePath, publicUrl };
|
||||
return { storagePath, publicUrl: result.url ?? this.getPublicUrl(storagePath) };
|
||||
}
|
||||
|
||||
async uploadFromUrl(
|
||||
|
|
@ -57,7 +48,6 @@ export class StorageService implements OnModuleInit {
|
|||
userId: string,
|
||||
filename: string
|
||||
): Promise<{ storagePath: string; publicUrl: string }> {
|
||||
// Download the file
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download file from ${url}`);
|
||||
|
|
@ -72,24 +62,24 @@ export class StorageService implements OnModuleInit {
|
|||
async deleteFile(storagePath: string): Promise<void> {
|
||||
try {
|
||||
await this.storage.delete(storagePath);
|
||||
this.logger.debug(`Deleted file: ${storagePath}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting file ${storagePath}`, error);
|
||||
throw error;
|
||||
this.logger.warn(`Failed to delete file ${storagePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBoardThumbnail(boardId: string, dataUrl: string): Promise<string> {
|
||||
const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
const storagePath = `boards/${boardId}/thumbnail-${Date.now()}.png`;
|
||||
const storagePath = `boards/${boardId}/thumbnail.png`;
|
||||
|
||||
const result = await this.storage.upload(storagePath, buffer, {
|
||||
contentType: 'image/png',
|
||||
public: true,
|
||||
maxSizeBytes: MAX_THUMBNAIL_SIZE,
|
||||
cacheControl: 'public, max-age=604800', // 7 days (thumbnails can be regenerated)
|
||||
});
|
||||
|
||||
return result.url || this.getPublicUrl(storagePath);
|
||||
return result.url ?? this.getPublicUrl(storagePath);
|
||||
}
|
||||
|
||||
async getFile(storagePath: string): Promise<Buffer | null> {
|
||||
|
|
@ -101,11 +91,14 @@ export class StorageService implements OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
getStorageMode(): StorageMode {
|
||||
return 's3';
|
||||
getPublicUrl(storagePath: string): string {
|
||||
return this.storage.getPublicUrl(storagePath) ?? '';
|
||||
}
|
||||
|
||||
getPublicUrl(storagePath: string): string {
|
||||
return this.storage.getPublicUrl(storagePath) || `${this.publicUrl}/${storagePath}`;
|
||||
/**
|
||||
* Delete all files for a user (account deletion).
|
||||
*/
|
||||
async deleteAllUserFiles(userId: string): Promise<number> {
|
||||
return this.storage.deleteByPrefix(`users/${userId}/`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue