Added minio and storage module

This commit is contained in:
GeorgeWebberley 2026-03-01 12:10:31 +01:00
parent 0ca35e85ef
commit 1059dd9fa8
6 changed files with 1764 additions and 4 deletions

View file

@ -1,9 +1,18 @@
# App config
PORT=3000
NODE_ENV=development
# Database config
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=pandektes
POSTGRES_PORT=5432
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/pandektes?schema=public"
PORT=3000
NODE_ENV=development
# Storage Config (MinIO / S3 / R2)
STORAGE_ENDPOINT=http://localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_BUCKET=cases
STORAGE_REGION=us-east-1
STORAGE_FORCE_PATH_STYLE=true

View file

@ -12,5 +12,19 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
minio:
image: minio/minio
container_name: pandektes-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
postgres_data:
postgres_data:
minio_data:

1653
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,8 @@
"dependencies": {
"@apollo/server": "^5.4.0",
"@as-integrations/express5": "^1.1.2",
"@aws-sdk/client-s3": "^3.1000.0",
"@aws-sdk/s3-request-presigner": "^3.1000.0",
"@nestjs/apollo": "^13.2.4",
"@nestjs/common": "^11.1.14",
"@nestjs/config": "^4.0.3",

View file

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { StorageService } from './storage.service';
@Global()
@Module({
providers: [StorageService],
exports: [StorageService],
})
export class StorageModule {}

View file

@ -0,0 +1,73 @@
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
S3Client,
PutObjectCommand,
HeadBucketCommand,
CreateBucketCommand,
GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@Injectable()
export class StorageService implements OnModuleInit {
private client: S3Client;
private bucket: string;
private readonly logger = new Logger(StorageService.name);
constructor(private configService: ConfigService) {
// Decided to use standard S3 patterns so it can swap out easily
// for other storage providers if need be.
this.client = new S3Client({
region: this.configService.get<string>('STORAGE_REGION'),
endpoint: this.configService.get<string>('STORAGE_ENDPOINT'),
credentials: {
accessKeyId: this.configService.get<string>('STORAGE_ACCESS_KEY')!,
secretAccessKey: this.configService.get<string>('STORAGE_SECRET_KEY')!,
},
forcePathStyle: this.configService.get<string>('STORAGE_FORCE_PATH_STYLE') === 'true',
});
this.bucket = this.configService.get<string>('STORAGE_BUCKET')!;
}
async onModuleInit() {
await this.ensureBucketExists();
}
private async ensureBucketExists() {
try {
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
} catch (error) {
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
this.logger.log(`Bucket ${this.bucket} not found. Creating it...`);
await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }));
} else {
this.logger.error('Error checking/creating bucket', error);
}
}
}
async upload(buffer: Buffer, filename: string, mimetype: string): Promise<string> {
const key = `${Date.now()}-${filename}`;
await this.client.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: buffer,
ContentType: mimetype,
}),
);
return key;
}
async getPresignedUrl(key: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.bucket,
Key: key,
});
// Expire in 1 hour
return getSignedUrl(this.client, command, { expiresIn: 3600 });
}
}