mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|