Skip to content

Commit 520a8d9

Browse files
committed
fix: multipart testing
1 parent d34ab7d commit 520a8d9

18 files changed

+1290
-950
lines changed

docs/examples/functions/create-execution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const functions = new Functions(client);
88

99
const result = await functions.createExecution(
1010
'<FUNCTION_ID>', // functionId
11-
, // body (optional)
11+
await pickSingle(), // body (optional)
1212
false, // async (optional)
1313
'<PATH>', // path (optional)
1414
ExecutionMethod.GET, // method (optional)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "react-native-appwrite",
33
"homepage": "https://appwrite.io/support",
44
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
5-
"version": "0.6.0-rc1",
5+
"version": "0.6.0",
66
"license": "BSD-3-Clause",
77
"main": "dist/cjs/sdk.js",
88
"exports": {
@@ -26,14 +26,14 @@
2626
},
2727
"devDependencies": {
2828
"@rollup/plugin-typescript": "8.3.2",
29-
"playwright": "1.15.0",
3029
"rollup": "2.75.4",
3130
"serve-handler": "6.1.0",
3231
"tslib": "2.4.0",
3332
"typescript": "4.7.2"
3433
},
3534
"dependencies": {
3635
"expo-file-system": "16.0.8",
36+
"parse-multipart-data": "^1.5.0",
3737
"react-native": "^0.73.6"
3838
},
3939
"peerDependencies": {

src/client.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { Models } from './models';
2-
import { Service } from './service';
31
import { Platform } from 'react-native';
2+
import { getBoundary, parse as parseMultipart} from './multipart';
3+
import { Service } from './service';
4+
import { Payload } from './payload';
5+
import { Models } from './models';
6+
47

5-
type Payload = {
8+
type Params = {
69
[key: string]: any;
710
}
811

@@ -103,7 +106,7 @@ class Client {
103106
'x-sdk-name': 'React Native',
104107
'x-sdk-platform': 'client',
105108
'x-sdk-language': 'reactnative',
106-
'x-sdk-version': '0.6.0-rc1',
109+
'x-sdk-version': '0.6.0',
107110
'X-Appwrite-Response-Format': '1.6.0',
108111
};
109112

@@ -383,7 +386,7 @@ class Client {
383386
}
384387
}
385388

386-
async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise<any> {
389+
async call(method: string, url: URL, headers: Headers = {}, params: Params = {}): Promise<any> {
387390
method = method.toUpperCase();
388391

389392
headers = Object.assign({}, this.headers, headers);
@@ -435,6 +438,36 @@ class Client {
435438

436439
if (response.headers.get('content-type')?.includes('application/json')) {
437440
data = await response.json();
441+
} else if (response.headers.get('content-type')?.includes('multipart/form-data')) {
442+
const boundary = getBoundary(
443+
response.headers.get("content-type") || ""
444+
);
445+
446+
const body = new Uint8Array(await response.arrayBuffer());
447+
const parts = parseMultipart(body, boundary);
448+
const partsObject: { [key: string]: any } = {};
449+
450+
for (const part of parts) {
451+
if (!part.name) {
452+
continue;
453+
}
454+
if (part.name === "responseBody") {
455+
partsObject[part.name] = Payload.fromBinary(part.data, part.filename);
456+
} else if (part.name === "responseStatusCode") {
457+
partsObject[part.name] = parseInt(part.data.toString());
458+
} else if (part.name === "duration") {
459+
partsObject[part.name] = parseFloat(part.data.toString());
460+
} else if (part.type === 'application/json') {
461+
try {
462+
partsObject[part.name] = JSON.parse(part.data.toString());
463+
} catch (e) {
464+
throw new Error(`Error parsing JSON for part ${part.name}: ${e instanceof Error ? e.message : 'Unknown error'}`);
465+
}
466+
} else {
467+
partsObject[part.name] = part.data.toString();
468+
}
469+
}
470+
data = partsObject;
438471
} else {
439472
data = {
440473
message: await response.text()
@@ -463,4 +496,4 @@ class Client {
463496
}
464497

465498
export { Client, AppwriteException };
466-
export type { Models, Payload };
499+
export type { Models, Params };

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ export { Locale } from './services/locale';
88
export { Messaging } from './services/messaging';
99
export { Storage } from './services/storage';
1010
export { Teams } from './services/teams';
11-
export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client';
11+
export type { Models, Params, RealtimeResponseEvent, UploadProgress } from './client';
1212
export type { QueryTypes, QueryTypesList } from './query';
1313
export { Query } from './query';
1414
export { Permission } from './permission';
1515
export { Role } from './role';
1616
export { ID } from './id';
17+
export { Payload } from './payload';
1718
export { AuthenticatorType } from './enums/authenticator-type';
1819
export { AuthenticationFactor } from './enums/authentication-factor';
1920
export { OAuthProvider } from './enums/o-auth-provider';

src/models.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Payload } from './payload';
2+
13
export namespace Models {
24
/**
35
* Documents List
@@ -926,7 +928,7 @@ export namespace Models {
926928
/**
927929
* HTTP response body. This will return empty unless execution is created as synchronous.
928930
*/
929-
responseBody: string;
931+
responseBody: Payload;
930932
/**
931933
* HTTP response headers as a key-value object. This will return only whitelisted headers. All headers are returned if execution is created as synchronous.
932934
*/

src/multipart.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* Port of: https://github.com/nachomazzara/parse-multipart-data/blob/master/src/multipart.ts
3+
* Includes few changes for Deno compatibility. Textdiff should show the changes.
4+
* Copied from master with commit 56052e860bc4e3fa7fe4763f69e88ec79b295a3c
5+
*
6+
*
7+
* Multipart Parser (Finite State Machine)
8+
* usage:
9+
* const multipart = require('./multipart.js');
10+
* const body = multipart.DemoData(); // raw body
11+
* const body = Buffer.from(event['body-json'].toString(),'base64'); // AWS case
12+
* const boundary = multipart.getBoundary(event.params.header['content-type']);
13+
* const parts = multipart.Parse(body,boundary);
14+
* each part is:
15+
* { filename: 'A.txt', type: 'text/plain', data: <Buffer 41 41 41 41 42 42 42 42> }
16+
* or { name: 'key', data: <Buffer 41 41 41 41 42 42 42 42> }
17+
*/
18+
19+
type Part = {
20+
contentDispositionHeader: string;
21+
contentTypeHeader: string;
22+
part: number[];
23+
};
24+
25+
type Input = {
26+
filename?: string;
27+
name?: string;
28+
type: string;
29+
data: Uint8Array;
30+
};
31+
32+
enum ParsingState {
33+
INIT,
34+
READING_HEADERS,
35+
READING_DATA,
36+
READING_PART_SEPARATOR,
37+
}
38+
39+
export function parse(
40+
multipartBodyBuffer: Uint8Array,
41+
boundary: string
42+
): Input[] {
43+
let lastline = "";
44+
let contentDispositionHeader = "";
45+
let contentTypeHeader = "";
46+
let state: ParsingState = ParsingState.INIT;
47+
let buffer: number[] = [];
48+
const allParts: Input[] = [];
49+
50+
let currentPartHeaders: string[] = [];
51+
52+
for (let i = 0; i < multipartBodyBuffer.length; i++) {
53+
const oneByte: number = multipartBodyBuffer[i];
54+
const prevByte: number | null = i > 0 ? multipartBodyBuffer[i - 1] : null;
55+
// 0x0a => \n
56+
// 0x0d => \r
57+
const newLineDetected: boolean = oneByte === 0x0a && prevByte === 0x0d;
58+
const newLineChar: boolean = oneByte === 0x0a || oneByte === 0x0d;
59+
60+
if (!newLineChar) lastline += String.fromCharCode(oneByte);
61+
if (ParsingState.INIT === state && newLineDetected) {
62+
// searching for boundary
63+
if ("--" + boundary === lastline) {
64+
state = ParsingState.READING_HEADERS; // found boundary. start reading headers
65+
}
66+
lastline = "";
67+
} else if (ParsingState.READING_HEADERS === state && newLineDetected) {
68+
// parsing headers. Headers are separated by an empty line from the content. Stop reading headers when the line is empty
69+
if (lastline.length) {
70+
currentPartHeaders.push(lastline);
71+
} else {
72+
// found empty line. search for the headers we want and set the values
73+
for (const h of currentPartHeaders) {
74+
if (h.toLowerCase().startsWith("content-disposition:")) {
75+
contentDispositionHeader = h;
76+
} else if (h.toLowerCase().startsWith("content-type:")) {
77+
contentTypeHeader = h;
78+
}
79+
}
80+
state = ParsingState.READING_DATA;
81+
buffer = [];
82+
}
83+
lastline = "";
84+
} else if (ParsingState.READING_DATA === state) {
85+
// parsing data
86+
if (lastline.length > boundary.length + 4) {
87+
lastline = ""; // mem save
88+
}
89+
if ("--" + boundary === lastline) {
90+
const j = buffer.length - lastline.length;
91+
const part = buffer.slice(0, j - 1);
92+
93+
allParts.push(
94+
process({ contentDispositionHeader, contentTypeHeader, part })
95+
);
96+
buffer = [];
97+
currentPartHeaders = [];
98+
lastline = "";
99+
state = ParsingState.READING_PART_SEPARATOR;
100+
contentDispositionHeader = "";
101+
contentTypeHeader = "";
102+
} else {
103+
buffer.push(oneByte);
104+
}
105+
if (newLineDetected) {
106+
lastline = "";
107+
}
108+
} else if (ParsingState.READING_PART_SEPARATOR === state) {
109+
if (newLineDetected) {
110+
state = ParsingState.READING_HEADERS;
111+
}
112+
}
113+
}
114+
return allParts;
115+
}
116+
117+
// read the boundary from the content-type header sent by the http client
118+
// this value may be similar to:
119+
// 'multipart/form-data; boundary=----WebKitFormBoundaryvm5A9tzU1ONaGP5B',
120+
export function getBoundary(header: string): string {
121+
const items = header.split(";");
122+
if (items) {
123+
for (let i = 0; i < items.length; i++) {
124+
const item = new String(items[i]).trim();
125+
if (item.indexOf("boundary") >= 0) {
126+
const k = item.split("=");
127+
return new String(k[1]).trim().replace(/^["']|["']$/g, "");
128+
}
129+
}
130+
}
131+
return "";
132+
}
133+
134+
export function DemoData(): { body: Uint8Array; boundary: string } {
135+
let body = "trash1\r\n";
136+
body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n";
137+
body += "Content-Type: text/plain\r\n";
138+
body +=
139+
'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"\r\n';
140+
body += "\r\n";
141+
body += "@11X";
142+
body += "111Y\r\n";
143+
body += "111Z\rCCCC\nCCCC\r\nCCCCC@\r\n\r\n";
144+
body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n";
145+
body += "Content-Type: text/plain\r\n";
146+
body +=
147+
'Content-Disposition: form-data; name="uploads[]"; filename="B.txt"\r\n';
148+
body += "\r\n";
149+
body += "@22X";
150+
body += "222Y\r\n";
151+
body += "222Z\r222W\n2220\r\n666@\r\n";
152+
body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n";
153+
body += 'Content-Disposition: form-data; name="input1"\r\n';
154+
body += "\r\n";
155+
body += "value1\r\n";
156+
body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp--\r\n";
157+
158+
return {
159+
body: new TextEncoder().encode(body),
160+
boundary: "----WebKitFormBoundaryvef1fLxmoUdYZWXp",
161+
};
162+
}
163+
164+
function process(part: Part): Input {
165+
// will transform this object:
166+
// { header: 'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"',
167+
// info: 'Content-Type: text/plain',
168+
// part: 'AAAABBBB' }
169+
// into this one:
170+
// { filename: 'A.txt', type: 'text/plain', data: <Buffer 41 41 41 41 42 42 42 42> }
171+
const obj = function (str: string) {
172+
const k = str.split("=");
173+
const a = k[0].trim();
174+
175+
const b = JSON.parse(k[1].trim());
176+
const o = {};
177+
Object.defineProperty(o, a, {
178+
value: b,
179+
writable: true,
180+
enumerable: true,
181+
configurable: true,
182+
});
183+
return o;
184+
};
185+
const header = part.contentDispositionHeader.split(";");
186+
187+
const filenameData = header[2];
188+
let input = {};
189+
if (filenameData) {
190+
input = obj(filenameData);
191+
const contentType = part.contentTypeHeader.split(":")[1].trim();
192+
Object.defineProperty(input, "type", {
193+
value: contentType,
194+
writable: true,
195+
enumerable: true,
196+
configurable: true,
197+
});
198+
}
199+
// always process the name field
200+
Object.defineProperty(input, "name", {
201+
value: header[1].split("=")[1].replace(/"/g, ""),
202+
writable: true,
203+
enumerable: true,
204+
configurable: true,
205+
});
206+
207+
Object.defineProperty(input, "data", {
208+
value: new Uint8Array(part.part),
209+
writable: true,
210+
enumerable: true,
211+
configurable: true,
212+
});
213+
return input as Input;
214+
}

0 commit comments

Comments
 (0)