Skip to content
Open
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
1 change: 1 addition & 0 deletions src/services/common/httpStatusCodes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const HTTP_BAD_REQUEST = 400;
export const HTTP_UNAUTHORIZED = 401;
export const HTTP_NOT_FOUND = 404;
export const HTTP_CONFLICT = 409;
Expand Down
43 changes: 37 additions & 6 deletions src/services/photos/PhotoUploadService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as RNFS from '@dr.pogodin/react-native-fs';
import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types';
import AppError from '@internxt/sdk/dist/shared/types/errors';
import * as MediaLibrary from 'expo-media-library';
import { uploadFile } from 'src/network/upload';
import asyncStorageService from 'src/services/AsyncStorageService';
import { isThumbnailSupported } from 'src/services/common/media/thumbnail.constants';
import { generateThumbnail } from 'src/services/common/media/thumbnail.generation';
import { uploadService } from 'src/services/common/network/upload/upload.service';
import { PhotoUploadService } from './PhotoUploadService';
import { photoBackupFolders } from './PhotoBackupFolders';
import { PhotoUploadService } from './PhotoUploadService';

jest.mock('expo-media-library', () => ({
getAssetInfoAsync: jest.fn(),
Expand Down Expand Up @@ -187,7 +188,10 @@ describe('PhotoUploadService.upload', () => {
});

test('when the thumbnail bucket upload throws, then the main upload still returns the drive file uuid', async () => {
mockUploadFile.mockReset().mockResolvedValueOnce('bucket-file-id').mockRejectedValueOnce(new Error('network error'));
mockUploadFile
.mockReset()
.mockResolvedValueOnce('bucket-file-id')
.mockRejectedValueOnce(new Error('network error'));

const result = await PhotoUploadService.upload(makeAsset(), DEVICE_ID);

Expand All @@ -196,7 +200,10 @@ describe('PhotoUploadService.upload', () => {
});

test('when the thumbnail bucket upload throws, then the thumbnail temp file is still cleaned up', async () => {
mockUploadFile.mockReset().mockResolvedValueOnce('bucket-file-id').mockRejectedValueOnce(new Error('network error'));
mockUploadFile
.mockReset()
.mockResolvedValueOnce('bucket-file-id')
.mockRejectedValueOnce(new Error('network error'));

await PhotoUploadService.upload(makeAsset(), DEVICE_ID);

Expand All @@ -211,9 +218,7 @@ describe('PhotoUploadService.replace', () => {
await PhotoUploadService.replace(asset, 'existing-remote-id', DEVICE_ID);

expect(mockGenerateThumbnail).toHaveBeenCalledWith(LOCAL_PATH, 'jpg');
expect(mockCreateThumbnailEntry).toHaveBeenCalledWith(
expect.objectContaining({ fileUuid: 'existing-remote-id' }),
);
expect(mockCreateThumbnailEntry).toHaveBeenCalledWith(expect.objectContaining({ fileUuid: 'existing-remote-id' }));
});

test('when replacing an asset, then the existing remote file id is returned', async () => {
Expand All @@ -229,4 +234,30 @@ describe('PhotoUploadService.replace', () => {

expect(result).toBe('existing-remote-id');
});

test('when the server rejects the replace with a 400, then a new drive entry is created and its uuid is returned', async () => {
mockReplaceFileEntry.mockRejectedValue(new AppError('file can not be replaced', 400));
mockCreateFileEntry.mockResolvedValue({ uuid: 'new-drive-uuid' });

const result = await PhotoUploadService.replace(makeAsset(), 'deleted-remote-id', DEVICE_ID);

expect(mockCreateFileEntry).toHaveBeenCalledTimes(1);
expect(result).toBe('new-drive-uuid');
});

test('when the server rejects the replace with a 400 and a new entry is created, then the thumbnail is registered against the new file uuid', async () => {
mockReplaceFileEntry.mockRejectedValue(new AppError('file can not be replaced', 400));
mockCreateFileEntry.mockResolvedValue({ uuid: 'new-drive-uuid' });

await PhotoUploadService.replace(makeAsset(), 'deleted-remote-id', DEVICE_ID);

expect(mockCreateThumbnailEntry).toHaveBeenCalledWith(expect.objectContaining({ fileUuid: 'new-drive-uuid' }));
});

test('when the server rejects the replace with a non-400 error, then the error is propagated without creating a new entry', async () => {
mockReplaceFileEntry.mockRejectedValue(new AppError('internal server error', 500));

await expect(PhotoUploadService.replace(makeAsset(), 'remote-id', DEVICE_ID)).rejects.toThrow();
expect(mockCreateFileEntry).not.toHaveBeenCalled();
});
});
53 changes: 43 additions & 10 deletions src/services/photos/PhotoUploadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { getEnvironmentConfigFromUser } from 'src/lib/network';
import { uploadFile } from 'src/network/upload';
import { constants } from 'src/services/AppService';
import asyncStorageService from 'src/services/AsyncStorageService';
import { HTTP_BAD_REQUEST } from 'src/services/common/httpStatusCodes';
import { isThumbnailSupported } from 'src/services/common/media/thumbnail.constants';
import { generateThumbnail } from 'src/services/common/media/thumbnail.generation';
import { uploadService } from 'src/services/common/network/upload/upload.service';
import { logger } from '../common';
import { FileAlreadyExistsError } from './errors';
import { photoBackupFolders } from './PhotoBackupFolders';
import {
Expand Down Expand Up @@ -121,6 +123,11 @@ const uploadAssetToBucket = async (
};
};

const isDeletedOrTrashedError = (error: unknown): boolean => {
const status = (error as { status?: unknown })?.status;
return status === HTTP_BAD_REQUEST;
};

const cleanupTempFile = async (tempPath?: string): Promise<void> => {
if (!tempPath) return;
await RNFS.unlink(tempPath).catch(() => null);
Expand Down Expand Up @@ -218,18 +225,44 @@ export const PhotoUploadService = {
deviceId: string,
onProgress?: (ratio: number) => void,
): Promise<string> {
const { fileId, fileSize, localFilePath, fileExtension, tempPath, credentials } = await uploadAssetToBucket(
asset,
deviceId,
onProgress,
);
const {
fileId,
fileSize,
localFilePath,
fileExtension,
tempPath,
credentials,
plainName,
bucketId,
folderUuid,
modificationIso,
creationIso,
} = await uploadAssetToBucket(asset, deviceId, onProgress);

try {
await uploadService.replaceFileEntry(existingRemoteFileId, { fileId, size: fileSize });

await uploadThumbnailForAsset(localFilePath, fileExtension, existingRemoteFileId, credentials);

return existingRemoteFileId;
try {
await uploadService.replaceFileEntry(existingRemoteFileId, { fileId, size: fileSize });
await uploadThumbnailForAsset(localFilePath, fileExtension, existingRemoteFileId, credentials);
return existingRemoteFileId;
} catch (replaceError) {
if (!isDeletedOrTrashedError(replaceError)) {
logger.error(`Failed to replace file entry for ${existingRemoteFileId}:`, replaceError);
throw replaceError;
}
const driveFile = await uploadService.createFileEntry({
fileId,
type: fileExtension,
size: fileSize,
plainName,
bucket: bucketId,
folderUuid,
encryptVersion: EncryptionVersion.Aes03,
modificationTime: modificationIso,
creationTime: creationIso,
});
await uploadThumbnailForAsset(localFilePath, fileExtension, driveFile.uuid, credentials);
return driveFile.uuid;
}
} finally {
await cleanupTempFile(tempPath);
}
Expand Down
Loading