@travetto/web-upload 7.1.4 → 8.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ yarn add @travetto/web-upload
15
15
 
16
16
  This module provides a clean and direct mechanism for processing uploads, built upon [@fastify/busboy](https://github.com/fastify/busboy). The module also provides some best practices with respect to temporary file management.
17
17
 
18
- Once the files are uploaded, they are exposed via [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) parameters using the [@Upload](https://github.com/travetto/travetto/tree/main/module/web-upload/src/decorator.ts#L20) decorator. This decorator requires the related field type to be a standard [Buffer](https://nodejs.org/api/buffer.html#class-file) object, or a [FileMap](https://github.com/travetto/travetto/tree/main/module/web-upload/src/types.ts#L4).
18
+ Once the files are uploaded, they are exposed via [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) parameters using the [@Upload](https://github.com/travetto/travetto/tree/main/module/web-upload/src/decorator.ts#L20) decorator. This decorator requires the related field type to be a standard [Buffer](https://nodejs.org/api/buffer.html#class-file) object, or a [FileMap](https://github.com/travetto/travetto/tree/main/module/web-upload/src/types.ts#L7).
19
19
 
20
20
  A simple example:
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/web-upload",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "Provides integration between the travetto asset and web module.",
6
6
  "keywords": [
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@fastify/busboy": "^3.2.0",
30
- "@travetto/config": "^7.1.4",
31
- "@travetto/web": "^7.1.4",
30
+ "@travetto/config": "^8.0.0-alpha.1",
31
+ "@travetto/web": "^8.0.0-alpha.1",
32
32
  "file-type": "^21.3.0",
33
33
  "mime": "^4.1.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/test": "^7.1.4"
36
+ "@travetto/test": "^8.0.0-alpha.1"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/test": {
package/src/decorator.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AppError, toConcrete, type ClassInstance, getClass } from '@travetto/runtime';
1
+ import { RuntimeError, toConcrete, type ClassInstance, getClass } from '@travetto/runtime';
2
2
  import { ControllerRegistryIndex, type EndpointParameterConfig, Param } from '@travetto/web';
3
3
  import { SchemaRegistryIndex } from '@travetto/schema';
4
4
 
@@ -55,11 +55,11 @@ export function Upload(
55
55
  const input = SchemaRegistryIndex.get(cls).getMethod(property).parameters[idx];
56
56
 
57
57
  if (!input) {
58
- throw new AppError(`Unknown field type, ensure you are using ${Blob.name}, ${File.name} or ${FileMapContract.name}`);
58
+ throw new RuntimeError(`Unknown field type, ensure you are using ${Blob.name}, ${File.name} or ${FileMapContract.name}`);
59
59
  }
60
60
 
61
61
  if (!(input.type === Blob || input.type === File || input.type === FileMapContract)) {
62
- throw new AppError(`Cannot use upload decorator with ${input.type.name}, but only an ${Blob.name}, ${File.name} or ${FileMapContract.name}`);
62
+ throw new RuntimeError(`Cannot use upload decorator with ${input.type.name}, but only an ${Blob.name}, ${File.name} or ${FileMapContract.name}`);
63
63
  }
64
64
 
65
65
  const isMap = input.type === FileMapContract;
@@ -6,7 +6,6 @@ import {
6
6
 
7
7
  import type { WebUploadConfig } from './config.ts';
8
8
  import { WebUploadUtil } from './util.ts';
9
- import type { FileMap } from './types.ts';
10
9
 
11
10
  @Injectable()
12
11
  export class WebUploadInterceptor implements WebInterceptor<WebUploadConfig> {
@@ -35,7 +34,7 @@ export class WebUploadInterceptor implements WebInterceptor<WebUploadConfig> {
35
34
  }
36
35
 
37
36
  async filter({ request, config, next }: WebChainedContext<WebUploadConfig>): Promise<WebResponse> {
38
- const uploads: FileMap = {};
37
+ const uploads: Record<string, File & { cleanup?: () => Promise<void> }> = {};
39
38
 
40
39
  try {
41
40
  for await (const item of WebUploadUtil.getUploads(request, config)) {
@@ -46,8 +45,8 @@ export class WebUploadInterceptor implements WebInterceptor<WebUploadConfig> {
46
45
 
47
46
  return await next();
48
47
  } finally {
49
- for (const [field, item] of Object.entries(uploads)) {
50
- await WebUploadUtil.finishUpload(item, config.uploads?.[field] ?? config);
48
+ for (const item of Object.values(uploads)) {
49
+ await item.cleanup?.();
51
50
  }
52
51
  }
53
52
  }
package/src/types.ts CHANGED
@@ -1,4 +1,10 @@
1
+ import { toConcrete } from '@travetto/runtime';
2
+ import { SchemaTypeUtil } from '@travetto/schema';
3
+
1
4
  /**
2
5
  * @concrete
3
6
  */
4
- export interface FileMap extends Record<string, File> { }
7
+ export interface FileMap extends Record<string, File> { }
8
+
9
+ SchemaTypeUtil.register(toConcrete<FileMap>(),
10
+ input => typeof input === 'object' && !!input && Object.values(input).every(v => v instanceof Blob));
package/src/util.ts CHANGED
@@ -3,21 +3,20 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import fs from 'node:fs/promises';
5
5
  import { pipeline } from 'node:stream/promises';
6
- import { Readable, Transform } from 'node:stream';
6
+ import { Transform } from 'node:stream';
7
7
 
8
8
  import busboy from '@fastify/busboy';
9
9
 
10
10
  import { type WebRequest, WebCommonUtil, WebBodyUtil, WebHeaderUtil } from '@travetto/web';
11
- import { AsyncQueue, AppError, castTo, Util, BinaryUtil } from '@travetto/runtime';
11
+ import { AsyncQueue, RuntimeError, CodecUtil, Util, BinaryUtil, type BinaryType, type BinaryStream, BinaryMetadataUtil } from '@travetto/runtime';
12
12
 
13
13
  import type { WebUploadConfig } from './config.ts';
14
14
  import type { FileMap } from './types.ts';
15
15
 
16
16
  const MULTIPART = new Set(['application/x-www-form-urlencoded', 'multipart/form-data']);
17
17
 
18
- type UploadItem = { stream: Readable, filename?: string, field: string };
18
+ type UploadItem = { stream: BinaryStream, filename?: string, field: string, contentType?: string };
19
19
  type FileType = { ext: string, mime: string };
20
- const RawFileSymbol = Symbol();
21
20
  const WebUploadSymbol = Symbol();
22
21
 
23
22
  /**
@@ -32,10 +31,11 @@ export class WebUploadUtil {
32
31
  static limitWrite(maxSize: number, field?: string): Transform {
33
32
  let read = 0;
34
33
  return new Transform({
35
- transform(chunk, encoding, callback): void {
36
- read += (Buffer.isBuffer(chunk) || typeof chunk === 'string') ? chunk.length : (chunk instanceof Uint8Array ? chunk.byteLength : 0);
34
+ transform(input, encoding, callback): void {
35
+ const chunk = CodecUtil.readChunk(input, encoding);
36
+ read += chunk.byteLength;
37
37
  if (read > maxSize) {
38
- callback(new AppError('File size exceeded', { category: 'data', details: { read, size: maxSize, field } }));
38
+ callback(new RuntimeError('File size exceeded', { category: 'data', details: { read, size: maxSize, field } }));
39
39
  } else {
40
40
  callback(null, chunk);
41
41
  }
@@ -43,22 +43,15 @@ export class WebUploadUtil {
43
43
  });
44
44
  }
45
45
 
46
- /**
47
- * Get uploaded file path location
48
- */
49
- static getUploadLocation(file: File): string {
50
- return castTo<{ [RawFileSymbol]: string }>(file)[RawFileSymbol];
51
- }
52
-
53
46
  /**
54
47
  * Get all the uploads, separating multipart from direct
55
48
  */
56
- static async* getUploads(request: WebRequest, config: Partial<WebUploadConfig>): AsyncIterable<UploadItem> {
57
- if (!WebBodyUtil.isRaw(request.body)) {
58
- throw new AppError('No input stream provided for upload', { category: 'data' });
49
+ static async * getUploads(request: WebRequest, config: Partial<WebUploadConfig>): AsyncIterable<UploadItem> {
50
+ if (!WebBodyUtil.isRawBinary(request.body)) {
51
+ throw new RuntimeError('No input stream provided for upload', { category: 'data' });
59
52
  }
60
53
 
61
- const bodyStream = Buffer.isBuffer(request.body) ? Readable.from(request.body) : request.body;
54
+ const requestBody = request.body;
62
55
  request.body = undefined;
63
56
 
64
57
  const contentType = WebHeaderUtil.parseHeaderSegment(request.headers.get('Content-Type'));
@@ -70,8 +63,7 @@ export class WebUploadUtil {
70
63
  const largestMax = fileMaxes.length ? Math.max(...fileMaxes) : config.maxSize;
71
64
  const queue = new AsyncQueue<UploadItem>();
72
65
 
73
- // Upload
74
- bodyStream.pipe(busboy({
66
+ const uploadHandler = busboy({
75
67
  headers: {
76
68
  'content-type': request.headers.get('Content-Type')!,
77
69
  'content-disposition': request.headers.get('Content-Disposition')!,
@@ -82,22 +74,69 @@ export class WebUploadUtil {
82
74
  },
83
75
  limits: { fileSize: largestMax }
84
76
  })
85
- .on('file', (field, stream, filename) => queue.add({ stream, filename, field }))
86
- .on('limit', field => queue.throw(new AppError(`File size exceeded for ${field}`, { category: 'data' })))
77
+ .on('file', (field, stream, filename, _encoding, mimetype) => queue.add({ stream, filename, field, contentType: mimetype }))
78
+ .on('limit', field => queue.throw(new RuntimeError(`File size exceeded for ${field}`, { category: 'data' })))
87
79
  .on('finish', () => queue.close())
88
- .on('error', (error) => queue.throw(error instanceof Error ? error : new Error(`${error}`))));
80
+ .on('error', (error) => queue.throw(error instanceof Error ? error : new Error(`${error}`)));
81
+
82
+ // Upload
83
+ void BinaryUtil.pipeline(requestBody, uploadHandler).catch(err => queue.throw(err));
89
84
 
90
85
  yield* queue;
91
86
  } else {
92
87
  const filename = WebHeaderUtil.parseHeaderSegment(request.headers.get('Content-Disposition')).parameters.filename;
93
- yield { stream: bodyStream, filename, field: 'file' };
88
+ yield { stream: BinaryUtil.toBinaryStream(requestBody), filename, field: 'file', contentType: contentType.value };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Detect mime from request input, usually http headers
94
+ */
95
+ static async detectMimeTypeFromRequestInput(location?: string, contentType?: string): Promise<FileType | undefined> {
96
+ const { Mime } = (await import('mime'));
97
+ const otherTypes = (await import('mime/types/other.js')).default;
98
+ const standardTypes = (await import('mime/types/standard.js')).default;
99
+ const checker = new Mime(standardTypes, otherTypes);
100
+ if (contentType) {
101
+ return { ext: checker.getExtension(contentType)!, mime: contentType };
102
+ } else if (location) {
103
+ const mime = checker.getType(location);
104
+ if (mime) {
105
+ return { mime, ext: checker.getExtension(mime)! };
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Detect mime from the binary source
112
+ */
113
+ static async detectMimeTypeFromBinary(input: BinaryType): Promise<FileType | undefined> {
114
+ let cleanup: (() => Promise<void>) | undefined;
115
+ try {
116
+ const { FileTypeParser } = await import('file-type');
117
+ const { fromWebStream } = await import('strtok3');
118
+ const parser = new FileTypeParser();
119
+ const token = fromWebStream(BinaryUtil.toReadableStream(input));
120
+ cleanup = (): Promise<void> => token.close();
121
+ return await parser.fromTokenizer(token);
122
+ } finally {
123
+ await cleanup?.();
94
124
  }
95
125
  }
96
126
 
127
+ /**
128
+ * Get file type
129
+ */
130
+ static async getFileType(input: BinaryType, filename?: string, contentType?: string): Promise<FileType> {
131
+ return (await this.detectMimeTypeFromBinary(input)) ??
132
+ (await this.detectMimeTypeFromRequestInput(filename, contentType)) ??
133
+ { ext: 'bin', mime: 'application/octet-stream' };
134
+ }
135
+
97
136
  /**
98
137
  * Convert an UploadItem to a File
99
138
  */
100
- static async toFile({ stream, filename, field }: UploadItem, config: Partial<WebUploadConfig>): Promise<File> {
139
+ static async toFile({ stream, filename, field, contentType }: UploadItem, config: Partial<WebUploadConfig>): Promise<File> {
101
140
  const uniqueDirectory = path.resolve(os.tmpdir(), `file_${Date.now()}_${Util.uuid(5)}`);
102
141
  await fs.mkdir(uniqueDirectory, { recursive: true });
103
142
 
@@ -106,6 +145,7 @@ export class WebUploadUtil {
106
145
  const location = path.resolve(uniqueDirectory, filename);
107
146
  const remove = (): Promise<void> => fs.rm(location).catch(() => { });
108
147
  const mimeCheck = config.matcher ??= WebCommonUtil.mimeTypeMatcher(config.types);
148
+ const response = (): BinaryStream => createReadStream(location);
109
149
 
110
150
  try {
111
151
  const target = createWriteStream(location);
@@ -114,25 +154,21 @@ export class WebUploadUtil {
114
154
  pipeline(stream, this.limitWrite(config.maxSize, field), target) :
115
155
  pipeline(stream, target));
116
156
 
117
- const detected = await this.getFileType(location);
157
+ const detected = await this.getFileType(response(), filename, contentType);
118
158
 
119
159
  if (!mimeCheck(detected.mime)) {
120
- throw new AppError(`Content type not allowed: ${detected.mime}`, { category: 'data' });
160
+ throw new RuntimeError(`Content type not allowed: ${detected.mime}`, { category: 'data' });
121
161
  }
122
162
 
123
163
  if (!path.extname(filename)) {
124
164
  filename = `${filename}.${detected.ext}`;
125
165
  }
126
166
 
127
- const file = BinaryUtil.readableBlob(() => createReadStream(location), {
128
- contentType: detected.mime,
129
- filename,
130
- hash: await BinaryUtil.hashInput(createReadStream(location)),
131
- size: (await fs.stat(location)).size,
167
+ const metadata = await BinaryMetadataUtil.compute(response, { contentType: detected.mime, filename, });
168
+ const file = BinaryMetadataUtil.defineBlob(new File([], ''), response, metadata);
169
+ Object.defineProperty(file, 'cleanup', {
170
+ value: () => config.cleanupFiles !== false && fs.rm(location).catch(() => { })
132
171
  });
133
-
134
- Object.assign(file, { [RawFileSymbol]: location });
135
-
136
172
  return file;
137
173
  } catch (error) {
138
174
  await remove();
@@ -140,46 +176,6 @@ export class WebUploadUtil {
140
176
  }
141
177
  }
142
178
 
143
- /**
144
- * Get file type
145
- */
146
- static async getFileType(input: string | Readable): Promise<FileType> {
147
- const { FileTypeParser } = await import('file-type');
148
- const { fromStream } = await import('strtok3');
149
-
150
- const parser = new FileTypeParser();
151
- let token: ReturnType<typeof fromStream> | undefined;
152
- let matched: FileType | undefined;
153
-
154
- try {
155
- token = await fromStream(typeof input === 'string' ? createReadStream(input) : input);
156
- matched = await parser.fromTokenizer(token);
157
- } finally {
158
- await token?.close();
159
- }
160
-
161
- if (!matched && typeof input === 'string') {
162
- const { Mime } = (await import('mime'));
163
- const otherTypes = (await import('mime/types/other.js')).default;
164
- const standardTypes = (await import('mime/types/standard.js')).default;
165
- const checker = new Mime(standardTypes, otherTypes);
166
- const mime = checker.getType(input);
167
- if (mime) {
168
- return { ext: checker.getExtension(mime)!, mime };
169
- }
170
- }
171
- return matched ?? { ext: 'bin', mime: 'application/octet-stream' };
172
- }
173
-
174
- /**
175
- * Finish upload
176
- */
177
- static async finishUpload(upload: File, config: Partial<WebUploadConfig>): Promise<void> {
178
- if (config.cleanupFiles !== false) {
179
- await fs.rm(this.getUploadLocation(upload), { force: true });
180
- }
181
- }
182
-
183
179
  /**
184
180
  * Get Uploads
185
181
  */
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { BinaryUtil, castTo } from '@travetto/runtime';
3
+ import { BinaryMetadataUtil, castTo } from '@travetto/runtime';
4
4
  import { Controller, Post } from '@travetto/web';
5
5
  import { BeforeAll, Suite, Test, TestFixtures } from '@travetto/test';
6
6
  import { Registry } from '@travetto/registry';
@@ -10,7 +10,7 @@ import { BaseWebSuite } from '@travetto/web/support/test/suite/base.ts';
10
10
  import { Upload } from '../../src/decorator.ts';
11
11
  import type { FileMap } from '../../src/types.ts';
12
12
 
13
- const bHash = (blob: Blob) => BinaryUtil.getBlobMeta(blob)?.hash;
13
+ const getHash = (blob: Blob) => BinaryMetadataUtil.read(blob)?.hash;
14
14
 
15
15
  @Controller('/test/upload')
16
16
  class TestUploadController {
@@ -18,28 +18,28 @@ class TestUploadController {
18
18
  @Post('/all')
19
19
  async uploadAll(@Upload() uploads: FileMap): Promise<{ hash?: string } | undefined> {
20
20
  for (const [, blob] of Object.entries(uploads)) {
21
- return { hash: bHash(blob) };
21
+ return { hash: getHash(blob) };
22
22
  }
23
23
  }
24
24
 
25
25
  @Post('/')
26
26
  async upload(@Upload() file: File) {
27
- return { hash: bHash(file) };
27
+ return { hash: getHash(file) };
28
28
  }
29
29
 
30
30
  @Post('/all-named')
31
31
  async uploads(@Upload() file1: Blob, @Upload() file2: Blob) {
32
- return { hash1: bHash(file1), hash2: bHash(file2) };
32
+ return { hash1: getHash(file1), hash2: getHash(file2) };
33
33
  }
34
34
 
35
35
  @Post('/all-named-custom')
36
36
  async uploadVariousLimits(@Upload({ types: ['!image/png'] }) file1: Blob, @Upload() file2: Blob) {
37
- return { hash1: bHash(file1), hash2: bHash(file2) };
37
+ return { hash1: getHash(file1), hash2: getHash(file2) };
38
38
  }
39
39
 
40
40
  @Post('/all-named-size')
41
41
  async uploadVariousSizeLimits(@Upload({ maxSize: 100 }) file1: File, @Upload({ maxSize: 8000 }) file2: File) {
42
- return { hash1: bHash(file1), hash2: bHash(file2) };
42
+ return { hash1: getHash(file1), hash2: getHash(file2) };
43
43
  }
44
44
  }
45
45
 
@@ -71,8 +71,8 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
71
71
  const uploads = await this.getUploads({ name: 'random', resource: 'logo.png', type: 'image/png' });
72
72
  const response = await this.request<{ hash: string }>({ body: uploads, context: { httpMethod: 'POST', path: '/test/upload/all' } });
73
73
 
74
- const file = await this.fixture.readStream('/logo.png');
75
- assert(response.body?.hash === await BinaryUtil.hashInput(file));
74
+ const file = await this.fixture.readBinaryStream('/logo.png');
75
+ assert(response.body?.hash === await BinaryMetadataUtil.hash(file, { hashAlgorithm: 'sha256' }));
76
76
  }
77
77
 
78
78
  @Test()
@@ -81,8 +81,8 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
81
81
  const sent = castTo<Blob>(uploads.get('file'));
82
82
  const response = await this.request<{ hash: string }>({ context: { httpMethod: 'POST', path: '/test/upload' }, body: sent });
83
83
 
84
- const file = await this.fixture.readStream('/logo.png');
85
- assert(response.body?.hash === await BinaryUtil.hashInput(file));
84
+ const file = await this.fixture.readBinaryStream('/logo.png');
85
+ assert(response.body?.hash === await BinaryMetadataUtil.hash(file, { hashAlgorithm: 'sha256' }));
86
86
  }
87
87
 
88
88
  @Test()
@@ -90,8 +90,8 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
90
90
  const uploads = await this.getUploads({ name: 'file', resource: 'logo.png', type: 'image/png' });
91
91
  const response = await this.request<{ hash: string }>({ body: uploads, context: { httpMethod: 'POST', path: '/test/upload' } });
92
92
 
93
- const file = await this.fixture.readStream('/logo.png');
94
- assert(response.body?.hash === await BinaryUtil.hashInput(file));
93
+ const file = await this.fixture.readBinaryStream('/logo.png');
94
+ assert(response.body?.hash === await BinaryMetadataUtil.hash(file, { hashAlgorithm: 'sha256' }));
95
95
  }
96
96
 
97
97
  @Test()
@@ -106,8 +106,8 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
106
106
  httpMethod: 'POST', path: '/test/upload/all-named'
107
107
  }
108
108
  });
109
- const file = await this.fixture.readStream('/logo.png');
110
- const hash = await BinaryUtil.hashInput(file);
109
+ const file = await this.fixture.readBinaryStream('/logo.png');
110
+ const hash = await BinaryMetadataUtil.hash(file, { hashAlgorithm: 'sha256' });
111
111
 
112
112
  assert(response.body?.hash1 === hash);
113
113
  assert(response.body?.hash2 === hash);
@@ -136,11 +136,11 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
136
136
  }, false);
137
137
  assert(response.context.httpStatusCode === 200);
138
138
 
139
- const file1 = await this.fixture.readStream('/logo.gif');
140
- const hash1 = await BinaryUtil.hashInput(file1);
139
+ const file1 = await this.fixture.readBinaryStream('/logo.gif');
140
+ const hash1 = await BinaryMetadataUtil.hash(file1, { hashAlgorithm: 'sha256' });
141
141
 
142
- const file2 = await this.fixture.readStream('/logo.png');
143
- const hash2 = await BinaryUtil.hashInput(file2);
142
+ const file2 = await this.fixture.readBinaryStream('/logo.png');
143
+ const hash2 = await BinaryMetadataUtil.hash(file2, { hashAlgorithm: 'sha256' });
144
144
 
145
145
  assert(response.body?.hash1 === hash1);
146
146
  assert(response.body?.hash2 === hash2);
@@ -169,11 +169,11 @@ export abstract class WebUploadServerSuite extends BaseWebSuite {
169
169
  }, false);
170
170
  assert(response.context.httpStatusCode === 200);
171
171
 
172
- const file1 = await this.fixture.readStream('/asset.yml');
173
- const hash1 = await BinaryUtil.hashInput(file1);
172
+ const file1 = await this.fixture.readBinaryStream('/asset.yml');
173
+ const hash1 = await BinaryMetadataUtil.hash(file1, { hashAlgorithm: 'sha256' });
174
174
 
175
- const file2 = await this.fixture.readStream('/logo.png');
176
- const hash2 = await BinaryUtil.hashInput(file2);
175
+ const file2 = await this.fixture.readBinaryStream('/logo.png');
176
+ const hash2 = await BinaryMetadataUtil.hash(file2, { hashAlgorithm: 'sha256' });
177
177
 
178
178
  assert(response.body?.hash1 === hash1);
179
179
  assert(response.body?.hash2 === hash2);