mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 16:06:41 +02:00
feat(storage): add MinIO local storage and @manacore/shared-storage package
- Add MinIO service to docker-compose.dev.yml with auto-bucket initialization - Create @manacore/shared-storage package with S3-compatible client - Add factory functions for each project (Picture, Chat, ManaDeck, etc.) - Include file utilities (generateFileKey, getContentType, validators) - Update environment variables for S3/MinIO configuration - Document storage architecture in CLAUDE.md Local dev uses MinIO, production will use Hetzner Object Storage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
eb173217c1
commit
2cfa09c84d
13 changed files with 1195 additions and 130 deletions
135
packages/shared-storage/src/utils.ts
Normal file
135
packages/shared-storage/src/utils.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { extname } from 'path';
|
||||
|
||||
/**
|
||||
* Generate a unique file key with optional folder structure
|
||||
*
|
||||
* @example
|
||||
* generateFileKey('image.png', 'user-123')
|
||||
* // => 'user-123/a1b2c3d4-e5f6-7890-abcd-ef1234567890.png'
|
||||
*
|
||||
* generateFileKey('photo.jpg', 'users', 'avatars')
|
||||
* // => 'users/avatars/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg'
|
||||
*/
|
||||
export function generateFileKey(filename: string, ...folders: string[]): string {
|
||||
const ext = extname(filename);
|
||||
const uuid = randomUUID();
|
||||
const key = `${uuid}${ext}`;
|
||||
|
||||
if (folders.length > 0) {
|
||||
return [...folders, key].join('/');
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a user-scoped file key
|
||||
*
|
||||
* @example
|
||||
* generateUserFileKey('user-123', 'avatar.png')
|
||||
* // => 'users/user-123/a1b2c3d4-e5f6-7890-abcd-ef1234567890.png'
|
||||
*/
|
||||
export function generateUserFileKey(userId: string, filename: string, subfolder?: string): string {
|
||||
const folders = subfolder ? ['users', userId, subfolder] : ['users', userId];
|
||||
return generateFileKey(filename, ...folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type from filename extension
|
||||
*/
|
||||
export function getContentType(filename: string): string {
|
||||
const ext = extname(filename).toLowerCase();
|
||||
|
||||
const mimeTypes: Record<string, string> = {
|
||||
// Images
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.avif': 'image/avif',
|
||||
|
||||
// Documents
|
||||
'.pdf': 'application/pdf',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// Text
|
||||
'.txt': 'text/plain',
|
||||
'.csv': 'text/csv',
|
||||
'.json': 'application/json',
|
||||
'.xml': 'application/xml',
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
|
||||
// Audio
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.m4a': 'audio/mp4',
|
||||
|
||||
// Video
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.mov': 'video/quicktime',
|
||||
'.avi': 'video/x-msvideo',
|
||||
|
||||
// Archives
|
||||
'.zip': 'application/zip',
|
||||
'.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip',
|
||||
'.rar': 'application/vnd.rar',
|
||||
|
||||
// Other
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.otf': 'font/otf',
|
||||
};
|
||||
|
||||
return mimeTypes[ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file size
|
||||
*/
|
||||
export function validateFileSize(sizeInBytes: number, maxSizeMB: number): boolean {
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
return sizeInBytes <= maxSizeBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file extension
|
||||
*/
|
||||
export function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {
|
||||
const ext = extname(filename).toLowerCase();
|
||||
return allowedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common allowed extensions for images
|
||||
*/
|
||||
export const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif'];
|
||||
|
||||
/**
|
||||
* Common allowed extensions for documents
|
||||
*/
|
||||
export const DOCUMENT_EXTENSIONS = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
|
||||
|
||||
/**
|
||||
* Common allowed extensions for audio
|
||||
*/
|
||||
export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a'];
|
||||
|
||||
/**
|
||||
* Common allowed extensions for video
|
||||
*/
|
||||
export const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi'];
|
||||
Loading…
Add table
Add a link
Reference in a new issue