Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/libs/fileDownload/FileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -754,6 +780,8 @@ export {
getFileType,
cleanFileName,
appendTimeToFileName,
ANDROID_SAFE_FILE_NAME_LENGTH,
truncateFileNameToSafeLengthOnAndroid,
readFileAsync,
base64ToFile,
isLocalFile,
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/FileUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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<string>;

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<string>;

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', () => {
Expand Down
Loading