mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
122 lines
4.0 KiB
TypeScript
122 lines
4.0 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";
|
|
|
|
@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>,
|
|
) {
|
|
console.log('[MinioService] Configuration loaded:', {
|
|
endPoint: config.endPoint,
|
|
port: config.port,
|
|
useSSL: config.useSSL,
|
|
accessKey: config.accessKey,
|
|
secretKey: config.secretKey ? '***HIDDEN***' : 'EMPTY',
|
|
bucket: config.bucket,
|
|
});
|
|
this.bucket = config.bucket;
|
|
this.client = new Client({
|
|
endPoint: config.endPoint,
|
|
port: config.port,
|
|
useSSL: config.useSSL,
|
|
accessKey: config.accessKey,
|
|
secretKey: config.secretKey,
|
|
// Presetting the region keeps presignedGetObject fully local — no live
|
|
// GetBucketLocation round-trip to the endpoint on each signed URL.
|
|
region: config.region,
|
|
});
|
|
}
|
|
|
|
async uploadFile(
|
|
objectName: string,
|
|
buffer: Buffer,
|
|
contentType: string,
|
|
): Promise<string> {
|
|
try {
|
|
await this.client.putObject(this.bucket, objectName, buffer, buffer.length, {
|
|
"Content-Type": contentType,
|
|
});
|
|
this.logger.log(`File uploaded successfully: ${objectName}`);
|
|
return this.getPublicUrl(objectName);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to upload file ${objectName}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
getPublicUrl(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);
|
|
// `url.pathname` percent-encodes the object key (e.g. a space becomes
|
|
// "%20"), but MinIO stores the key with its literal characters. Decode each
|
|
// segment so the recovered key matches what was uploaded — otherwise a file
|
|
// whose name had spaces/unicode 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 deleteFile(objectName: string): Promise<void> {
|
|
try {
|
|
await this.client.removeObject(this.bucket, objectName);
|
|
this.logger.log(`File deleted successfully: ${objectName}`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to delete file ${objectName}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getFileStream(objectName: string): Promise<Readable> {
|
|
try {
|
|
return this.client.getObject(this.bucket, objectName);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to get file ${objectName}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getSignedUrl(objectName: string, expirySeconds: number = 300): Promise<string> {
|
|
try {
|
|
return await this.client.presignedGetObject(this.bucket, objectName, expirySeconds);
|
|
} catch (error) {
|
|
// Signing a file URL must never break a booking/transition response — the
|
|
// caller only needs SOMETHING to link to. Degrade to the public object URL
|
|
// and log, rather than throwing (which would 500 an otherwise-good load).
|
|
this.logger.error(`Failed to generate signed URL for ${objectName}:`, error);
|
|
return this.getPublicUrl(objectName);
|
|
}
|
|
}
|
|
}
|