mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +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
|
|
@ -36,6 +36,9 @@ S3_REGION=us-east-1
|
|||
S3_ACCESS_KEY=minioadmin
|
||||
S3_SECRET_KEY=minioadmin
|
||||
|
||||
# Unified Storage (single bucket for all apps)
|
||||
MANACORE_STORAGE_PUBLIC_URL=http://localhost:9000/manacore-storage
|
||||
|
||||
# ============================================
|
||||
# MANA-CORE-AUTH SERVICE
|
||||
# ============================================
|
||||
|
|
@ -127,9 +130,7 @@ PICTURE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/picture
|
|||
# Replicate API Token for AI image generation
|
||||
PICTURE_REPLICATE_API_TOKEN=r8_QlvkstNhIc6NBX1ktpQ6ibvzOE2d2UQ1Emamd
|
||||
|
||||
# Storage Configuration (uses MinIO locally, Hetzner in production)
|
||||
# Uses shared S3_* variables from above - no project-specific override needed for local dev
|
||||
PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-storage
|
||||
# Storage: Uses unified manacore-storage bucket (see MANACORE_STORAGE_PUBLIC_URL above)
|
||||
|
||||
# Credit System (staging only - freemium: 3 free images, then credits)
|
||||
PICTURE_APP_ID=picture-app
|
||||
|
|
@ -148,8 +149,7 @@ NUTRIPHI_DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435
|
|||
NUTRIPHI_APP_ID=nutriphi
|
||||
NUTRIPHI_GEMINI_API_KEY=your-gemini-api-key-here
|
||||
|
||||
# S3 Storage (uses MinIO locally via shared S3_* variables, Hetzner in production)
|
||||
NUTRIPHI_S3_PUBLIC_URL=http://localhost:9000/nutriphi-storage
|
||||
# Storage: Uses unified manacore-storage bucket
|
||||
|
||||
# ============================================
|
||||
# ZITARE PROJECT
|
||||
|
|
@ -180,9 +180,7 @@ VOXEL_LAVA_API_URL=http://localhost:3010
|
|||
CONTACTS_BACKEND_PORT=3015
|
||||
CONTACTS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/contacts
|
||||
|
||||
# S3 Storage for contact photos
|
||||
CONTACTS_S3_BUCKET=contacts-photos
|
||||
CONTACTS_S3_PUBLIC_URL=http://localhost:9000/contacts-photos
|
||||
# Storage: Uses unified manacore-storage bucket
|
||||
|
||||
# Google OAuth for contacts import
|
||||
# Get credentials from https://console.cloud.google.com/apis/credentials
|
||||
|
|
@ -204,7 +202,6 @@ CALENDAR_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/calendar
|
|||
|
||||
STORAGE_BACKEND_PORT=3016
|
||||
STORAGE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/storage
|
||||
STORAGE_S3_PUBLIC_URL=http://localhost:9000/storage-storage
|
||||
STORAGE_MAX_FILE_SIZE=104857600
|
||||
STORAGE_MAX_FILES_PER_UPLOAD=10
|
||||
|
||||
|
|
@ -265,7 +262,6 @@ FINANCE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/finance
|
|||
|
||||
INVENTORY_BACKEND_PORT=3020
|
||||
INVENTORY_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/inventory
|
||||
INVENTORY_S3_PUBLIC_URL=http://localhost:9000/inventory-storage
|
||||
|
||||
# ============================================
|
||||
# TECHBASE PROJECT
|
||||
|
|
|
|||
27
.github/workflows/cd-staging.yml
vendored
27
.github/workflows/cd-staging.yml
vendored
|
|
@ -98,29 +98,36 @@ jobs:
|
|||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=manacore
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=${{ secrets.STAGING_POSTGRES_PASSWORD }}
|
||||
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
|
||||
|
||||
# Redis - Configuration
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=${{ secrets.STAGING_REDIS_PASSWORD }}
|
||||
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
|
||||
|
||||
# Mana Core Auth - Configuration
|
||||
MANA_SERVICE_URL=http://mana-core-auth:3001
|
||||
JWT_SECRET=${{ secrets.STAGING_JWT_SECRET }}
|
||||
JWT_PUBLIC_KEY=${{ secrets.STAGING_JWT_PUBLIC_KEY }}
|
||||
JWT_PRIVATE_KEY=${{ secrets.STAGING_JWT_PRIVATE_KEY }}
|
||||
JWT_SECRET=${{ secrets.JWT_SECRET }}
|
||||
JWT_PUBLIC_KEY=${{ secrets.JWT_PUBLIC_KEY }}
|
||||
JWT_PRIVATE_KEY=${{ secrets.JWT_PRIVATE_KEY }}
|
||||
|
||||
# Supabase
|
||||
SUPABASE_URL=${{ secrets.STAGING_SUPABASE_URL }}
|
||||
SUPABASE_ANON_KEY=${{ secrets.STAGING_SUPABASE_ANON_KEY }}
|
||||
SUPABASE_SERVICE_ROLE_KEY=${{ secrets.STAGING_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
SUPABASE_URL=${{ secrets.SUPABASE_URL }}
|
||||
SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}
|
||||
SUPABASE_SERVICE_ROLE_KEY=${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
# Azure OpenAI
|
||||
AZURE_OPENAI_ENDPOINT=${{ secrets.STAGING_AZURE_OPENAI_ENDPOINT }}
|
||||
AZURE_OPENAI_API_KEY=${{ secrets.STAGING_AZURE_OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_ENDPOINT=${{ secrets.AZURE_OPENAI_ENDPOINT }}
|
||||
AZURE_OPENAI_API_KEY=${{ secrets.AZURE_OPENAI_API_KEY }}
|
||||
AZURE_OPENAI_API_VERSION=2024-12-01-preview
|
||||
|
||||
# Hetzner Object Storage (S3-compatible)
|
||||
S3_ENDPOINT=${{ secrets.S3_ENDPOINT }}
|
||||
S3_REGION=${{ secrets.S3_REGION }}
|
||||
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}
|
||||
MANACORE_STORAGE_PUBLIC_URL=${{ secrets.MANACORE_STORAGE_PUBLIC_URL }}
|
||||
|
||||
# Environment
|
||||
NODE_ENV=staging
|
||||
EOF
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -76,18 +76,9 @@ services:
|
|||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set myminio http://minio:9000 $${MINIO_ROOT_USER:-minioadmin} $${MINIO_ROOT_PASSWORD:-minioadmin};
|
||||
mc mb --ignore-existing myminio/picture-storage;
|
||||
mc mb --ignore-existing myminio/chat-storage;
|
||||
mc mb --ignore-existing myminio/manadeck-storage;
|
||||
mc mb --ignore-existing myminio/nutriphi-storage;
|
||||
mc mb --ignore-existing myminio/presi-storage;
|
||||
mc mb --ignore-existing myminio/calendar-storage;
|
||||
mc mb --ignore-existing myminio/contacts-storage;
|
||||
mc mb --ignore-existing myminio/storage-storage;
|
||||
mc mb --ignore-existing myminio/inventory-storage;
|
||||
mc anonymous set download myminio/picture-storage;
|
||||
mc anonymous set download myminio/inventory-storage;
|
||||
echo 'Buckets created successfully';
|
||||
mc mb --ignore-existing myminio/manacore-storage;
|
||||
mc anonymous set download myminio/manacore-storage;
|
||||
echo 'Bucket manacore-storage created successfully';
|
||||
exit 0;
|
||||
"
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -2,53 +2,71 @@
|
|||
|
||||
S3-compatible object storage client for the Manacore monorepo. Uses MinIO for local development and Hetzner Object Storage in production.
|
||||
|
||||
## Architecture
|
||||
|
||||
All apps use a **single unified bucket** with folder structure:
|
||||
|
||||
```
|
||||
manacore-storage/
|
||||
├── {userId}/
|
||||
│ ├── picture/... # Picture app files
|
||||
│ ├── chat/... # Chat attachments
|
||||
│ ├── manadeck/... # Card assets
|
||||
│ ├── contacts/... # Contact avatars
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start MinIO with Docker:
|
||||
|
||||
```bash
|
||||
# Start MinIO with Docker
|
||||
pnpm docker:up
|
||||
|
||||
# MinIO Console: http://localhost:9001
|
||||
# Username: minioadmin
|
||||
# Password: minioadmin
|
||||
```
|
||||
|
||||
2. Access MinIO Console at http://localhost:9001
|
||||
- Username: `minioadmin`
|
||||
- Password: `minioadmin`
|
||||
### Production (Hetzner Object Storage)
|
||||
|
||||
### Pre-created Buckets
|
||||
1. Create Hetzner Object Storage in [Hetzner Cloud Console](https://console.hetzner.cloud/)
|
||||
2. Generate S3 credentials (Access Key + Secret Key)
|
||||
3. Run the setup script:
|
||||
|
||||
The following buckets are automatically created:
|
||||
|
||||
| Bucket | Project | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `picture-storage` | Picture | Generated AI images |
|
||||
| `chat-storage` | Chat | User file uploads |
|
||||
| `manadeck-storage` | ManaDeck | Card/deck assets |
|
||||
| `nutriphi-storage` | NutriPhi | Meal photos |
|
||||
| `presi-storage` | Presi | Presentation slides |
|
||||
| `calendar-storage` | Calendar | Calendar attachments |
|
||||
| `contacts-storage` | Contacts | Contact avatars/files |
|
||||
| `storage-storage` | Storage | Cloud drive files |
|
||||
```bash
|
||||
export S3_ENDPOINT="https://fsn1.your-objectstorage.com"
|
||||
export S3_ACCESS_KEY="your-access-key"
|
||||
export S3_SECRET_KEY="your-secret-key"
|
||||
./scripts/setup-hetzner-storage.sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage';
|
||||
import {
|
||||
createUnifiedStorage,
|
||||
generateStorageKey,
|
||||
getContentType,
|
||||
APPS,
|
||||
} from '@manacore/shared-storage';
|
||||
|
||||
// Create client for Picture project
|
||||
const storage = createPictureStorage();
|
||||
// Create storage client
|
||||
const storage = createUnifiedStorage();
|
||||
|
||||
// Generate a key for a user's file
|
||||
const key = generateStorageKey('user-123', APPS.PICTURE, 'photo.jpg');
|
||||
// => 'user-123/picture/a1b2c3d4-uuid.jpg'
|
||||
|
||||
// Upload a file
|
||||
const key = generateUserFileKey('user-123', 'avatar.png');
|
||||
const result = await storage.upload(key, imageBuffer, {
|
||||
contentType: getContentType('avatar.png'),
|
||||
contentType: getContentType('photo.jpg'),
|
||||
public: true,
|
||||
});
|
||||
|
||||
console.log(result.url); // http://localhost:9000/picture-storage/users/user-123/uuid.png
|
||||
console.log(result.url);
|
||||
// => 'http://localhost:9000/manacore-storage/user-123/picture/uuid.jpg'
|
||||
|
||||
// Download a file
|
||||
const buffer = await storage.download(key);
|
||||
|
|
@ -56,42 +74,63 @@ const buffer = await storage.download(key);
|
|||
// Delete a file
|
||||
await storage.delete(key);
|
||||
|
||||
// List files
|
||||
const files = await storage.list('users/user-123/');
|
||||
// List files for a user's app
|
||||
const files = await storage.list('user-123/picture/');
|
||||
|
||||
// Generate presigned URLs
|
||||
const uploadUrl = await storage.getUploadUrl('temp/upload.png', { expiresIn: 3600 });
|
||||
const downloadUrl = await storage.getDownloadUrl(key, { expiresIn: 3600 });
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
## Available Apps
|
||||
|
||||
```typescript
|
||||
import { createStorageClient, BUCKETS } from '@manacore/shared-storage';
|
||||
import { APPS } from '@manacore/shared-storage';
|
||||
|
||||
// Override default config
|
||||
const storage = createStorageClient(BUCKETS.PICTURE, {
|
||||
endpoint: 'https://fsn1.your-objectstorage.com',
|
||||
region: 'fsn1',
|
||||
accessKeyId: process.env.HETZNER_ACCESS_KEY,
|
||||
secretAccessKey: process.env.HETZNER_SECRET_KEY,
|
||||
forcePathStyle: false,
|
||||
});
|
||||
APPS.PICTURE // 'picture'
|
||||
APPS.CHAT // 'chat'
|
||||
APPS.MANADECK // 'manadeck'
|
||||
APPS.NUTRIPHI // 'nutriphi'
|
||||
APPS.PRESI // 'presi'
|
||||
APPS.CALENDAR // 'calendar'
|
||||
APPS.CONTACTS // 'contacts'
|
||||
APPS.STORAGE // 'storage'
|
||||
APPS.MAIL // 'mail'
|
||||
APPS.INVENTORY // 'inventory'
|
||||
APPS.MANACORE // 'manacore'
|
||||
```
|
||||
|
||||
### Available Factory Functions
|
||||
## Key Generation Utilities
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createPictureStorage,
|
||||
createChatStorage,
|
||||
createManaDeckStorage,
|
||||
createNutriPhiStorage,
|
||||
createPresiStorage,
|
||||
createCalendarStorage,
|
||||
createContactsStorage,
|
||||
createStorageStorage,
|
||||
generateStorageKey,
|
||||
generateFileKey,
|
||||
generateUserFileKey,
|
||||
getContentType,
|
||||
validateFileSize,
|
||||
validateFileExtension,
|
||||
IMAGE_EXTENSIONS,
|
||||
} from '@manacore/shared-storage';
|
||||
|
||||
// Recommended: App-scoped key
|
||||
generateStorageKey('user-123', 'picture', 'photo.jpg');
|
||||
// => 'user-123/picture/uuid.jpg'
|
||||
|
||||
// With subfolder
|
||||
generateStorageKey('user-123', 'chat', 'doc.pdf', 'attachments');
|
||||
// => 'user-123/chat/attachments/uuid.pdf'
|
||||
|
||||
// Generic file key
|
||||
generateFileKey('photo.jpg', 'uploads', '2024');
|
||||
// => 'uploads/2024/uuid.jpg'
|
||||
|
||||
// Get MIME type
|
||||
getContentType('image.png'); // => 'image/png'
|
||||
|
||||
// Validate file
|
||||
validateFileSize(fileSize, 10); // max 10MB
|
||||
validateFileExtension('photo.jpg', IMAGE_EXTENSIONS);
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
|
@ -105,59 +144,25 @@ S3_ENDPOINT=http://localhost:9000
|
|||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY=minioadmin
|
||||
S3_SECRET_KEY=minioadmin
|
||||
MANACORE_STORAGE_PUBLIC_URL=http://localhost:9000/manacore-storage
|
||||
```
|
||||
|
||||
### Production (Hetzner Object Storage)
|
||||
### Production (Hetzner)
|
||||
|
||||
```env
|
||||
S3_ENDPOINT=https://fsn1.your-objectstorage.com
|
||||
S3_REGION=fsn1
|
||||
S3_ACCESS_KEY=your-access-key
|
||||
S3_SECRET_KEY=your-secret-key
|
||||
|
||||
# Optional: public URLs for CDN access
|
||||
PICTURE_STORAGE_PUBLIC_URL=https://picture-storage.fsn1.your-objectstorage.com
|
||||
NUTRIPHI_S3_PUBLIC_URL=https://nutriphi-storage.fsn1.your-objectstorage.com
|
||||
```
|
||||
|
||||
## Utilities
|
||||
|
||||
```typescript
|
||||
import {
|
||||
generateFileKey,
|
||||
generateUserFileKey,
|
||||
getContentType,
|
||||
validateFileSize,
|
||||
validateFileExtension,
|
||||
IMAGE_EXTENSIONS,
|
||||
DOCUMENT_EXTENSIONS,
|
||||
} from '@manacore/shared-storage';
|
||||
|
||||
// Generate unique file key
|
||||
const key = generateFileKey('photo.jpg', 'uploads', '2024');
|
||||
// => 'uploads/2024/uuid.jpg'
|
||||
|
||||
// User-scoped key
|
||||
const userKey = generateUserFileKey('user-123', 'avatar.png', 'avatars');
|
||||
// => 'users/user-123/avatars/uuid.png'
|
||||
|
||||
// Get MIME type
|
||||
const contentType = getContentType('image.png'); // => 'image/png'
|
||||
|
||||
// Validate file
|
||||
const isValidSize = validateFileSize(fileSize, 10); // max 10MB
|
||||
const isValidType = validateFileExtension('photo.jpg', IMAGE_EXTENSIONS);
|
||||
MANACORE_STORAGE_PUBLIC_URL=https://manacore-storage.fsn1.your-objectstorage.com
|
||||
```
|
||||
|
||||
## Docker Commands
|
||||
|
||||
```bash
|
||||
# Start all infrastructure (Postgres, Redis, MinIO)
|
||||
# Start infrastructure (Postgres, Redis, MinIO)
|
||||
pnpm docker:up
|
||||
|
||||
# Start only database services (no MinIO)
|
||||
pnpm docker:up:db
|
||||
|
||||
# View MinIO logs
|
||||
docker logs manacore-minio
|
||||
|
||||
|
|
|
|||
|
|
@ -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