mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-25 06:44:39 +02:00
feat(storage): unified single-bucket architecture with Hetzner S3
- Refactor @manacore/shared-storage to use single `manacore-storage` bucket
- Add generateStorageKey() for path structure: {userId}/{appName}/...
- Update docker-compose.dev.yml for unified MinIO bucket
- Migrate CD workflow to use GitHub Environment Secrets
- Update picture and contacts backends to use unified storage
- Remove per-app bucket configuration (cleaner architecture)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d268e8e463
commit
78cd59a77a
10 changed files with 225 additions and 251 deletions
|
|
@ -4,19 +4,19 @@ import { DATABASE_CONNECTION } from '../db/database.module';
|
|||
import { Database } from '../db/connection';
|
||||
import { contacts } from '../db/schema';
|
||||
import {
|
||||
createContactsStorage,
|
||||
generateUserFileKey,
|
||||
createUnifiedStorage,
|
||||
getContentType,
|
||||
validateFileSize,
|
||||
validateFileExtension,
|
||||
IMAGE_EXTENSIONS,
|
||||
APPS,
|
||||
} from '@manacore/shared-storage';
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
@Injectable()
|
||||
export class PhotoService {
|
||||
private storage = createContactsStorage();
|
||||
private storage = createUnifiedStorage();
|
||||
|
||||
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
|
||||
|
||||
|
|
@ -66,19 +66,22 @@ export class PhotoService {
|
|||
}
|
||||
}
|
||||
|
||||
// Generate unique key for the new photo
|
||||
// Generate unique key for the new photo: {userId}/contacts/{contactId}.{ext}
|
||||
const filename = `${contactId}.${extension}`;
|
||||
const key = generateUserFileKey(userId, filename);
|
||||
const key = `${userId}/${APPS.CONTACTS}/${filename}`;
|
||||
|
||||
// Upload to S3
|
||||
const contentType = getContentType(filename);
|
||||
await this.storage.upload(key, file.buffer, {
|
||||
const result = await this.storage.upload(key, file.buffer, {
|
||||
contentType,
|
||||
public: true,
|
||||
});
|
||||
|
||||
// Generate the URL (for MinIO, construct it manually)
|
||||
const photoUrl = `http://localhost:9000/contacts-storage/${key}`;
|
||||
// Get URL from storage client or construct manually
|
||||
const photoUrl =
|
||||
result.url ||
|
||||
this.storage.getPublicUrl(key) ||
|
||||
`${process.env.MANACORE_STORAGE_PUBLIC_URL || 'http://localhost:9000/manacore-storage'}/${key}`;
|
||||
|
||||
// Update contact with photo URL
|
||||
await this.db
|
||||
|
|
@ -125,8 +128,12 @@ export class PhotoService {
|
|||
}
|
||||
|
||||
private extractKeyFromUrl(url: string): string | null {
|
||||
// Extract key from URLs like http://localhost:9000/contacts-storage/users/xxx/file.jpg
|
||||
const match = url.match(/contacts-storage\/(.+)$/);
|
||||
return match ? match[1] : null;
|
||||
// Extract key from URLs like http://localhost:9000/manacore-storage/userId/contacts/file.jpg
|
||||
// Also support old format: http://localhost:9000/contacts-storage/users/xxx/file.jpg
|
||||
const unifiedMatch = url.match(/manacore-storage\/(.+)$/);
|
||||
if (unifiedMatch) return unifiedMatch[1];
|
||||
|
||||
const legacyMatch = url.match(/contacts-storage\/(.+)$/);
|
||||
return legacyMatch ? legacyMatch[1] : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
createPictureStorage,
|
||||
StorageClient,
|
||||
generateUserFileKey,
|
||||
getContentType,
|
||||
} from '@manacore/shared-storage';
|
||||
import { createUnifiedStorage, StorageClient, APPS } from '@manacore/shared-storage';
|
||||
|
||||
export type StorageMode = 's3';
|
||||
|
||||
|
|
@ -18,15 +13,15 @@ export class StorageService implements OnModuleInit {
|
|||
constructor(private configService: ConfigService) {
|
||||
// Get public URL from config
|
||||
this.publicUrl = this.configService.get<string>(
|
||||
'STORAGE_PUBLIC_URL',
|
||||
'http://localhost:9000/picture-storage'
|
||||
'MANACORE_STORAGE_PUBLIC_URL',
|
||||
'http://localhost:9000/manacore-storage'
|
||||
);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
// Initialize storage client
|
||||
this.storage = createPictureStorage(this.publicUrl);
|
||||
this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: picture-storage)`);
|
||||
// Initialize unified storage client
|
||||
this.storage = createUnifiedStorage(this.publicUrl);
|
||||
this.logger.log(`Storage initialized with @manacore/shared-storage (bucket: manacore-storage)`);
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
|
|
@ -38,7 +33,8 @@ export class StorageService implements OnModuleInit {
|
|||
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}`;
|
||||
// Path: {userId}/picture/{timestamp}-{randomId}.{ext}
|
||||
const storagePath = `${userId}/${APPS.PICTURE}/${timestamp}-${randomId}.${ext}`;
|
||||
|
||||
const result = await this.storage.upload(storagePath, buffer, {
|
||||
contentType,
|
||||
|
|
@ -82,7 +78,8 @@ export class StorageService implements OnModuleInit {
|
|||
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`;
|
||||
// Path: boards/picture/{boardId}/thumbnail-{timestamp}.png
|
||||
const storagePath = `boards/${APPS.PICTURE}/${boardId}/thumbnail-${Date.now()}.png`;
|
||||
|
||||
const result = await this.storage.upload(storagePath, buffer, {
|
||||
contentType: 'image/png',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue