Files
edr-platform/apps/edr-freight-api/src/modules/minio/minio.service.ts

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);
}
}
}