te.js 2.0.3 → 2.1.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 +197 -187
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -0
- package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
- package/auto-docs/{llm → docs-llm}/provider.js +132 -187
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -489
- package/docs/auto-docs.md +216 -215
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -233
- package/docs/database.md +390 -391
- package/docs/error-handling.md +438 -417
- package/docs/file-uploads.md +333 -334
- package/docs/getting-started.md +214 -215
- package/docs/middleware.md +355 -356
- package/docs/rate-limiting.md +393 -394
- package/docs/routing.md +302 -302
- package/package.json +62 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -356
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -0
- package/server/errors/llm-error-service.js +140 -0
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -113
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -363
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -0
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
- package/auto-docs/llm/index.js +0 -6
- package/auto-docs/llm/parse.js +0 -88
package/docs/file-uploads.md
CHANGED
|
@@ -1,334 +1,333 @@
|
|
|
1
|
-
# File Uploads
|
|
2
|
-
|
|
3
|
-
Tejas provides a built-in `TejFileUploader` class for handling file uploads with ease.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```javascript
|
|
8
|
-
import { Target, TejFileUploader } from 'te.js';
|
|
9
|
-
|
|
10
|
-
const upload = new TejFileUploader({
|
|
11
|
-
destination: 'uploads/',
|
|
12
|
-
maxFileSize: 5 * 1024 * 1024 // 5MB
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const target = new Target('/files');
|
|
16
|
-
|
|
17
|
-
target.register('/upload', upload.file('avatar'), (ammo) => {
|
|
18
|
-
ammo.fire({ file: ammo.payload.avatar });
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Configuration
|
|
23
|
-
|
|
24
|
-
```javascript
|
|
25
|
-
const upload = new TejFileUploader({
|
|
26
|
-
destination: 'public/uploads', // Where to save files
|
|
27
|
-
name: 'custom-name', // Optional: custom filename
|
|
28
|
-
maxFileSize: 10 * 1024 * 1024 // Max file size in bytes (10MB)
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Options
|
|
33
|
-
|
|
34
|
-
| Option | Type | Description |
|
|
35
|
-
|--------|------|-------------|
|
|
36
|
-
| `destination` | string | Directory to save uploaded files |
|
|
37
|
-
| `name` | string | Optional custom filename |
|
|
38
|
-
| `maxFileSize` | number | Maximum file size in bytes |
|
|
39
|
-
|
|
40
|
-
## Single File Upload
|
|
41
|
-
|
|
42
|
-
Use `upload.file()` for single file uploads:
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
// Expects a file field named 'avatar'
|
|
46
|
-
target.register('/avatar', upload.file('avatar'), (ammo) => {
|
|
47
|
-
const file = ammo.payload.avatar;
|
|
48
|
-
|
|
49
|
-
ammo.fire({
|
|
50
|
-
filename: file.filename,
|
|
51
|
-
path: file.path.relative,
|
|
52
|
-
mimetype: file.mimetype,
|
|
53
|
-
size: file.size
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### File Object Structure
|
|
59
|
-
|
|
60
|
-
When a file is uploaded, `ammo.payload[fieldName]` contains:
|
|
61
|
-
|
|
62
|
-
```javascript
|
|
63
|
-
{
|
|
64
|
-
filename: 'photo.jpg', // Original filename
|
|
65
|
-
extension: 'jpg', // File extension
|
|
66
|
-
path: {
|
|
67
|
-
absolute: '/var/www/uploads/photo.jpg', // Absolute path on disk
|
|
68
|
-
relative: '\\uploads\\photo.jpg' // Relative to cwd
|
|
69
|
-
},
|
|
70
|
-
mimetype: 'image/jpeg', // MIME type
|
|
71
|
-
size: { // From the filesize library
|
|
72
|
-
value: 245, // Numeric value
|
|
73
|
-
symbol: 'KB' // Unit (B, KB, MB, etc.)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The `size` object is produced by the [filesize](https://www.npmjs.com/package/filesize) library. Use `${size.value} ${size.symbol}` for display (e.g. "245 KB").
|
|
79
|
-
|
|
80
|
-
## Multiple File Upload
|
|
81
|
-
|
|
82
|
-
Use `upload.files()` for multiple files:
|
|
83
|
-
|
|
84
|
-
```javascript
|
|
85
|
-
// Expects files in 'photos' and 'documents' fields
|
|
86
|
-
target.register('/documents', upload.files('photos', 'documents'), (ammo) => {
|
|
87
|
-
const { photos, documents } = ammo.payload;
|
|
88
|
-
|
|
89
|
-
ammo.fire({
|
|
90
|
-
photos: photos || [], // Array of file objects
|
|
91
|
-
documents: documents || [] // Array of file objects
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Multiple Files Response
|
|
97
|
-
|
|
98
|
-
Each field contains an array of file objects:
|
|
99
|
-
|
|
100
|
-
```javascript
|
|
101
|
-
{
|
|
102
|
-
photos: [
|
|
103
|
-
{ filename: 'photo1.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} },
|
|
104
|
-
{ filename: 'photo2.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} }
|
|
105
|
-
],
|
|
106
|
-
documents: [
|
|
107
|
-
{ filename: 'doc.pdf', path: {...}, mimetype: 'application/pdf', size: {...} }
|
|
108
|
-
]
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Mixed Fields (Files + Data)
|
|
113
|
-
|
|
114
|
-
File uploads can include regular form fields:
|
|
115
|
-
|
|
116
|
-
```javascript
|
|
117
|
-
target.register('/profile', upload.file('avatar'), (ammo) => {
|
|
118
|
-
const { avatar, name, bio } = ammo.payload;
|
|
119
|
-
|
|
120
|
-
ammo.fire({
|
|
121
|
-
name, // Regular form field
|
|
122
|
-
bio, // Regular form field
|
|
123
|
-
avatar: avatar // File object
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## File Size Limits
|
|
129
|
-
|
|
130
|
-
When a file exceeds `maxFileSize`, a `413 Payload Too Large` error is thrown automatically. The error message includes the human-readable limit (e.g. "File size exceeds 2 MB"):
|
|
131
|
-
|
|
132
|
-
```javascript
|
|
133
|
-
const upload = new TejFileUploader({
|
|
134
|
-
destination: 'uploads/',
|
|
135
|
-
maxFileSize: 2 * 1024 * 1024 // 2MB limit
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
target.register('/upload', upload.file('file'), (ammo) => {
|
|
139
|
-
// If file > 2MB, this handler never runs
|
|
140
|
-
// Client receives: 413 "File size exceeds 2 MB"
|
|
141
|
-
ammo.fire({ success: true });
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Note that the overall request body is also subject to the global `body.max_size` limit (default 10 MB). See [Configuration](./configuration.md).
|
|
146
|
-
|
|
147
|
-
## Client-Side Examples
|
|
148
|
-
|
|
149
|
-
### HTML Form
|
|
150
|
-
|
|
151
|
-
```html
|
|
152
|
-
<form action="/files/upload" method="POST" enctype="multipart/form-data">
|
|
153
|
-
<input type="file" name="avatar" />
|
|
154
|
-
<input type="text" name="username" />
|
|
155
|
-
<button type="submit">Upload</button>
|
|
156
|
-
</form>
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### JavaScript (Fetch)
|
|
160
|
-
|
|
161
|
-
```javascript
|
|
162
|
-
const formData = new FormData();
|
|
163
|
-
formData.append('avatar', fileInput.files[0]);
|
|
164
|
-
formData.append('username', 'john');
|
|
165
|
-
|
|
166
|
-
const response = await fetch('/files/upload', {
|
|
167
|
-
method: 'POST',
|
|
168
|
-
body: formData
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### JavaScript (Multiple Files)
|
|
173
|
-
|
|
174
|
-
```javascript
|
|
175
|
-
const formData = new FormData();
|
|
176
|
-
|
|
177
|
-
// Add multiple files to same field
|
|
178
|
-
for (const file of fileInput.files) {
|
|
179
|
-
formData.append('photos', file);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const response = await fetch('/files/documents', {
|
|
183
|
-
method: 'POST',
|
|
184
|
-
body: formData
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Complete Example
|
|
189
|
-
|
|
190
|
-
```javascript
|
|
191
|
-
import { Target, TejFileUploader, TejError } from 'te.js';
|
|
192
|
-
import fs from 'fs';
|
|
193
|
-
import path from 'path';
|
|
194
|
-
|
|
195
|
-
const target = new Target('/api/files');
|
|
196
|
-
|
|
197
|
-
// Configure uploader
|
|
198
|
-
const imageUpload = new TejFileUploader({
|
|
199
|
-
destination: 'public/images',
|
|
200
|
-
maxFileSize: 5 * 1024 * 1024 // 5MB
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const documentUpload = new TejFileUploader({
|
|
204
|
-
destination: 'private/documents',
|
|
205
|
-
maxFileSize: 20 * 1024 * 1024 // 20MB
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Upload profile image
|
|
209
|
-
target.register('/profile-image', imageUpload.file('image'), (ammo) => {
|
|
210
|
-
if (!ammo.POST) return ammo.notAllowed();
|
|
211
|
-
|
|
212
|
-
const { image } = ammo.payload;
|
|
213
|
-
|
|
214
|
-
if (!image) {
|
|
215
|
-
throw new TejError(400, 'No image provided');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Validate image type
|
|
219
|
-
if (!image.mimetype.startsWith('image/')) {
|
|
220
|
-
// Delete uploaded file
|
|
221
|
-
fs.unlinkSync(image.path.absolute);
|
|
222
|
-
throw new TejError(400, 'File must be an image');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
ammo.fire({
|
|
226
|
-
message: 'Profile image uploaded',
|
|
227
|
-
url: `/images/${image.filename}`
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Upload multiple documents
|
|
232
|
-
target.register('/documents', documentUpload.files('files'), (ammo) => {
|
|
233
|
-
if (!ammo.POST) return ammo.notAllowed();
|
|
234
|
-
|
|
235
|
-
const { files } = ammo.payload;
|
|
236
|
-
|
|
237
|
-
if (!files || files.length === 0) {
|
|
238
|
-
throw new TejError(400, 'No files provided');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
ammo.fire({
|
|
242
|
-
message: `${files.length} files uploaded`,
|
|
243
|
-
files: files.map(f => ({
|
|
244
|
-
name: f.filename,
|
|
245
|
-
size: `${f.size.value} ${f.size.symbol}`
|
|
246
|
-
}))
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Delete a file
|
|
251
|
-
target.register('/delete/:filename', (ammo) => {
|
|
252
|
-
if (!ammo.DELETE) return ammo.notAllowed();
|
|
253
|
-
|
|
254
|
-
const { filename } = ammo.payload;
|
|
255
|
-
const filepath = path.join('public/images', filename);
|
|
256
|
-
|
|
257
|
-
if (!fs.existsSync(filepath)) {
|
|
258
|
-
throw new TejError(404, 'File not found');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
fs.unlinkSync(filepath);
|
|
262
|
-
ammo.fire({ message: 'File deleted' });
|
|
263
|
-
});
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Validation Middleware
|
|
267
|
-
|
|
268
|
-
Create reusable validation middleware:
|
|
269
|
-
|
|
270
|
-
```javascript
|
|
271
|
-
// middleware/validate-image.js
|
|
272
|
-
import { TejError } from 'te.js';
|
|
273
|
-
import fs from 'fs';
|
|
274
|
-
|
|
275
|
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
276
|
-
|
|
277
|
-
export const validateImage = (fieldName) => (ammo, next) => {
|
|
278
|
-
const file = ammo.payload[fieldName];
|
|
279
|
-
|
|
280
|
-
if (!file) {
|
|
281
|
-
throw new TejError(400, `${fieldName} is required`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (!allowedTypes.includes(file.mimetype)) {
|
|
285
|
-
fs.unlinkSync(file.path.absolute);
|
|
286
|
-
throw new TejError(400, 'Only JPEG, PNG, GIF, and WebP images are allowed');
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
next();
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// Usage
|
|
293
|
-
target.register('/avatar',
|
|
294
|
-
upload.file('avatar'),
|
|
295
|
-
validateImage('avatar'),
|
|
296
|
-
(ammo) => {
|
|
297
|
-
ammo.fire({ success: true });
|
|
298
|
-
}
|
|
299
|
-
);
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## Serving Uploaded Files
|
|
303
|
-
|
|
304
|
-
Tejas doesn't include a static file server, but you can serve files manually:
|
|
305
|
-
|
|
306
|
-
```javascript
|
|
307
|
-
import fs from 'fs';
|
|
308
|
-
import path from 'path';
|
|
309
|
-
import mime from 'mime';
|
|
310
|
-
|
|
311
|
-
target.register('/images/:filename', (ammo) => {
|
|
312
|
-
const { filename } = ammo.payload;
|
|
313
|
-
const filepath = path.join('public/images', filename);
|
|
314
|
-
|
|
315
|
-
if (!fs.existsSync(filepath)) {
|
|
316
|
-
return ammo.notFound();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const file = fs.readFileSync(filepath);
|
|
320
|
-
const contentType = mime.getType(filepath) || 'application/octet-stream';
|
|
321
|
-
|
|
322
|
-
ammo.fire(200, file, contentType);
|
|
323
|
-
});
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Best Practices
|
|
327
|
-
|
|
328
|
-
1. **Validate file types** — Don't trust client-reported MIME types
|
|
329
|
-
2. **Set size limits** — Prevent disk exhaustion attacks
|
|
330
|
-
3. **Use unique filenames** — Avoid overwrites with UUID or timestamps
|
|
331
|
-
4. **Store outside web root** — For sensitive files, store in private directories
|
|
332
|
-
5. **Clean up on errors** — Delete uploaded files if validation fails
|
|
333
|
-
6. **Scan for malware** — For production systems, integrate virus scanning
|
|
334
|
-
|
|
1
|
+
# File Uploads
|
|
2
|
+
|
|
3
|
+
Tejas provides a built-in `TejFileUploader` class for handling file uploads with ease.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { Target, TejFileUploader } from 'te.js';
|
|
9
|
+
|
|
10
|
+
const upload = new TejFileUploader({
|
|
11
|
+
destination: 'uploads/',
|
|
12
|
+
maxFileSize: 5 * 1024 * 1024 // 5MB
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const target = new Target('/files');
|
|
16
|
+
|
|
17
|
+
target.register('/upload', upload.file('avatar'), (ammo) => {
|
|
18
|
+
ammo.fire({ file: ammo.payload.avatar });
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const upload = new TejFileUploader({
|
|
26
|
+
destination: 'public/uploads', // Where to save files
|
|
27
|
+
name: 'custom-name', // Optional: custom filename
|
|
28
|
+
maxFileSize: 10 * 1024 * 1024 // Max file size in bytes (10MB)
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Options
|
|
33
|
+
|
|
34
|
+
| Option | Type | Description |
|
|
35
|
+
|--------|------|-------------|
|
|
36
|
+
| `destination` | string | Directory to save uploaded files |
|
|
37
|
+
| `name` | string | Optional custom filename |
|
|
38
|
+
| `maxFileSize` | number | Maximum file size in bytes |
|
|
39
|
+
|
|
40
|
+
## Single File Upload
|
|
41
|
+
|
|
42
|
+
Use `upload.file()` for single file uploads:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Expects a file field named 'avatar'
|
|
46
|
+
target.register('/avatar', upload.file('avatar'), (ammo) => {
|
|
47
|
+
const file = ammo.payload.avatar;
|
|
48
|
+
|
|
49
|
+
ammo.fire({
|
|
50
|
+
filename: file.filename,
|
|
51
|
+
path: file.path.relative,
|
|
52
|
+
mimetype: file.mimetype,
|
|
53
|
+
size: file.size
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### File Object Structure
|
|
59
|
+
|
|
60
|
+
When a file is uploaded, `ammo.payload[fieldName]` contains:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
{
|
|
64
|
+
filename: 'photo.jpg', // Original filename
|
|
65
|
+
extension: 'jpg', // File extension
|
|
66
|
+
path: {
|
|
67
|
+
absolute: '/var/www/uploads/photo.jpg', // Absolute path on disk
|
|
68
|
+
relative: '\\uploads\\photo.jpg' // Relative to cwd
|
|
69
|
+
},
|
|
70
|
+
mimetype: 'image/jpeg', // MIME type
|
|
71
|
+
size: { // From the filesize library
|
|
72
|
+
value: 245, // Numeric value
|
|
73
|
+
symbol: 'KB' // Unit (B, KB, MB, etc.)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `size` object is produced by the [filesize](https://www.npmjs.com/package/filesize) library. Use `${size.value} ${size.symbol}` for display (e.g. "245 KB").
|
|
79
|
+
|
|
80
|
+
## Multiple File Upload
|
|
81
|
+
|
|
82
|
+
Use `upload.files()` for multiple files:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Expects files in 'photos' and 'documents' fields
|
|
86
|
+
target.register('/documents', upload.files('photos', 'documents'), (ammo) => {
|
|
87
|
+
const { photos, documents } = ammo.payload;
|
|
88
|
+
|
|
89
|
+
ammo.fire({
|
|
90
|
+
photos: photos || [], // Array of file objects
|
|
91
|
+
documents: documents || [] // Array of file objects
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Multiple Files Response
|
|
97
|
+
|
|
98
|
+
Each field contains an array of file objects:
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
{
|
|
102
|
+
photos: [
|
|
103
|
+
{ filename: 'photo1.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} },
|
|
104
|
+
{ filename: 'photo2.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} }
|
|
105
|
+
],
|
|
106
|
+
documents: [
|
|
107
|
+
{ filename: 'doc.pdf', path: {...}, mimetype: 'application/pdf', size: {...} }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Mixed Fields (Files + Data)
|
|
113
|
+
|
|
114
|
+
File uploads can include regular form fields:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
target.register('/profile', upload.file('avatar'), (ammo) => {
|
|
118
|
+
const { avatar, name, bio } = ammo.payload;
|
|
119
|
+
|
|
120
|
+
ammo.fire({
|
|
121
|
+
name, // Regular form field
|
|
122
|
+
bio, // Regular form field
|
|
123
|
+
avatar: avatar // File object
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## File Size Limits
|
|
129
|
+
|
|
130
|
+
When a file exceeds `maxFileSize`, a `413 Payload Too Large` error is thrown automatically. The error message includes the human-readable limit (e.g. "File size exceeds 2 MB"):
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const upload = new TejFileUploader({
|
|
134
|
+
destination: 'uploads/',
|
|
135
|
+
maxFileSize: 2 * 1024 * 1024 // 2MB limit
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
target.register('/upload', upload.file('file'), (ammo) => {
|
|
139
|
+
// If file > 2MB, this handler never runs
|
|
140
|
+
// Client receives: 413 "File size exceeds 2 MB"
|
|
141
|
+
ammo.fire({ success: true });
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Note that the overall request body is also subject to the global `body.max_size` limit (default 10 MB). See [Configuration](./configuration.md).
|
|
146
|
+
|
|
147
|
+
## Client-Side Examples
|
|
148
|
+
|
|
149
|
+
### HTML Form
|
|
150
|
+
|
|
151
|
+
```html
|
|
152
|
+
<form action="/files/upload" method="POST" enctype="multipart/form-data">
|
|
153
|
+
<input type="file" name="avatar" />
|
|
154
|
+
<input type="text" name="username" />
|
|
155
|
+
<button type="submit">Upload</button>
|
|
156
|
+
</form>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### JavaScript (Fetch)
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const formData = new FormData();
|
|
163
|
+
formData.append('avatar', fileInput.files[0]);
|
|
164
|
+
formData.append('username', 'john');
|
|
165
|
+
|
|
166
|
+
const response = await fetch('/files/upload', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: formData
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### JavaScript (Multiple Files)
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
const formData = new FormData();
|
|
176
|
+
|
|
177
|
+
// Add multiple files to same field
|
|
178
|
+
for (const file of fileInput.files) {
|
|
179
|
+
formData.append('photos', file);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = await fetch('/files/documents', {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
body: formData
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Complete Example
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
import { Target, TejFileUploader, TejError } from 'te.js';
|
|
192
|
+
import fs from 'fs';
|
|
193
|
+
import path from 'path';
|
|
194
|
+
|
|
195
|
+
const target = new Target('/api/files');
|
|
196
|
+
|
|
197
|
+
// Configure uploader
|
|
198
|
+
const imageUpload = new TejFileUploader({
|
|
199
|
+
destination: 'public/images',
|
|
200
|
+
maxFileSize: 5 * 1024 * 1024 // 5MB
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const documentUpload = new TejFileUploader({
|
|
204
|
+
destination: 'private/documents',
|
|
205
|
+
maxFileSize: 20 * 1024 * 1024 // 20MB
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Upload profile image
|
|
209
|
+
target.register('/profile-image', imageUpload.file('image'), (ammo) => {
|
|
210
|
+
if (!ammo.POST) return ammo.notAllowed();
|
|
211
|
+
|
|
212
|
+
const { image } = ammo.payload;
|
|
213
|
+
|
|
214
|
+
if (!image) {
|
|
215
|
+
throw new TejError(400, 'No image provided');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate image type
|
|
219
|
+
if (!image.mimetype.startsWith('image/')) {
|
|
220
|
+
// Delete uploaded file
|
|
221
|
+
fs.unlinkSync(image.path.absolute);
|
|
222
|
+
throw new TejError(400, 'File must be an image');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ammo.fire({
|
|
226
|
+
message: 'Profile image uploaded',
|
|
227
|
+
url: `/images/${image.filename}`
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Upload multiple documents
|
|
232
|
+
target.register('/documents', documentUpload.files('files'), (ammo) => {
|
|
233
|
+
if (!ammo.POST) return ammo.notAllowed();
|
|
234
|
+
|
|
235
|
+
const { files } = ammo.payload;
|
|
236
|
+
|
|
237
|
+
if (!files || files.length === 0) {
|
|
238
|
+
throw new TejError(400, 'No files provided');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ammo.fire({
|
|
242
|
+
message: `${files.length} files uploaded`,
|
|
243
|
+
files: files.map(f => ({
|
|
244
|
+
name: f.filename,
|
|
245
|
+
size: `${f.size.value} ${f.size.symbol}`
|
|
246
|
+
}))
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Delete a file
|
|
251
|
+
target.register('/delete/:filename', (ammo) => {
|
|
252
|
+
if (!ammo.DELETE) return ammo.notAllowed();
|
|
253
|
+
|
|
254
|
+
const { filename } = ammo.payload;
|
|
255
|
+
const filepath = path.join('public/images', filename);
|
|
256
|
+
|
|
257
|
+
if (!fs.existsSync(filepath)) {
|
|
258
|
+
throw new TejError(404, 'File not found');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fs.unlinkSync(filepath);
|
|
262
|
+
ammo.fire({ message: 'File deleted' });
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Validation Middleware
|
|
267
|
+
|
|
268
|
+
Create reusable validation middleware:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// middleware/validate-image.js
|
|
272
|
+
import { TejError } from 'te.js';
|
|
273
|
+
import fs from 'fs';
|
|
274
|
+
|
|
275
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
276
|
+
|
|
277
|
+
export const validateImage = (fieldName) => (ammo, next) => {
|
|
278
|
+
const file = ammo.payload[fieldName];
|
|
279
|
+
|
|
280
|
+
if (!file) {
|
|
281
|
+
throw new TejError(400, `${fieldName} is required`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
285
|
+
fs.unlinkSync(file.path.absolute);
|
|
286
|
+
throw new TejError(400, 'Only JPEG, PNG, GIF, and WebP images are allowed');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
next();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Usage
|
|
293
|
+
target.register('/avatar',
|
|
294
|
+
upload.file('avatar'),
|
|
295
|
+
validateImage('avatar'),
|
|
296
|
+
(ammo) => {
|
|
297
|
+
ammo.fire({ success: true });
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Serving Uploaded Files
|
|
303
|
+
|
|
304
|
+
Tejas doesn't include a static file server, but you can serve files manually:
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
import fs from 'fs';
|
|
308
|
+
import path from 'path';
|
|
309
|
+
import mime from 'mime';
|
|
310
|
+
|
|
311
|
+
target.register('/images/:filename', (ammo) => {
|
|
312
|
+
const { filename } = ammo.payload;
|
|
313
|
+
const filepath = path.join('public/images', filename);
|
|
314
|
+
|
|
315
|
+
if (!fs.existsSync(filepath)) {
|
|
316
|
+
return ammo.notFound();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const file = fs.readFileSync(filepath);
|
|
320
|
+
const contentType = mime.getType(filepath) || 'application/octet-stream';
|
|
321
|
+
|
|
322
|
+
ammo.fire(200, file, contentType);
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Best Practices
|
|
327
|
+
|
|
328
|
+
1. **Validate file types** — Don't trust client-reported MIME types
|
|
329
|
+
2. **Set size limits** — Prevent disk exhaustion attacks
|
|
330
|
+
3. **Use unique filenames** — Avoid overwrites with UUID or timestamps
|
|
331
|
+
4. **Store outside web root** — For sensitive files, store in private directories
|
|
332
|
+
5. **Clean up on errors** — Delete uploaded files if validation fails
|
|
333
|
+
6. **Scan for malware** — For production systems, integrate virus scanning
|