Skip to content

Commit

Permalink
BC-5583 - Display pdf preview (#4516)
Browse files Browse the repository at this point in the history
  • Loading branch information
bischofmax authored Nov 22, 2023
1 parent bbd7075 commit f48b9a0
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 57 deletions.
1 change: 1 addition & 0 deletions apps/server/src/infra/preview-generator/interface/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './preview';
export * from './preview-input-mime-types.enum';
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export enum PreviewInputMimeTypes {
IMAGE_HEIF = 'image/heif',
IMAGE_TIFF = 'image/tiff',
IMAGE_WEBP = 'image/webp',
APPLICATION_PDF = 'application/pdf',
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { GetFile, S3ClientAdapter } from '@infra/s3-client';
import { UnprocessableEntityException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@src/core/logger';
import { Readable } from 'node:stream';
import { PreviewGeneratorService } from './preview-generator.service';

const streamMock = jest.fn();
const resizeMock = jest.fn();
const selectFrameMock = jest.fn();
const imageMagickMock = () => {
return { stream: streamMock, resize: resizeMock, data: Readable.from('text') };
return { stream: streamMock, resize: resizeMock, selectFrame: selectFrameMock, data: Readable.from('text') };
};
jest.mock('gm', () => {
return {
subClass: () => imageMagickMock,
};
});

const createFile = (contentRange?: string): GetFile => {
const createFile = (contentRange?: string, contentType?: string): GetFile => {
const text = 'testText';
const readable = Readable.from(text);

const fileResponse = {
data: readable,
contentType: 'image/jpeg',
contentType,
contentLength: text.length,
contentRange,
etag: 'testTag',
Expand Down Expand Up @@ -68,76 +70,172 @@ describe('PreviewGeneratorService', () => {
});

describe('generatePreview', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile();
s3ClientAdapter.get.mockResolvedValueOnce(originFile);
describe('WHEN download of original and preview file is successful', () => {
describe('WHEN preview is possible', () => {
describe('WHEN mime type is jpeg', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);
const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};
const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};
return { params, originFile, expectedFileData };
};

describe('WHEN download of original and preview file is successful', () => {
it('should call storageClient get method with originFilePath', async () => {
const { params } = setup();
it('should call storageClient get method with originFilePath', async () => {
const { params } = setup();

await service.generatePreview(params);
await service.generatePreview(params);

expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath);
});
expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath);
});

it('should call imagemagicks resize method', async () => {
const { params } = setup();
it('should call imagemagicks resize method', async () => {
const { params } = setup();

await service.generatePreview(params);
await service.generatePreview(params);

expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>');
expect(resizeMock).toHaveBeenCalledTimes(1);
});
expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>');
expect(resizeMock).toHaveBeenCalledTimes(1);
});

it('should call imagemagicks stream method', async () => {
const { params } = setup();
it('should call imagemagicks stream method', async () => {
const { params } = setup();

await service.generatePreview(params);
await service.generatePreview(params);

expect(streamMock).toHaveBeenCalledWith(params.previewOptions.format);
expect(streamMock).toHaveBeenCalledTimes(1);
});
expect(streamMock).toHaveBeenCalledWith(params.previewOptions.format);
expect(streamMock).toHaveBeenCalledTimes(1);
});

it('should call S3ClientAdapters create method', async () => {
const { params, expectedFileData } = setup();
it('should call S3ClientAdapters create method', async () => {
const { params, expectedFileData } = setup();

await service.generatePreview(params);
await service.generatePreview(params);

expect(s3ClientAdapter.create).toHaveBeenCalledWith(params.previewFilePath, expectedFileData);
expect(s3ClientAdapter.create).toHaveBeenCalledTimes(1);
});
expect(s3ClientAdapter.create).toHaveBeenCalledWith(params.previewFilePath, expectedFileData);
expect(s3ClientAdapter.create).toHaveBeenCalledTimes(1);
});

it('should should return values', async () => {
const { params } = setup();
const expectedValue = { previewFilePath: params.previewFilePath, status: true };

const result = await service.generatePreview(params);

expect(result).toEqual(expectedValue);
});
});

describe('WHEN mime type is pdf', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.pdf',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'application/pdf');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

it('should should return values', async () => {
const { params } = setup();
const expectedValue = { previewFilePath: params.previewFilePath, status: true };
const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

const result = await service.generatePreview(params);
return { params, originFile, expectedFileData };
};

expect(result).toEqual(expectedValue);
it('should call imagemagicks resize method', async () => {
const { params } = setup();

await service.generatePreview(params);

expect(selectFrameMock).toHaveBeenCalledWith(0);
expect(resizeMock).toHaveBeenCalledTimes(1);
});
});
});

describe('WHEN preview is not possible', () => {
const setup = (mimeType?: string, width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, mimeType);
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

return { params, originFile };
};

describe('WHEN mimeType is undefined', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup();

const error = new UnprocessableEntityException();
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});

describe('WHEN mimeType is text/plain ', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup('text/plain');

const error = new UnprocessableEntityException();
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});
});
});

describe('WHEN previewParams.width not set', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};

it('should not call imagemagicks resize method', async () => {
const { params } = setup(0);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { GetFile, S3ClientAdapter } from '@infra/s3-client';
import { Injectable } from '@nestjs/common';
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { Logger } from '@src/core/logger';
import { subClass } from 'gm';
import { PassThrough } from 'stream';
import { PreviewFileOptions, PreviewOptions, PreviewResponseMessage } from './interface';
import { PreviewFileOptions, PreviewInputMimeTypes, PreviewOptions, PreviewResponseMessage } from './interface';
import { PreviewActionsLoggable } from './loggable/preview-actions.loggable';
import { PreviewGeneratorBuilder } from './preview-generator.builder';

Expand All @@ -20,6 +20,9 @@ export class PreviewGeneratorService {
const { originFilePath, previewFilePath, previewOptions } = params;

const original = await this.downloadOriginFile(originFilePath);

this.checkIfPreviewPossible(original, params);

const preview = this.resizeAndConvert(original, previewOptions);

const file = PreviewGeneratorBuilder.buildFile(preview, params.previewOptions);
Expand All @@ -34,6 +37,16 @@ export class PreviewGeneratorService {
};
}

private checkIfPreviewPossible(original: GetFile, params: PreviewFileOptions): void | UnprocessableEntityException {
const isPreviewPossible =
original.contentType && Object.values<string>(PreviewInputMimeTypes).includes(original.contentType);

if (!isPreviewPossible) {
this.logger.warning(new PreviewActionsLoggable('PreviewGeneratorService.previewNotPossible', params));
throw new UnprocessableEntityException();
}
}

private async downloadOriginFile(pathToFile: string): Promise<GetFile> {
const file = await this.storageClient.get(pathToFile);

Expand All @@ -45,6 +58,10 @@ export class PreviewGeneratorService {

const preview = this.imageMagick(original.data);

if (original.contentType === PreviewInputMimeTypes.APPLICATION_PDF) {
preview.selectFrame(0);
}

if (width) {
preview.resize(width, undefined, '>');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PreviewInputMimeTypes } from '@infra/preview-generator';
import { ObjectId } from '@mikro-orm/mongodb';
import { BadRequestException } from '@nestjs/common';
import { fileRecordFactory, setupEntities } from '@shared/testing';
import { ErrorType } from '../error';
import { PreviewInputMimeTypes } from '../interface';
import {
FileRecord,
FileRecordParentType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PreviewInputMimeTypes } from '@infra/preview-generator';
import { Embeddable, Embedded, Entity, Enum, Index, Property } from '@mikro-orm/core';
import { ObjectId } from '@mikro-orm/mongodb';
import { BadRequestException } from '@nestjs/common';
import { BaseEntityWithTimestamps, EntityId } from '@shared/domain';
import path from 'path';
import { v4 as uuid } from 'uuid';
import { ErrorType } from '../error';
import { PreviewInputMimeTypes } from '../interface/preview-input-mime-types.enum';

export enum ScanStatus {
PENDING = 'pending',
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/modules/files-storage/interface/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './interfaces';
export * from './preview-input-mime-types.enum';
export * from './preview-output-mime-types.enum';
export * from './preview-width.enum';

0 comments on commit f48b9a0

Please sign in to comment.