Skip to content

Commit

Permalink
Merge pull request #70 from javad-zobeidi/dev
Browse files Browse the repository at this point in the history
Add AWS s3 client
  • Loading branch information
javad-zobeidi committed May 30, 2024
2 parents fb36579 + d684d41 commit 1dac058
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 110 deletions.
2 changes: 1 addition & 1 deletion lib/src/authentication/authenticate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
85 changes: 85 additions & 0 deletions lib/src/aws/s3_client.dart
Original file line number Diff line number Diff line change
@@ -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<String>('S3_REGION', '');
final String _bucket = env<String>('S3_BUCKET', '');
final String _secretKey = env<String>('S3_SECRET_KEY', '');
final String _accessKey = env<String>('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<String, String> 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,
};
}
}
17 changes: 6 additions & 11 deletions lib/src/cache/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@ class Cache {
factory Cache() => _singleton;
Cache._internal();

CacheDriver get _driver {
switch (env<String>('CACHE_DRIVER', 'file')) {
case 'file':
return FileCacheDriver();
case 'redis':
return RedisCacheDriver();
/*case 'memcached':
final CacheDriver _driver = switch (env<String>('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
Expand Down
8 changes: 8 additions & 0 deletions lib/src/extensions/date_time_aws_format.dart
Original file line number Diff line number Diff line change
@@ -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';
}
}
21 changes: 17 additions & 4 deletions lib/src/http/response/response.dart
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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"}));
Expand All @@ -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"}));
Expand All @@ -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);
}
39 changes: 20 additions & 19 deletions lib/src/http/response/stream_file.dart
Original file line number Diff line number Diff line change
@@ -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<List<int>>? _stream;
Expand All @@ -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<int>? 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<List<int>>.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<List<int>>.fromIterable([bytes]);

_length = bytes.length;

return this;
}
}
12 changes: 8 additions & 4 deletions lib/src/route/set_static_path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool?> 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 {
Expand Down
11 changes: 7 additions & 4 deletions lib/src/storage/local_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ class LocalStorage implements StorageDriver {
String storagePath = "storage/app/public";

@override
Future<bool> exists(String filename) {
Future<bool> exists(String filename) async {
File file = File(sanitizeRoutePath('$storagePath/$filename'));
return file.exists();
return file.existsSync();
}

@override
Expand Down Expand Up @@ -43,10 +43,13 @@ class LocalStorage implements StorageDriver {
}

@override
Future<dynamic> delete(String filename) async {
Future<bool> delete(String filename) async {
File file = File(sanitizeRoutePath('$storagePath/$filename'));
if (file.existsSync()) {
return await file.delete();
file.deleteSync();
return true;
} else {
return false;
}
}

Expand Down
Loading

0 comments on commit 1dac058

Please sign in to comment.