diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index c53aacc0..eb7ff520 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -11,7 +11,6 @@ import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { ThumbnailService } from '../services/thumbnail.service'; import { AuthService } from '../services/auth.service'; import { UploadUtils } from '../utils/upload.utils'; -import { BufferStream } from '../utils/stream.utils'; export default class UploadFile extends Command { static readonly args = {}; @@ -81,14 +80,11 @@ export default class UploadFile extends Command { progressBar?.start(100, 0); let fileId: string | undefined; - let thumbnailStream: BufferStream | undefined; const fileSize = stats.size ?? 0; if (fileSize > 0) { // Upload file to the Network const readStream = createReadStream(filePath); - const preparedStreams = UploadUtils.prepareUploadStreams(readStream, fileType); - thumbnailStream = preparedStreams.thumbnailStream; const progressCallback = (progress: number) => { progressBar?.update(progress * 100 * 0.99); @@ -97,7 +93,7 @@ export default class UploadFile extends Command { const abortable = new AbortController(); fileId = await networkFacade.uploadFile({ - from: preparedStreams.fileStream, + from: readStream, size: fileSize, bucketId: bucket, progressCallback, @@ -128,7 +124,7 @@ export default class UploadFile extends Command { const thumbnailTimer = CLIUtils.timer(); await ThumbnailService.instance.tryUploadThumbnail({ - bufferStream: thumbnailStream, + input: filePath, fileType, bucket, fileUuid: createdDriveFile.uuid, diff --git a/src/services/network/upload/upload-file.service.ts b/src/services/network/upload/upload-file.service.ts index 3e72d711..14bac108 100644 --- a/src/services/network/upload/upload-file.service.ts +++ b/src/services/network/upload/upload-file.service.ts @@ -11,11 +11,11 @@ import { dirname, extname } from 'node:path'; import { ErrorUtils } from '../../../utils/errors.utils'; import { stat } from 'node:fs/promises'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; -import { BufferStream } from '../../../utils/stream.utils'; import { DriveFileItem } from '../../../types/drive.types'; import { CLIUtils } from '../../../utils/cli.utils'; import { ThumbnailService } from '../../thumbnail.service'; import { FormatUtils } from '../../../utils/format.utils'; +import { createReadStream } from 'node:fs'; export class UploadFileService { static readonly instance = new UploadFileService(); @@ -84,7 +84,6 @@ export class UploadFileService { const fileType = extname(file.absolutePath).replaceAll('.', ''); let fileId: string | undefined; - let thumbnailStream: BufferStream | undefined; const timings = { networkUpload: 0, @@ -93,16 +92,12 @@ export class UploadFileService { }; if (fileSize > 0) { - const { fileStream, bufferStream } = ThumbnailService.instance.createFileStreamWithBuffer({ - path: file.absolutePath, - fileType, - }); + const readStream = createReadStream(file.absolutePath); const uploadTimer = CLIUtils.timer(); - thumbnailStream = bufferStream; fileId = await network.uploadFile({ - from: fileStream, + from: readStream, size: fileSize, bucketId: bucket, progressCallback: () => {}, @@ -125,9 +120,9 @@ export class UploadFileService { timings.driveUpload = driveTimer.stop(); const thumbnailTimer = CLIUtils.timer(); - if (thumbnailStream && fileSize > 0) { + if (fileSize > 0) { await ThumbnailService.instance.tryUploadThumbnail({ - bufferStream: thumbnailStream, + input: file.absolutePath, fileType, bucket, fileUuid: createdDriveFile.uuid, diff --git a/src/services/thumbnail.service.ts b/src/services/thumbnail.service.ts index 78c3eb40..4c21b718 100644 --- a/src/services/thumbnail.service.ts +++ b/src/services/thumbnail.service.ts @@ -1,10 +1,8 @@ import { Readable } from 'node:stream'; -import { createReadStream } from 'node:fs'; import { DriveFileService } from './drive/drive-file.service'; import { StorageTypes } from '@internxt/sdk/dist/drive'; import { NetworkFacade } from './network/network-facade.service'; import { ThumbnailConfig, ThumbnailUtils } from '../utils/thumbnail.utils'; -import { BufferStream } from '../utils/stream.utils'; import { ErrorUtils } from '../utils/errors.utils'; import { AsyncUtils } from '../utils/async.utils'; @@ -26,7 +24,7 @@ export class ThumbnailService { private static readonly MAX_THUMBNAIL_TIMEOUT = 30000; public uploadThumbnail = async ( - fileContent: Buffer, + input: string | Buffer, fileType: string, userBucket: string, file_id: string, @@ -35,7 +33,7 @@ export class ThumbnailService { ): Promise => { let thumbnailBuffer: Buffer | undefined; if (ThumbnailUtils.isImageThumbnailable(fileType, fileSize)) { - thumbnailBuffer = await this.getThumbnailFromImageBuffer(fileContent); + thumbnailBuffer = await this.generateThumbnail(input); } if (thumbnailBuffer) { const size = thumbnailBuffer.length; @@ -61,10 +59,10 @@ export class ThumbnailService { } }; - private readonly getThumbnailFromImageBuffer = async (buffer: Buffer): Promise => { + private readonly generateThumbnail = async (input: string | Buffer): Promise => { const sharp = await getSharp(); if (sharp) { - return sharp(buffer, { failOn: 'none' }) + return sharp(input, { failOn: 'none' }) .resize({ height: ThumbnailConfig.MaxHeight, width: ThumbnailConfig.MaxWidth, @@ -78,14 +76,14 @@ export class ThumbnailService { }; public tryUploadThumbnail = async ({ - bufferStream, + input, fileType, bucket, fileUuid, networkFacade, size, }: { - bufferStream?: BufferStream; + input?: string | Buffer; fileType: string; bucket: string; fileUuid: string; @@ -93,10 +91,9 @@ export class ThumbnailService { size: number; }) => { try { - const thumbnailBuffer = bufferStream?.getBuffer(); - if (thumbnailBuffer && size > 0) { + if (input && size > 0) { await AsyncUtils.withTimeout( - ThumbnailService.instance.uploadThumbnail(thumbnailBuffer, fileType, bucket, fileUuid, networkFacade, size), + ThumbnailService.instance.uploadThumbnail(input, fileType, bucket, fileUuid, networkFacade, size), ThumbnailService.MAX_THUMBNAIL_TIMEOUT, 'Thumbnail upload timeout', ); @@ -105,25 +102,4 @@ export class ThumbnailService { ErrorUtils.report(error); } }; - - public createFileStreamWithBuffer = ({ - path, - fileType, - }: { - path: string; - fileType: string; - }): { - bufferStream?: BufferStream; - fileStream: Readable; - } => { - const readable: Readable = createReadStream(path); - if (ThumbnailUtils.isFileThumbnailable(fileType)) { - const bufferStream = new BufferStream(); - return { - bufferStream, - fileStream: readable.pipe(bufferStream), - }; - } - return { fileStream: readable }; - }; } diff --git a/src/utils/stream.utils.ts b/src/utils/stream.utils.ts index 756e6fab..0961d44f 100644 --- a/src/utils/stream.utils.ts +++ b/src/utils/stream.utils.ts @@ -67,16 +67,15 @@ export class StreamUtils { } export class BufferStream extends Transform { - public buffer: Buffer | null; + private chunks: Buffer[]; constructor(opts?: TransformOptions) { super(opts); - this.buffer = null; + this.chunks = []; } _transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback) { - const currentBuffer = this.buffer ?? Buffer.alloc(0); - this.buffer = Buffer.concat([currentBuffer, chunk]); + this.chunks.push(chunk); callback(null, chunk); } @@ -85,10 +84,11 @@ export class BufferStream extends Transform { } reset() { - this.buffer = null; + this.chunks = []; } - getBuffer(): Buffer | null { - return this.buffer; + getBuffer(): Buffer | undefined { + if (this.chunks.length === 0) return undefined; + return Buffer.concat(this.chunks); } } diff --git a/src/utils/thumbnail.utils.ts b/src/utils/thumbnail.utils.ts index baeeed7c..ee35c691 100644 --- a/src/utils/thumbnail.utils.ts +++ b/src/utils/thumbnail.utils.ts @@ -17,9 +17,6 @@ const imageExtensions: FileExtensionMap = { raw: ['raw', 'cr2', 'nef', 'orf', 'sr2'], webp: ['webp'], }; -const pdfExtensions: FileExtensionMap = { - pdf: ['pdf'], -}; const thumbnailableImageExtension: Set = new Set([ ...imageExtensions['jpg'], ...imageExtensions['png'], @@ -27,22 +24,14 @@ const thumbnailableImageExtension: Set = new Set([ ...imageExtensions['gif'], ...imageExtensions['tiff'], ]); -const thumbnailablePdfExtension: Set = new Set(pdfExtensions['pdf']); -const thumbnailableExtension: Set = new Set(thumbnailableImageExtension); export class ThumbnailUtils { - static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB = 500 * 1024 * 1024; - - static readonly isFileThumbnailable = (fileType: string) => { - return fileType.trim().length > 0 && thumbnailableExtension.has(fileType.trim().toLowerCase()); - }; - - static readonly isPDFThumbnailable = (fileType: string) => { - return fileType.trim().length > 0 && thumbnailablePdfExtension.has(fileType.trim().toLowerCase()); - }; + static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES = 128 * 1024 * 1024; static readonly isImageThumbnailable = (fileType: string, size: number) => { - if (size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB) return false; + if (size <= 0 || size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES) { + return false; + } return fileType.trim().length > 0 && thumbnailableImageExtension.has(fileType.trim().toLowerCase()); }; } diff --git a/src/utils/upload.utils.ts b/src/utils/upload.utils.ts index 137128e3..f45c04e0 100644 --- a/src/utils/upload.utils.ts +++ b/src/utils/upload.utils.ts @@ -25,12 +25,13 @@ export class UploadUtils { static readonly prepareUploadStreams = ( readable: Readable, fileType: string, + size: number, ): { fileStream: Readable; thumbnailStream: BufferStream | undefined; isThumbnailable: boolean; } => { - const isThumbnailable = ThumbnailUtils.isFileThumbnailable(fileType); + const isThumbnailable = ThumbnailUtils.isImageThumbnailable(fileType, size); if (!isThumbnailable) { return { fileStream: readable, thumbnailStream: undefined, isThumbnailable }; } diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index c8899801..13009915 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -63,7 +63,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { const { user } = await AuthService.instance.getAuthDetails(); const fileType = resource.path.ext.replace('.', ''); - const { fileStream, thumbnailStream } = UploadUtils.prepareUploadStreams(req, fileType); + const { fileStream, thumbnailStream } = UploadUtils.prepareUploadStreams(req, fileType, contentLength); const { networkFacade, bucket } = await CLIUtils.prepareNetwork(user); @@ -128,7 +128,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { const thumbnailTimer = CLIUtils.timer(); await ThumbnailService.instance.tryUploadThumbnail({ fileUuid: file.uuid, - bufferStream: thumbnailStream, + input: thumbnailStream?.getBuffer(), fileType, bucket, networkFacade, diff --git a/test/services/network/upload/upload-file.service.test.ts b/test/services/network/upload/upload-file.service.test.ts index 4d503289..744d55fa 100644 --- a/test/services/network/upload/upload-file.service.test.ts +++ b/test/services/network/upload/upload-file.service.test.ts @@ -13,7 +13,6 @@ import { createProgressFixtures, } from './upload.service.helpers'; import { newFileItem } from '../../../fixtures/drive.fixture'; -import { ThumbnailUtils } from '../../../../src/utils/thumbnail.utils'; import { ThumbnailService } from '../../../../src/services/thumbnail.service'; vi.mock('fs', () => ({ @@ -37,12 +36,7 @@ describe('Upload File Service', () => { vi.mocked(stat).mockResolvedValue(createMockStats(1024) as Awaited>); vi.mocked(createReadStream).mockReturnValue(createMockReadStream() as ReturnType); vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(false); - vi.spyOn(ThumbnailUtils, 'isFileThumbnailable').mockReturnValue(false); vi.spyOn(ThumbnailService.instance, 'tryUploadThumbnail').mockResolvedValue(undefined); - vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({ - fileStream: createMockReadStream() as ReturnType, - bufferStream: undefined, - }); vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(mockFile); }); @@ -300,14 +294,6 @@ describe('Upload File Service', () => { }); test('when a thumbnailable file is uploaded, then a thumbnail is generated', async () => { - const mockBufferStream = { getBuffer: vi.fn() }; - vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({ - fileStream: createMockReadStream() as ReturnType, - bufferStream: mockBufferStream as unknown as ReturnType< - typeof ThumbnailService.instance.createFileStreamWithBuffer - >['bufferStream'], - }); - const file = createFileSystemNodeFixture({ type: 'file', name: 'image.png', @@ -325,7 +311,7 @@ describe('Upload File Service', () => { }); expect(ThumbnailService.instance.tryUploadThumbnail).toHaveBeenCalledWith({ - bufferStream: mockBufferStream, + input: file.absolutePath, fileType: 'png', bucket, fileUuid: mockFile.uuid, diff --git a/test/services/thumbnail.service.test.ts b/test/services/thumbnail.service.test.ts index fb072829..4c3876ee 100644 --- a/test/services/thumbnail.service.test.ts +++ b/test/services/thumbnail.service.test.ts @@ -1,28 +1,80 @@ -import { describe, expect, test } from 'vitest'; -import { BufferStream } from '../../src/utils/stream.utils'; +import { afterEach, describe, expect, test, vi } from 'vitest'; import { ThumbnailService } from '../../src/services/thumbnail.service'; -import path from 'node:path'; -import { Readable } from 'node:stream'; +import { NetworkFacade } from '../../src/services/network/network-facade.service'; describe('Thumbnail Service tests', () => { - const testFilePath = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); + const networkFacade = {} as NetworkFacade; - describe('createFileStreamWithBuffer', () => { - test('when the file has a supported image type, then a buffer stream and file stream are created', () => { - const result = ThumbnailService.instance.createFileStreamWithBuffer({ path: testFilePath, fileType: 'png' }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('tryUploadThumbnail', () => { + test('when an input is provided and the size is greater than zero, then the thumbnail is uploaded', async () => { + const uploadThumbnailSpy = vi.spyOn(ThumbnailService.instance, 'uploadThumbnail').mockResolvedValue(undefined); + + await ThumbnailService.instance.tryUploadThumbnail({ + input: '/path/to/image.png', + fileType: 'png', + bucket: 'bucket-id', + fileUuid: 'file-uuid', + networkFacade, + size: 1024, + }); + + expect(uploadThumbnailSpy).toHaveBeenCalledWith( + '/path/to/image.png', + 'png', + 'bucket-id', + 'file-uuid', + networkFacade, + 1024, + ); + }); + + test('when no input is provided, then no thumbnail is uploaded', async () => { + const uploadThumbnailSpy = vi.spyOn(ThumbnailService.instance, 'uploadThumbnail').mockResolvedValue(undefined); + + await ThumbnailService.instance.tryUploadThumbnail({ + input: undefined, + fileType: 'png', + bucket: 'bucket-id', + fileUuid: 'file-uuid', + networkFacade, + size: 1024, + }); + + expect(uploadThumbnailSpy).not.toHaveBeenCalled(); + }); + + test('when the size is zero, then no thumbnail is uploaded', async () => { + const uploadThumbnailSpy = vi.spyOn(ThumbnailService.instance, 'uploadThumbnail').mockResolvedValue(undefined); + + await ThumbnailService.instance.tryUploadThumbnail({ + input: '/path/to/image.png', + fileType: 'png', + bucket: 'bucket-id', + fileUuid: 'file-uuid', + networkFacade, + size: 0, + }); - expect(result.bufferStream).toBeDefined(); - expect(result.bufferStream).toBeInstanceOf(BufferStream); - expect(result.fileStream).toBeDefined(); - expect(result.fileStream).toBeInstanceOf(Readable); + expect(uploadThumbnailSpy).not.toHaveBeenCalled(); }); - test('when the file has an unsupported type, then only a file stream is created without buffering', () => { - const result = ThumbnailService.instance.createFileStreamWithBuffer({ path: testFilePath, fileType: 'txt' }); + test('when the thumbnail upload fails, then the error is swallowed', async () => { + vi.spyOn(ThumbnailService.instance, 'uploadThumbnail').mockRejectedValue(new Error('upload failed')); - expect(result.bufferStream).toBeUndefined(); - expect(result.fileStream).toBeDefined(); - expect(result.fileStream).toBeInstanceOf(Readable); + await expect( + ThumbnailService.instance.tryUploadThumbnail({ + input: '/path/to/image.png', + fileType: 'png', + bucket: 'bucket-id', + fileUuid: 'file-uuid', + networkFacade, + size: 1024, + }), + ).resolves.toBeUndefined(); }); }); }); diff --git a/test/utils/thumbnail.utils.test.ts b/test/utils/thumbnail.utils.test.ts index 4137ad18..0611a6bd 100644 --- a/test/utils/thumbnail.utils.test.ts +++ b/test/utils/thumbnail.utils.test.ts @@ -2,139 +2,69 @@ import { describe, expect, test } from 'vitest'; import { ThumbnailUtils } from '../../src/utils/thumbnail.utils'; describe('Thumbnail Utils tests', () => { - describe('isFileThumbnailable', () => { - test('when a valid image extension is given, then true is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable('jpg')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('jpeg')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('png')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('webp')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('gif')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('tif')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('tiff')).toBe(true); - }); - - test('when an extension has mixed case, then true is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable('JPG')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('PNG')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('Webp')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('GIF')).toBe(true); - }); - - test('when an extension has surrounding whitespace, then true is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable(' jpg ')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable(' png ')).toBe(true); - expect(ThumbnailUtils.isFileThumbnailable('\tgif\t')).toBe(true); - }); - - test('when a non-thumbnailable extension is given, then false is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable('pdf')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('doc')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('txt')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('mp4')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('bmp')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('raw')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('heic')).toBe(false); - }); - - test('when an empty or blank string is given, then false is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable('')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable(' ')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('\t\n')).toBe(false); - }); - - test('when an invalid extension is given, then false is returned', () => { - expect(ThumbnailUtils.isFileThumbnailable('unknown')).toBe(false); - expect(ThumbnailUtils.isFileThumbnailable('jpgg')).toBe(false); - }); - }); - - describe('isPDFThumbnailable', () => { - test('when a pdf extension is given, then true is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable('pdf')).toBe(true); - }); - - test('when an extension has mixed case, then true is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable('PDF')).toBe(true); - expect(ThumbnailUtils.isPDFThumbnailable('Pdf')).toBe(true); - expect(ThumbnailUtils.isPDFThumbnailable('pDf')).toBe(true); - }); - - test('when an extension has surrounding whitespace, then true is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable(' pdf ')).toBe(true); - expect(ThumbnailUtils.isPDFThumbnailable(' PDF ')).toBe(true); - expect(ThumbnailUtils.isPDFThumbnailable('\tpdf\n')).toBe(true); - }); - - test('when a non-pdf extension is given, then false is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable('jpg')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('png')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('doc')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('docx')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('txt')).toBe(false); - }); - - test('when an empty or blank string is given, then false is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable('')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable(' ')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('\t\n')).toBe(false); - }); - - test('when an invalid extension is given, then false is returned', () => { - expect(ThumbnailUtils.isPDFThumbnailable('pdff')).toBe(false); - expect(ThumbnailUtils.isPDFThumbnailable('pd')).toBe(false); - }); - }); - describe('isImageThumbnailable', () => { + const validSize = 1024; + test('when a thumbnailable image extension is given, then true is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('jpg')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('jpeg')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('png')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('webp')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('gif')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('tif')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('tiff')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('jpg', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('jpeg', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('png', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('webp', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('gif', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('tif', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('tiff', validSize)).toBe(true); }); test('when an extension has mixed case, then true is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('JPG')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('PNG')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('GIF')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('Jpeg')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('JPG', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('PNG', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('GIF', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('Jpeg', validSize)).toBe(true); }); test('when an extension has surrounding whitespace, then true is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable(' jpg ')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable(' png ')).toBe(true); - expect(ThumbnailUtils.isImageThumbnailable('\twebp\n')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable(' jpg ', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable(' png ', validSize)).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('\twebp\n', validSize)).toBe(true); }); test('when a non-thumbnailable image format is given, then false is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('bmp')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('heic')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('raw')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('cr2')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('nef')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('eps')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('bmp', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('heic', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('raw', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('cr2', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('nef', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('eps', validSize)).toBe(false); }); test('when a non-image extension is given, then false is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('pdf')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('doc')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('txt')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('mp4')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('mp3')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('pdf', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('doc', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('txt', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('mp4', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('mp3', validSize)).toBe(false); }); test('when an empty or blank string is given, then false is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable(' ')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('\t\n')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable(' ', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('\t\n', validSize)).toBe(false); }); test('when an invalid extension is given, then false is returned', () => { - expect(ThumbnailUtils.isImageThumbnailable('jpgg')).toBe(false); - expect(ThumbnailUtils.isImageThumbnailable('unknown')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('jpgg', validSize)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('unknown', validSize)).toBe(false); + }); + + test('when the size is zero or negative, then false is returned', () => { + expect(ThumbnailUtils.isImageThumbnailable('jpg', 0)).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('jpg', -1)).toBe(false); + }); + + test('when the size exceeds the max thumbnailable size, then false is returned', () => { + expect(ThumbnailUtils.isImageThumbnailable('jpg', ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES + 1)).toBe( + false, + ); }); }); }); diff --git a/test/utils/upload.utils.test.ts b/test/utils/upload.utils.test.ts index 4f349611..153b2c5b 100644 --- a/test/utils/upload.utils.test.ts +++ b/test/utils/upload.utils.test.ts @@ -93,10 +93,10 @@ describe('UploadUtils', () => { describe('prepareUploadStreams', () => { test('when the file type does not support thumbnails, then the original stream is returned without a thumbnail', () => { - vi.spyOn(ThumbnailUtils, 'isFileThumbnailable').mockReturnValue(false); + vi.spyOn(ThumbnailUtils, 'isImageThumbnailable').mockReturnValue(false); const readable = Readable.from(['test']); - const result = UploadUtils.prepareUploadStreams(readable, 'pdf'); + const result = UploadUtils.prepareUploadStreams(readable, 'pdf', 1024); expect(result.fileStream).toBe(readable); expect(result.thumbnailStream).toBeUndefined(); @@ -104,16 +104,37 @@ describe('UploadUtils', () => { }); test('when the file type supports thumbnails, then a piped stream and thumbnail stream are returned', () => { - vi.spyOn(ThumbnailUtils, 'isFileThumbnailable').mockReturnValue(true); + vi.spyOn(ThumbnailUtils, 'isImageThumbnailable').mockReturnValue(true); const readable = Readable.from(['test-data']); - const result = UploadUtils.prepareUploadStreams(readable, 'jpg'); + const result = UploadUtils.prepareUploadStreams(readable, 'jpg', 1024); expect(result.fileStream).not.toBe(readable); expect(result.fileStream).toBeInstanceOf(Readable); expect(result.thumbnailStream).toBeInstanceOf(BufferStream); expect(result.isThumbnailable).toBe(true); }); + + test('when the file exceeds the max thumbnailable size, then no thumbnail stream is created', () => { + const readable = Readable.from(['test-data']); + const size = ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES + 1; + + const result = UploadUtils.prepareUploadStreams(readable, 'jpg', size); + + expect(result.fileStream).toBe(readable); + expect(result.thumbnailStream).toBeUndefined(); + expect(result.isThumbnailable).toBe(false); + }); + + test('when the file size is zero, then no thumbnail stream is created', () => { + const readable = Readable.from([]); + + const result = UploadUtils.prepareUploadStreams(readable, 'jpg', 0); + + expect(result.fileStream).toBe(readable); + expect(result.thumbnailStream).toBeUndefined(); + expect(result.isThumbnailable).toBe(false); + }); }); describe('getTimings', () => {