From 106c03985171b7a4837e9f3dac439b8c055f68f2 Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Thu, 30 May 2024 12:23:46 +0330 Subject: [PATCH 1/7] Add await for next handle Authentication middleware --- lib/src/authentication/authenticate.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/authentication/authenticate.dart b/lib/src/authentication/authenticate.dart index 2300aa7..461101a 100644 --- a/lib/src/authentication/authenticate.dart +++ b/lib/src/authentication/authenticate.dart @@ -17,7 +17,7 @@ class Authenticate extends Middleware { } else { await Auth().guard(guard!).check(token ?? ''); } - return next?.handle(req); + return await next?.handle(req); } on JWTExpiredException { throw Unauthenticated(message: 'Token expired'); } From 78f08023823583fc8f94a2d7fc12f118c4de0e79 Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:50:12 +0330 Subject: [PATCH 2/7] Refactor Cache class --- lib/src/cache/cache.dart | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/src/cache/cache.dart b/lib/src/cache/cache.dart index 4493da3..8f24633 100644 --- a/lib/src/cache/cache.dart +++ b/lib/src/cache/cache.dart @@ -6,20 +6,15 @@ class Cache { factory Cache() => _singleton; Cache._internal(); - CacheDriver get _driver { - switch (env('CACHE_DRIVER', 'file')) { - case 'file': - return FileCacheDriver(); - case 'redis': - return RedisCacheDriver(); - /*case 'memcached': + final CacheDriver _driver = switch (env('CACHE_DRIVER', 'file')) { + 'file' => FileCacheDriver(), + 'redis' => RedisCacheDriver(), + /*case 'memcached': case 'database': case 'memcache': break;*/ - default: - return FileCacheDriver(); - } - } + _ => FileCacheDriver(), + }; /// set key => value to cache /// default duration is 1 hour From 9538fbe2935b6a6594a8b2b5990a13eae4c8e702 Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:50:48 +0330 Subject: [PATCH 3/7] Refactor Storage class --- lib/src/storage/storage.dart | 85 +++++++++--------------------------- 1 file changed, 21 insertions(+), 64 deletions(-) diff --git a/lib/src/storage/storage.dart b/lib/src/storage/storage.dart index a8ec2a4..378d643 100644 --- a/lib/src/storage/storage.dart +++ b/lib/src/storage/storage.dart @@ -1,65 +1,43 @@ -import 'dart:io'; import 'dart:typed_data'; -import 'package:mime/mime.dart'; import 'package:vania/src/storage/local_storage.dart'; +import 'package:vania/src/storage/s3_storage.dart'; import 'package:vania/vania.dart'; -class DownloadFile { - String? fileName; - ContentType? contentType; - String? contentDisposition; - Uint8List? data; - DownloadFile( - {this.fileName, this.contentType, this.contentDisposition, this.data}); -} class Storage { static final Storage _singleton = Storage._internal(); factory Storage() => _singleton; Storage._internal(); - String? _disk; - - Map storageDriver = { - 'local': LocalStorage(), - ...Config().get("storage")?.drivers + final StorageDriver _driver = switch (env('STORAGE', 'local')) { + 'local' => LocalStorage(), + 's3' => S3Storage(), + _ => LocalStorage(), }; - Storage disk(String disk) { - _disk = disk; - return this; - } - - StorageDriver get _driver { - return storageDriver[_disk] ?? - storageDriver[Config().get("storage")?.defaultDriver] ?? - LocalStorage(); - } - - static delete(String filepath) { - return Storage()._driver.delete(filepath); + static Future delete(String file) async { + return await Storage()._driver.delete(file); } - static Future exists(String filename) { - File file = File(filename); - return Future.value(file.existsSync()); + static Future exists(String file) async { + return await Storage()._driver.exists(file); } - static Future getAsBytes(String filename) async { - return await Storage()._driver.getAsBytes(filename); + static Future getAsBytes(String file) async { + return await Storage()._driver.getAsBytes(file); } - static Future get(String filename) async { - return await Storage()._driver.get(filename); + static Future get(String file) async { + return await Storage()._driver.get(file); } - static Future?> json(String filename) async { - return await Storage()._driver.json(filename); + static Future?> json(String file) async { + return await Storage()._driver.json(file); } static Future put( - String directory, String filename, dynamic content) { + String directory, String file, dynamic content) { if (content == null) { throw Exception("Content can't bew null"); } @@ -69,36 +47,15 @@ class Storage { } directory = directory.endsWith("/") ? directory : "$directory/"; - String path = '$directory$filename'; + String path = '$directory$file'; return Storage()._driver.put(path, content); } - static Future mimeType(String filename) async { - return await Storage()._driver.mimeType(filename); - } - - static Future size(String filename) async { - return Storage()._driver.size(filename); + static Future mimeType(String file) async { + return await Storage()._driver.mimeType(file); } - static Future downloadFile(String filename) async { - File file = File((filename)); - if (file.existsSync()) { - final dataBytes = await file.readAsBytes(); - String? mimeType = - lookupMimeType(file.uri.pathSegments.last, headerBytes: dataBytes); - String primaryType = mimeType!.split('/').first; - String subType = mimeType.split('/').last; - ContentType contentType = ContentType(primaryType, subType); - - return DownloadFile( - fileName: file.uri.pathSegments.last, - contentType: contentType, - contentDisposition: - 'attachment; filename="${file.uri.pathSegments.last}"', - data: dataBytes, - ); - } - return null; + static Future size(String file) async { + return Storage()._driver.size(file); } } From e15d0f95657398a42058b9e04dca5b99db3a73a0 Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:51:29 +0330 Subject: [PATCH 4/7] Refactor Response class --- lib/src/http/response/response.dart | 21 +++++++++++--- lib/src/http/response/stream_file.dart | 39 +++++++++++++------------- lib/src/route/set_static_path.dart | 12 +++++--- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/lib/src/http/response/response.dart b/lib/src/http/response/response.dart index e0cc512..6252a6f 100644 --- a/lib/src/http/response/response.dart +++ b/lib/src/http/response/response.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:vania/src/http/response/stream_file.dart'; enum ResponseType { json, html, streamFile, download } @@ -32,7 +33,10 @@ class Response { res.close(); break; case ResponseType.streamFile: - StreamFile? stream = StreamFile(filename: data).call(); + StreamFile? stream = StreamFile( + fileName: data['fileName'], + bytes: data['bytes'], + ).call(); if (stream == null) { res.headers.contentType = ContentType.json; res.write(jsonEncode({"message": "File not found"})); @@ -44,7 +48,10 @@ class Response { res.addStream(stream.stream!).then((_) => res.close()); break; case ResponseType.download: - StreamFile? stream = StreamFile(filename: data).call(); + StreamFile? stream = StreamFile( + fileName: data['fileName'], + bytes: data['bytes'], + ).call(); if (stream == null) { res.headers.contentType = ContentType.json; res.write(jsonEncode({"message": "File not found"})); @@ -70,7 +77,13 @@ class Response { static html(dynamic htmlData) => Response(htmlData, ResponseType.html); - static file(String file) => Response(file, ResponseType.streamFile); + static file(String fileName, Uint8List bytes) => Response({ + "fileName": fileName, + "bytes": bytes, + }, ResponseType.streamFile); - static download(dynamic file) => Response(file, ResponseType.download); + static download(String fileName, Uint8List bytes) => Response({ + "fileName": fileName, + "bytes": bytes, + }, ResponseType.download); } diff --git a/lib/src/http/response/stream_file.dart b/lib/src/http/response/stream_file.dart index 578f9a5..81245f9 100644 --- a/lib/src/http/response/stream_file.dart +++ b/lib/src/http/response/stream_file.dart @@ -1,10 +1,15 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:path/path.dart' as path; import 'package:mime/mime.dart'; class StreamFile { - final String filename; - StreamFile({required this.filename}); + final String fileName; + final Uint8List bytes; + StreamFile({ + required this.fileName, + required this.bytes, + }); ContentType? _contentType; Stream>? _stream; @@ -17,25 +22,21 @@ class StreamFile { int get length => _length; String get contentDisposition => - 'attachment; filename="${path.basename(filename)}"'; + 'attachment; filename="${path.basename(fileName)}"'; StreamFile? call() { - File file = File(filename); - - if (!file.existsSync()) { - return null; - } else { - List? bytes = file.readAsBytesSync(); - String mimeType = - lookupMimeType(path.basename(filename), headerBytes: bytes) ?? ""; - - String primaryType = mimeType.split('/').first; - String subType = mimeType.split('/').last; - - _contentType = ContentType(primaryType, subType); - _stream = Stream>.fromIterable([bytes]); - _length = bytes.length; - } + String mimeType = + lookupMimeType(path.basename(fileName), headerBytes: bytes) ?? ""; + + String primaryType = mimeType.split('/').first; + String subType = mimeType.split('/').last; + + _contentType = ContentType(primaryType, subType); + + _stream = Stream>.fromIterable([bytes]); + + _length = bytes.length; + return this; } } diff --git a/lib/src/route/set_static_path.dart b/lib/src/route/set_static_path.dart index 82f4f89..63fd4d2 100644 --- a/lib/src/route/set_static_path.dart +++ b/lib/src/route/set_static_path.dart @@ -2,13 +2,17 @@ import 'dart:io'; import 'package:vania/src/utils/functions.dart'; import 'package:vania/vania.dart'; +import 'package:path/path.dart' as path; Future setStaticPath(HttpRequest req) { - String path = Uri.decodeComponent(req.uri.path); - if (!path.endsWith("/")) { - File file = File(sanitizeRoutePath("public/$path")); + String routePath = Uri.decodeComponent(req.uri.path); + if (!routePath.endsWith("/")) { + File file = File(sanitizeRoutePath("public/$routePath")); if (file.existsSync()) { - Response response = Response.file(file.path); + Response response = Response.file( + path.basename(file.path), + file.readAsBytesSync(), + ); response.makeResponse(req.response); return Future.value(true); } else { From e7daf2f0980164b63b678f6374d76b9a62efd82e Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:51:53 +0330 Subject: [PATCH 5/7] Refactor Local storage class --- lib/src/storage/local_storage.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/storage/local_storage.dart b/lib/src/storage/local_storage.dart index ff5ba1a..bcbb032 100644 --- a/lib/src/storage/local_storage.dart +++ b/lib/src/storage/local_storage.dart @@ -10,9 +10,9 @@ class LocalStorage implements StorageDriver { String storagePath = "storage/app/public"; @override - Future exists(String filename) { + Future exists(String filename) async { File file = File(sanitizeRoutePath('$storagePath/$filename')); - return file.exists(); + return file.existsSync(); } @override @@ -43,10 +43,13 @@ class LocalStorage implements StorageDriver { } @override - Future delete(String filename) async { + Future delete(String filename) async { File file = File(sanitizeRoutePath('$storagePath/$filename')); if (file.existsSync()) { - return await file.delete(); + file.deleteSync(); + return true; + } else { + return false; } } From 72aa1768f148116238753880f80522fc96b68bce Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:55:22 +0330 Subject: [PATCH 6/7] Add AWS S3 client and storage driver --- lib/src/aws/s3_client.dart | 85 ++++++++++ lib/src/extensions/date_time_aws_format.dart | 8 + lib/src/storage/s3_storage.dart | 155 +++++++++++++++++++ lib/src/storage/storage_driver.dart | 2 +- 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 lib/src/aws/s3_client.dart create mode 100644 lib/src/extensions/date_time_aws_format.dart create mode 100644 lib/src/storage/s3_storage.dart diff --git a/lib/src/aws/s3_client.dart b/lib/src/aws/s3_client.dart new file mode 100644 index 0000000..45075c7 --- /dev/null +++ b/lib/src/aws/s3_client.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:vania/src/extensions/date_time_aws_format.dart'; +import 'package:vania/src/utils/helper.dart'; + +class S3Client { + static final S3Client _singleton = S3Client._internal(); + factory S3Client() => _singleton; + S3Client._internal(); + + final String _region = env('S3_REGION', ''); + final String _bucket = env('S3_BUCKET', ''); + final String _secretKey = env('S3_SECRET_KEY', ''); + final String _accessKey = env('S3_ACCESS_KEY', ''); + + Uri buildUri(String key) { + return Uri.https('$_bucket.s3.$_region.amazonaws.com', '/$key'); + } + + Uint8List _hmacSha256(Uint8List key, String data) { + var hmac = Hmac(sha256, key); + return Uint8List.fromList(hmac.convert(utf8.encode(data)).bytes); + } + + Uint8List _getSignatureKey( + String key, String date, String regionName, String serviceName) { + var kDate = _hmacSha256(Uint8List.fromList(utf8.encode('AWS4$key')), date); + var kRegion = _hmacSha256(kDate, regionName); + var kService = _hmacSha256(kRegion, serviceName); + var kSigning = _hmacSha256(kService, 'aws4_request'); + return kSigning; + } + + Map generateS3Headers( + String method, + String key, { + String? hash, + }) { + final algorithm = 'AWS4-HMAC-SHA256'; + final service = 's3'; + final dateTime = DateTime.now().toUtc().toAwsFormat(); + final date = dateTime.substring(0, 8).toString(); + final scope = '$date/$_region/$service/aws4_request'; + + final signedHeaders = 'host;x-amz-content-sha256;x-amz-date'; + hash ??= sha256.convert(utf8.encode('')).toString(); + final canonicalRequest = [ + method, + '/$key', + '', + 'host:$_bucket.s3.$_region.amazonaws.com', + 'x-amz-content-sha256:$hash', + 'x-amz-date:$dateTime', + '', + signedHeaders, + hash + ].join('\n'); + + final stringToSign = [ + algorithm, + dateTime, + scope, + sha256.convert(utf8.encode(canonicalRequest)).toString() + ].join('\n'); + + final signingKey = _getSignatureKey(_secretKey, date, _region, service); + final signature = _hmacSha256(signingKey, stringToSign) + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(); + + final authorizationHeader = [ + '$algorithm Credential=$_accessKey/$scope', + 'SignedHeaders=$signedHeaders', + 'Signature=$signature' + ].join(', '); + + return { + 'Authorization': authorizationHeader, + 'x-amz-content-sha256': hash, + 'x-amz-date': dateTime, + }; + } +} diff --git a/lib/src/extensions/date_time_aws_format.dart b/lib/src/extensions/date_time_aws_format.dart new file mode 100644 index 0000000..5ee5c8f --- /dev/null +++ b/lib/src/extensions/date_time_aws_format.dart @@ -0,0 +1,8 @@ +extension DateTimeAwsFormat on DateTime { + String toAwsFormat() { + String zeroPad(int number) => number.toString().padLeft(2, '0'); + + return '${zeroPad(year)}${zeroPad(month)}${zeroPad(day)}T' + '${zeroPad(hour)}${zeroPad(minute)}${zeroPad(second)}Z'; + } +} diff --git a/lib/src/storage/s3_storage.dart b/lib/src/storage/s3_storage.dart new file mode 100644 index 0000000..086dca1 --- /dev/null +++ b/lib/src/storage/s3_storage.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:mime/mime.dart'; +import 'package:vania/src/aws/s3_client.dart'; + +import 'storage_driver.dart'; + +class S3Storage implements StorageDriver { + String removeLeadingSlash(String file) { + return file.startsWith('/') ? file.replaceFirst('/', '') : file; + } + + @override + String fullPath(String file) { + return S3Client().buildUri(file).toString(); + } + + @override + Future put(String filePath, dynamic content) async { + final HttpClient client = HttpClient(); + filePath = removeLeadingSlash(filePath); + var uri = S3Client().buildUri(filePath); + final request = await client.putUrl(uri); + request.headers.set( + 'Content-Type', + lookupMimeType(filePath) ?? 'application/octet-stream', + ); + request.headers.set( + 'Content-Length', + content.length.toString(), + ); + final payloadHash = sha256.convert(content).toString(); + S3Client() + .generateS3Headers('PUT', filePath, hash: payloadHash) + .forEach((key, value) { + request.headers.set(key, value); + }); + + request.add(content); + + var response = await request.close(); + //String reply = await response.transform(utf8.decoder).join(); + + client.close(); + if (response.statusCode == 200) { + return uri.toString(); + } else { + throw Exception('Failed to upload file: ${response.statusCode}'); + } + } + + @override + Future get(String file) async { + final HttpClient client = HttpClient(); + file = removeLeadingSlash(file); + var uri = S3Client().buildUri(file); + var request = await client.getUrl(uri); + S3Client().generateS3Headers('GET', file).forEach((key, value) { + request.headers.set(key, value); + }); + var response = await request.close(); + client.close(); + if (response.statusCode == 200) { + return await response.transform(utf8.decoder).join(); + } else { + return null; + } + } + + @override + Future getAsBytes(String file) async { + final HttpClient client = HttpClient(); + file = removeLeadingSlash(file); + var uri = S3Client().buildUri(file); + var request = await client.getUrl(uri); + S3Client().generateS3Headers('GET', file).forEach((key, value) { + request.headers.set(key, value); + }); + var response = await request.close(); + client.close(); + if (response.statusCode == 200) { + var bytes = await response + .fold(BytesBuilder(), (b, d) => b..add(d)) + .then((b) => b.takeBytes()); + return Uint8List.fromList(bytes); + } else { + return null; + } + } + + @override + Future?> json(String file) async { + file = removeLeadingSlash(file); + var str = await get(file); + return str == null ? null : jsonDecode(str); + } + + @override + Future mimeType(String file) async { + file = removeLeadingSlash(file); + var bytes = await getAsBytes(file); + if (bytes != null) { + return lookupMimeType(file, + headerBytes: bytes.sublist(0, min(4096, bytes.length))); + } + return null; + } + + @override + Future size(String file) async { + final HttpClient client = HttpClient(); + file = removeLeadingSlash(file); + var uri = S3Client().buildUri(file); + var request = await client.headUrl(uri); + S3Client().generateS3Headers('HEAD', file).forEach((key, value) { + request.headers.set(key, value); + }); + var response = await request.close(); + client.close(); + if (response.statusCode == 200) { + return int.tryParse(response.headers.value('content-length') ?? ''); + } else { + return null; + } + } + + @override + Future exists(String file) async { + final HttpClient client = HttpClient(); + var uri = S3Client().buildUri(file); + var request = await client.headUrl(uri); + S3Client().generateS3Headers('HEAD', file).forEach((key, value) { + request.headers.set(key, value); + }); + var response = await request.close(); + client.close(); + return response.statusCode == 200; + } + + @override + Future delete(String file) async { + final HttpClient client = HttpClient(); + var uri = S3Client().buildUri(file); + var request = await client.deleteUrl(uri); + S3Client().generateS3Headers('DELETE', file).forEach((key, value) { + request.headers.set(key, value); + }); + var response = await request.close(); + client.close(); + return response.statusCode == 204; + } +} diff --git a/lib/src/storage/storage_driver.dart b/lib/src/storage/storage_driver.dart index c95ddef..045157c 100644 --- a/lib/src/storage/storage_driver.dart +++ b/lib/src/storage/storage_driver.dart @@ -16,5 +16,5 @@ abstract class StorageDriver { Future exists(String filename); - Future delete(String filename); + Future delete(String filename); } From d684d41aee1f14f16450334eb6c7d6919e89e364 Mon Sep 17 00:00:00 2001 From: Javad Zobeidi Date: Fri, 31 May 2024 01:56:26 +0330 Subject: [PATCH 7/7] Dart format --- lib/src/storage/storage.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/storage/storage.dart b/lib/src/storage/storage.dart index 378d643..fa33896 100644 --- a/lib/src/storage/storage.dart +++ b/lib/src/storage/storage.dart @@ -4,7 +4,6 @@ import 'package:vania/src/storage/local_storage.dart'; import 'package:vania/src/storage/s3_storage.dart'; import 'package:vania/vania.dart'; - class Storage { static final Storage _singleton = Storage._internal(); factory Storage() => _singleton; @@ -36,8 +35,7 @@ class Storage { return await Storage()._driver.json(file); } - static Future put( - String directory, String file, dynamic content) { + static Future put(String directory, String file, dynamic content) { if (content == null) { throw Exception("Content can't bew null"); }