formdata-io 1.0.0 → 1.2.0
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 +404 -15
- package/dist/client/index.d.mts +13 -34
- package/dist/client/index.d.ts +13 -34
- package/dist/client/index.js +70 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +66 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/server/index.d.mts +27 -21
- package/dist/server/index.d.ts +27 -21
- package/dist/server/index.js +72 -37
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +72 -37
- package/dist/server/index.mjs.map +1 -1
- package/dist/storage/index.d.mts +108 -0
- package/dist/storage/index.d.ts +108 -0
- package/dist/storage/index.js +271 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +264 -0
- package/dist/storage/index.mjs.map +1 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🚀 FormData IO
|
|
2
2
|
|
|
3
|
-
> TypeScript-first library for seamless FormData handling in frontend and backend.
|
|
3
|
+
> TypeScript-first library for seamless FormData handling in frontend and backend — plus cloud storage for AWS S3 and Supabase.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/formdata-io)
|
|
6
6
|
[](https://bundlephobia.com/package/formdata-io)
|
|
@@ -25,6 +25,10 @@ const formData = payload({ name: "João", avatar: file });
|
|
|
25
25
|
app.post('/upload', parser(), (req, res) => {
|
|
26
26
|
const { name, avatar } = req.payload; // ✨ Type-safe!
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
// Storage: Upload to S3 or Supabase in one line
|
|
30
|
+
const result = await storage.upload(avatar);
|
|
31
|
+
console.log(result.url); // "https://bucket.s3.us-east-1.amazonaws.com/..."
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
## Installation
|
|
@@ -33,6 +37,15 @@ app.post('/upload', parser(), (req, res) => {
|
|
|
33
37
|
npm install formdata-io
|
|
34
38
|
```
|
|
35
39
|
|
|
40
|
+
**Optional peer dependencies** (install only what you need):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# For AWS S3 storage
|
|
44
|
+
npm install @aws-sdk/client-s3
|
|
45
|
+
|
|
46
|
+
# Supabase Storage uses native fetch — no extra dependencies needed
|
|
47
|
+
```
|
|
48
|
+
|
|
36
49
|
## Quick Start
|
|
37
50
|
|
|
38
51
|
### Frontend (React, Vue, Vanilla JS)
|
|
@@ -79,6 +92,27 @@ app.post('/api/upload', parser(), (req, res) => {
|
|
|
79
92
|
});
|
|
80
93
|
```
|
|
81
94
|
|
|
95
|
+
### Storage (AWS S3 or Supabase)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { parser } from 'formdata-io/server';
|
|
99
|
+
import { createStorage } from 'formdata-io/storage';
|
|
100
|
+
|
|
101
|
+
const storage = createStorage({
|
|
102
|
+
provider: 'aws',
|
|
103
|
+
bucket: process.env.AWS_BUCKET!,
|
|
104
|
+
region: process.env.AWS_REGION!,
|
|
105
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
106
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
app.post('/api/upload', parser(), async (req, res) => {
|
|
110
|
+
const { avatar } = req.payload;
|
|
111
|
+
const result = await storage.upload(avatar);
|
|
112
|
+
res.json({ url: result.url });
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
82
116
|
## API Reference
|
|
83
117
|
|
|
84
118
|
### Client API
|
|
@@ -134,6 +168,111 @@ const formData = payload({
|
|
|
134
168
|
});
|
|
135
169
|
```
|
|
136
170
|
|
|
171
|
+
#### Base64 Converters
|
|
172
|
+
|
|
173
|
+
Utilities for bidirectional conversion between Files/Blobs and base64 strings, giving you flexibility to choose between FormData/multipart or JSON/base64 approaches.
|
|
174
|
+
|
|
175
|
+
**`fileToBase64(file: File | Blob): Promise<Base64String>`**
|
|
176
|
+
|
|
177
|
+
Converts File or Blob to base64 data URI with MIME type preservation.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { fileToBase64 } from 'formdata-io/client';
|
|
181
|
+
|
|
182
|
+
const file = new File(['content'], 'doc.txt', { type: 'text/plain' });
|
|
183
|
+
const base64 = await fileToBase64(file);
|
|
184
|
+
// → "data:text/plain;base64,Y29udGVudA=="
|
|
185
|
+
|
|
186
|
+
// Use in JSON API (no FormData/multipart)
|
|
187
|
+
await fetch('/api/user', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: JSON.stringify({ name: 'João', avatar: base64 })
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**`base64ToBlob(dataUri: Base64String): Blob`**
|
|
195
|
+
|
|
196
|
+
Converts base64 data URI to Blob with MIME type extraction.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { base64ToBlob } from 'formdata-io/client';
|
|
200
|
+
|
|
201
|
+
const dataUri = "data:image/png;base64,iVBORw0KG...";
|
|
202
|
+
const blob = base64ToBlob(dataUri);
|
|
203
|
+
// → Blob { type: "image/png", size: 1234 }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**`base64ToFile(dataUri: Base64String, filename: string): File`**
|
|
207
|
+
|
|
208
|
+
Converts base64 data URI to File with filename and metadata.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { base64ToFile } from 'formdata-io/client';
|
|
212
|
+
|
|
213
|
+
const dataUri = "data:application/pdf;base64,JVBERi0x...";
|
|
214
|
+
const file = base64ToFile(dataUri, 'document.pdf');
|
|
215
|
+
// → File { name: "document.pdf", type: "application/pdf" }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**`blobToFile(blob: Blob, filename: string): File`**
|
|
219
|
+
|
|
220
|
+
Converts Blob to File with specified filename.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { blobToFile } from 'formdata-io/client';
|
|
224
|
+
|
|
225
|
+
const blob = new Blob(['content'], { type: 'text/plain' });
|
|
226
|
+
const file = blobToFile(blob, 'output.txt');
|
|
227
|
+
// → File { name: "output.txt", type: "text/plain" }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**`fileToBlob(file: File): Blob`**
|
|
231
|
+
|
|
232
|
+
Converts File to Blob for type conversion.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { fileToBlob } from 'formdata-io/client';
|
|
236
|
+
|
|
237
|
+
const file = new File(['data'], 'file.txt', { type: 'text/plain' });
|
|
238
|
+
const blob = fileToBlob(file);
|
|
239
|
+
// → Blob { type: "text/plain", size: 4 }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Supported Formats:**
|
|
243
|
+
- ✅ Images: JPEG, PNG, SVG, WebP, GIF
|
|
244
|
+
- ✅ Documents: PDF, DOCX, XLSX, PPTX
|
|
245
|
+
- ✅ Text files: CSV, TXT, JSON, XML
|
|
246
|
+
- ✅ Media: Video (MP4, WebM), Audio (MP3, WAV)
|
|
247
|
+
- ✅ Any Blob/File type with MIME type preservation
|
|
248
|
+
|
|
249
|
+
**Use Cases:**
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Option 1: JSON API (no FormData/multipart)
|
|
253
|
+
import { fileToBase64 } from 'formdata-io/client';
|
|
254
|
+
|
|
255
|
+
const avatar = await fileToBase64(file);
|
|
256
|
+
await fetch('/api/user', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ name: 'João', avatar })
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Option 2: Traditional multipart (existing behavior)
|
|
263
|
+
import { payload } from 'formdata-io/client';
|
|
264
|
+
|
|
265
|
+
const formData = payload({ avatar: file });
|
|
266
|
+
await fetch('/api/upload', { method: 'POST', body: formData });
|
|
267
|
+
|
|
268
|
+
// Bidirectional conversion (File → base64 → File roundtrip)
|
|
269
|
+
import { fileToBase64, base64ToFile } from 'formdata-io/client';
|
|
270
|
+
|
|
271
|
+
const original = new File(['content'], 'test.txt', { type: 'text/plain' });
|
|
272
|
+
const base64 = await fileToBase64(original);
|
|
273
|
+
const restored = base64ToFile(base64, 'test.txt');
|
|
274
|
+
```
|
|
275
|
+
|
|
137
276
|
### Server API
|
|
138
277
|
|
|
139
278
|
#### `parser(options?)`
|
|
@@ -146,20 +285,22 @@ Express middleware for parsing multipart/form-data.
|
|
|
146
285
|
**Returns:** Express middleware function
|
|
147
286
|
|
|
148
287
|
**Options:**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
288
|
+
|
|
289
|
+
| Option | Type | Default | Description |
|
|
290
|
+
|--------|------|---------|-------------|
|
|
291
|
+
| `maxFileSize` | `number` | `10485760` (10MB) | Max size per file in bytes |
|
|
292
|
+
| `maxFiles` | `number` | `10` | Max number of files per request |
|
|
293
|
+
| `maxFields` | `number` | `100` | Max number of text fields per request |
|
|
294
|
+
| `maxFieldSize` | `number` | `65536` (64KB) | Max size of each text field in bytes |
|
|
295
|
+
| `maxTotalFileSize` | `number` | `Infinity` | Combined size limit for all files in bytes |
|
|
296
|
+
| `autoParseJSON` | `boolean` | `true` | Auto-parse JSON strings to objects |
|
|
297
|
+
| `autoParseNumbers` | `boolean` | `true` | Auto-convert numeric strings to numbers |
|
|
298
|
+
| `autoParseBooleans` | `boolean` | `true` | Auto-convert "true"/"false" to booleans |
|
|
158
299
|
|
|
159
300
|
**Examples:**
|
|
160
301
|
|
|
161
302
|
```typescript
|
|
162
|
-
// Default options (10MB, 10 files)
|
|
303
|
+
// Default options (10MB per file, 10 files, 100 fields)
|
|
163
304
|
app.post('/upload', parser(), (req, res) => {
|
|
164
305
|
// req.payload contains all fields and files
|
|
165
306
|
});
|
|
@@ -169,12 +310,34 @@ app.post('/photos', parser({ maxFileSize: 50 * 1024 * 1024 }), (req, res) => {
|
|
|
169
310
|
// Allow up to 50MB files
|
|
170
311
|
});
|
|
171
312
|
|
|
313
|
+
// Cap total upload size (e.g. gallery endpoint)
|
|
314
|
+
app.post('/gallery', parser({ maxTotalFileSize: 100 * 1024 * 1024 }), (req, res) => {
|
|
315
|
+
// All files combined must be under 100MB
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Limit text fields to prevent DoS
|
|
319
|
+
app.post('/form', parser({ maxFields: 20, maxFieldSize: 8 * 1024 }), (req, res) => {
|
|
320
|
+
// Max 20 fields, each up to 8KB
|
|
321
|
+
});
|
|
322
|
+
|
|
172
323
|
// Disable auto-parsing
|
|
173
324
|
app.post('/raw', parser({ autoParseJSON: false }), (req, res) => {
|
|
174
325
|
// All fields remain as strings
|
|
175
326
|
});
|
|
176
327
|
```
|
|
177
328
|
|
|
329
|
+
#### `parseMultipart(req, options?)`
|
|
330
|
+
|
|
331
|
+
Lower-level function that parses a multipart request and returns a promise — useful when you need direct control outside of Express middleware.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { parseMultipart } from 'formdata-io/server';
|
|
335
|
+
|
|
336
|
+
// Inside a custom handler or framework adapter
|
|
337
|
+
const payload = await parseMultipart(req, { maxFiles: 5 });
|
|
338
|
+
console.log(payload.avatar); // ParsedFile
|
|
339
|
+
```
|
|
340
|
+
|
|
178
341
|
#### `ParsedFile` Interface
|
|
179
342
|
|
|
180
343
|
```typescript
|
|
@@ -188,20 +351,226 @@ interface ParsedFile {
|
|
|
188
351
|
}
|
|
189
352
|
```
|
|
190
353
|
|
|
354
|
+
### Storage API
|
|
355
|
+
|
|
356
|
+
The `formdata-io/storage` package provides a unified adapter for uploading and deleting files on AWS S3 and Supabase Storage.
|
|
357
|
+
|
|
358
|
+
#### `createStorage(config)`
|
|
359
|
+
|
|
360
|
+
Factory function that returns a `StorageAdapter` for the configured provider.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { createStorage } from 'formdata-io/storage';
|
|
364
|
+
|
|
365
|
+
const storage = createStorage(config);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**AWS S3 config:**
|
|
369
|
+
|
|
370
|
+
| Field | Type | Required | Description |
|
|
371
|
+
|-------|------|----------|-------------|
|
|
372
|
+
| `provider` | `'aws'` | ✅ | Provider identifier |
|
|
373
|
+
| `bucket` | `string` | ✅ | S3 bucket name |
|
|
374
|
+
| `region` | `string` | ✅ | AWS region (e.g. `'us-east-1'`) |
|
|
375
|
+
| `accessKeyId` | `string` | ✅ | AWS access key ID |
|
|
376
|
+
| `secretAccessKey` | `string` | ✅ | AWS secret access key |
|
|
377
|
+
| `sessionToken` | `string` | — | STS/IAM temporary session token |
|
|
378
|
+
| `endpoint` | `string` | — | Custom endpoint for S3-compatible providers |
|
|
379
|
+
| `keyPrefix` | `string` | — | Default path prefix for all keys |
|
|
380
|
+
| `acl` | `'public-read' \| 'private'` | — | Object ACL (see note below) |
|
|
381
|
+
|
|
382
|
+
> **ACL note:** Since April 2023, new S3 buckets have Object Ownership set to "Bucket owner enforced", which disables ACLs entirely. Setting `acl` on such buckets throws an `AccessControlListNotSupported` error. Either remove the `acl` option or change the bucket's Object Ownership setting in the S3 console.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// .env
|
|
386
|
+
// AWS_BUCKET=my-bucket
|
|
387
|
+
// AWS_REGION=us-east-1
|
|
388
|
+
// AWS_ACCESS_KEY_ID=AKIA...
|
|
389
|
+
// AWS_SECRET_ACCESS_KEY=...
|
|
390
|
+
|
|
391
|
+
const storage = createStorage({
|
|
392
|
+
provider: 'aws',
|
|
393
|
+
bucket: process.env.AWS_BUCKET!,
|
|
394
|
+
region: process.env.AWS_REGION!,
|
|
395
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
396
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
397
|
+
keyPrefix: 'uploads',
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Supabase Storage config:**
|
|
402
|
+
|
|
403
|
+
| Field | Type | Required | Description |
|
|
404
|
+
|-------|------|----------|-------------|
|
|
405
|
+
| `provider` | `'supabase'` | ✅ | Provider identifier |
|
|
406
|
+
| `bucket` | `string` | ✅ | Supabase storage bucket name |
|
|
407
|
+
| `url` | `string` | ✅ | Supabase project URL |
|
|
408
|
+
| `serviceKey` | `string` | ✅ | Supabase service role key |
|
|
409
|
+
| `keyPrefix` | `string` | — | Default path prefix for all keys |
|
|
410
|
+
| `publicBucket` | `boolean` | — | Whether the bucket is public (default: `true`) |
|
|
411
|
+
|
|
412
|
+
> When `publicBucket` is `true`, `UploadResult.url` is the full public URL. When `false`, `url` contains only the storage key and you are responsible for generating a signed URL via the Supabase client before serving the file.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// .env
|
|
416
|
+
// SUPABASE_URL=https://xyz.supabase.co
|
|
417
|
+
// SUPABASE_SERVICE_KEY=eyJ...
|
|
418
|
+
|
|
419
|
+
const storage = createStorage({
|
|
420
|
+
provider: 'supabase',
|
|
421
|
+
bucket: 'avatars',
|
|
422
|
+
url: process.env.SUPABASE_URL!,
|
|
423
|
+
serviceKey: process.env.SUPABASE_SERVICE_KEY!,
|
|
424
|
+
keyPrefix: 'users',
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `storage.upload(input, options?)`
|
|
429
|
+
|
|
430
|
+
Uploads a single file and returns an `UploadResult`.
|
|
431
|
+
|
|
432
|
+
**`UploadInput`** — accepts three forms:
|
|
433
|
+
- `ParsedFile` — file parsed by `parser()` middleware (preferred)
|
|
434
|
+
- `Buffer` — raw buffer (requires `filename` in options)
|
|
435
|
+
- `string` — base64 data URI (requires `filename` in options)
|
|
436
|
+
|
|
437
|
+
**`UploadOptions`:**
|
|
438
|
+
|
|
439
|
+
| Field | Type | Description |
|
|
440
|
+
|-------|------|-------------|
|
|
441
|
+
| `filename` | `string` | Required for Buffer and base64 inputs |
|
|
442
|
+
| `mimetype` | `string` | Override detected MIME type |
|
|
443
|
+
| `keyPrefix` | `string` | Override key prefix for this upload only |
|
|
444
|
+
|
|
445
|
+
**`UploadResult`:**
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
interface UploadResult {
|
|
449
|
+
url: string; // Public URL (or key for private Supabase buckets)
|
|
450
|
+
key: string; // Storage key: "{prefix}/{uuid}-{sanitized-filename}"
|
|
451
|
+
size: number; // File size in bytes
|
|
452
|
+
mimetype: string; // MIME type
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Examples:**
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// Upload a ParsedFile from parser()
|
|
460
|
+
const result = await storage.upload(req.payload.avatar);
|
|
461
|
+
console.log(result.url); // "https://bucket.s3.us-east-1.amazonaws.com/uploads/abc-avatar.jpg"
|
|
462
|
+
console.log(result.key); // "uploads/abc123-avatar.jpg"
|
|
463
|
+
console.log(result.size); // 204800
|
|
464
|
+
console.log(result.mimetype); // "image/jpeg"
|
|
465
|
+
|
|
466
|
+
// Upload a Buffer
|
|
467
|
+
const result = await storage.upload(buffer, {
|
|
468
|
+
filename: 'report.pdf',
|
|
469
|
+
mimetype: 'application/pdf',
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Upload a base64 data URI
|
|
473
|
+
const result = await storage.upload(dataUri, { filename: 'photo.png' });
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### `storage.uploadMany(inputs, options?)`
|
|
477
|
+
|
|
478
|
+
Uploads multiple files in parallel and returns an array of `UploadResult`.
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
const files = [req.payload.photo1, req.payload.photo2, req.payload.photo3];
|
|
482
|
+
const results = await storage.uploadMany(files);
|
|
483
|
+
// → [{ url, key, size, mimetype }, ...]
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### `storage.delete(key)`
|
|
487
|
+
|
|
488
|
+
Deletes a file by its storage key.
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
await storage.delete('uploads/abc123-avatar.jpg');
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### End-to-end Express example
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import express from 'express';
|
|
498
|
+
import { parser } from 'formdata-io/server';
|
|
499
|
+
import { createStorage } from 'formdata-io/storage';
|
|
500
|
+
|
|
501
|
+
const app = express();
|
|
502
|
+
|
|
503
|
+
const storage = createStorage({
|
|
504
|
+
provider: 'aws',
|
|
505
|
+
bucket: process.env.AWS_BUCKET!,
|
|
506
|
+
region: process.env.AWS_REGION!,
|
|
507
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
508
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
509
|
+
keyPrefix: 'avatars',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
app.post('/api/profile', parser({ maxFileSize: 5 * 1024 * 1024 }), async (req, res) => {
|
|
513
|
+
try {
|
|
514
|
+
const { name, avatar } = req.payload;
|
|
515
|
+
|
|
516
|
+
const uploaded = await storage.upload(avatar);
|
|
517
|
+
|
|
518
|
+
res.json({
|
|
519
|
+
name,
|
|
520
|
+
avatarUrl: uploaded.url,
|
|
521
|
+
avatarKey: uploaded.key,
|
|
522
|
+
});
|
|
523
|
+
} catch (err) {
|
|
524
|
+
res.status(500).json({ error: (err as Error).message });
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### S3-compatible providers (MinIO, Cloudflare R2, etc.)
|
|
530
|
+
|
|
531
|
+
Pass a custom `endpoint` to use any S3-compatible storage service:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// MinIO
|
|
535
|
+
const storage = createStorage({
|
|
536
|
+
provider: 'aws',
|
|
537
|
+
bucket: 'my-bucket',
|
|
538
|
+
region: 'us-east-1',
|
|
539
|
+
accessKeyId: process.env.MINIO_ACCESS_KEY!,
|
|
540
|
+
secretAccessKey: process.env.MINIO_SECRET_KEY!,
|
|
541
|
+
endpoint: 'http://localhost:9000',
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Cloudflare R2
|
|
545
|
+
const storage = createStorage({
|
|
546
|
+
provider: 'aws',
|
|
547
|
+
bucket: 'my-bucket',
|
|
548
|
+
region: 'auto',
|
|
549
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
550
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
551
|
+
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
191
555
|
## TypeScript Support
|
|
192
556
|
|
|
193
557
|
Full TypeScript support with type inference:
|
|
194
558
|
|
|
195
559
|
```typescript
|
|
196
560
|
import type { ParsedFile } from 'formdata-io/server';
|
|
561
|
+
import type { UploadResult } from 'formdata-io/storage';
|
|
197
562
|
|
|
198
|
-
app.post('/upload', parser(), (req, res) => {
|
|
563
|
+
app.post('/upload', parser(), async (req, res) => {
|
|
199
564
|
const avatar = req.payload?.avatar as ParsedFile;
|
|
200
565
|
|
|
201
566
|
avatar.buffer; // Buffer
|
|
202
567
|
avatar.originalname; // string
|
|
203
568
|
avatar.mimetype; // string
|
|
204
569
|
avatar.size; // number
|
|
570
|
+
|
|
571
|
+
const result: UploadResult = await storage.upload(avatar);
|
|
572
|
+
result.url; // string
|
|
573
|
+
result.key; // string
|
|
205
574
|
});
|
|
206
575
|
```
|
|
207
576
|
|
|
@@ -228,6 +597,7 @@ open examples/basic/client.html
|
|
|
228
597
|
| TypeScript-first | ✅ | ⚠️ | ⚠️ | ❌ |
|
|
229
598
|
| Zero config | ✅ | ❌ | ❌ | ✅ |
|
|
230
599
|
| Auto-parsing | ✅ | ❌ | ❌ | N/A |
|
|
600
|
+
| Cloud storage | ✅ | ❌ | ❌ | ❌ |
|
|
231
601
|
| Bundle size | ~6KB | ~30KB | ~10KB | ~2KB |
|
|
232
602
|
|
|
233
603
|
## How It Works
|
|
@@ -241,28 +611,47 @@ The `payload()` function converts JavaScript objects to FormData by:
|
|
|
241
611
|
3. **Object serialization**: Nested objects are JSON-serialized
|
|
242
612
|
4. **Type conversion**: Booleans, numbers, dates converted to strings
|
|
243
613
|
|
|
614
|
+
**Base64 Converters** provide alternative file handling:
|
|
615
|
+
|
|
616
|
+
1. **Bidirectional conversion**: File ↔ Base64 ↔ Blob transformations
|
|
617
|
+
2. **MIME type preservation**: Data URIs maintain original file types
|
|
618
|
+
3. **JSON API support**: Enable file uploads via JSON payloads
|
|
619
|
+
4. **Flexibility**: Choose between FormData/multipart or JSON/base64 approaches
|
|
620
|
+
|
|
244
621
|
### Server Side
|
|
245
622
|
|
|
246
623
|
The `parser()` middleware uses [busboy](https://github.com/mscdex/busboy) for stream-based parsing:
|
|
247
624
|
|
|
248
625
|
1. **Stream processing**: Memory-efficient file handling
|
|
249
|
-
2. **Size limits**: Enforced per-file and total limits
|
|
626
|
+
2. **Size limits**: Enforced per-file, per-field, and total file size limits
|
|
250
627
|
3. **Auto-parsing**: Automatic type conversion (JSON, numbers, booleans)
|
|
251
628
|
4. **Array normalization**: Multiple values with same key become arrays
|
|
252
629
|
|
|
630
|
+
### Storage Side
|
|
631
|
+
|
|
632
|
+
The `createStorage()` factory returns a provider-agnostic `StorageAdapter`:
|
|
633
|
+
|
|
634
|
+
1. **Unified interface**: Same `upload` / `uploadMany` / `delete` API across providers
|
|
635
|
+
2. **Key generation**: Storage keys use `{prefix}/{uuid}-{sanitized-filename}` format, with NFD normalization to produce readable keys from accented filenames
|
|
636
|
+
3. **Lazy loading**: The AWS SDK is loaded on first upload, keeping startup time unaffected if storage is unused
|
|
637
|
+
4. **Input flexibility**: Accepts `ParsedFile`, raw `Buffer`, or base64 data URI as upload input
|
|
638
|
+
|
|
253
639
|
## Security
|
|
254
640
|
|
|
255
641
|
**Built-in protections:**
|
|
256
642
|
- ✅ File size limits (default: 10MB per file)
|
|
257
643
|
- ✅ File count limits (default: 10 files max)
|
|
644
|
+
- ✅ Text field limits (default: 100 fields, 64KB each)
|
|
645
|
+
- ✅ Total file size limit (configurable via `maxTotalFileSize`)
|
|
258
646
|
- ✅ Stream-based processing (no memory exhaustion)
|
|
259
647
|
- ✅ Safe JSON parsing (fallback to string on error)
|
|
648
|
+
- ✅ ReDoS protection for base64 parsing (regex runs in isolated `vm` context with 50ms timeout, throws `RegExpTimeoutError` on timeout)
|
|
649
|
+
- ✅ Storage key sanitization (NFD normalization + accent stripping + alphanumeric enforcement prevents path traversal)
|
|
260
650
|
|
|
261
651
|
**Your responsibility:**
|
|
262
652
|
- ⚠️ File type validation (check `mimetype` and magic bytes)
|
|
263
|
-
- ⚠️ Filename sanitization (prevent path traversal)
|
|
264
653
|
- ⚠️ Virus scanning (if accepting user files)
|
|
265
|
-
- ⚠️ Storage
|
|
654
|
+
- ⚠️ Storage permissions (S3 bucket policies, Supabase RLS)
|
|
266
655
|
|
|
267
656
|
## License
|
|
268
657
|
|
package/dist/client/index.d.mts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64-encoded data URI string format
|
|
3
|
+
*
|
|
4
|
+
* @example "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
|
|
5
|
+
*/
|
|
6
|
+
type Base64String = `data:${string};base64,${string}`;
|
|
1
7
|
/**
|
|
2
8
|
* Supported value types for FormData conversion
|
|
3
9
|
*/
|
|
@@ -52,39 +58,12 @@ interface PayloadOptions {
|
|
|
52
58
|
booleansAsIntegers?: boolean;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
/**
|
|
56
|
-
* Converts a JavaScript object to FormData
|
|
57
|
-
*
|
|
58
|
-
* @param data - Object to be converted
|
|
59
|
-
* @param options - Configuration options
|
|
60
|
-
* @returns FormData instance ready for submission
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```typescript
|
|
64
|
-
* const formData = payload({
|
|
65
|
-
* name: "João Silva",
|
|
66
|
-
* age: 25,
|
|
67
|
-
* avatar: fileInput.files[0],
|
|
68
|
-
* tags: ["admin", "user"],
|
|
69
|
-
* metadata: { source: "web" }
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* // Use with fetch
|
|
73
|
-
* fetch('/upload', { method: 'POST', body: formData });
|
|
74
|
-
*
|
|
75
|
-
* // Use with axios
|
|
76
|
-
* axios.post('/upload', formData);
|
|
77
|
-
* ```
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* ```typescript
|
|
81
|
-
* // With custom options
|
|
82
|
-
* const formData = payload(data, {
|
|
83
|
-
* indices: true, // tags[0]=admin&tags[1]=user
|
|
84
|
-
* booleansAsIntegers: false // active=true instead of active=1
|
|
85
|
-
* });
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
61
|
declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
|
|
89
62
|
|
|
90
|
-
|
|
63
|
+
declare function fileToBase64(file: File | Blob): Promise<Base64String>;
|
|
64
|
+
declare function base64ToBlob(dataUri: Base64String): Blob;
|
|
65
|
+
declare function base64ToFile(dataUri: Base64String, filename: string): File;
|
|
66
|
+
declare function blobToFile(blob: Blob, filename: string): File;
|
|
67
|
+
declare function fileToBlob(file: File): Blob;
|
|
68
|
+
|
|
69
|
+
export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64-encoded data URI string format
|
|
3
|
+
*
|
|
4
|
+
* @example "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
|
|
5
|
+
*/
|
|
6
|
+
type Base64String = `data:${string};base64,${string}`;
|
|
1
7
|
/**
|
|
2
8
|
* Supported value types for FormData conversion
|
|
3
9
|
*/
|
|
@@ -52,39 +58,12 @@ interface PayloadOptions {
|
|
|
52
58
|
booleansAsIntegers?: boolean;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
/**
|
|
56
|
-
* Converts a JavaScript object to FormData
|
|
57
|
-
*
|
|
58
|
-
* @param data - Object to be converted
|
|
59
|
-
* @param options - Configuration options
|
|
60
|
-
* @returns FormData instance ready for submission
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```typescript
|
|
64
|
-
* const formData = payload({
|
|
65
|
-
* name: "João Silva",
|
|
66
|
-
* age: 25,
|
|
67
|
-
* avatar: fileInput.files[0],
|
|
68
|
-
* tags: ["admin", "user"],
|
|
69
|
-
* metadata: { source: "web" }
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* // Use with fetch
|
|
73
|
-
* fetch('/upload', { method: 'POST', body: formData });
|
|
74
|
-
*
|
|
75
|
-
* // Use with axios
|
|
76
|
-
* axios.post('/upload', formData);
|
|
77
|
-
* ```
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* ```typescript
|
|
81
|
-
* // With custom options
|
|
82
|
-
* const formData = payload(data, {
|
|
83
|
-
* indices: true, // tags[0]=admin&tags[1]=user
|
|
84
|
-
* booleansAsIntegers: false // active=true instead of active=1
|
|
85
|
-
* });
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
61
|
declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
|
|
89
62
|
|
|
90
|
-
|
|
63
|
+
declare function fileToBase64(file: File | Blob): Promise<Base64String>;
|
|
64
|
+
declare function base64ToBlob(dataUri: Base64String): Blob;
|
|
65
|
+
declare function base64ToFile(dataUri: Base64String, filename: string): File;
|
|
66
|
+
declare function blobToFile(blob: Blob, filename: string): File;
|
|
67
|
+
declare function fileToBlob(file: File): Blob;
|
|
68
|
+
|
|
69
|
+
export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
|