json-rest-api 0.1.1 → 1.0.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.
Files changed (50) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -20
  3. package/README.md +728 -120
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +28 -202
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +37 -20
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/.jshintrc +0 -48
  46. package/.npmignore +0 -15
  47. package/examples/client.js +0 -18
  48. package/examples/client2.js +0 -27
  49. package/examples/server.js +0 -23
  50. package/test.js +0 -101
@@ -0,0 +1,722 @@
1
+ # File Uploads Guide
2
+
3
+ This guide explains how to handle file uploads in JSON REST API using the FileHandlingPlugin. The system is designed to be protocol-agnostic, storage-pluggable, and schema-driven.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Overview](#overview)
8
+ 2. [Quick Start](#quick-start)
9
+ 3. [Schema Configuration](#schema-configuration)
10
+ 4. [Storage Adapters](#storage-adapters)
11
+ 5. [Protocol Configuration](#protocol-configuration)
12
+ 6. [File Validation](#file-validation)
13
+ 7. [Complete Examples](#complete-examples)
14
+ 8. [Troubleshooting](#troubleshooting)
15
+
16
+ ## Overview
17
+
18
+ The file handling system consists of three main components:
19
+
20
+ 1. **FileHandlingPlugin** - Orchestrates file detection and processing
21
+ 2. **Protocol Detectors** - Parse files from different protocols (HTTP, Express)
22
+ 3. **Storage Adapters** - Save files to different backends (local, S3, etc.)
23
+
24
+ ### How It Works
25
+
26
+ 1. You define file fields in your schema with `type: 'file'`
27
+ 2. Protocol plugins detect and parse multipart uploads
28
+ 3. FileHandlingPlugin validates and processes files
29
+ 4. Storage adapters save files and return URLs
30
+ 5. File fields are replaced with URLs in your data
31
+
32
+ ## Quick Start
33
+
34
+ ```javascript
35
+ import { Api } from 'hooked-api';
36
+ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'jsonrestapi';
37
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
38
+
39
+ // Create API
40
+ const api = new Api({ name: 'my-api', version: '1.0.0' });
41
+
42
+ // Create storage
43
+ const storage = new LocalStorage({
44
+ directory: './uploads',
45
+ baseUrl: 'http://localhost:3000/uploads'
46
+ });
47
+
48
+ // Use plugins (order matters!)
49
+ api.use(RestApiPlugin);
50
+ api.use(FileHandlingPlugin);
51
+ api.use(ExpressPlugin);
52
+
53
+ // Define schema with file field
54
+ api.addScope('images', {
55
+ schema: {
56
+ title: { type: 'string', required: true },
57
+ file: {
58
+ type: 'file',
59
+ storage: storage,
60
+ accepts: ['image/*'],
61
+ maxSize: '10mb'
62
+ }
63
+ }
64
+ });
65
+ ```
66
+
67
+ ## Schema Configuration
68
+
69
+ File fields are defined in your scope schema with `type: 'file'`:
70
+
71
+ ```javascript
72
+ api.addScope('documents', {
73
+ schema: {
74
+ // Regular fields
75
+ title: { type: 'string', required: true },
76
+ description: { type: 'string' },
77
+
78
+ // File field
79
+ attachment: {
80
+ type: 'file',
81
+ storage: myStorage, // Required: storage adapter instance
82
+ accepts: ['*'], // Optional: accepted mime types (default: ['*'])
83
+ maxSize: '50mb', // Optional: max file size
84
+ required: false // Optional: is field required? (default: false)
85
+ }
86
+ }
87
+ });
88
+ ```
89
+
90
+ ### File Field Options
91
+
92
+ - **type**: Must be `'file'`
93
+ - **storage**: Storage adapter instance (required)
94
+ - **accepts**: Array of accepted MIME types
95
+ - `['*']` - Accept any file type (default)
96
+ - `['image/*']` - Accept any image
97
+ - `['image/jpeg', 'image/png']` - Accept specific types
98
+ - `['application/pdf', 'text/*']` - Mix specific and wildcard
99
+ - **maxSize**: Maximum file size
100
+ - `'10mb'`, `'1.5gb'`, `'500kb'` - Human readable format
101
+ - Number of bytes also supported
102
+ - **required**: Whether the file is required
103
+
104
+ ## Storage Adapters
105
+
106
+ Storage adapters handle where and how files are saved. The library includes two built-in adapters.
107
+
108
+ ### Storage Adapter Comparison
109
+
110
+ | Feature | LocalStorage | S3Storage |
111
+ |---------|-------------|-----------|
112
+ | **Production Ready** | ✅ Yes | ⚠️ Mock only |
113
+ | **Filename Strategies** | 4 (hash, timestamp, original, custom) | 1 (hash only) |
114
+ | **Path Traversal Protection** | ✅ Full | ✅ N/A |
115
+ | **Extension Whitelist** | ✅ Yes | ❌ No |
116
+ | **Duplicate Handling** | ✅ Yes | ✅ Automatic |
117
+ | **Custom Naming** | ✅ Yes | ❌ No |
118
+ | **Best For** | Local file storage | Cloud storage |
119
+
120
+ ### S3Storage
121
+
122
+ Saves files to Amazon S3 or S3-compatible storage:
123
+
124
+ ```javascript
125
+ import { S3Storage } from 'jsonrestapi/plugins/storage/s3-storage.js';
126
+
127
+ const s3Storage = new S3Storage({
128
+ bucket: 'my-uploads', // S3 bucket name (required)
129
+ region: 'us-east-1', // AWS region (default: 'us-east-1')
130
+ prefix: 'uploads/', // Path prefix in bucket (default: '')
131
+ acl: 'public-read', // Access control (default: 'public-read')
132
+ mockMode: false // Use mock mode? (default: true)
133
+ });
134
+ ```
135
+
136
+ **Filename Handling:**
137
+ - Always generates random hash + extension (e.g., `uploads/a7f8d9e2b4c6e1f3.jpg`)
138
+ - Original filenames are never used for security
139
+
140
+ **Note**: The included S3Storage is a mock implementation for demonstration. For production use, you'll need to implement the actual AWS SDK calls.
141
+
142
+ ### LocalStorage
143
+
144
+ Saves files to the local filesystem with secure filename handling:
145
+
146
+ ```javascript
147
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
148
+
149
+ const localStorage = new LocalStorage({
150
+ directory: './uploads', // Where to save files
151
+ baseUrl: '/uploads', // Public URL prefix
152
+ nameStrategy: 'hash', // Filename strategy (see below)
153
+ preserveExtension: true, // Keep file extensions? (default: true)
154
+ allowedExtensions: ['.jpg', '.png'], // Extension whitelist (optional)
155
+ maxFilenameLength: 255, // Max filename length
156
+ nameGenerator: async (file) => {...} // Custom name generator (optional)
157
+ });
158
+ ```
159
+
160
+ **Filename Strategies:**
161
+
162
+ 1. **`'hash'`** (default) - Cryptographically secure random names
163
+ ```javascript
164
+ nameStrategy: 'hash'
165
+ // Result: "a7f8d9e2b4c6e1f3.jpg"
166
+ ```
167
+
168
+ 2. **`'timestamp'`** - Timestamp with random suffix (sortable)
169
+ ```javascript
170
+ nameStrategy: 'timestamp'
171
+ // Result: "1672531200000_a8f9.pdf"
172
+ ```
173
+
174
+ 3. **`'original'`** - Sanitized original filename (user-friendly)
175
+ ```javascript
176
+ nameStrategy: 'original'
177
+ // "My Photo!.jpg" → "My_Photo_.jpg"
178
+ // Duplicates → "My_Photo_1.jpg", "My_Photo_2.jpg"
179
+ ```
180
+
181
+ 4. **`'custom'`** - Your own naming logic
182
+ ```javascript
183
+ nameStrategy: 'custom',
184
+ nameGenerator: async (file) => {
185
+ const userId = file.metadata?.userId || 'anonymous';
186
+ return `user_${userId}_${Date.now()}`;
187
+ }
188
+ // Result: "user_12345_1672531200000.jpg"
189
+ ```
190
+
191
+ **Security Features:**
192
+ - Path traversal protection (removes `..` and `/`)
193
+ - Control character filtering
194
+ - Extension validation against whitelist
195
+ - Automatic duplicate handling
196
+ - MIME type to extension mapping
197
+
198
+ ### Custom Storage Adapters
199
+
200
+ Create your own storage adapter by implementing the required interface:
201
+
202
+ ```javascript
203
+ class MyCustomStorage {
204
+ async upload(file) {
205
+ // file object contains:
206
+ // - filename: original filename
207
+ // - mimetype: MIME type
208
+ // - size: size in bytes
209
+ // - data: Buffer with file contents
210
+ // - filepath: temp file path (if using formidable)
211
+ // - cleanup: async function to cleanup temp files
212
+
213
+ // Save the file somewhere
214
+ const url = await saveFileSomewhere(file);
215
+
216
+ // Return the public URL
217
+ return url;
218
+ }
219
+
220
+ async delete(url) {
221
+ // Optional: implement file deletion
222
+ await deleteFileSomewhere(url);
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Protocol Configuration
228
+
229
+ Different protocols have different configuration options for file parsing.
230
+
231
+ ### ExpressPlugin Configuration
232
+
233
+ The Express plugin supports multiple file parsers:
234
+
235
+ ```javascript
236
+ api.use(ExpressPlugin, {
237
+ // Choose parser: 'busboy', 'formidable', or a function
238
+ fileParser: 'busboy',
239
+
240
+ // Parser-specific options
241
+ fileParserOptions: {
242
+ // For busboy
243
+ limits: {
244
+ fileSize: 10 * 1024 * 1024, // 10MB max file size
245
+ files: 5, // Max 5 files per request
246
+ fields: 20, // Max 20 non-file fields
247
+ parts: 25 // Max 25 total parts
248
+ }
249
+ },
250
+
251
+ // Or use formidable
252
+ // fileParser: 'formidable',
253
+ // fileParserOptions: {
254
+ // uploadDir: './temp', // Temp directory
255
+ // keepExtensions: true, // Keep file extensions
256
+ // maxFileSize: 200 * 1024 * 1024 // 200MB max
257
+ // }
258
+
259
+ // Disable file uploads entirely
260
+ // enableFileUploads: false
261
+ });
262
+ ```
263
+
264
+ #### Using Express Middleware
265
+
266
+ For advanced use cases, you can use Express middleware for file handling:
267
+
268
+ ```javascript
269
+ import multer from 'multer';
270
+
271
+ const upload = multer({ dest: 'uploads/' });
272
+
273
+ api.use(ExpressPlugin, {
274
+ middleware: {
275
+ beforeScope: {
276
+ // Add multer to specific scope
277
+ images: [upload.single('file')],
278
+
279
+ // Multiple files for another scope
280
+ gallery: [upload.array('photos', 10)]
281
+ }
282
+ },
283
+
284
+ // Disable built-in file handling since we're using multer
285
+ enableFileUploads: false
286
+ });
287
+ ```
288
+
289
+ ### HttpPlugin Configuration
290
+
291
+ The HTTP plugin has similar configuration:
292
+
293
+ ```javascript
294
+ api.use(HttpPlugin, {
295
+ // Choose parser: 'busboy', 'formidable', or a function
296
+ fileParser: 'formidable',
297
+
298
+ fileParserOptions: {
299
+ uploadDir: './uploads/temp',
300
+ keepExtensions: true,
301
+ maxFileSize: 100 * 1024 * 1024 // 100MB
302
+ },
303
+
304
+ // Other HTTP options
305
+ port: 3000,
306
+ basePath: '/api'
307
+ });
308
+ ```
309
+
310
+ ### Custom File Parsers
311
+
312
+ You can provide a custom file parser:
313
+
314
+ ```javascript
315
+ api.use(HttpPlugin, {
316
+ fileParser: (options) => ({
317
+ name: 'my-custom-parser',
318
+ detect: (params) => {
319
+ // Return true if this parser can handle the request
320
+ const req = params._httpReq;
321
+ return req.headers['content-type']?.includes('multipart/form-data');
322
+ },
323
+ parse: async (params) => {
324
+ // Parse the request and return { fields, files }
325
+ const req = params._httpReq;
326
+ const { fields, files } = await myCustomParser(req);
327
+ return { fields, files };
328
+ }
329
+ })
330
+ });
331
+ ```
332
+
333
+ ## File Validation
334
+
335
+ The FileHandlingPlugin automatically validates files based on schema configuration.
336
+
337
+ ### MIME Type Validation
338
+
339
+ ```javascript
340
+ // Accept only images
341
+ file: {
342
+ type: 'file',
343
+ storage: localStorage,
344
+ accepts: ['image/*']
345
+ }
346
+
347
+ // Accept specific types
348
+ document: {
349
+ type: 'file',
350
+ storage: s3Storage,
351
+ accepts: ['application/pdf', 'application/msword', 'text/plain']
352
+ }
353
+
354
+ // Accept anything (not recommended)
355
+ attachment: {
356
+ type: 'file',
357
+ storage: localStorage,
358
+ accepts: ['*']
359
+ }
360
+ ```
361
+
362
+ ### Size Validation
363
+
364
+ ```javascript
365
+ // Human-readable format
366
+ avatar: {
367
+ type: 'file',
368
+ storage: localStorage,
369
+ maxSize: '5mb'
370
+ }
371
+
372
+ // Supports: b, kb, mb, gb
373
+ largeFile: {
374
+ type: 'file',
375
+ storage: s3Storage,
376
+ maxSize: '1.5gb'
377
+ }
378
+ ```
379
+
380
+ ### Required Files
381
+
382
+ ```javascript
383
+ // This file must be provided
384
+ document: {
385
+ type: 'file',
386
+ storage: s3Storage,
387
+ required: true
388
+ }
389
+ ```
390
+
391
+ ### Validation Errors
392
+
393
+ When validation fails, you'll get appropriate error responses:
394
+
395
+ ```json
396
+ {
397
+ "errors": [{
398
+ "status": "422",
399
+ "title": "Validation Error",
400
+ "detail": "Invalid file type for field 'avatar'",
401
+ "source": {
402
+ "pointer": "/data/attributes/avatar"
403
+ }
404
+ }]
405
+ }
406
+ ```
407
+
408
+ ## Complete Examples
409
+
410
+ ### Basic Image Upload
411
+
412
+ ```javascript
413
+ import { Api } from 'hooked-api';
414
+ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'jsonrestapi';
415
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
416
+ import express from 'express';
417
+
418
+ // Setup
419
+ const api = new Api({ name: 'image-api', version: '1.0.0' });
420
+ const storage = new LocalStorage({
421
+ directory: './uploads/images',
422
+ baseUrl: 'http://localhost:3000/uploads/images'
423
+ });
424
+
425
+ // Plugins
426
+ api.use(RestApiPlugin);
427
+ api.use(FileHandlingPlugin);
428
+ api.use(ExpressPlugin);
429
+
430
+ // Schema
431
+ api.addScope('photos', {
432
+ schema: {
433
+ caption: { type: 'string', required: true },
434
+ image: {
435
+ type: 'file',
436
+ storage: storage,
437
+ accepts: ['image/jpeg', 'image/png'],
438
+ maxSize: '10mb',
439
+ required: true
440
+ }
441
+ }
442
+ });
443
+
444
+ // Express app
445
+ const app = express();
446
+ app.use('/uploads', express.static('./uploads'));
447
+ api.express.mount(app);
448
+
449
+ // HTML form for testing
450
+ app.get('/', (req, res) => {
451
+ res.send(`
452
+ <form action="/api/photos" method="POST" enctype="multipart/form-data">
453
+ <input name="caption" placeholder="Caption" required>
454
+ <input name="image" type="file" accept="image/*" required>
455
+ <button type="submit">Upload Photo</button>
456
+ </form>
457
+ `);
458
+ });
459
+
460
+ app.listen(3000);
461
+ ```
462
+
463
+ ### Multiple Storage Backends
464
+
465
+ ```javascript
466
+ // Different storage for different fields
467
+ api.addScope('articles', {
468
+ schema: {
469
+ title: { type: 'string', required: true },
470
+ content: { type: 'string', required: true },
471
+
472
+ // Featured image goes to S3
473
+ featuredImage: {
474
+ type: 'file',
475
+ storage: s3Storage,
476
+ accepts: ['image/*'],
477
+ maxSize: '20mb'
478
+ },
479
+
480
+ // Attachments stay local
481
+ attachment: {
482
+ type: 'file',
483
+ storage: localStorage,
484
+ accepts: ['application/pdf', 'application/zip'],
485
+ maxSize: '100mb'
486
+ }
487
+ }
488
+ });
489
+ ```
490
+
491
+ ### Filename Handling Examples
492
+
493
+ Different strategies for different use cases:
494
+
495
+ ```javascript
496
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
497
+
498
+ // User avatars - use hash for security and deduplication
499
+ const avatarStorage = new LocalStorage({
500
+ directory: './uploads/avatars',
501
+ baseUrl: '/uploads/avatars',
502
+ nameStrategy: 'hash'
503
+ });
504
+
505
+ // Documents - use timestamp for sorting
506
+ const documentStorage = new LocalStorage({
507
+ directory: './uploads/documents',
508
+ baseUrl: '/uploads/documents',
509
+ nameStrategy: 'timestamp'
510
+ });
511
+
512
+ // User downloads - preserve original names
513
+ const downloadStorage = new LocalStorage({
514
+ directory: './uploads/downloads',
515
+ baseUrl: '/uploads/downloads',
516
+ nameStrategy: 'original',
517
+ maxFilenameLength: 100
518
+ });
519
+
520
+ // High security - no extensions
521
+ const secureStorage = new LocalStorage({
522
+ directory: './uploads/secure',
523
+ baseUrl: '/uploads/secure',
524
+ nameStrategy: 'hash',
525
+ preserveExtension: false, // All files saved as .bin
526
+ allowedExtensions: ['.pdf', '.doc', '.docx'] // Still validates input
527
+ });
528
+
529
+ // Organized by date
530
+ const organizedStorage = new LocalStorage({
531
+ directory: './uploads',
532
+ baseUrl: '/uploads',
533
+ nameStrategy: 'custom',
534
+ nameGenerator: async (file) => {
535
+ const date = new Date();
536
+ const dateDir = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}`;
537
+ return `${dateDir}/${crypto.randomBytes(16).toString('hex')}`;
538
+ }
539
+ });
540
+ // Saves as: "2024/01/a7f8d9e2b4c6e1f3.jpg"
541
+ ```
542
+
543
+ ### Using cURL
544
+
545
+ ```bash
546
+ # Upload with cURL
547
+ curl -X POST http://localhost:3000/api/photos \
548
+ -F "caption=Beautiful sunset" \
549
+ -F "image=@/path/to/sunset.jpg"
550
+
551
+ # Multiple files
552
+ curl -X POST http://localhost:3000/api/articles \
553
+ -F "title=My Article" \
554
+ -F "content=Article content here" \
555
+ -F "featuredImage=@/path/to/hero.jpg" \
556
+ -F "attachment=@/path/to/document.pdf"
557
+ ```
558
+
559
+ ### Programmatic Upload
560
+
561
+ ```javascript
562
+ // Using fetch with FormData
563
+ const formData = new FormData();
564
+ formData.append('caption', 'My photo');
565
+ formData.append('image', fileInput.files[0]);
566
+
567
+ const response = await fetch('/api/photos', {
568
+ method: 'POST',
569
+ body: formData
570
+ });
571
+
572
+ const result = await response.json();
573
+ console.log('Uploaded:', result.data.attributes.image); // URL of uploaded file
574
+ ```
575
+
576
+ ## Troubleshooting
577
+
578
+ ### Common Issues
579
+
580
+ #### 1. "Busboy not available, file uploads disabled"
581
+
582
+ Install the peer dependency:
583
+ ```bash
584
+ npm install busboy
585
+ ```
586
+
587
+ #### 2. "No storage configured for file field"
588
+
589
+ Make sure you've set the storage property:
590
+ ```javascript
591
+ file: {
592
+ type: 'file',
593
+ storage: myStorage // This is required!
594
+ }
595
+ ```
596
+
597
+ #### 3. Files not being detected
598
+
599
+ Check that:
600
+ 1. The FileHandlingPlugin is loaded AFTER RestApiPlugin
601
+ 2. Your protocol plugin has file uploads enabled
602
+ 3. The request has proper multipart headers
603
+ 4. You're using the correct form encoding
604
+
605
+ ```html
606
+ <!-- HTML forms need this -->
607
+ <form enctype="multipart/form-data">
608
+ ```
609
+
610
+ #### 4. "File too large" errors
611
+
612
+ Check both:
613
+ 1. Schema `maxSize` configuration
614
+ 2. Parser limits in plugin options
615
+
616
+ ```javascript
617
+ // Both limits apply!
618
+ api.use(ExpressPlugin, {
619
+ fileParserOptions: {
620
+ limits: { fileSize: 10 * 1024 * 1024 } // 10MB parser limit
621
+ }
622
+ });
623
+
624
+ api.addScope('images', {
625
+ schema: {
626
+ photo: {
627
+ type: 'file',
628
+ maxSize: '5mb' // 5MB schema limit (lower wins)
629
+ }
630
+ }
631
+ });
632
+ ```
633
+
634
+ ### Debug Mode
635
+
636
+ Enable debug logging to see what's happening:
637
+
638
+ ```javascript
639
+ const api = new Api({
640
+ name: 'my-api',
641
+ version: '1.0.0',
642
+ logLevel: 'debug' // or 'trace' for more detail
643
+ });
644
+ ```
645
+
646
+ ### Testing File Uploads
647
+
648
+ Use the included example to test your setup:
649
+
650
+ ```javascript
651
+ // Run the example
652
+ node ./node_modules/jsonrestapi/examples/file-upload-example.js
653
+ ```
654
+
655
+ Then visit http://localhost:3000 to see the test forms.
656
+
657
+ ## Best Practices
658
+
659
+ 1. **Always validate file types** - Don't use `accepts: ['*']` in production
660
+ 2. **Set reasonable size limits** - Prevent abuse and server overload
661
+ 3. **Use appropriate storage** - Local for small files, S3 for large/many files
662
+ 4. **Clean up temp files** - Storage adapters should handle cleanup
663
+ 5. **Serve files separately** - Don't serve uploaded files through your API
664
+ 6. **Validate file contents** - Consider virus scanning for user uploads
665
+ 7. **Use CDN for images** - Serve uploaded images through a CDN in production
666
+
667
+ ## Security Considerations
668
+
669
+ ### Filename Security
670
+
671
+ 1. **Never trust user filenames** - Always sanitize or generate new names
672
+ ```javascript
673
+ // BAD - Direct use of user filename
674
+ const filename = file.originalname;
675
+
676
+ // GOOD - Generate secure name
677
+ const filename = crypto.randomBytes(16).toString('hex') + path.extname(file.originalname);
678
+ ```
679
+
680
+ 2. **Prevent path traversal** - Remove dangerous characters
681
+ ```javascript
682
+ // Dangerous filenames to watch for:
683
+ // "../../../etc/passwd"
684
+ // "..\\..\\windows\\system32\\config\\sam"
685
+ // "uploads/../../../index.js"
686
+
687
+ // LocalStorage handles this automatically
688
+ ```
689
+
690
+ 3. **Extension validation** - Whitelist allowed extensions
691
+ ```javascript
692
+ // Use LocalStorage with whitelist
693
+ const storage = new LocalStorage({
694
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf']
695
+ });
696
+ ```
697
+
698
+ 4. **Consider removing extensions entirely** for sensitive files
699
+ ```javascript
700
+ const highSecurityStorage = new LocalStorage({
701
+ nameStrategy: 'hash',
702
+ preserveExtension: false // All files become .bin
703
+ });
704
+ ```
705
+
706
+ ### General File Security
707
+
708
+ 1. **Validate MIME types** - But remember they can be spoofed
709
+ 2. **Check file contents** - Use libraries like `file-type` for verification
710
+ 3. **Limit upload sizes** - Prevent denial of service
711
+ 4. **Store files outside web root** - Prevent direct execution
712
+ 5. **Use virus scanning** - For user-uploaded content
713
+ 6. **Set proper permissions** - Uploaded files shouldn't be executable
714
+ 7. **Serve files with proper headers** - Use Content-Disposition for downloads
715
+
716
+ ## Next Steps
717
+
718
+ - Implement production S3 storage with actual AWS SDK
719
+ - Add image processing (thumbnails, resizing)
720
+ - Implement virus scanning for uploads
721
+ - Add progress tracking for large files
722
+ - Create a chunked upload system for very large files