mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 06:06:42 +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
|
|
@ -1,6 +1,6 @@
|
|||
import { StorageClient } from './client';
|
||||
import { BUCKETS } from './types';
|
||||
import type { StorageConfig, BucketConfig, BucketName } from './types';
|
||||
import { UNIFIED_BUCKET, APPS } from './types';
|
||||
import type { StorageConfig, BucketConfig, AppName } from './types';
|
||||
|
||||
/**
|
||||
* Environment variable names for storage configuration
|
||||
|
|
@ -30,7 +30,6 @@ const MINIO_DEFAULTS: StorageConfig = {
|
|||
export function getStorageConfig(): StorageConfig {
|
||||
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
|
||||
|
||||
// Use environment variables if available, otherwise use MinIO defaults
|
||||
return {
|
||||
endpoint: process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''),
|
||||
region: process.env[ENV_KEYS.REGION] ?? MINIO_DEFAULTS.region,
|
||||
|
|
@ -45,7 +44,7 @@ export function getStorageConfig(): StorageConfig {
|
|||
* Create a storage client for a specific bucket
|
||||
*/
|
||||
export function createStorageClient(
|
||||
bucket: BucketName | BucketConfig,
|
||||
bucket: string | BucketConfig,
|
||||
config?: Partial<StorageConfig>
|
||||
): StorageClient {
|
||||
const storageConfig = {
|
||||
|
|
@ -55,7 +54,6 @@ export function createStorageClient(
|
|||
|
||||
const bucketConfig: BucketConfig = typeof bucket === 'string' ? { name: bucket } : bucket;
|
||||
|
||||
// Validate configuration
|
||||
if (!storageConfig.endpoint) {
|
||||
throw new Error('S3_ENDPOINT is required for storage configuration');
|
||||
}
|
||||
|
|
@ -67,83 +65,31 @@ export function createStorageClient(
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Picture project
|
||||
* Create the unified storage client for all Manacore apps
|
||||
*
|
||||
* Uses a single bucket with folder structure: {userId}/{appName}/...
|
||||
*
|
||||
* @example
|
||||
* import { createUnifiedStorage, generateStorageKey, APPS } from '@manacore/shared-storage';
|
||||
*
|
||||
* const storage = createUnifiedStorage();
|
||||
*
|
||||
* // Upload for a specific user and app
|
||||
* const key = generateStorageKey('user-123', APPS.PICTURE, 'photo.jpg');
|
||||
* await storage.upload(key, imageBuffer, { contentType: 'image/jpeg', public: true });
|
||||
*
|
||||
* // List all files for a user in an app
|
||||
* const files = await storage.list('user-123/picture/');
|
||||
*/
|
||||
export function createPictureStorage(publicUrl?: string): StorageClient {
|
||||
export function createUnifiedStorage(publicUrl?: string): StorageClient {
|
||||
return createStorageClient({
|
||||
name: BUCKETS.PICTURE,
|
||||
publicUrl: publicUrl ?? process.env.PICTURE_STORAGE_PUBLIC_URL,
|
||||
name: UNIFIED_BUCKET,
|
||||
publicUrl: publicUrl ?? process.env.MANACORE_STORAGE_PUBLIC_URL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Chat project
|
||||
* Re-export constants and types for convenience
|
||||
*/
|
||||
export function createChatStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.CHAT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the ManaDeck project
|
||||
*/
|
||||
export function createManaDeckStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.MANADECK });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the NutriPhi project
|
||||
*/
|
||||
export function createNutriPhiStorage(publicUrl?: string): StorageClient {
|
||||
return createStorageClient({
|
||||
name: BUCKETS.NUTRIPHI,
|
||||
publicUrl: publicUrl ?? process.env.NUTRIPHI_S3_PUBLIC_URL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Presi project
|
||||
*/
|
||||
export function createPresiStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.PRESI });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Calendar project
|
||||
*/
|
||||
export function createCalendarStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.CALENDAR });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Contacts project
|
||||
*/
|
||||
export function createContactsStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.CONTACTS });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Storage project (cloud drive)
|
||||
*/
|
||||
export function createStorageStorage(publicUrl?: string): StorageClient {
|
||||
return createStorageClient({
|
||||
name: BUCKETS.STORAGE,
|
||||
publicUrl: publicUrl ?? process.env.STORAGE_S3_PUBLIC_URL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Mail project
|
||||
*/
|
||||
export function createMailStorage(): StorageClient {
|
||||
return createStorageClient({ name: BUCKETS.MAIL });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage client for the Inventory project
|
||||
*/
|
||||
export function createInventoryStorage(publicUrl?: string): StorageClient {
|
||||
return createStorageClient({
|
||||
name: BUCKETS.INVENTORY,
|
||||
publicUrl: publicUrl ?? process.env.INVENTORY_S3_PUBLIC_URL,
|
||||
});
|
||||
}
|
||||
export { UNIFIED_BUCKET, APPS };
|
||||
export type { AppName };
|
||||
|
|
|
|||
|
|
@ -4,23 +4,17 @@ export { StorageClient } from './client';
|
|||
// Factory functions
|
||||
export {
|
||||
createStorageClient,
|
||||
createUnifiedStorage,
|
||||
getStorageConfig,
|
||||
createPictureStorage,
|
||||
createChatStorage,
|
||||
createManaDeckStorage,
|
||||
createNutriPhiStorage,
|
||||
createPresiStorage,
|
||||
createCalendarStorage,
|
||||
createContactsStorage,
|
||||
createStorageStorage,
|
||||
createMailStorage,
|
||||
createInventoryStorage,
|
||||
UNIFIED_BUCKET,
|
||||
APPS,
|
||||
} from './factory';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
generateFileKey,
|
||||
generateUserFileKey,
|
||||
generateStorageKey,
|
||||
getContentType,
|
||||
validateFileSize,
|
||||
validateFileExtension,
|
||||
|
|
@ -31,13 +25,12 @@ export {
|
|||
} from './utils';
|
||||
|
||||
// Types
|
||||
export {
|
||||
BUCKETS,
|
||||
type StorageConfig,
|
||||
type BucketConfig,
|
||||
type BucketName,
|
||||
type UploadOptions,
|
||||
type PresignedUrlOptions,
|
||||
type UploadResult,
|
||||
type FileInfo,
|
||||
export type {
|
||||
StorageConfig,
|
||||
BucketConfig,
|
||||
AppName,
|
||||
UploadOptions,
|
||||
PresignedUrlOptions,
|
||||
UploadResult,
|
||||
FileInfo,
|
||||
} from './types';
|
||||
|
|
|
|||
|
|
@ -73,19 +73,26 @@ export interface FileInfo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Predefined bucket names for each project
|
||||
* Unified bucket name for all Manacore storage
|
||||
* Structure: manacore-storage/{userId}/{appName}/...
|
||||
*/
|
||||
export const BUCKETS = {
|
||||
PICTURE: 'picture-storage',
|
||||
CHAT: 'chat-storage',
|
||||
MANADECK: 'manadeck-storage',
|
||||
NUTRIPHI: 'nutriphi-storage',
|
||||
PRESI: 'presi-storage',
|
||||
CALENDAR: 'calendar-storage',
|
||||
CONTACTS: 'contacts-storage',
|
||||
STORAGE: 'storage-storage',
|
||||
MAIL: 'mail-storage',
|
||||
INVENTORY: 'inventory-storage',
|
||||
export const UNIFIED_BUCKET = 'manacore-storage';
|
||||
|
||||
/**
|
||||
* App identifiers for folder structure within the unified bucket
|
||||
*/
|
||||
export const APPS = {
|
||||
PICTURE: 'picture',
|
||||
CHAT: 'chat',
|
||||
MANADECK: 'manadeck',
|
||||
NUTRIPHI: 'nutriphi',
|
||||
PRESI: 'presi',
|
||||
CALENDAR: 'calendar',
|
||||
CONTACTS: 'contacts',
|
||||
STORAGE: 'storage',
|
||||
MAIL: 'mail',
|
||||
INVENTORY: 'inventory',
|
||||
MANACORE: 'manacore',
|
||||
} as const;
|
||||
|
||||
export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS];
|
||||
export type AppName = (typeof APPS)[keyof typeof APPS];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { extname } from 'path';
|
||||
import type { AppName } from './types';
|
||||
|
||||
/**
|
||||
* Generate a unique file key with optional folder structure
|
||||
|
|
@ -23,6 +24,30 @@ export function generateFileKey(filename: string, ...folders: string[]): string
|
|||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a storage key for the unified bucket structure
|
||||
*
|
||||
* @example
|
||||
* generateStorageKey('user-123', 'picture', 'photo.jpg')
|
||||
* // => 'user-123/picture/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg'
|
||||
*
|
||||
* generateStorageKey('user-123', 'chat', 'document.pdf', 'attachments')
|
||||
* // => 'user-123/chat/attachments/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf'
|
||||
*/
|
||||
export function generateStorageKey(
|
||||
userId: string,
|
||||
appName: AppName | string,
|
||||
filename: string,
|
||||
...subfolders: string[]
|
||||
): string {
|
||||
const ext = extname(filename);
|
||||
const uuid = randomUUID();
|
||||
const file = `${uuid}${ext}`;
|
||||
|
||||
const parts = [userId, appName, ...subfolders, file];
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a user-scoped file key
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue