diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 54dd2d717b3a..1b4175b2170f 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -161,15 +161,41 @@ function cleanFileName(fileName: string): string { function appendTimeToFileName(fileName: string): string { const file = splitExtensionFromFileName(fileName); - let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`; + + const fileNameWithoutExtension = file.fileName; + const fileExtension = file.fileExtension; + + const time = DateUtils.getDBTime(); + const timeSuffix = `-${time}`; + + const lengthSafeFileNameWithoutExtension = + Platform.OS === 'android' ? truncateFileNameToSafeLengthOnAndroid({fileNameWithoutExtension, fileSuffixLength: timeSuffix.length}) : fileNameWithoutExtension; + + let newFileName = `${lengthSafeFileNameWithoutExtension}${timeSuffix}`; + // Replace illegal characters before trying to download the attachment. newFileName = newFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'); - if (file.fileExtension) { - newFileName += `.${file.fileExtension}`; + if (fileExtension) { + newFileName += `.${fileExtension}`; } return newFileName; } +const ANDROID_SAFE_FILE_NAME_LENGTH = 70; + +/** + * Truncates the file name to a safe length on Android + * @param params - An object containing: + * @param params.fileNameWithoutExtension - The file name without the extension + * @param params.fileSuffixLength - The length of the file suffix + * @returns The truncated file name + */ +function truncateFileNameToSafeLengthOnAndroid({fileNameWithoutExtension, fileSuffixLength}: {fileNameWithoutExtension: string; fileSuffixLength: number}): string { + const safeFileNameLengthWithoutSuffix = ANDROID_SAFE_FILE_NAME_LENGTH - fileSuffixLength; + + return fileNameWithoutExtension.substring(0, safeFileNameLengthWithoutSuffix); +} + /** * Reads a locally uploaded file * @param path - the blob url of the locally uploaded file @@ -754,6 +780,8 @@ export { getFileType, cleanFileName, appendTimeToFileName, + ANDROID_SAFE_FILE_NAME_LENGTH, + truncateFileNameToSafeLengthOnAndroid, readFileAsync, base64ToFile, isLocalFile, diff --git a/tests/unit/FileUtilsTest.ts b/tests/unit/FileUtilsTest.ts index f1b3e05d47f6..bfbf326d5f46 100644 --- a/tests/unit/FileUtilsTest.ts +++ b/tests/unit/FileUtilsTest.ts @@ -1,3 +1,4 @@ +import {Platform} from 'react-native'; import CONST from '../../src/CONST'; import DateUtils from '../../src/libs/DateUtils'; import * as FileUtils from '../../src/libs/fileDownload/FileUtils'; @@ -9,6 +10,8 @@ const createMockFile = (name: string, size: number) => ({ size, }); +const createFileNameFromLength = ({length, extension}: {length: number; extension?: string | undefined}): string => `${'a'.repeat(length)}${extension ? `.${extension}` : ''}`; + describe('FileUtils', () => { describe('splitExtensionFromFileName', () => { it('should return correct file name and extension', () => { @@ -42,6 +45,52 @@ describe('FileUtils', () => { const expectedFileName = `image-${DateUtils.getDBTime()}`; expect(actualFileName).toEqual(expectedFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_')); }); + + describe('on Android', () => { + let platformReplaceProperty: jest.ReplaceProperty; + + beforeEach(() => { + platformReplaceProperty = jest.replaceProperty(Platform, 'OS', 'android'); + }); + + afterEach(() => { + platformReplaceProperty.restore(); + }); + + it('should truncate the file name to safe length when length exceeds the safe length', () => { + const fileNameExceedingSafeLength = createFileNameFromLength({length: FileUtils.ANDROID_SAFE_FILE_NAME_LENGTH + 1, extension: 'doc'}); + + const actualFileName = FileUtils.appendTimeToFileName(fileNameExceedingSafeLength); + const expectedTruncatedFileName = `${createFileNameFromLength({length: FileUtils.ANDROID_SAFE_FILE_NAME_LENGTH - 24})}-${DateUtils.getDBTime()}.doc`; + + expect(actualFileName).toEqual(expectedTruncatedFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_')); + }); + }); + + describe('on Non-Android', () => { + const nonAndroidPlatforms = ['ios', 'macos', 'windows', 'web'] as const; + + describe.each(nonAndroidPlatforms)('%s', (platform) => { + let platformReplaceProperty: jest.ReplaceProperty; + + beforeEach(() => { + platformReplaceProperty = jest.replaceProperty(Platform, 'OS', platform); + }); + + afterEach(() => { + platformReplaceProperty.restore(); + }); + + it('should not truncate the file name even when length exceeds the Android safe length', () => { + const fileNameExceedingAndroidSafeLength = createFileNameFromLength({length: FileUtils.ANDROID_SAFE_FILE_NAME_LENGTH + 1, extension: 'doc'}); + + const actualFileName = FileUtils.appendTimeToFileName(fileNameExceedingAndroidSafeLength); + const expectedFileName = `${createFileNameFromLength({length: FileUtils.ANDROID_SAFE_FILE_NAME_LENGTH + 1})}-${DateUtils.getDBTime()}.doc`; + + expect(actualFileName).toEqual(expectedFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_')); + }); + }); + }); }); describe('validateAttachment', () => {