mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 21:59:39 +02:00
refactor(auth,planta): optimize storage usage
mana-core-auth: - Replace manual key generation (Date.now) with generateUserFileKey() - Replace manual validateFileSize with maxSizeBytes in upload() - Remove OnModuleInit — init storage directly in constructor - Add upload hooks for structured logging - Remove redundant getPublicUrl() fallback chain (presigned URL for 1 year) - Add deleteAllUserAvatars() for account deletion - Simplify getAvatarUploadUrl() using storage.getPublicUrl() planta: - Replace createStorageClient() with manual config by createPlantaStorage() - Replace manual uuid + path construction with generateUserFileKey() - Remove uuid dependency for key generation - Add maxSizeBytes validation (20MB) - Add cacheControl header (immutable, 1 year) - Add upload hooks for structured logging - Add error handling in deletePhoto() - Add deleteAllUserPhotos() for account deletion - Make getPhotoUrl() synchronous (was async unnecessarily) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6476521fd1
commit
e64c298cec
2 changed files with 61 additions and 107 deletions
|
|
@ -1,55 +1,64 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createStorageClient, StorageClient } from '@manacore/shared-storage';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
createPlantaStorage,
|
||||
generateUserFileKey,
|
||||
type StorageClient,
|
||||
} from '@manacore/shared-storage';
|
||||
|
||||
const MAX_PHOTO_SIZE = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
@Injectable()
|
||||
export class StorageService {
|
||||
private readonly logger = new Logger(StorageService.name);
|
||||
private storage: StorageClient;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const publicUrl = this.configService.get<string>('PLANTA_S3_PUBLIC_URL');
|
||||
this.storage = createStorageClient(
|
||||
{
|
||||
name: 'planta-storage',
|
||||
publicUrl,
|
||||
},
|
||||
{
|
||||
endpoint: this.configService.get<string>('S3_ENDPOINT'),
|
||||
region: this.configService.get<string>('S3_REGION'),
|
||||
accessKeyId: this.configService.get<string>('S3_ACCESS_KEY'),
|
||||
secretAccessKey: this.configService.get<string>('S3_SECRET_KEY'),
|
||||
}
|
||||
);
|
||||
constructor() {
|
||||
this.storage = createPlantaStorage();
|
||||
|
||||
this.storage.hooks.on('upload', ({ key, sizeBytes }) => {
|
||||
this.logger.debug(`Uploaded photo ${key} (${sizeBytes} bytes)`);
|
||||
});
|
||||
this.storage.hooks.on('upload:error', ({ key, error }) => {
|
||||
this.logger.error(`Photo upload failed for ${key}: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async uploadPhoto(
|
||||
userId: string,
|
||||
file: Express.Multer.File
|
||||
): Promise<{ storagePath: string; publicUrl: string }> {
|
||||
const extension = file.originalname.split('.').pop() || 'jpg';
|
||||
const filename = `${uuidv4()}.${extension}`;
|
||||
const storagePath = `users/${userId}/photos/${filename}`;
|
||||
const storagePath = generateUserFileKey(userId, file.originalname, 'photos');
|
||||
|
||||
await this.storage.upload(storagePath, file.buffer, {
|
||||
const result = await this.storage.upload(storagePath, file.buffer, {
|
||||
contentType: file.mimetype,
|
||||
public: true,
|
||||
maxSizeBytes: MAX_PHOTO_SIZE,
|
||||
cacheControl: 'public, max-age=31536000, immutable',
|
||||
});
|
||||
|
||||
const publicUrl = this.storage.getPublicUrl(storagePath) ?? '';
|
||||
|
||||
return { storagePath, publicUrl };
|
||||
return { storagePath, publicUrl: result.url ?? this.storage.getPublicUrl(storagePath) ?? '' };
|
||||
}
|
||||
|
||||
async deletePhoto(storagePath: string): Promise<void> {
|
||||
await this.storage.delete(storagePath);
|
||||
try {
|
||||
await this.storage.delete(storagePath);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to delete photo ${storagePath}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getPhotoUrl(storagePath: string): Promise<string> {
|
||||
getPhotoUrl(storagePath: string): string {
|
||||
return this.storage.getPublicUrl(storagePath) ?? '';
|
||||
}
|
||||
|
||||
async downloadPhoto(storagePath: string): Promise<Buffer> {
|
||||
return this.storage.download(storagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all photos for a user (account deletion).
|
||||
*/
|
||||
async deleteAllUserPhotos(userId: string): Promise<number> {
|
||||
return this.storage.deleteByPrefix(`users/${userId}/`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue