mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 00:50:56 +00:00
feat: setup the attachment to the passenger api
This commit is contained in:
20
apps/edr-passenger-api/src/modules/storage/minio.config.ts
Normal file
20
apps/edr-passenger-api/src/modules/storage/minio.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* Mirrors the freight API's MinIO config so both apps read the same env vars and
|
||||
* behave the same against the same object store. Kept as a copy rather than a
|
||||
* shared package because the two APIs share no runtime code today, and a config
|
||||
* package for six fields would be more coupling than it saves.
|
||||
*/
|
||||
export const minioConfig = registerAs('minio', () => ({
|
||||
endPoint: process.env.MINIO_ENDPOINT || 'minio-dev.smart.aaca.gov.et',
|
||||
port: parseInt(process.env.MINIO_PORT || '443', 10),
|
||||
useSSL: process.env.MINIO_USE_SSL !== 'false',
|
||||
accessKey: process.env.MINIO_ACCESS_KEY || '',
|
||||
secretKey: process.env.MINIO_SECRET_KEY || '',
|
||||
bucket: process.env.MINIO_BUCKET || 'edr-dev',
|
||||
// Preset the region so presignedGetObject signs URLs locally. Without it the
|
||||
// minio client fires a live GetBucketLocation request on every sign, which
|
||||
// blocks (no timeout) when MinIO is slow and would hang every thread load.
|
||||
region: process.env.MINIO_REGION || 'us-east-1',
|
||||
}));
|
||||
90
apps/edr-passenger-api/src/modules/storage/minio.service.ts
Normal file
90
apps/edr-passenger-api/src/modules/storage/minio.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigType } from '@nestjs/config';
|
||||
import { Client } from 'minio';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { minioConfig } from './minio.config';
|
||||
|
||||
/**
|
||||
* Minimal object-storage client for the passenger API.
|
||||
*
|
||||
* A deliberate subset of the freight MinioService — only what chat attachments
|
||||
* need (put, sign, stream, key-from-url). Freight's extra surface (delete,
|
||||
* public URLs for unauthenticated links) is omitted rather than copied
|
||||
* speculatively.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MinioService {
|
||||
private readonly client: Client;
|
||||
private readonly logger = new Logger(MinioService.name);
|
||||
private readonly bucket: string;
|
||||
|
||||
constructor(
|
||||
@Inject(minioConfig.KEY)
|
||||
private readonly config: ConfigType<typeof minioConfig>,
|
||||
) {
|
||||
this.bucket = config.bucket;
|
||||
this.client = new Client({
|
||||
endPoint: config.endPoint,
|
||||
port: config.port,
|
||||
useSSL: config.useSSL,
|
||||
accessKey: config.accessKey,
|
||||
secretKey: config.secretKey,
|
||||
region: config.region,
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile(objectName: string, buffer: Buffer, contentType: string): Promise<string> {
|
||||
await this.client.putObject(this.bucket, objectName, buffer, buffer.length, {
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
return this.getObjectUrl(objectName);
|
||||
}
|
||||
|
||||
/** Unsigned object URL — what gets persisted. Not browser-fetchable. */
|
||||
getObjectUrl(objectName: string): string {
|
||||
const protocol = this.config.useSSL ? 'https' : 'http';
|
||||
return `${protocol}://${this.config.endPoint}:${this.config.port}/${this.bucket}/${objectName}`;
|
||||
}
|
||||
|
||||
getObjectNameFromUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) throw new NotFoundException('File object path is empty');
|
||||
if (!/^https?:\/\//i.test(trimmed)) return trimmed.replace(/^\/+/, '');
|
||||
|
||||
const url = new URL(trimmed);
|
||||
// pathname percent-encodes the key (a space becomes "%20") but MinIO stores
|
||||
// the literal characters, so decode each segment or a file whose name had
|
||||
// spaces 404s with "specified key does not exist".
|
||||
const parts = url.pathname
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((segment) => decodeURIComponent(segment));
|
||||
if (parts[0] === this.bucket) parts.shift();
|
||||
|
||||
const objectName = parts.join('/');
|
||||
if (!objectName) throw new NotFoundException('File object path is empty');
|
||||
return objectName;
|
||||
}
|
||||
|
||||
async getFileStream(objectName: string): Promise<Readable> {
|
||||
return this.client.getObject(this.bucket, objectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-lived signed URL for inline preview.
|
||||
*
|
||||
* Unlike the freight twin this does NOT degrade to an unsigned public URL when
|
||||
* signing fails: a chat attachment is another passenger's file, and quietly
|
||||
* handing back a URL that only works if the bucket is world-readable trades a
|
||||
* visible error for a silent access-control surprise. Fail loudly instead.
|
||||
*/
|
||||
async getSignedUrl(objectName: string, expirySeconds: number): Promise<string> {
|
||||
try {
|
||||
return await this.client.presignedGetObject(this.bucket, objectName, expirySeconds);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to sign URL for ${objectName}: ${(error as Error).message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
apps/edr-passenger-api/src/modules/storage/storage.module.ts
Normal file
12
apps/edr-passenger-api/src/modules/storage/storage.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { minioConfig } from './minio.config';
|
||||
import { MinioService } from './minio.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(minioConfig)],
|
||||
providers: [MinioService],
|
||||
exports: [MinioService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
Reference in New Issue
Block a user