Skip to content

Commit

Permalink
improved: file uploads go to temp folder, TODO: add multer-like utili…
Browse files Browse the repository at this point in the history
…ty function to read files directly from the event and do other cool things
  • Loading branch information
iyifr committed Dec 11, 2024
1 parent e06ccf2 commit b59a956
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 54 deletions.
8 changes: 3 additions & 5 deletions bin/main.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';

import 'package:h4/create.dart';

void main() async {
Expand Down Expand Up @@ -27,11 +30,6 @@ void main() async {
var username = formData.get('username');
var password = formData.get('password');

print(getRequestIp(event));

// userService.signup(username, password);
event.statusCode = 201;

return 'Hi from /api with $username, $password';
});
}
1 change: 1 addition & 0 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class H4Event {
// Handle non-async handler.
_resolveRequest(this, handlerResult);

// Workaround for dart's lack of support for union types
if (middlewares?['afterResponse'] != null) {
if (middlewares?['afterResponse']?.left != null) {
middlewares?['afterResponse']?.left!(this);
Expand Down
11 changes: 3 additions & 8 deletions lib/src/h4.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,6 @@ class H4 {
/// is a function that takes an [H4Event] as input and returns a modified
/// [H4Event].
///
/// The registered middleware function can be used to perform various tasks, such
/// as:
///
/// - Logging or monitoring requests
/// - Validating or transforming request data
/// - Adding headers or other metadata to the request
Expand Down Expand Up @@ -155,9 +152,6 @@ class H4 {
/// - `String` representation of the stack trace
/// - The [H4Event] that triggered the error (if available)
///
/// This error handling function can be used to log, report, or handle errors in
/// a custom way within your application.
///
/// Example usage:
/// ```dart
/// h4.onError((String error, String stackTrace, H4Event event) {
Expand Down Expand Up @@ -281,7 +275,8 @@ NotFoundHandler return404(HttpRequest request) {
return {
"statusCode": 404,
"statusMessage": "Not found",
"message": "Cannot ${event.method.toUpperCase()} - ${event.path}"
"message":
"Cannot perform ${event.method.toUpperCase()} request at ${event.path}, PATH not found."
};
},
stack,
Expand Down Expand Up @@ -310,7 +305,7 @@ MethodNotAllowedHandler return405(HttpRequest request) {
"statusCode": 405,
"statusMessage": "Method Not Allowed",
"message":
"Cannot ${event.method.toUpperCase()} - ${event.path}. Reason: Method not allowed."
"Cannot perform ${event.method.toUpperCase()} request at ${event.path}\n Reason: Method not allowed."
};
},
stack,
Expand Down
160 changes: 120 additions & 40 deletions lib/utils/req_utils.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:h4/create.dart';
import 'package:h4/src/logger.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as path;

export 'package:h4/utils/req_utils.dart' hide handleMultipartFormdata, FormData;

Expand Down Expand Up @@ -65,23 +67,54 @@ class FormData {
print(_data);
}

@override
String toString() {
return _data.toString();
}

dynamic get(String name) {
final values = _data[name];
return values?.isNotEmpty == true ? values!.first : null;
var result = values?.isNotEmpty == true ? values!.first : null;
return result;
}

List<dynamic>? getAll(String name) {
return _data[name];
}
}

Future<FormData> readFormData(dynamic event) async {
final HttpRequest request = event.node["value"];
final contentType = request.headers.contentType;
var formData = FormData();

if (contentType?.mimeType == null) {
logger.warning("NO formdata fields in request body");
throw CreateError(message: "No formdata fields found");
}

if (contentType?.mimeType == 'multipart/form-data') {
final boundary = contentType!.parameters['boundary'];
if (boundary != null) {
formData = await handleMultipartFormdata(request, boundary, formData);
} else {
throw Exception('Missing boundary in multipart/form-data');
}
} else if (contentType?.mimeType == 'application/x-www-form-urlencoded') {
await _handleUrlEncodedFormData(request, formData);
} else {
throw Exception('Unsupported content type: ${contentType?.mimeType}');
}

return formData;
}

Future<FormData> handleMultipartFormdata(
HttpRequest request, String boundary, FormData formData) async {
final parts = await request
.transform(StreamTransformer.castFrom(MimeMultipartTransformer(boundary)))
.toList();
final mimeTransformer = MimeMultipartTransformer(boundary);
final parts = request.cast<List<int>>().transform(mimeTransformer);

for (var part in parts) {
await for (final part in parts) {
final headers = part.headers;
final contentType = headers['content-type'];
final contentDisposition = headers['content-disposition'];
Expand All @@ -94,17 +127,18 @@ Future<FormData> handleMultipartFormdata(

if (fieldName != null) {
if (contentType != null || filename != null) {
// Handle all file data as bytes
final bytes = await part.fold<dynamic>(
[],
(prev, element) => prev..addAll(element),
);
// Stream file to temporary storage
final fileInfo = await _streamToStorage(part, filename);

Map<String, dynamic> fieldData = {
'data': bytes,
'path': fileInfo['path'],
'mimeType': contentType,
'originalname': filename,
'fieldName': fieldName,
'size': fileInfo['size'],
'tempFilename': fileInfo['tempFilename'],
};
if (contentType != null) fieldData['contentType'] = contentType;
if (filename != null) fieldData['filename'] = filename;
formData.append(fieldName, fieldData.toString());
formData.append(fieldName, json.encode(fieldData));
} else {
// Handle plain text data as string
final content = await utf8.decoder.bind(part).join();
Expand All @@ -116,32 +150,6 @@ Future<FormData> handleMultipartFormdata(
return formData;
}

Future<FormData> readFormData(dynamic event) async {
final HttpRequest request = event.node["value"];
final contentType = request.headers.contentType;
var formData = FormData();

if (contentType?.mimeType == null) {
logger.warning("NO formdata fields in request body!");
throw CreateError(message: "No formdata fields found");
}

if (contentType?.mimeType == 'multipart/form-data') {
final boundary = contentType!.parameters['boundary'];
if (boundary != null) {
formData = await handleMultipartFormdata(request, boundary, formData);
} else {
throw Exception('Missing boundary in multipart/form-data');
}
} else if (contentType?.mimeType == 'application/x-www-form-urlencoded') {
await _handleUrlEncodedFormData(request, formData);
} else {
throw Exception('Unsupported content type: ${contentType?.mimeType}');
}

return formData;
}

Future<void> _handleUrlEncodedFormData(
HttpRequest request, FormData formData) async {
final body = await utf8.decodeStream(request);
Expand All @@ -155,3 +163,75 @@ Future<void> _handleUrlEncodedFormData(
}
}
}

String detectEncoding(Uint8List bytes) {
if (bytes.length < 4) return 'unknown';

// Check UTF-8 BOM
if (bytes.length >= 3 &&
bytes[0] == 0xEF &&
bytes[1] == 0xBB &&
bytes[2] == 0xBF) {
return 'UTF-8';
}

// Check UTF-16 LE BOM
if (bytes.length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) {
return 'UTF-16LE';
}

// Check UTF-16 BE BOM
if (bytes.length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) {
return 'UTF-16BE';
}

// If no BOM is found, assume UTF-8
return 'UTF-8';
}

Future<Map<String, dynamic>> _streamToStorage(
Stream<List<int>> dataStream, String? originalFilename) async {
// Create unique filename to avoid collisions
final timestamp = DateTime.now().millisecondsSinceEpoch;
final randomId =
DateTime.now().microsecondsSinceEpoch.toString().substring(8);
final safeFilename =
originalFilename?.replaceAll(RegExp(r'[^a-zA-Z0-9.-]'), '_') ?? 'unnamed';
final filename = '${timestamp}_${randomId}_$safeFilename';

// Get system temp directory
final tempDir = Directory.systemTemp;
final filePath = path.join(tempDir.path, filename);
final file = File(filePath);

// Stream metrics
var totalSize = 0;
const maxSize = 500 * 1024 * 1024; // 10MB limit, adjust as needed

try {
final sink = file.openWrite();
await for (var chunk in dataStream) {
totalSize += chunk.length;
if (totalSize > maxSize) {
await sink.close();
await file.delete();
throw CreateError(message: 'File size exceeds maximum allowed size');
}
sink.add(chunk);
}
await sink.close();

return {
'path': filePath,
'size': totalSize,
'originalname': originalFilename,
'tempFilename': filename,
};
} catch (e) {
// Clean up on error
if (await file.exists()) {
await file.delete();
}
throw CreateError(message: 'Failed to save file: ${e.toString()}');
}
}
1 change: 0 additions & 1 deletion test/h4_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:dio/dio.dart';
import 'package:h4/create.dart';
import 'package:h4/src/h4.dart';
import 'package:h4/src/router.dart';
import 'package:h4/utils/body_utils.dart';
import 'package:h4/utils/request_utils.dart';
import 'package:test/test.dart';

Expand Down

0 comments on commit b59a956

Please sign in to comment.