diff --git a/apps/picture/apps/backend/src/upload/storage.service.ts b/apps/picture/apps/backend/src/upload/storage.service.ts index 72e2eabf3..5fe3df955 100644 --- a/apps/picture/apps/backend/src/upload/storage.service.ts +++ b/apps/picture/apps/backend/src/upload/storage.service.ts @@ -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( + const publicUrl = this.configService.get( '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 { 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 { 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 { @@ -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 { + return this.storage.deleteByPrefix(`users/${userId}/`); } }