Skip to content

Commit c821557

Browse files
authored
Merge pull request Expensify#70740 from margelo/@chrispader/attachment-validation-refactor
refactor: Improve and simplify attachment and file validation
2 parents 4d16c37 + dfd392e commit c821557

9 files changed

Lines changed: 655 additions & 466 deletions

File tree

src/CONST/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,16 +2359,16 @@ const CONST = {
23592359
},
23602360

23612361
FILE_VALIDATION_ERRORS: {
2362+
FILE_INVALID: 'fileInvalid',
23622363
WRONG_FILE_TYPE: 'wrongFileType',
2363-
WRONG_FILE_TYPE_MULTIPLE: 'wrongFileTypeMultiple',
23642364
FILE_TOO_LARGE: 'fileTooLarge',
2365-
FILE_TOO_LARGE_MULTIPLE: 'fileTooLargeMultiple',
23662365
FILE_TOO_SMALL: 'fileTooSmall',
23672366
FILE_CORRUPTED: 'fileCorrupted',
2368-
FOLDER_NOT_ALLOWED: 'folderNotAllowed',
2369-
MAX_FILE_LIMIT_EXCEEDED: 'fileLimitExceeded',
23702367
PROTECTED_FILE: 'protectedFile',
2368+
HEIC_OR_HEIF_IMAGE: 'heicOrHeifImage',
23712369
IMAGE_DIMENSIONS_TOO_LARGE: 'imageDimensionsTooLarge',
2370+
FOLDER_NOT_ALLOWED: 'folderNotAllowed',
2371+
MAX_FILE_LIMIT_EXCEEDED: 'maxFileLimitExceeded',
23722372
},
23732373

23742374
IOS_CAMERA_ROLL_ACCESS_ERROR: 'Access to photo library was denied',

src/hooks/useFilesValidation.tsx

Lines changed: 163 additions & 187 deletions
Large diffs are not rendered by default.

src/libs/AttachmentUtils.ts

Lines changed: 2 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,4 @@
11
import CONST from '@src/CONST';
2-
import type {FileObject} from '@src/types/utils/Attachment';
3-
import {cleanFileName, validateImageForCorruption} from './fileDownload/FileUtils';
4-
5-
type AttachmentValidationError = CorruptionError | 'fileDoesNotExist' | 'fileInvalid';
6-
type ValidResult = {
7-
isValid: true;
8-
fileType: 'file' | 'uri';
9-
source: string;
10-
file: FileObject;
11-
};
12-
type InvalidResult = {
13-
isValid: false;
14-
error: AttachmentValidationError;
15-
};
16-
17-
type AttachmentValidationResult = ValidResult | InvalidResult;
18-
19-
function validateAttachmentFile(file: FileObject): Promise<AttachmentValidationResult> {
20-
if (!file || !isDirectoryCheck(file)) {
21-
return Promise.resolve({isValid: false, error: 'fileDoesNotExist'});
22-
}
23-
24-
let fileObject = file;
25-
const fileConverted = file.getAsFile?.();
26-
if (fileConverted) {
27-
fileObject = fileConverted;
28-
}
29-
if (!fileObject) {
30-
return Promise.resolve({isValid: false, error: 'fileInvalid'});
31-
}
32-
33-
return isFileCorrupted(fileObject).then((corruptionResult) => {
34-
if (!corruptionResult.isValid) {
35-
return corruptionResult as InvalidResult;
36-
}
37-
38-
if (fileObject instanceof File) {
39-
/**
40-
* Cleaning file name, done here so that it covers all cases:
41-
* upload, drag and drop, copy-paste
42-
*/
43-
let updatedFile = fileObject;
44-
const cleanName = cleanFileName(updatedFile.name);
45-
if (updatedFile.name !== cleanName) {
46-
updatedFile = new File([updatedFile], cleanName, {type: updatedFile.type});
47-
}
48-
const inputSource = URL.createObjectURL(updatedFile);
49-
updatedFile.uri = inputSource;
50-
51-
return {isValid: true, fileType: 'file', source: inputSource, file: updatedFile} as ValidResult;
52-
}
53-
54-
return {isValid: true, fileType: 'uri', source: fileObject.uri, file: fileObject} as ValidResult;
55-
});
56-
}
57-
58-
type CorruptionError = 'tooLarge' | 'tooSmall' | 'error';
59-
type NoCorruptionResult = {
60-
isValid: true;
61-
};
62-
type CorruptionResult = {
63-
isValid: false;
64-
error: CorruptionError;
65-
};
66-
type AttachmentCorruptionValidationResult = NoCorruptionResult | CorruptionResult;
67-
68-
function isFileCorrupted(fileObject: FileObject): Promise<AttachmentCorruptionValidationResult> {
69-
return validateImageForCorruption(fileObject)
70-
.then(() => {
71-
if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
72-
return {
73-
isValid: false,
74-
error: 'tooLarge',
75-
} satisfies AttachmentCorruptionValidationResult;
76-
}
77-
78-
if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
79-
return {
80-
isValid: false,
81-
error: 'tooSmall',
82-
} satisfies AttachmentCorruptionValidationResult;
83-
}
84-
85-
return {
86-
isValid: true,
87-
} satisfies AttachmentCorruptionValidationResult;
88-
})
89-
.catch(() => {
90-
return {
91-
isValid: false,
92-
error: 'error',
93-
};
94-
});
95-
}
96-
97-
function isDirectoryCheck(data: FileObject) {
98-
if ('webkitGetAsEntry' in data && (data as DataTransferItem).webkitGetAsEntry()?.isDirectory) {
99-
return false;
100-
}
101-
102-
return true;
103-
}
1042

1053
/**
1064
* Returns image cache file extension based from mime type
@@ -110,5 +8,5 @@ function getImageCacheFileExtension(contentType: string) {
1108
return imageCacheFileTypes[contentType] ?? '';
1119
}
11210

113-
export {validateAttachmentFile, getImageCacheFileExtension};
114-
export type {AttachmentValidationResult};
11+
// eslint-disable-next-line import/prefer-default-export
12+
export {getImageCacheFileExtension};

src/libs/fileDownload/FileUtils.ts

Lines changed: 53 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -644,71 +644,44 @@ const hasHeicOrHeifExtension = (file: FileObject) => {
644644
* Otherwise, it attempts to fetch the file via its URI and reconstruct a File
645645
* with full metadata (name, size, type).
646646
*/
647-
const normalizeFileObject = (file: FileObject): Promise<FileObject> => {
647+
const normalizeFileObject = async (file: FileObject): Promise<FileObject> => {
648648
if (file instanceof File || file instanceof Blob) {
649-
return Promise.resolve(file);
649+
return file;
650650
}
651651

652652
const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID;
653653
const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
654654
const isNativePlatform = isAndroidNative || isIOSNative;
655655

656656
if (!isNativePlatform || 'size' in file) {
657-
return Promise.resolve(file);
657+
return file;
658658
}
659659

660660
if (typeof file.uri !== 'string') {
661-
return Promise.resolve(file);
661+
return file;
662662
}
663663

664-
return fetch(file.uri)
665-
.then((response) => response.blob())
666-
.then((blob) => {
667-
const name = file.name ?? 'unknown';
668-
const type = file.type ?? blob.type ?? 'application/octet-stream';
669-
const normalizedFile = new File([blob], name, {type});
670-
return normalizedFile;
671-
})
672-
.catch((error) => {
673-
return Promise.reject(error);
674-
});
664+
const response = await fetch(file.uri);
665+
const blob = await response.blob();
666+
const name = file.name ?? 'unknown';
667+
const type = file.type ?? blob.type ?? 'application/octet-stream';
668+
return new File([blob], name, {type});
675669
};
676670

677-
type ValidateAttachmentOptions = {
678-
isValidatingReceipts?: boolean;
671+
type FileValidationError = {
672+
error: ValueOf<typeof CONST.FILE_VALIDATION_ERRORS>;
679673
isValidatingMultipleFiles?: boolean;
674+
fileType?: string;
680675
};
681676

682-
const validateAttachment = (file: FileObject, validationOptions?: ValidateAttachmentOptions) => {
683-
const maxFileSize = validationOptions?.isValidatingReceipts ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
684-
685-
if (validationOptions?.isValidatingReceipts && !isValidReceiptExtension(file)) {
686-
return validationOptions?.isValidatingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE;
687-
}
688-
689-
// Images are exempt from file size check since they will be resized
690-
if (!Str.isImage(file.name ?? '') && !hasHeicOrHeifExtension(file) && (file?.size ?? 0) > maxFileSize) {
691-
return validationOptions?.isValidatingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE;
692-
}
693-
694-
if (validationOptions?.isValidatingReceipts && (file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
695-
return CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL;
696-
}
697-
698-
return '';
699-
};
700-
701-
type TranslationAdditionalData = {
702-
maxUploadSizeInMB?: number;
703-
fileLimit?: number;
704-
fileType?: string;
677+
type GetFileValidationErrorTextOptions = {
678+
isValidatingReceipt?: boolean;
705679
};
706680

707681
const getFileValidationErrorText = (
708682
translate: LocalizedTranslate,
709-
validationError: ValueOf<typeof CONST.FILE_VALIDATION_ERRORS> | null,
710-
additionalData: TranslationAdditionalData = {},
711-
isValidatingReceipt = false,
683+
validationError: FileValidationError | null,
684+
options: GetFileValidationErrorTextOptions = {},
712685
): {
713686
title: string;
714687
reason: string;
@@ -719,45 +692,51 @@ const getFileValidationErrorText = (
719692
reason: '',
720693
};
721694
}
722-
const maxSize = isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
723-
switch (validationError) {
695+
const maxSize = options.isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
696+
697+
if (validationError.isValidatingMultipleFiles) {
698+
switch (validationError.error) {
699+
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE:
700+
return {
701+
title: translate('attachmentPicker.someFilesCantBeUploaded'),
702+
reason: translate('attachmentPicker.unsupportedFileType', validationError.fileType ?? ''),
703+
};
704+
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE:
705+
return {
706+
title: translate('attachmentPicker.someFilesCantBeUploaded'),
707+
reason: translate('attachmentPicker.sizeLimitExceeded', maxSize / 1024 / 1024),
708+
};
709+
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
710+
return {
711+
title: translate('attachmentPicker.attachmentError'),
712+
reason: translate('attachmentPicker.folderNotAllowedMessage'),
713+
};
714+
case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED:
715+
return {
716+
title: translate('attachmentPicker.someFilesCantBeUploaded'),
717+
reason: translate('attachmentPicker.maxFileLimitExceeded'),
718+
};
719+
default:
720+
break;
721+
}
722+
}
723+
724+
switch (validationError.error) {
724725
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE:
725726
return {
726727
title: translate('attachmentPicker.wrongFileType'),
727728
reason: translate('attachmentPicker.notAllowedExtension'),
728729
};
729-
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE:
730-
return {
731-
title: translate('attachmentPicker.someFilesCantBeUploaded'),
732-
reason: translate('attachmentPicker.unsupportedFileType', additionalData.fileType ?? ''),
733-
};
734730
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE:
735731
return {
736732
title: translate('attachmentPicker.attachmentTooLarge'),
737-
reason: isValidatingReceipt
738-
? translate('attachmentPicker.sizeExceededWithLimit', additionalData.maxUploadSizeInMB ?? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / 1024 / 1024)
739-
: translate('attachmentPicker.sizeExceeded'),
740-
};
741-
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE:
742-
return {
743-
title: translate('attachmentPicker.someFilesCantBeUploaded'),
744-
reason: translate('attachmentPicker.sizeLimitExceeded', additionalData.maxUploadSizeInMB ?? maxSize / 1024 / 1024),
733+
reason: options.isValidatingReceipt ? translate('attachmentPicker.sizeExceededWithLimit', maxSize / 1024 / 1024) : translate('attachmentPicker.sizeExceeded'),
745734
};
746735
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL:
747736
return {
748737
title: translate('attachmentPicker.attachmentTooSmall'),
749738
reason: translate('attachmentPicker.sizeNotMet'),
750739
};
751-
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
752-
return {
753-
title: translate('attachmentPicker.attachmentError'),
754-
reason: translate('attachmentPicker.folderNotAllowedMessage'),
755-
};
756-
case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED:
757-
return {
758-
title: translate('attachmentPicker.someFilesCantBeUploaded'),
759-
reason: translate('attachmentPicker.maxFileLimitExceeded'),
760-
};
761740
case CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED:
762741
return {
763742
title: translate('attachmentPicker.attachmentError'),
@@ -774,21 +753,13 @@ const getFileValidationErrorText = (
774753
reason: translate('attachmentPicker.imageDimensionsTooLarge'),
775754
};
776755
default:
777-
return {
778-
title: translate('attachmentPicker.attachmentError'),
779-
reason: translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'),
780-
};
756+
break;
781757
}
782-
};
783758

784-
const getConfirmModalPrompt = (translate: LocalizedTranslate, attachmentInvalidReason: TranslationPaths | undefined) => {
785-
if (!attachmentInvalidReason) {
786-
return '';
787-
}
788-
if (attachmentInvalidReason === 'attachmentPicker.sizeExceededWithLimit') {
789-
return translate(attachmentInvalidReason, CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / (1024 * 1024));
790-
}
791-
return translate(attachmentInvalidReason);
759+
return {
760+
title: translate('attachmentPicker.attachmentError'),
761+
reason: translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'),
762+
};
792763
};
793764

794765
const MAX_CANVAS_SIZE = 4096;
@@ -902,16 +873,13 @@ export {
902873
resizeImageIfNeeded,
903874
createFile,
904875
validateReceipt,
905-
validateAttachment,
906876
normalizeFileObject,
907877
isValidReceiptExtension,
908878
getFileValidationErrorText,
909879
hasHeicOrHeifExtension,
910-
getConfirmModalPrompt,
911880
canvasFallback,
912881
getFilesFromClipboardEvent,
913882
cleanFileObject,
914883
cleanFileObjectName,
915884
};
916-
917-
export type {ValidateAttachmentOptions};
885+
export type {FileValidationError};

0 commit comments

Comments
 (0)