Skip to content
Closed
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/app/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const HTTP_CODES = {
MAX_SPACE_USED: 420,
FORBIDDEN: 403,
NOT_FOUND: 404,
PAYMENT_REQUIRED: 402,
};
export enum ErrorMessages {
ServerUnavailable = 'Server Unavailable',
Expand Down
11 changes: 11 additions & 0 deletions src/app/drive/services/file.service/upload.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ export class BucketNotFoundError extends Error {
Object.setPrototypeOf(this, BucketNotFoundError.prototype);
}
}

export class EmptyFileNotAllowedError extends Error {
readonly fileName: string;

constructor(fileName: string) {
super('Empty files are not allowed by current plan');
this.name = 'EmptyFileNotAllowedError';
this.fileName = fileName;
Object.setPrototypeOf(this, EmptyFileNotAllowedError.prototype);
}
}
75 changes: 74 additions & 1 deletion src/app/drive/services/file.service/uploadFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ vi.mock('services/navigation.service', () => ({
import { SdkFactory } from 'app/core/factory/sdk';
import workspacesService from 'services/workspace.service';
import { Network, getEnvironmentConfig } from 'app/drive/services/network.service';
import { BucketNotFoundError, FileIdRequiredError } from './upload.errors';
import { BucketNotFoundError, EmptyFileNotAllowedError, FileIdRequiredError } from './upload.errors';
import { DriveFileData } from 'app/drive/types';

const mockSdkFactory = vi.mocked(SdkFactory);
Expand Down Expand Up @@ -340,6 +340,79 @@ describe('Uploading a file', () => {
expect(mockNetwork).not.toHaveBeenCalled();
});

test('When the plan does not allow empty files, then the user is informed', async () => {
const file: FileToUpload = {
name: 'empty-file.txt',
size: 0,
type: 'txt',
content: new File([], 'empty-file.txt'),
parentFolderId: 'folder-123',
};
const bucketId = 'bucket-123';

mockGetEnvironmentConfig.mockReturnValue({
bridgeUser: 'user',
bridgePass: 'pass',
encryptionKey: 'key',
bucketId,
} as any);

const paymentRequiredError = Object.assign(new Error('Payment required'), { status: 402 });
const mockCreateFileEntryByUuid = vi.fn().mockRejectedValue(paymentRequiredError);
mockSdkFactory.getNewApiInstance.mockReturnValue({
createNewStorageClient: vi.fn(() => ({
createFileEntryByUuid: mockCreateFileEntryByUuid,
})),
} as any);

await expect(
uploadFile(
'user@test.com',
file,
vi.fn(),
{ isTeam: false },
{ taskId: 'task-1', isPaused: false, isRetriedUpload: false },
),
).rejects.toThrow(EmptyFileNotAllowedError);
expect(mockNetwork).not.toHaveBeenCalled();
});

test('When uploading an empty file fails unexpectedly, then the original error is shown', async () => {
const file: FileToUpload = {
name: 'empty-file.txt',
size: 0,
type: 'txt',
content: new File([], 'empty-file.txt'),
parentFolderId: 'folder-123',
};
const bucketId = 'bucket-123';

mockGetEnvironmentConfig.mockReturnValue({
bridgeUser: 'user',
bridgePass: 'pass',
encryptionKey: 'key',
bucketId,
} as any);

const serverError = Object.assign(new Error('Server error'), { status: 500 });
const mockCreateFileEntryByUuid = vi.fn().mockRejectedValue(serverError);
mockSdkFactory.getNewApiInstance.mockReturnValue({
createNewStorageClient: vi.fn(() => ({
createFileEntryByUuid: mockCreateFileEntryByUuid,
})),
} as any);

await expect(
uploadFile(
'user@test.com',
file,
vi.fn(),
{ isTeam: false },
{ taskId: 'task-1', isPaused: false, isRetriedUpload: false },
),
).rejects.toThrow('Server error');
});

test('When uploading a file without bucket id, then an error indicating so is thrown', async () => {
const file: FileToUpload = {
name: 'test-file',
Expand Down
26 changes: 18 additions & 8 deletions src/app/drive/services/file.service/uploadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { generateThumbnailFromFile } from '../thumbnail.service';
import { OwnerUserAuthenticationData } from 'app/network/types';
import { FileToUpload } from './types';
import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types';
import { BucketNotFoundError, FileIdRequiredError } from './upload.errors';
import { BucketNotFoundError, EmptyFileNotAllowedError, FileIdRequiredError } from './upload.errors';
import { HTTP_CODES } from 'app/core/constants';
import errorService from 'services/error.service';
import { isFileEmpty } from 'utils/isFileEmpty';
import { FileEntry } from '@internxt/sdk/dist/workspaces';

Expand Down Expand Up @@ -101,13 +103,21 @@ export async function uploadFile(
const isWorkspacesUpload = workspaceId && workspacesToken;

if (isFileEmpty(file.content) && !isWorkspacesUpload) {
return createFileEntry({
bucketId: bucketId,
file,
resourcesToken: resourcesToken,
workspaceId: workspaceId,
ownerToken: workspacesToken,
});
try {
return await createFileEntry({
bucketId: bucketId,
file,
resourcesToken: resourcesToken,
workspaceId: workspaceId,
ownerToken: workspacesToken,
});
} catch (err) {
const error = errorService.castError(err);
if (error.status === HTTP_CODES.PAYMENT_REQUIRED) {
throw new EmptyFileNotAllowedError(file.name);
}
throw err;
}
}

if (!bucketId) {
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,7 @@
"errorMovingToTrash": "Beim Verschieben der Elemente in den Papierkorb ist ein Fehler aufgetreten.",
"errorDeletingFromTrash": "Beim Löschen der Elemente aus dem Papierkorb ist ein Fehler aufgetreten.",
"maxSizeUploadLimitError": "Datei zu groß (größer als 40 GB)",
"emptyFileNotAllowed": "\"{{fileName}}\" wurde übersprungen: Aktualisieren Sie Ihren Tarif, um leere Dateien hochzuladen",
"braveNotSupportMultiplePhotosDowload": "Brave unterstützt nicht das Herunterladen mehrerer Fotos.",
"updateAvatarError": "Fehler beim Aktualisieren des Avatars",
"featuresUnavailable": "Einige Funktionen sind möglicherweise nicht verfügbar",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@
"errorMovingToTrash": "An error occurred while moving the items to trash.",
"errorDeletingFromTrash": "An error occurred while deleting the items from trash.",
"maxSizeUploadLimitError": "File too large (greater than 40GB)",
"emptyFileNotAllowed": "\"{{fileName}}\" was skipped: upgrade your plan to upload empty files",
"connectionLostError": "Internet connection lost",
"errorLoadingTrashItems": "Error loading trash items",
"updateAvatarError": "Error updating avatar",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,7 @@
"errorMovingToTrash": "Ha ocurrido un error mientras se movian los items a la basura.",
"errorDeletingFromTrash": "Ha ocurrido un error mientras se eliminaban los items de la basura.",
"maxSizeUploadLimitError": "Archivo demasiado grande (mayor de 40GB)",
"emptyFileNotAllowed": "\"{{fileName}}\" fue omitido: actualiza tu plan para subir archivos vacíos",
"connectionLostError": "Conexión a internet perdida",
"errorLoadingTrashItems": "Error al cargar items de la papelera",
"updateAvatarError": "Error al actualizar el avatar",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,7 @@
"emptyPassword": "Le mot de passe précédent ne doit pas être vide",
"errorMovingToTrash": "Error lors du déplacement des éléments.",
"maxSizeUploadLimitError": "Fichier trop volumineux (plus de 40GB)",
"emptyFileNotAllowed": "\"{{fileName}}\" a été ignoré : améliorez votre forfait pour téléverser des fichiers vides",
"connectionLostError": "Lost Internet connection",
"errorLoadingTrashItems": "Erreur de chargement des éléments de la corbeille",
"updateAvatarError": "Erreur lors de la mise à jour de l'avatar",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,7 @@
"errorMovingToTrash": "Si è verificato un errore durante lo spostamento degli elementi nel cestino.",
"errorDeletingFromTrash": "Si è verificato un errore durante l'eliminazione degli elementi dal cestino.",
"maxSizeUploadLimitError": "File troppo grande (superiore a 40GB)",
"emptyFileNotAllowed": "\"{{fileName}}\" è stato ignorato: aggiorna il tuo piano per caricare file vuoti",
"connectionLostError": "Connessione Internet persa",
"errorLoadingTrashItems": "Errore nel caricamento degli elementi del cestino",
"updateAvatarError": "Errore nell'aggiornamento dell'avatar",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,7 @@
"errorMovingToTrash": "При перемещении элементов в корзину произошла ошибка.",
"errorDeletingFromTrash": "При удалении элементов из корзины произошла ошибка.",
"maxSizeUploadLimitError": "Слишком большой файл (более 40 ГБ)",
"emptyFileNotAllowed": "\"{{fileName}}\" пропущен: обновите тарифный план, чтобы загружать пустые файлы",
"connectionLostError": "Потеряно подключение к Интернету",
"errorLoadingTrashItems": "Ошибка при загрузке элементов корзины",
"updateAvatarError": "Ошибка при обновлении аватара",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@
"errorMovingToTrash": "將項目移動到垃圾桶時出錯。",
"errorDeletingFromTrash": "從垃圾桶中刪除項目時出錯。",
"maxSizeUploadLimitError": "文件過大(大於40GB)",
"emptyFileNotAllowed": "已略過 \"{{fileName}}\":請升級方案以上傳空檔案",
"connectionLostError": "網絡連接已丟失",
"errorLoadingTrashItems": "加載垃圾桶項目時出錯",
"updateAvatarError": "更新頭像時發生錯誤",
Expand Down
1 change: 1 addition & 0 deletions src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,7 @@
"errorMovingToTrash": "将项目移至垃圾箱时出错。",
"errorDeletingFromTrash": "从垃圾箱中删除项目时出错。",
"maxSizeUploadLimitError": "文件太大(大于40GB)",
"emptyFileNotAllowed": "已跳过 \"{{fileName}}\":请升级套餐以上传空文件",
"connectionLostError": "互联网连接丢失",
"errorLoadingTrashItems": "加载垃圾箱内的项目时出错",
"updateAvatarError": "更新头像时出错",
Expand Down
6 changes: 2 additions & 4 deletions src/app/network/UploadFolderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,8 @@ export class UploadFoldersManager {
}),
)
.unwrap()
.catch(() => {
this.stopUploadTask(taskId, abortController);
this.killQueueAndNotifyError(taskId);
return;
.catch((error) => {
errorService.reportError(error);
});
};

Expand Down
49 changes: 49 additions & 0 deletions src/app/network/UploadManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DriveFileData } from 'app/drive/types';
import RetryManager from './RetryManager';
import { TaskStatus } from 'app/tasks/types';
import { ErrorMessages } from 'app/core/constants';
import { EmptyFileNotAllowedError } from 'app/drive/services/file.service/upload.errors';

vi.mock('app/drive/services/file.service/uploadFile', () => ({
default: vi.fn(() => Promise.resolve({} as DriveFileData)),
Expand Down Expand Up @@ -670,4 +671,52 @@ describe('checkUploadFiles', () => {
expect.any(Object),
);
});

it('When the plan does not allow empty files, then the upload is skipped and the user is informed', async () => {
const fileName = 'empty-file.txt';
(uploadFile as Mock).mockRejectedValueOnce(new EmptyFileNotAllowedError(fileName));

const updateTaskSpy = vi.spyOn(tasksService, 'updateTask').mockReturnValue();
vi.spyOn(tasksService, 'create').mockReturnValue('taskId');
vi.spyOn(tasksService, 'addListener').mockReturnValue();
vi.spyOn(tasksService, 'removeListener').mockReturnValue();
const reportErrorSpy = vi.spyOn(errorService, 'reportError').mockReturnValue();
const emptyFileNotAllowedCallback = vi.fn();

await uploadFileWithManager(
[
{
taskId: 'taskId',
filecontent: {
content: 'file-content' as unknown as File,
type: 'text/plain',
name: fileName,
size: 0,
parentFolderId: 'folder-1',
},
userEmail: '',
parentFolderId: '',
},
],
openMaxSpaceOccupiedDialogMock,
DatabaseUploadRepository.getInstance(),
undefined,
{
ownerUserAuthenticationData: undefined,
sharedItemData: {
isDeepFolder: false,
currentFolderId: 'parentFolderId',
},
isUploadedFromFolder: true,
},
{ emptyFileNotAllowedCallback },
);

expect(emptyFileNotAllowedCallback).toHaveBeenCalledWith(fileName);
expect(updateTaskSpy).toHaveBeenCalledWith({
taskId: 'taskId',
merge: { status: TaskStatus.Error, subtitle: expect.any(String) },
});
expect(reportErrorSpy).not.toHaveBeenCalled();
});
});
33 changes: 25 additions & 8 deletions src/app/network/UploadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { t } from 'i18next';
import errorService from 'services/error.service';
import { HTTP_CODES } from '../core/constants';
import uploadFile from 'app/drive/services/file.service/uploadFile';
import { EmptyFileNotAllowedError } from 'app/drive/services/file.service/upload.errors';
import { DriveFileData } from 'app/drive/types';
import { PersistUploadRepository } from '../repositories/DatabaseUploadRepository';
import tasksService from '../tasks/services/tasks.service';
Expand Down Expand Up @@ -46,23 +47,27 @@ export type UploadManagerFileParams = {
isUploadedFromFolder?: boolean;
};

export type UploadFileWithManagerCallbacks = {
relatedTaskProgress?: { filesUploaded: number; totalFilesToUpload: number };
onFileUploadCallback?: (driveFileData: DriveFileData) => void;
emptyFileNotAllowedCallback?: (fileName: string) => void;
};

export const uploadFileWithManager = (
files: UploadManagerFileParams[],
maxSpaceOccupiedCallback: () => void,
uploadRepository: PersistUploadRepository,
abortController?: AbortController,
options?: Options,
relatedTaskProgress?: { filesUploaded: number; totalFilesToUpload: number },
onFileUploadCallback?: (driveFileData: DriveFileData) => void,
callbacks?: UploadFileWithManagerCallbacks,
): Promise<{ uploadedFiles: DriveFileData[] }> => {
const uploadManager = new UploadManager(
files,
maxSpaceOccupiedCallback,
uploadRepository,
abortController,
options,
relatedTaskProgress,
onFileUploadCallback,
callbacks,
);
return uploadManager.run();
};
Expand All @@ -76,6 +81,7 @@ class UploadManager {
private options?: Options;
private relatedTaskProgress?: { filesUploaded: number; totalFilesToUpload: number };
private maxSpaceOccupiedCallback: () => void;
private readonly emptyFileNotAllowedCallback?: (fileName: string) => void;
private onFileUploadCallback?: (driveFileData: DriveFileData) => void;
private uploadRepository?: PersistUploadRepository;
private filesUploadedList: (DriveFileData & { taskId: string })[] = [];
Expand Down Expand Up @@ -240,6 +246,17 @@ class UploadManager {
!!this.abortController?.signal.aborted || !!fileData.abortController?.signal.aborted || error === 'abort';
const isLostConnectionError =
error instanceof ConnectionLostError || error.message === ErrorMessages.NetworkError;
const isEmptyFileNotAllowed = error instanceof EmptyFileNotAllowedError;

if (isEmptyFileNotAllowed) {
this.emptyFileNotAllowedCallback?.(error.fileName);
tasksService.updateTask({
taskId,
merge: { status: TaskStatus.Error, subtitle: t('tasks.subtitles.upload-failed') as string },
});
next(null);
return;
}

if (uploadAttempts < MAX_UPLOAD_ATTEMPTS && !isUploadAborted && !isLostConnectionError) {
upload();
Expand Down Expand Up @@ -277,16 +294,16 @@ class UploadManager {
uploadRepository?: PersistUploadRepository,
abortController?: AbortController,
options?: Options,
relatedTaskProgress?: { filesUploaded: number; totalFilesToUpload: number },
onFileUploadCallback?: (driveFileData: DriveFileData) => void,
callbacks?: UploadFileWithManagerCallbacks,
) {
this.items = items;
this.abortController = abortController;
this.options = options;
this.relatedTaskProgress = relatedTaskProgress;
this.relatedTaskProgress = callbacks?.relatedTaskProgress;
this.maxSpaceOccupiedCallback = maxSpaceOccupiedCallback;
this.emptyFileNotAllowedCallback = callbacks?.emptyFileNotAllowedCallback;
this.uploadRepository = uploadRepository;
this.onFileUploadCallback = onFileUploadCallback;
this.onFileUploadCallback = callbacks?.onFileUploadCallback;
}

private handleUploadErrors({
Expand Down
Loading
Loading