Skip to content

Commit 0a17c74

Browse files
committed
feat: implement minio:// prefix format (v2.0.0 breaking change)
1 parent f78de73 commit 0a17c74

File tree

3 files changed

+65
-62
lines changed

3 files changed

+65
-62
lines changed

src/interceptors/file-url-transform.interceptor.ts

Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { map } from 'rxjs/operators';
44
import { MinioService } from '../minio.service';
55
import { Socket } from 'net';
66
import { IncomingMessage, ServerResponse } from 'http';
7-
import { MINIO_FILE_FIELD_METADATA } from '../constants';
87

98
@Injectable()
109
export class FileUrlTransformInterceptor implements NestInterceptor {
@@ -31,6 +30,7 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
3130

3231
/**
3332
* Transforms URLs in the given data to presigned URLs using the Minio service.
33+
* Only processes strings that start with minio:// prefix.
3434
* @param data - The data to transform.
3535
* @returns The transformed data with URLs replaced by presigned URLs.
3636
*/
@@ -63,24 +63,14 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
6363
return obj;
6464
}
6565

66-
// Get the schema if it's a Mongoose document
67-
const schema = data.schema || (data.constructor && data.constructor.schema);
68-
6966
// Process each property recursively
7067
for (const [key, value] of Object.entries(obj)) {
71-
// Check if this field is decorated with FileSchemaField or FileColumn
72-
const isFileField =
73-
schema?.paths?.[key]?.options?.isFileField ||
74-
this.hasFileFieldMetadata(data, key) ||
75-
this.hasFileFieldMetadata(obj, key);
76-
77-
const inferredPath = !isFileField ? this.extractMinioPath(value) : null;
78-
79-
if ((isFileField || inferredPath) && typeof value === 'string') {
80-
const split = inferredPath ?? this.splitBucketAndObject(value);
81-
if (split) {
68+
// Only process strings that start with minio:// prefix
69+
if (typeof value === 'string' && value.startsWith('minio://')) {
70+
const minioPath = this.minioService.parseMinioUrl(value);
71+
if (minioPath) {
8272
try {
83-
obj[key] = await this.minioService.getPresignedUrl(split.bucketName, split.objectName);
73+
obj[key] = await this.minioService.getPresignedUrl(minioPath.bucketName, minioPath.objectName);
8474
} catch (error) {
8575
this.logger.error(`Error generating presigned URL for ${key}:`, error);
8676
}
@@ -107,42 +97,4 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
10797
return obj;
10898
}
10999

110-
private hasFileFieldMetadata(target: unknown, propertyKey: string): boolean {
111-
if (!target) return false;
112-
113-
const directMetadata = Reflect.getMetadata(MINIO_FILE_FIELD_METADATA, target, propertyKey);
114-
if (directMetadata) {
115-
return true;
116-
}
117-
118-
const prototype = typeof target === 'object' ? Object.getPrototypeOf(target) : undefined;
119-
if (!prototype) {
120-
return false;
121-
}
122-
123-
return Boolean(Reflect.getMetadata(MINIO_FILE_FIELD_METADATA, prototype, propertyKey));
124-
}
125-
126-
private extractMinioPath(value: unknown): { bucketName: string; objectName: string } | null {
127-
if (typeof value !== 'string') {
128-
return null;
129-
}
130-
131-
const split = this.splitBucketAndObject(value);
132-
return split;
133-
}
134-
135-
private splitBucketAndObject(value: string): { bucketName: string; objectName: string } | null {
136-
if (!value.includes('/')) {
137-
return null;
138-
}
139-
const [bucketName, ...pathParts] = value.split('/');
140-
if (!bucketName || pathParts.length === 0) {
141-
return null;
142-
}
143-
return {
144-
bucketName,
145-
objectName: pathParts.join('/'),
146-
};
147-
}
148100
}

src/interceptors/file.interceptor.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,16 @@ export class MinioFileInterceptor implements NestInterceptor {
9898
private async transformUrls(data: any): Promise<any> {
9999
if (!data) return data;
100100

101-
// // If it's a plain object (not a mongoose document), process it directly
101+
// If it's a plain object (not a mongoose document), process it directly
102102
const obj = data.toJSON ? await data.toJSON() : data;
103103

104-
// Transform all fields that look like Minio paths (bucket-name/path)
104+
// Transform all fields that start with minio:// prefix
105105
for (const [key, value] of Object.entries(obj)) {
106-
if (typeof value === 'string' && value.includes('/')) {
107-
const [bucketName, ...pathParts] = value.split('/');
108-
if (pathParts.length > 0) {
106+
if (typeof value === 'string' && value.startsWith('minio://')) {
107+
const minioPath = this.minioService.parseMinioUrl(value);
108+
if (minioPath) {
109109
try {
110-
obj[key] = await this.minioService.getPresignedUrl(bucketName, pathParts.join('/'));
110+
obj[key] = await this.minioService.getPresignedUrl(minioPath.bucketName, minioPath.objectName);
111111
} catch (error) {
112112
this.logger.error(`Error generating presigned URL for ${key}:`, error);
113113
}

src/minio.service.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ export class MinioService implements OnModuleInit {
241241
'Content-Type': file.mimetype,
242242
});
243243

244-
return `${bucketName}/${fileName}`;
244+
// Return minio:// format for explicit identification
245+
return `minio://${bucketName}/${fileName}`;
245246
}
246247

247248
async getPresignedUrl(bucketName: string, objectName: string): Promise<string> {
@@ -261,11 +262,61 @@ export class MinioService implements OnModuleInit {
261262
);
262263
}
263264

264-
async deleteFile(bucketName: string, objectName: string): Promise<void> {
265+
/**
266+
* Deletes a file from MinIO
267+
* @param bucketName - Bucket name or minio:// URL
268+
* @param objectName - Object name (optional if first param is minio:// URL)
269+
* @returns Promise that resolves when file is deleted
270+
*/
271+
async deleteFile(bucketName: string, objectName?: string): Promise<void> {
272+
// If bucketName is a minio:// URL, parse it
273+
if (bucketName.startsWith('minio://')) {
274+
const parsed = this.parseMinioUrl(bucketName);
275+
if (!parsed) {
276+
throw new Error(`Invalid minio:// URL: ${bucketName}`);
277+
}
278+
await this.minioClient.removeObject(parsed.bucketName, parsed.objectName);
279+
return;
280+
}
281+
282+
// Otherwise, use bucketName and objectName as separate parameters
283+
if (!objectName) {
284+
throw new Error('objectName is required when bucketName is not a minio:// URL');
285+
}
265286
await this.minioClient.removeObject(bucketName, objectName);
266287
}
267288

268289
getMinioClient(): Minio.Client {
269290
return this.minioClient;
270291
}
292+
293+
/**
294+
* Parses a minio:// URL to extract bucket name and object name
295+
* @param minioUrl - URL in format minio://bucket-name/object-path
296+
* @returns Object with bucketName and objectName, or null if invalid
297+
*/
298+
parseMinioUrl(minioUrl: string): { bucketName: string; objectName: string } | null {
299+
if (!minioUrl || typeof minioUrl !== 'string') {
300+
return null;
301+
}
302+
303+
if (!minioUrl.startsWith('minio://')) {
304+
return null;
305+
}
306+
307+
const path = minioUrl.substring(8); // Remove 'minio://' prefix
308+
if (!path.includes('/')) {
309+
return null;
310+
}
311+
312+
const [bucketName, ...pathParts] = path.split('/');
313+
if (!bucketName || pathParts.length === 0) {
314+
return null;
315+
}
316+
317+
return {
318+
bucketName,
319+
objectName: pathParts.join('/'),
320+
};
321+
}
271322
}

0 commit comments

Comments
 (0)