Skip to content

Commit a0667ba

Browse files
committed
Normalize android names when upload from camera roll
1 parent 0417c4f commit a0667ba

3 files changed

Lines changed: 155 additions & 29 deletions

File tree

src/components/modals/AddModal/index.tsx

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import { useDrive } from '@internxt-mobile/hooks/drive';
1616
import { imageService, logger } from '@internxt-mobile/services/common';
1717
import { uploadService } from '@internxt-mobile/services/common/network/upload/upload.service';
1818
import drive from '@internxt-mobile/services/drive';
19+
import {
20+
generateAndroidFileName,
21+
isTemporaryAndroidFileName,
22+
parseExifDate,
23+
} from '@internxt-mobile/services/drive/file/utils/exifHelpers';
1924
import errorService from '@internxt-mobile/services/ErrorService';
2025
import { DriveFileData, EncryptionVersion, FileEntryByUuid, Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
2126
import { SaveFormat } from 'expo-image-manipulator';
@@ -446,7 +451,6 @@ function AddModal(): JSX.Element {
446451
try {
447452
const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.assetId || asset.uri);
448453
const cleanUri = assetInfo.mediaType === 'video' ? asset.uri : assetInfo.localUri || asset.uri;
449-
// asset info has the correct format (heic issue)
450454
const originalFileName = assetInfo.filename || asset.fileName;
451455

452456
let fileSize = asset.fileSize;
@@ -534,7 +538,15 @@ function AddModal(): JSX.Element {
534538
for (const asset of result.assets) {
535539
try {
536540
const cleanUri = asset.uri;
537-
const originalFileName = asset.fileName || `media_${Date.now()}.jpg`;
541+
let originalFileName = asset.fileName;
542+
const exif = asset.exif || null;
543+
544+
const creationTime = parseExifDate(exif?.DateTimeOriginal);
545+
const modificationTime = parseExifDate(exif?.DateTime);
546+
if (isTemporaryAndroidFileName(originalFileName)) {
547+
originalFileName = generateAndroidFileName(cleanUri, creationTime, modificationTime);
548+
}
549+
538550
let fileSize = asset.fileSize ?? 0;
539551
if (!fileSize) {
540552
try {
@@ -545,40 +557,15 @@ function AddModal(): JSX.Element {
545557
fileSize = 0;
546558
}
547559
}
548-
const exif = asset.exif || null;
549-
550-
// Extract this logic to function before PR
551-
// Parse EXIF DateTimeOriginal (creation time) - formato: "YYYY:MM:DD HH:mm:ss"
552-
let parsedCreationTime;
553-
if (exif?.DateTimeOriginal) {
554-
const fixedDate = exif.DateTimeOriginal.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3');
555-
parsedCreationTime = new Date(fixedDate);
556-
}
557-
558-
// Parse EXIF DateTime (modification time) - formato: "YYYY:MM:DD HH:mm:ss"
559-
let parsedModificationTime;
560-
if (exif?.DateTime) {
561-
const fixedDate = exif.DateTime.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3');
562-
parsedModificationTime = new Date(fixedDate);
563-
}
564-
565-
console.log(
566-
'[ANDROID-METADATA-DEBUG] Creation time (DateTimeOriginal):',
567-
parsedCreationTime?.toISOString(),
568-
);
569-
console.log(
570-
'[ANDROID-METADATA-DEBUG] Modification time (DateTime):',
571-
parsedModificationTime?.toISOString(),
572-
);
573560

574561
documents.push({
575562
fileCopyUri: cleanUri,
576563
name: decodeURIComponent(originalFileName ?? ''),
577564
size: fileSize,
578565
type: drive.file.getExtensionFromUri(cleanUri)?.toLowerCase() ?? '',
579566
uri: cleanUri,
580-
creationTime: parsedCreationTime ? parsedCreationTime.toISOString() : undefined,
581-
modificationTime: parsedModificationTime ? parsedModificationTime.toISOString() : undefined,
567+
creationTime,
568+
modificationTime,
582569
});
583570
} catch (error) {
584571
logger.error('Error obtaining original asset info:', error);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
jest.mock('../..', () => ({
2+
default: {
3+
file: {
4+
getExtensionFromUri: jest.fn(),
5+
},
6+
},
7+
}));
8+
9+
import { isTemporaryAndroidFileName, parseExifDate } from './exifHelpers';
10+
11+
describe('parseExifDate', () => {
12+
test('should parse valid EXIF date format', () => {
13+
const exifDate = '2024:12:25 14:30:45';
14+
const result = parseExifDate(exifDate);
15+
expect(result).toMatch(/2024-12-25T\d{2}:30:45\.\d{3}Z/);
16+
});
17+
18+
test('should return undefined for undefined input', () => {
19+
const result = parseExifDate(undefined);
20+
expect(result).toBeUndefined();
21+
});
22+
23+
test('should return undefined for invalid date', () => {
24+
const result = parseExifDate('invalid:date:format');
25+
expect(result).toBeUndefined();
26+
});
27+
28+
test('should return undefined for malformed EXIF date', () => {
29+
const result = parseExifDate('2024:13:45 25:99:99');
30+
expect(result).toBeUndefined();
31+
});
32+
33+
test('should handle partial EXIF dates', () => {
34+
const result = parseExifDate('2024:01:15');
35+
expect(result).toBeDefined();
36+
});
37+
});
38+
39+
describe('isTemporaryAndroidFileName', () => {
40+
test('should return true for numeric filenames', () => {
41+
expect(isTemporaryAndroidFileName('12345.jpg')).toBe(true);
42+
expect(isTemporaryAndroidFileName('987654321.png')).toBe(true);
43+
expect(isTemporaryAndroidFileName('1234.mp4')).toBe(true);
44+
});
45+
46+
test('should return true for undefined or null', () => {
47+
expect(isTemporaryAndroidFileName(undefined)).toBe(true);
48+
expect(isTemporaryAndroidFileName(null)).toBe(true);
49+
});
50+
51+
test('should return true for empty string', () => {
52+
expect(isTemporaryAndroidFileName('')).toBe(true);
53+
});
54+
55+
test('should return false for descriptive filenames', () => {
56+
expect(isTemporaryAndroidFileName('IMG_20240512_143045.jpg')).toBe(false);
57+
expect(isTemporaryAndroidFileName('photo_2024.png')).toBe(false);
58+
expect(isTemporaryAndroidFileName('vacation.mp4')).toBe(false);
59+
});
60+
61+
test('should return false for filenames with letters', () => {
62+
expect(isTemporaryAndroidFileName('abc123.jpg')).toBe(false);
63+
expect(isTemporaryAndroidFileName('12abc.png')).toBe(false);
64+
});
65+
66+
test('should handle different file extensions', () => {
67+
expect(isTemporaryAndroidFileName('12345.heic')).toBe(true);
68+
expect(isTemporaryAndroidFileName('67890.mov')).toBe(true);
69+
expect(isTemporaryAndroidFileName('11111.webp')).toBe(true);
70+
});
71+
72+
test('should return false for filenames without extension', () => {
73+
expect(isTemporaryAndroidFileName('12345')).toBe(false);
74+
});
75+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import drive from '../..';
2+
3+
/**
4+
* Parses EXIF date string (format: "YYYY:MM:DD HH:MM:SS") to ISO string
5+
* @param exifDate - EXIF date string
6+
* @returns ISO date string or undefined if parsing fails
7+
*/
8+
export function parseExifDate(exifDate: string | undefined): string | undefined {
9+
if (!exifDate) return undefined;
10+
11+
try {
12+
const fixedDate = exifDate.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3');
13+
const parsedDate = new Date(fixedDate);
14+
return isNaN(parsedDate.getTime()) ? undefined : parsedDate.toISOString();
15+
} catch {
16+
return undefined;
17+
}
18+
}
19+
20+
/**
21+
* Checks if filename is a temporary Android numeric name
22+
* @param fileName - Original filename
23+
* @returns true if it's a temporary Android name
24+
*/
25+
export function isTemporaryAndroidFileName(fileName: string | undefined | null): boolean {
26+
if (!fileName) return true;
27+
return /^\d+\.\w+$/i.test(fileName);
28+
}
29+
30+
/**
31+
* Generates a descriptive filename for Android media files
32+
* @param uri - File URI to extract extension
33+
* @param creationTime - ISO string of creation time
34+
* @param modificationTime - ISO string of modification time
35+
* @returns Generated filename in format IMG_YYYY-MM-DD_HH-MM-SS.extension
36+
*/
37+
export function generateAndroidFileName(uri: string, creationTime?: string, modificationTime?: string): string {
38+
let timestamp: Date;
39+
40+
try {
41+
const dateString = modificationTime || creationTime;
42+
timestamp = dateString ? new Date(dateString) : new Date();
43+
44+
if (isNaN(timestamp.getTime())) {
45+
timestamp = new Date();
46+
}
47+
} catch (error) {
48+
timestamp = new Date();
49+
}
50+
51+
try {
52+
const isoString = timestamp.toISOString();
53+
const [datePart, timePart] = isoString.split('T');
54+
const timeStr = timePart.split('.')[0].replace(/:/g, '');
55+
const extension = drive.file.getExtensionFromUri(uri)?.toLowerCase();
56+
57+
const generatedName = `IMG_${datePart}_${timeStr}.${extension}`;
58+
59+
return generatedName;
60+
} catch (error) {
61+
const fallbackExtension = drive.file.getExtensionFromUri(uri)?.toLowerCase();
62+
return `IMG_${Date.now()}.${fallbackExtension}`;
63+
}
64+
}

0 commit comments

Comments
 (0)