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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@internxt/lib": "^1.2.0",
"@internxt/mobile-sdk": "^0.2.41",
"@internxt/rn-crypto": "0.1.15",
"@internxt/sdk": "1.11.0",
"@internxt/sdk": "=1.11.6",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-navigation/bottom-tabs": "^6.2.0",
"@react-navigation/native": "^6.1.18",
Expand Down
157 changes: 118 additions & 39 deletions src/components/modals/AddModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { useDrive } from '@internxt-mobile/hooks/drive';
import { imageService, logger } from '@internxt-mobile/services/common';
import { uploadService } from '@internxt-mobile/services/common/network/upload/upload.service';
import drive from '@internxt-mobile/services/drive';
import {
generateFileName,
isTemporaryFileName,
parseExifDate,
} from '@internxt-mobile/services/drive/file/utils/exifHelpers';
import errorService from '@internxt-mobile/services/ErrorService';
import { DriveFileData, EncryptionVersion, FileEntryByUuid, Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
import { SaveFormat } from 'expo-image-manipulator';
Expand Down Expand Up @@ -46,7 +51,7 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { driveActions, driveThunks } from '../../../store/slices/drive';
import { uiActions } from '../../../store/slices/ui';
import { NotificationType, ProgressCallback } from '../../../types';
import { DriveEventKey, UPLOAD_FILE_SIZE_LIMIT, UploadingFile } from '../../../types/drive';
import { DocumentPickerFile, DriveEventKey, UPLOAD_FILE_SIZE_LIMIT, UploadingFile } from '../../../types/drive';
import AppText from '../../AppText';
import BottomModal from '../BottomModal';
import CreateFolderModal from '../CreateFolderModal';
Expand Down Expand Up @@ -127,6 +132,8 @@ function AddModal(): JSX.Element {
fileExtension,
fileToUpload.parentUuid,
progressCallback,
fileToUpload.modificationTime,
fileToUpload.creationTime,
);

dispatch(driveActions.uploadingFileEnd(fileToUpload.id));
Expand Down Expand Up @@ -157,6 +164,8 @@ function AddModal(): JSX.Element {
fileExtension: string,
currentFolderId: string,
progressCallback: ProgressCallback,
modificationTime?: string,
creationTime?: string,
) {
const { bucket, bridgeUser, mnemonic, userId } = await asyncStorage.getUser();
logger.info('Stating file...');
Expand Down Expand Up @@ -184,22 +193,29 @@ function AddModal(): JSX.Element {
notifyProgress: progressCallback,
},
);

logger.info('File uploaded with fileId: ', fileId);
logger.info('File uploaded with name: ', fileName);
logger.info('File uploaded with modificationTime: ', modificationTime);
logger.info('File uploaded with creationTime: ', creationTime);

const folderId = currentFolderId;
const plainName = fileName;
const modTimestamp = modificationTime ?? fileStat.mtime;
const modificationTimeISO = modTimestamp ? new Date(modTimestamp).toISOString() : undefined;

const createTimestamp = creationTime ?? fileStat.ctime;
const creationTimeISO = createTimestamp ? new Date(createTimestamp).toISOString() : undefined;

const fileEntryByUuid: FileEntryByUuid = {
id: fileId,
fileId: fileId,
type: fileExtension,
size: fileSize,
name: plainName,
plain_name: plainName,
plainName: plainName,
bucket,
folder_id: folderId,
encrypt_version: EncryptionVersion.Aes03,
folderUuid: folderId,
encryptVersion: EncryptionVersion.Aes03,
modificationTime: modificationTimeISO,
creationTime: creationTimeISO,
};

let uploadedThumbnail: Thumbnail | null = null;
Expand Down Expand Up @@ -319,22 +335,21 @@ function AddModal(): JSX.Element {
dispatch(driveActions.setUri(undefined));
}

function processFilesFromPicker(documents: DocumentPickerResponse[]): Promise<void> {
function processFilesFromPicker(documents: DocumentPickerFile[]): Promise<void> {
documents.forEach((doc) => (doc.uri = doc.fileCopyUri));
dispatch(uiActions.setShowUploadFileModal(false));

return uploadDocuments(documents);
}

async function uploadDocuments(documents: DocumentPickerResponse[]) {
async function uploadDocuments(documents: DocumentPickerFile[]) {
if (!focusedFolder) {
throw new Error('No current folder found');
}

const { filesToUpload, filesExcluded } = validateAndFilterFiles(documents);
showFileSizeAlert(filesExcluded);
const filesToProcess = await handleDuplicateFiles(filesToUpload, focusedFolder.uuid);

if (filesToProcess.length === 0) {
dispatch(uiActions.setShowUploadFileModal(false));
return;
Expand Down Expand Up @@ -431,7 +446,6 @@ function AddModal(): JSX.Element {
try {
const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.assetId || asset.uri);
const cleanUri = assetInfo.mediaType === 'video' ? asset.uri : assetInfo.localUri || asset.uri;
// asset info has the correct format (heic issue)
const originalFileName = assetInfo.filename || asset.fileName;

let fileSize = asset.fileSize;
Expand All @@ -455,10 +469,11 @@ function AddModal(): JSX.Element {
} catch (error) {
logger.error('Error obtaining original asset info:', error);
const cleanUri = asset.uri;
const formatInfo = detectImageFormat(asset);
const fallbackName = generateFileName(cleanUri);

documents.push({
fileCopyUri: cleanUri,
name: asset.fileName ?? `media_${Date.now()}.${formatInfo.extension ?? 'jpg'}`,
name: fallbackName,
size: asset.fileSize ?? 0,
type: asset.type ?? '',
uri: cleanUri,
Expand Down Expand Up @@ -496,35 +511,99 @@ function AddModal(): JSX.Element {
}
}
} else {
DocumentPicker.pickMultiple({
type: [DocumentPicker.types.images],
copyTo: 'cachesDirectory',
})
.then(processFilesFromPicker)
.then(async () => {
dispatch(driveThunks.loadUsageThunk());
const { status } = await MediaLibrary.requestPermissionsAsync();

if (focusedFolder) {
await SLEEP_BECAUSE_MAYBE_BACKEND_IS_NOT_RETURNING_FRESHLY_MODIFIED_OR_CREATED_ITEMS_YET(1000);
driveCtx.loadFolderContent(focusedFolder.uuid, {
pullFrom: ['network'],
resetPagination: true,
});
}
})
.catch((err) => {
if (err.message === 'User canceled document picker') {
return;
}
logger.error('Error on handleUploadFromCameraRoll function:', JSON.stringify(err));
notificationsService.show({
type: NotificationType.Error,
text1: strings.formatString(strings.errors.uploadFile, err.message) as string,
if (status === 'granted') {
try {
const result = await launchImageLibraryAsync({
mediaTypes: MediaTypeOptions.All,
allowsMultipleSelection: true,
selectionLimit: MAX_FILES_BULK_UPLOAD,
allowsEditing: false,
preferredAssetRepresentationMode: UIImagePickerPreferredAssetRepresentationMode.Current,
exif: true,
base64: false,
});
})
.finally(() => {

if (result.canceled || !result.assets?.length) return;

const documents: DocumentPickerFile[] = [];

for (const asset of result.assets) {
try {
const cleanUri = asset.uri;
let originalFileName = asset.fileName;
const exif = asset.exif || null;

const creationTime = parseExifDate(exif?.DateTimeOriginal);
const modificationTime = parseExifDate(exif?.DateTime);
if (isTemporaryFileName(originalFileName)) {
originalFileName = generateFileName(cleanUri, creationTime, modificationTime);
}

let fileSize = asset.fileSize ?? 0;
if (!fileSize) {
try {
const fileInfo = await FileSystem.getInfoAsync(cleanUri);
fileSize = fileInfo.exists ? fileInfo.size || 0 : 0;
} catch (error) {
logger.warn('The file size could not be obtained:', error);
fileSize = 0;
}
}

documents.push({
fileCopyUri: cleanUri,
name: decodeURIComponent(originalFileName ?? ''),
size: fileSize,
type: drive.file.getExtensionFromUri(cleanUri)?.toLowerCase() ?? '',
uri: cleanUri,
creationTime,
modificationTime,
});
} catch (error) {
logger.error('Error obtaining original asset info:', error);
const cleanUri = asset.uri;
const fallbackName = generateFileName(cleanUri);

documents.push({
fileCopyUri: cleanUri,
name: fallbackName,
size: asset.fileSize ?? 0,
type: asset.type ?? '',
uri: cleanUri,
});
}
}

dispatch(uiActions.setShowUploadFileModal(false));
});

uploadDocuments(documents)
.then(async () => {
dispatch(driveThunks.loadUsageThunk());

if (focusedFolder) {
await SLEEP_BECAUSE_MAYBE_BACKEND_IS_NOT_RETURNING_FRESHLY_MODIFIED_OR_CREATED_ITEMS_YET(500);
driveCtx.loadFolderContent(focusedFolder.uuid, {
pullFrom: ['network'],
resetPagination: true,
});
}
})
.catch((err) => {
logger.error('Error on handleUploadFromCameraRoll (Android):', JSON.stringify(err));
notificationsService.show({
type: NotificationType.Error,
text1: strings.formatString(strings.errors.uploadFile, err.message) as string,
});
})
.finally(() => {
dispatch(uiActions.setShowUploadFileModal(false));
});
} catch (error) {
logger.error('Error accessing media library (Android):', error);
}
}
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/services/drive/file/driveFile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import asyncStorageService from '@internxt-mobile/services/AsyncStorageService';
import { imageService, SdkManager } from '@internxt-mobile/services/common';
import fileSystemService, { fs } from '@internxt-mobile/services/FileSystemService';
import { Abortable, AsyncStorageKey } from '@internxt-mobile/types/index';
import { EncryptionVersion, MoveFileUuidPayload, Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
import { EncryptionVersion, FileMeta, Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import { SaveFormat } from 'expo-image-manipulator';
import { Image } from 'react-native';
Expand Down Expand Up @@ -97,8 +97,14 @@ class DriveFileService {
});
}

public async moveFile(moveFilePayload: MoveFileUuidPayload) {
return this.sdk.storageV2.moveFileByUuid(moveFilePayload);
public async moveFile({
fileUuid,
destinationFolderUuid,
}: {
fileUuid: string;
destinationFolderUuid: string;
}): Promise<FileMeta> {
return this.sdk.storageV2.moveFileByUuid(fileUuid, { destinationFolder: destinationFolderUuid });
}

public getSortFunction({
Expand Down
2 changes: 2 additions & 0 deletions src/services/drive/file/utils/checkDuplicatedFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface File {
uri: string;
size: number;
type?: string;
modificationTime?: string;
creationTime?: string;
}

export const checkDuplicatedFiles = async (files: File[], parentFolderUuid: string): Promise<DuplicatedFilesResult> => {
Expand Down
75 changes: 75 additions & 0 deletions src/services/drive/file/utils/exifHelpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
jest.mock('../..', () => ({
default: {
file: {
getExtensionFromUri: jest.fn(),
},
},
}));

import { isTemporaryFileName, parseExifDate } from './exifHelpers';

describe('parseExifDate', () => {
test('should parse valid EXIF date format', () => {
const exifDate = '2024:12:25 14:30:45';
const result = parseExifDate(exifDate);
expect(result).toMatch(/2024-12-25T\d{2}:30:45\.\d{3}Z/);
});

test('should return undefined for undefined input', () => {
const result = parseExifDate(undefined);
expect(result).toBeUndefined();
});

test('should return undefined for invalid date', () => {
const result = parseExifDate('invalid:date:format');
expect(result).toBeUndefined();
});

test('should return undefined for malformed EXIF date', () => {
const result = parseExifDate('2024:13:45 25:99:99');
expect(result).toBeUndefined();
});

test('should handle partial EXIF dates', () => {
const result = parseExifDate('2024:01:15');
expect(result).toBeDefined();
});
});

describe('isTemporaryFileName', () => {
test('should return true for numeric filenames', () => {
expect(isTemporaryFileName('12345.jpg')).toBe(true);
expect(isTemporaryFileName('987654321.png')).toBe(true);
expect(isTemporaryFileName('1234.mp4')).toBe(true);
});

test('should return true for undefined or null', () => {
expect(isTemporaryFileName(undefined)).toBe(true);
expect(isTemporaryFileName(null)).toBe(true);
});

test('should return true for empty string', () => {
expect(isTemporaryFileName('')).toBe(true);
});

test('should return false for descriptive filenames', () => {
expect(isTemporaryFileName('IMG_20240512_143045.jpg')).toBe(false);
expect(isTemporaryFileName('photo_2024.png')).toBe(false);
expect(isTemporaryFileName('vacation.mp4')).toBe(false);
});

test('should return false for filenames with letters', () => {
expect(isTemporaryFileName('abc123.jpg')).toBe(false);
expect(isTemporaryFileName('12abc.png')).toBe(false);
});

test('should handle different file extensions', () => {
expect(isTemporaryFileName('12345.heic')).toBe(true);
expect(isTemporaryFileName('67890.mov')).toBe(true);
expect(isTemporaryFileName('11111.webp')).toBe(true);
});

test('should return false for filenames without extension', () => {
expect(isTemporaryFileName('12345')).toBe(false);
});
});
Loading
Loading