aim_server_multipart
Multipart form data parser for the Aim framework. Handles file uploads and form data with security in mind.
Overview
aim_server_multipart provides secure and efficient parsing of multipart/form-data requests for the Aim framework. It's designed as a separate package to keep applications that don't need file uploads lightweight.
Features
- ๐ค File Upload Support - Handle single and multiple file uploads
- ๐ Security First - Automatic filename sanitization prevents path traversal attacks
- ๐ Size Limits - Configure per-file and total upload size limits
- ๐ฏ MIME Type Filtering - Allow only specific file types with wildcard support
- โก Stream Processing - Efficient memory usage with streaming
- ๐งช Well Tested - Comprehensive test suite with 39 tests
- ๐ RFC 7578 Compliant - Follows the multipart/form-data standard
Installation
Add aim_server_multipart to your pubspec.yaml:
dependencies:
aim_server: ^0.0.5
aim_server_multipart: ^0.0.1
Then run:
dart pub get
Usage
Basic File Upload
import 'package:aim_server/aim_server.dart';
import 'package:aim_server_multipart/aim_server_multipart.dart';
void main() async {
final app = Aim();
app.post('/upload', (c) async {
final form = await c.req.multipart();
final avatar = form.file('avatar');
if (avatar != null) {
// Save the file
await avatar.saveTo('uploads/${avatar.filename}');
return c.json({
'uploaded': true,
'filename': avatar.filename,
'size': avatar.size,
});
}
return c.json({'error': 'No file uploaded'}, 400);
});
await app.serve(port: 8080);
}
Multiple Files and Fields
app.post('/upload', (c) async {
final form = await c.req.multipart();
// Get text fields
final title = form.field('title'); // String?
final tags = form.fields('tags'); // List<String>
// Get multiple files
final images = form.files('images'); // List<UploadedFile>
for (final image in images) {
await image.saveTo('uploads/${image.filename}');
}
return c.json({
'title': title,
'tags': tags,
'uploaded': images.length,
});
});
With Validation
app.post('/upload', (c) async {
try {
final form = await c.req.multipart(
maxFileSize: 10 * 1024 * 1024, // 10MB per file
maxTotalSize: 50 * 1024 * 1024, // 50MB total
allowedMimeTypes: ['image/*'], // Only images
);
final avatar = form.file('avatar');
if (avatar != null) {
await avatar.saveTo('uploads/${avatar.filename}');
return c.json({'success': true});
}
return c.json({'error': 'No file uploaded'}, 400);
} on Exception catch (e) {
// File too large, wrong type, etc.
return c.json({'error': e.toString()}, 400);
}
});
Text File Processing
app.post('/upload-csv', (c) async {
final form = await c.req.multipart(
allowedMimeTypes: ['text/csv', 'text/plain'],
);
final csvFile = form.file('data');
if (csvFile != null) {
// Read as string
final csvContent = csvFile.asString();
final rows = csvContent.split('\n');
return c.json({
'rows': rows.length,
'preview': rows.take(5).toList(),
});
}
return c.json({'error': 'No file uploaded'}, 400);
});
API Reference
MultipartFormData
Represents parsed multipart form data.
Methods:
field(String name)- Get single text field โString?fields(String name)- Get multiple fields with same name โList<String>file(String name)- Get single file โUploadedFile?files(String name)- Get multiple files with same name โList<UploadedFile>has(String name)- Check if field/file exists โbool
UploadedFile
Represents an uploaded file.
Properties:
filename- Sanitized safe filename (String)originalFilename- Original filename from client (String?)contentType- MIME type (String)bytes- File contents (List<int>)size- File size in bytes (int)
Methods:
saveTo(String path)- Save file to diskasString([Encoding encoding])- Read file as string
Request Extension
extension MultipartRequest on Request {
Future<MultipartFormData> multipart({
int? maxFileSize,
int? maxTotalSize,
List<String>? allowedMimeTypes,
});
}
Parameters:
maxFileSize- Maximum size per file in bytesmaxTotalSize- Maximum total size in bytesallowedMimeTypes- List of allowed MIME types- Exact match:
['image/png', 'application/pdf'] - Wildcard:
['image/*', 'video/*']
- Exact match:
Security
Filename Sanitization
Uploaded filenames are automatically sanitized to prevent security issues:
- โ Path traversal attacks (
../../../etc/passwdโ safe filename) - โ Absolute paths (
/tmp/evil.shโ safe filename) - โ Dangerous characters removed
- โ Random unique filenames generated (collision-free)
- โ
Original filename preserved in
originalFilenameproperty
Example generated filename:
file_1234567890_abc123de.jpg
Size Limits
final form = await c.req.multipart(
maxFileSize: 10 * 1024 * 1024, // 10MB limit
);
Throws Exception if limit exceeded.
MIME Type Filtering
final form = await c.req.multipart(
allowedMimeTypes: [
'image/png',
'image/jpeg',
'image/*', // All image types
],
);
Throws Exception for disallowed types.
Error Handling
app.post('/upload', (c) async {
try {
final form = await c.req.multipart(
maxFileSize: 10 * 1024 * 1024,
allowedMimeTypes: ['image/*'],
);
// Process files...
} on FormatException catch (e) {
// Invalid Content-Type, missing name parameter, etc.
return c.json({'error': 'Invalid request: ${e.message}'}, 400);
} on Exception catch (e) {
// File too large, wrong MIME type, etc.
return c.json({'error': e.toString()}, 400);
}
});
Examples
See the example directory for complete working examples.
Contributing
Contributions are welcome! Please see the main repository for contribution guidelines.
License
See the LICENSE file in the main repository.