Skip to content

Commit c5b4848

Browse files
authored
Merge pull request #639 from internxt/feat/fix-upload-file-streams
[_]: fix/upload-file-streams
2 parents 1de6119 + f8276b6 commit c5b4848

11 files changed

Lines changed: 168 additions & 222 deletions

File tree

src/commands/upload-file.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types';
1111
import { ThumbnailService } from '../services/thumbnail.service';
1212
import { AuthService } from '../services/auth.service';
1313
import { UploadUtils } from '../utils/upload.utils';
14-
import { BufferStream } from '../utils/stream.utils';
1514

1615
export default class UploadFile extends Command {
1716
static readonly args = {};
@@ -81,14 +80,11 @@ export default class UploadFile extends Command {
8180
progressBar?.start(100, 0);
8281

8382
let fileId: string | undefined;
84-
let thumbnailStream: BufferStream | undefined;
8583
const fileSize = stats.size ?? 0;
8684

8785
if (fileSize > 0) {
8886
// Upload file to the Network
8987
const readStream = createReadStream(filePath);
90-
const preparedStreams = UploadUtils.prepareUploadStreams(readStream, fileType);
91-
thumbnailStream = preparedStreams.thumbnailStream;
9288

9389
const progressCallback = (progress: number) => {
9490
progressBar?.update(progress * 100 * 0.99);
@@ -97,7 +93,7 @@ export default class UploadFile extends Command {
9793
const abortable = new AbortController();
9894

9995
fileId = await networkFacade.uploadFile({
100-
from: preparedStreams.fileStream,
96+
from: readStream,
10197
size: fileSize,
10298
bucketId: bucket,
10399
progressCallback,
@@ -128,7 +124,7 @@ export default class UploadFile extends Command {
128124

129125
const thumbnailTimer = CLIUtils.timer();
130126
await ThumbnailService.instance.tryUploadThumbnail({
131-
bufferStream: thumbnailStream,
127+
input: filePath,
132128
fileType,
133129
bucket,
134130
fileUuid: createdDriveFile.uuid,

src/services/network/upload/upload-file.service.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { dirname, extname } from 'node:path';
1111
import { ErrorUtils } from '../../../utils/errors.utils';
1212
import { stat } from 'node:fs/promises';
1313
import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types';
14-
import { BufferStream } from '../../../utils/stream.utils';
1514
import { DriveFileItem } from '../../../types/drive.types';
1615
import { CLIUtils } from '../../../utils/cli.utils';
1716
import { ThumbnailService } from '../../thumbnail.service';
1817
import { FormatUtils } from '../../../utils/format.utils';
18+
import { createReadStream } from 'node:fs';
1919

2020
export class UploadFileService {
2121
static readonly instance = new UploadFileService();
@@ -84,7 +84,6 @@ export class UploadFileService {
8484
const fileType = extname(file.absolutePath).replaceAll('.', '');
8585

8686
let fileId: string | undefined;
87-
let thumbnailStream: BufferStream | undefined;
8887

8988
const timings = {
9089
networkUpload: 0,
@@ -93,16 +92,12 @@ export class UploadFileService {
9392
};
9493

9594
if (fileSize > 0) {
96-
const { fileStream, bufferStream } = ThumbnailService.instance.createFileStreamWithBuffer({
97-
path: file.absolutePath,
98-
fileType,
99-
});
95+
const readStream = createReadStream(file.absolutePath);
10096

10197
const uploadTimer = CLIUtils.timer();
102-
thumbnailStream = bufferStream;
10398

10499
fileId = await network.uploadFile({
105-
from: fileStream,
100+
from: readStream,
106101
size: fileSize,
107102
bucketId: bucket,
108103
progressCallback: () => {},
@@ -125,9 +120,9 @@ export class UploadFileService {
125120
timings.driveUpload = driveTimer.stop();
126121

127122
const thumbnailTimer = CLIUtils.timer();
128-
if (thumbnailStream && fileSize > 0) {
123+
if (fileSize > 0) {
129124
await ThumbnailService.instance.tryUploadThumbnail({
130-
bufferStream: thumbnailStream,
125+
input: file.absolutePath,
131126
fileType,
132127
bucket,
133128
fileUuid: createdDriveFile.uuid,

src/services/thumbnail.service.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Readable } from 'node:stream';
2-
import { createReadStream } from 'node:fs';
32
import { DriveFileService } from './drive/drive-file.service';
43
import { StorageTypes } from '@internxt/sdk/dist/drive';
54
import { NetworkFacade } from './network/network-facade.service';
65
import { ThumbnailConfig, ThumbnailUtils } from '../utils/thumbnail.utils';
7-
import { BufferStream } from '../utils/stream.utils';
86
import { ErrorUtils } from '../utils/errors.utils';
97
import { AsyncUtils } from '../utils/async.utils';
108

@@ -26,7 +24,7 @@ export class ThumbnailService {
2624
private static readonly MAX_THUMBNAIL_TIMEOUT = 30000;
2725

2826
public uploadThumbnail = async (
29-
fileContent: Buffer,
27+
input: string | Buffer,
3028
fileType: string,
3129
userBucket: string,
3230
file_id: string,
@@ -35,7 +33,7 @@ export class ThumbnailService {
3533
): Promise<StorageTypes.Thumbnail | undefined> => {
3634
let thumbnailBuffer: Buffer | undefined;
3735
if (ThumbnailUtils.isImageThumbnailable(fileType, fileSize)) {
38-
thumbnailBuffer = await this.getThumbnailFromImageBuffer(fileContent);
36+
thumbnailBuffer = await this.generateThumbnail(input);
3937
}
4038
if (thumbnailBuffer) {
4139
const size = thumbnailBuffer.length;
@@ -61,10 +59,10 @@ export class ThumbnailService {
6159
}
6260
};
6361

64-
private readonly getThumbnailFromImageBuffer = async (buffer: Buffer): Promise<Buffer | undefined> => {
62+
private readonly generateThumbnail = async (input: string | Buffer): Promise<Buffer | undefined> => {
6563
const sharp = await getSharp();
6664
if (sharp) {
67-
return sharp(buffer, { failOn: 'none' })
65+
return sharp(input, { failOn: 'none' })
6866
.resize({
6967
height: ThumbnailConfig.MaxHeight,
7068
width: ThumbnailConfig.MaxWidth,
@@ -78,25 +76,24 @@ export class ThumbnailService {
7876
};
7977

8078
public tryUploadThumbnail = async ({
81-
bufferStream,
79+
input,
8280
fileType,
8381
bucket,
8482
fileUuid,
8583
networkFacade,
8684
size,
8785
}: {
88-
bufferStream?: BufferStream;
86+
input?: string | Buffer;
8987
fileType: string;
9088
bucket: string;
9189
fileUuid: string;
9290
networkFacade: NetworkFacade;
9391
size: number;
9492
}) => {
9593
try {
96-
const thumbnailBuffer = bufferStream?.getBuffer();
97-
if (thumbnailBuffer && size > 0) {
94+
if (input && size > 0) {
9895
await AsyncUtils.withTimeout(
99-
ThumbnailService.instance.uploadThumbnail(thumbnailBuffer, fileType, bucket, fileUuid, networkFacade, size),
96+
ThumbnailService.instance.uploadThumbnail(input, fileType, bucket, fileUuid, networkFacade, size),
10097
ThumbnailService.MAX_THUMBNAIL_TIMEOUT,
10198
'Thumbnail upload timeout',
10299
);
@@ -105,25 +102,4 @@ export class ThumbnailService {
105102
ErrorUtils.report(error);
106103
}
107104
};
108-
109-
public createFileStreamWithBuffer = ({
110-
path,
111-
fileType,
112-
}: {
113-
path: string;
114-
fileType: string;
115-
}): {
116-
bufferStream?: BufferStream;
117-
fileStream: Readable;
118-
} => {
119-
const readable: Readable = createReadStream(path);
120-
if (ThumbnailUtils.isFileThumbnailable(fileType)) {
121-
const bufferStream = new BufferStream();
122-
return {
123-
bufferStream,
124-
fileStream: readable.pipe(bufferStream),
125-
};
126-
}
127-
return { fileStream: readable };
128-
};
129105
}

src/utils/stream.utils.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,15 @@ export class StreamUtils {
6767
}
6868

6969
export class BufferStream extends Transform {
70-
public buffer: Buffer | null;
70+
private chunks: Buffer[];
7171

7272
constructor(opts?: TransformOptions) {
7373
super(opts);
74-
this.buffer = null;
74+
this.chunks = [];
7575
}
7676

7777
_transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback) {
78-
const currentBuffer = this.buffer ?? Buffer.alloc(0);
79-
this.buffer = Buffer.concat([currentBuffer, chunk]);
78+
this.chunks.push(chunk);
8079
callback(null, chunk);
8180
}
8281

@@ -85,10 +84,11 @@ export class BufferStream extends Transform {
8584
}
8685

8786
reset() {
88-
this.buffer = null;
87+
this.chunks = [];
8988
}
9089

91-
getBuffer(): Buffer | null {
92-
return this.buffer;
90+
getBuffer(): Buffer | undefined {
91+
if (this.chunks.length === 0) return undefined;
92+
return Buffer.concat(this.chunks);
9393
}
9494
}

src/utils/thumbnail.utils.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,21 @@ const imageExtensions: FileExtensionMap = {
1717
raw: ['raw', 'cr2', 'nef', 'orf', 'sr2'],
1818
webp: ['webp'],
1919
};
20-
const pdfExtensions: FileExtensionMap = {
21-
pdf: ['pdf'],
22-
};
2320
const thumbnailableImageExtension: Set<string> = new Set([
2421
...imageExtensions['jpg'],
2522
...imageExtensions['png'],
2623
...imageExtensions['webp'],
2724
...imageExtensions['gif'],
2825
...imageExtensions['tiff'],
2926
]);
30-
const thumbnailablePdfExtension: Set<string> = new Set(pdfExtensions['pdf']);
31-
const thumbnailableExtension: Set<string> = new Set(thumbnailableImageExtension);
3227

3328
export class ThumbnailUtils {
34-
static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB = 500 * 1024 * 1024;
35-
36-
static readonly isFileThumbnailable = (fileType: string) => {
37-
return fileType.trim().length > 0 && thumbnailableExtension.has(fileType.trim().toLowerCase());
38-
};
39-
40-
static readonly isPDFThumbnailable = (fileType: string) => {
41-
return fileType.trim().length > 0 && thumbnailablePdfExtension.has(fileType.trim().toLowerCase());
42-
};
29+
static readonly MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES = 128 * 1024 * 1024;
4330

4431
static readonly isImageThumbnailable = (fileType: string, size: number) => {
45-
if (size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_MB) return false;
32+
if (size <= 0 || size > ThumbnailUtils.MAX_IMAGE_THUMBNAILABLE_SIZE_IN_BYTES) {
33+
return false;
34+
}
4635
return fileType.trim().length > 0 && thumbnailableImageExtension.has(fileType.trim().toLowerCase());
4736
};
4837
}

src/utils/upload.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ export class UploadUtils {
2525
static readonly prepareUploadStreams = (
2626
readable: Readable,
2727
fileType: string,
28+
size: number,
2829
): {
2930
fileStream: Readable;
3031
thumbnailStream: BufferStream | undefined;
3132
isThumbnailable: boolean;
3233
} => {
33-
const isThumbnailable = ThumbnailUtils.isFileThumbnailable(fileType);
34+
const isThumbnailable = ThumbnailUtils.isImageThumbnailable(fileType, size);
3435
if (!isThumbnailable) {
3536
return { fileStream: readable, thumbnailStream: undefined, isThumbnailable };
3637
}

src/webdav/handlers/PUT.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class PUTRequestHandler implements WebDavMethodHandler {
5757
const { user } = await AuthService.instance.getAuthDetails();
5858
const fileType = resource.path.ext.replace('.', '');
5959

60-
const { fileStream, thumbnailStream } = UploadUtils.prepareUploadStreams(req, fileType);
60+
const { fileStream, thumbnailStream } = UploadUtils.prepareUploadStreams(req, fileType, contentLength);
6161

6262
const { networkFacade, bucket } = await CLIUtils.prepareNetwork(user);
6363

@@ -144,7 +144,7 @@ export class PUTRequestHandler implements WebDavMethodHandler {
144144
const thumbnailTimer = CLIUtils.timer();
145145
await ThumbnailService.instance.tryUploadThumbnail({
146146
fileUuid: file.uuid,
147-
bufferStream: thumbnailStream,
147+
input: thumbnailStream?.getBuffer(),
148148
fileType,
149149
bucket,
150150
networkFacade,

test/services/network/upload/upload-file.service.test.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
createProgressFixtures,
1414
} from './upload.service.helpers';
1515
import { newFileItem } from '../../../fixtures/drive.fixture';
16-
import { ThumbnailUtils } from '../../../../src/utils/thumbnail.utils';
1716
import { ThumbnailService } from '../../../../src/services/thumbnail.service';
1817

1918
vi.mock('fs', () => ({
@@ -37,12 +36,7 @@ describe('Upload File Service', () => {
3736
vi.mocked(stat).mockResolvedValue(createMockStats(1024) as Awaited<ReturnType<typeof stat>>);
3837
vi.mocked(createReadStream).mockReturnValue(createMockReadStream() as ReturnType<typeof createReadStream>);
3938
vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(false);
40-
vi.spyOn(ThumbnailUtils, 'isFileThumbnailable').mockReturnValue(false);
4139
vi.spyOn(ThumbnailService.instance, 'tryUploadThumbnail').mockResolvedValue(undefined);
42-
vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({
43-
fileStream: createMockReadStream() as ReturnType<typeof createReadStream>,
44-
bufferStream: undefined,
45-
});
4640
vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(mockFile);
4741
});
4842

@@ -300,14 +294,6 @@ describe('Upload File Service', () => {
300294
});
301295

302296
test('when a thumbnailable file is uploaded, then a thumbnail is generated', async () => {
303-
const mockBufferStream = { getBuffer: vi.fn() };
304-
vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({
305-
fileStream: createMockReadStream() as ReturnType<typeof createReadStream>,
306-
bufferStream: mockBufferStream as unknown as ReturnType<
307-
typeof ThumbnailService.instance.createFileStreamWithBuffer
308-
>['bufferStream'],
309-
});
310-
311297
const file = createFileSystemNodeFixture({
312298
type: 'file',
313299
name: 'image.png',
@@ -325,7 +311,7 @@ describe('Upload File Service', () => {
325311
});
326312

327313
expect(ThumbnailService.instance.tryUploadThumbnail).toHaveBeenCalledWith({
328-
bufferStream: mockBufferStream,
314+
input: file.absolutePath,
329315
fileType: 'png',
330316
bucket,
331317
fileUuid: mockFile.uuid,

0 commit comments

Comments
 (0)