Skip to content

Commit c48e1eb

Browse files
committed
#353, #392 Form data enhancements (#398)
* eslint improvements * #388: removing ~ from dist folder * v5.2.1 * new form-data usage PoC * code formatting and fixes * test fixes
1 parent 94b1b53 commit c48e1eb

File tree

4 files changed

+105
-9
lines changed

4 files changed

+105
-9
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { open } from 'fs/promises';
2+
import { basename } from 'path';
3+
import type { Attachment } from '../version2/parameters';
4+
5+
export async function createAttachmentFromPath(path: string, contentType?: string): Promise<Attachment> {
6+
const filename = basename(path);
7+
8+
const fileHandle = await open(path, 'r');
9+
10+
const { size } = await fileHandle.stat();
11+
12+
return {
13+
filename,
14+
content: fileHandle.readableWebStream(),
15+
contentType,
16+
contentLength: size,
17+
};
18+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import mime from 'mime';
2+
import type { ReadableStream as ReadableNodeStream } from 'node:stream/web';
3+
4+
type FormDataValue = string | Blob | ArrayBuffer | ReadableStream | ReadableNodeStream;
5+
6+
class FileWithSize extends File {
7+
size: number = 0;
8+
}
9+
10+
interface AppendOptions {
11+
contentLength?: number;
12+
contentType?: string;
13+
}
14+
15+
export class FormDataService {
16+
formData: FormData;
17+
18+
constructor() {
19+
this.formData = new FormData();
20+
}
21+
22+
async append(value: FormDataValue, filename: string, options: AppendOptions = {}) {
23+
const blobOptions = {
24+
type: options.contentType ?? mime.getType(filename) ?? undefined,
25+
};
26+
27+
if (typeof value === 'string') {
28+
this.formData.append('file', new Blob([value], blobOptions), filename);
29+
} else if (value instanceof Blob) {
30+
this.formData.append('file', new Blob([value], blobOptions), filename);
31+
} else if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
32+
this.formData.append('file', new Blob([value], blobOptions), filename);
33+
} else if (value instanceof ReadableStream) {
34+
const file = new FileWithSize([], filename, blobOptions);
35+
36+
if (options.contentLength != undefined) {
37+
file.size = options.contentLength;
38+
file.stream = () => value as ReadableStream;
39+
} else {
40+
const [streamForSize, streamForContent] = value.tee();
41+
42+
file.size = await this.getStreamSize(streamForSize);
43+
file.stream = () => streamForContent as ReadableStream;
44+
}
45+
46+
this.formData.append('file', file);
47+
} else {
48+
throw new Error('Invalid value'); // todo error handling
49+
}
50+
}
51+
52+
private async getStreamSize(stream: ReadableStream | ReadableNodeStream): Promise<number> {
53+
let totalSize = 0;
54+
const reader = stream.getReader();
55+
56+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
57+
while (true) {
58+
const { done, value } = await reader.read();
59+
if (done) break;
60+
61+
if (value instanceof Uint8Array) {
62+
totalSize += value.length;
63+
} else if (typeof value === 'string') {
64+
totalSize += new TextEncoder().encode(value).length;
65+
} else if (value instanceof Blob) {
66+
totalSize += value.size;
67+
} else if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
68+
totalSize += value.byteLength;
69+
} else if (value === null || value === undefined) {
70+
continue;
71+
} else {
72+
throw new Error(`Unsupported value type: ${typeof value}`);
73+
}
74+
}
75+
76+
return totalSize;
77+
}
78+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './formDataService';

src/version2/parameters/addAttachment.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Readable } from 'node:stream';
1+
import type { ReadableStream as ReadableNodeStream } from 'node:stream/web';
22

33
/**
44
* Represents an attachment to be added to an issue.
@@ -13,6 +13,7 @@ import type { Readable } from 'node:stream';
1313
* ```
1414
*/
1515
export interface Attachment {
16+
// todo JSDoc
1617
/**
1718
* The name of the attachment file.
1819
*
@@ -37,22 +38,20 @@ export interface Attachment {
3738
* const fileContent = fs.readFileSync('./document.pdf');
3839
* ```
3940
*/
40-
file: Buffer | ReadableStream | Readable | string | Blob | File;
41+
content: ArrayBuffer | ReadableStream | ReadableNodeStream | string | Blob;
4142

4243
/**
43-
* Optional MIME type of the attachment. Example values include:
44-
*
45-
* - 'application/pdf'
46-
* - 'image/png'
44+
* Optional MIME type of the attachment.
4745
*
4846
* If not provided, the MIME type will be automatically detected based on the filename.
4947
*
5048
* @example
5149
* ```typescript
52-
* const mimeType = 'application/pdf';
50+
* 'application/pdf'
5351
* ```
5452
*/
55-
mimeType?: string;
53+
contentType?: string;
54+
contentLength?: number; // todo JSDoc
5655
}
5756

5857
/**
@@ -99,5 +98,5 @@ export interface AddAttachment {
9998
* ];
10099
* ```
101100
*/
102-
attachment: Attachment | Attachment[];
101+
attachment: Attachment | Attachment[]; // todo JSDoc
103102
}

0 commit comments

Comments
 (0)