mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
fix(common): report the detected file type in file type errors (#18289)
This commit is contained in:
1 parent
870c54b44d
commit
f991703156
2 files changed
+121
-1
No files matched your search
@@ -81,6 +81,14 @@ export class FileTypeValidator extends FileValidator<
|
||||
FileTypeValidatorOptions,
|
||||
IFile
|
||||
> {
|
||||
/**
|
||||
* The type detected from the content of each validated file (`null` when
|
||||
* none was), so that the error message can report the type that was
|
||||
* actually checked rather than the one the client declared. Keyed by file,
|
||||
* as the validator is shared by concurrent uploads.
|
||||
*/
|
||||
private readonly detectedMimeTypes = new WeakMap<IFile, string | null>();
|
||||
|
||||
buildErrorMessage(file?: IFile): string {
|
||||
const { errorMessage, ...config } = this.validationOptions;
|
||||
|
||||
@@ -105,6 +113,17 @@ export class FileTypeValidator extends FileValidator<
|
||||
return `Validation failed (file buffer is not available; file type validation could not be performed; expected type is ${this.validationOptions.fileType})`;
|
||||
}
|
||||
|
||||
const detectedMimeType = file && this.detectedMimeTypes.get(file);
|
||||
if (detectedMimeType) {
|
||||
return `Validation failed (current file type is ${detectedMimeType}, expected type is ${this.validationOptions.fileType})`;
|
||||
}
|
||||
if (
|
||||
detectedMimeType === null &&
|
||||
!this.validationOptions.fallbackToMimetype
|
||||
) {
|
||||
return `Validation failed (file type could not be detected from its content; expected type is ${this.validationOptions.fileType})`;
|
||||
}
|
||||
|
||||
if (file?.mimetype) {
|
||||
const baseMessage = `Validation failed (current file type is ${file.mimetype}, expected type is ${this.validationOptions.fileType})`;
|
||||
|
||||
@@ -115,7 +134,10 @@ export class FileTypeValidator extends FileValidator<
|
||||
*
|
||||
* This message clarifies that fallback logic was used, in case users rely on file signatures.
|
||||
*/
|
||||
if (this.validationOptions.fallbackToMimetype) {
|
||||
if (
|
||||
this.validationOptions.fallbackToMimetype &&
|
||||
!this.validationOptions.skipMagicNumbersValidation
|
||||
) {
|
||||
return `${baseMessage} - magic number detection failed, used mimetype fallback`;
|
||||
}
|
||||
|
||||
@@ -149,6 +171,7 @@ export class FileTypeValidator extends FileValidator<
|
||||
try {
|
||||
const { fileTypeFromBuffer } = await import('file-type');
|
||||
const fileType = await fileTypeFromBuffer(file.buffer);
|
||||
this.detectedMimeTypes.set(file, fileType?.mime ?? null);
|
||||
|
||||
if (fileType) {
|
||||
if (this.validationOptions.overrideMimeType) {
|
||||
@@ -168,6 +191,7 @@ export class FileTypeValidator extends FileValidator<
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.detectedMimeTypes.set(file, null);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
|
||||
@@ -467,6 +467,102 @@ describe('FileTypeValidator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should report the type detected from the content rather than the declared mimetype', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'image/jpeg',
|
||||
});
|
||||
const file = { mimetype: 'image/jpeg', buffer: pngBuffer } as IFile;
|
||||
|
||||
expect(await fileTypeValidator.isValid(file)).toBe(false);
|
||||
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
|
||||
'Validation failed (current file type is image/png, expected type is image/jpeg)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not claim a mimetype fallback when the type was detected from the content', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'image/jpeg',
|
||||
fallbackToMimetype: true,
|
||||
});
|
||||
const file = { mimetype: 'image/jpeg', buffer: pngBuffer } as IFile;
|
||||
|
||||
expect(await fileTypeValidator.isValid(file)).toBe(false);
|
||||
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
|
||||
'Validation failed (current file type is image/png, expected type is image/jpeg)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should report that the type could not be detected from the content', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'text/plain',
|
||||
});
|
||||
const file = {
|
||||
mimetype: 'text/plain',
|
||||
buffer: Buffer.from('hi'),
|
||||
} as IFile;
|
||||
|
||||
expect(await fileTypeValidator.isValid(file)).toBe(false);
|
||||
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
|
||||
'Validation failed (file type could not be detected from its content; expected type is text/plain)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should mention the mimetype fallback when the type could not be detected from the content', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'text/plain',
|
||||
fallbackToMimetype: true,
|
||||
});
|
||||
const file = {
|
||||
mimetype: 'text/csv',
|
||||
buffer: Buffer.from('hi'),
|
||||
} as IFile;
|
||||
|
||||
expect(await fileTypeValidator.isValid(file)).toBe(false);
|
||||
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
|
||||
'Validation failed (current file type is text/csv, expected type is text/plain) - magic number detection failed, used mimetype fallback',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not claim a mimetype fallback when magic number validation is skipped', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'image/png',
|
||||
skipMagicNumbersValidation: true,
|
||||
fallbackToMimetype: true,
|
||||
});
|
||||
const file = { mimetype: 'image/jpeg', buffer: pngBuffer } as IFile;
|
||||
|
||||
expect(await fileTypeValidator.isValid(file)).toBe(false);
|
||||
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
|
||||
'Validation failed (current file type is image/jpeg, expected type is image/png)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should report the type detected for each file when the validator is shared', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'application/pdf',
|
||||
});
|
||||
const pngFile = {
|
||||
mimetype: 'application/pdf',
|
||||
buffer: pngBuffer,
|
||||
} as IFile;
|
||||
const jpegFile = {
|
||||
mimetype: 'application/pdf',
|
||||
buffer: jpegBuffer,
|
||||
} as IFile;
|
||||
|
||||
await Promise.all([
|
||||
fileTypeValidator.isValid(pngFile),
|
||||
fileTypeValidator.isValid(jpegFile),
|
||||
]);
|
||||
|
||||
expect(fileTypeValidator.buildErrorMessage(pngFile)).toBe(
|
||||
'Validation failed (current file type is image/png, expected type is application/pdf)',
|
||||
);
|
||||
expect(fileTypeValidator.buildErrorMessage(jpegFile)).toBe(
|
||||
'Validation failed (current file type is image/jpeg, expected type is application/pdf)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for text file with small buffer and correct mimetype but fail magic number validation', async () => {
|
||||
const fileTypeValidator = new FileTypeValidator({
|
||||
fileType: 'text/plain',
|
||||
|
||||
Reference in new issue
Block a user