From b91cee32c28e03241a83a536bd9d87d361acc786 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:12:37 +0100 Subject: [PATCH 01/31] basic blossom without auth --- packages/amber/pubspec.lock | 2 +- packages/isar/pubspec.lock | 2 +- .../data_layer/data_sources/http_request.dart | 42 ++++ .../repositories/blossom/blossom_impl.dart | 207 ++++++++++++++++++ .../domain_layer/repositories/blossom.dart | 81 +++++++ .../domain_layer/usecases/files/files.dart | 39 ++++ packages/ndk/pubspec.lock | 30 ++- packages/ndk/pubspec.yaml | 4 + .../ndk/test/mocks/mock_blossom_server.dart | 194 ++++++++++++++++ .../ndk/test/usecases/files/files_test.dart | 82 +++++++ packages/objectbox/pubspec.lock | 2 +- packages/rust_verifier/.flutter-plugins | 14 +- .../.flutter-plugins-dependencies | 2 +- packages/rust_verifier/pubspec.lock | 2 +- 14 files changed, 684 insertions(+), 19 deletions(-) create mode 100644 packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart create mode 100644 packages/ndk/lib/domain_layer/repositories/blossom.dart create mode 100644 packages/ndk/lib/domain_layer/usecases/files/files.dart create mode 100644 packages/ndk/test/mocks/mock_blossom_server.dart create mode 100644 packages/ndk/test/usecases/files/files_test.dart diff --git a/packages/amber/pubspec.lock b/packages/amber/pubspec.lock index 949f5a38..d580538d 100644 --- a/packages/amber/pubspec.lock +++ b/packages/amber/pubspec.lock @@ -469,7 +469,7 @@ packages: path: "../ndk" relative: true source: path - version: "0.2.0" + version: "0.2.4" package_config: dependency: transitive description: diff --git a/packages/isar/pubspec.lock b/packages/isar/pubspec.lock index f3ec2d1c..8fe8f3a1 100644 --- a/packages/isar/pubspec.lock +++ b/packages/isar/pubspec.lock @@ -375,7 +375,7 @@ packages: path: "../ndk" relative: true source: path - version: "0.2.0" + version: "0.2.4" node_preamble: dependency: transitive description: diff --git a/packages/ndk/lib/data_layer/data_sources/http_request.dart b/packages/ndk/lib/data_layer/data_sources/http_request.dart index a21f382f..43b84890 100644 --- a/packages/ndk/lib/data_layer/data_sources/http_request.dart +++ b/packages/ndk/lib/data_layer/data_sources/http_request.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:http/http.dart' as http; @@ -21,4 +22,45 @@ class HttpRequestDS { } return jsonDecode(response.body); } + + Future put({ + required Uri url, + required Uint8List body, + required headers, + }) async { + http.Response response = await _client.put( + url, + body: body, + headers: headers, + ); + + if (response.statusCode != 200) { + throw Exception( + "error fetching STATUS: ${response.statusCode}, Link: $url"); + } + + return response; + } + + Future get(Uri url) async { + http.Response response = await _client.get(url); + + if (response.statusCode != 200) { + throw Exception( + "error fetching STATUS: ${response.statusCode}, Link: $url"); + } + + return response; + } + + Future delete(Uri url) async { + http.Response response = await _client.delete(url); + + if (response.statusCode != 200) { + throw Exception( + "error fetching STATUS: ${response.statusCode}, Link: $url"); + } + + return response; + } } diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart new file mode 100644 index 00000000..791d5dba --- /dev/null +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -0,0 +1,207 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../../../domain_layer/repositories/blossom.dart'; +import '../../data_sources/http_request.dart'; + +class BlossomRepositoryImpl implements BlossomRepository { + final HttpRequestDS client; + final List serverUrls; + + BlossomRepositoryImpl({ + required this.client, + required this.serverUrls, + }) { + if (serverUrls.isEmpty) { + throw ArgumentError('At least one server URL must be provided'); + } + } + + @override + Future> uploadBlob( + Uint8List data, { + String? contentType, + UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, + }) async { + switch (strategy) { + case UploadStrategy.mirrorAfterSuccess: + return _uploadWithMirroring(data, contentType); + case UploadStrategy.allSimultaneous: + return _uploadToAllServers(data, contentType); + case UploadStrategy.firstSuccess: + return _uploadToFirstSuccess(data, contentType); + } + } + + Future> _uploadWithMirroring( + Uint8List data, + String? contentType, + ) async { + final results = []; + + // Try primary upload + final primaryResult = await _uploadToServer( + serverUrls.first, + data, + contentType, + ); + results.add(primaryResult); + + if (primaryResult.success) { + // Mirror to other servers + final mirrorResults = await Future.wait(serverUrls + .skip(1) + .map((url) => _uploadToServer(url, data, contentType))); + results.addAll(mirrorResults); + } + + return results; + } + + Future> _uploadToAllServers( + Uint8List data, + String? contentType, + ) async { + final results = await Future.wait( + serverUrls.map((url) => _uploadToServer(url, data, contentType))); + return results; + } + + Future> _uploadToFirstSuccess( + Uint8List data, + String? contentType, + ) async { + for (final url in serverUrls) { + final result = await _uploadToServer(url, data, contentType); + if (result.success) { + return [result]; + } + } + + // If all servers failed, return all errors + final results = await _uploadToAllServers(data, contentType); + return results; + } + + Future _uploadToServer( + String serverUrl, + Uint8List data, + String? contentType, + ) async { + try { + final response = await client.put( + url: Uri.parse('$serverUrl/upload'), + body: data, + headers: { + if (contentType != null) 'Content-Type': contentType, + 'Content-Length': '${data.length}', + }, + ); + + if (response.statusCode != 200) { + return BlobUploadResult( + serverUrl: serverUrl, + success: false, + error: 'HTTP ${response.statusCode}', + ); + } + + return BlobUploadResult( + serverUrl: serverUrl, + success: true, + descriptor: BlobDescriptor.fromJson(jsonDecode(response.body)), + ); + } catch (e) { + return BlobUploadResult( + serverUrl: serverUrl, + success: false, + error: e.toString(), + ); + } + } + + @override + Future getBlob(String sha256) async { + Exception? lastError; + + for (final url in serverUrls) { + try { + final response = await client.get( + Uri.parse('$url/$sha256'), + ); + + if (response.statusCode == 200) { + return response.bodyBytes; + } + lastError = Exception('HTTP ${response.statusCode}'); + } catch (e) { + lastError = e is Exception ? e : Exception(e.toString()); + } + } + + throw Exception( + 'Failed to get blob from all servers. Last error: $lastError'); + } + + @override + Future> listBlobs( + String pubkey, { + DateTime? since, + DateTime? until, + }) async { + Exception? lastError; + + for (final url in serverUrls) { + try { + final queryParams = { + if (since != null) 'since': '${since.millisecondsSinceEpoch ~/ 1000}', + if (until != null) 'until': '${until.millisecondsSinceEpoch ~/ 1000}', + }; + + final response = await client.get( + Uri.parse('$url/list/$pubkey').replace(queryParameters: queryParams), + ); + + if (response.statusCode == 200) { + final List json = jsonDecode(response.body); + return json.map((j) => BlobDescriptor.fromJson(j)).toList(); + } + lastError = Exception('HTTP ${response.statusCode}'); + } catch (e) { + lastError = e is Exception ? e : Exception(e.toString()); + } + } + + throw Exception( + 'Failed to list blobs from all servers. Last error: $lastError'); + } + + @override + Future> deleteBlob(String sha256) async { + final results = await Future.wait( + serverUrls.map((url) => _deleteFromServer(url, sha256))); + return results; + } + + Future _deleteFromServer( + String serverUrl, String sha256) async { + try { + final response = await client.delete( + Uri.parse('$serverUrl/$sha256'), + ); + + return BlobDeleteResult( + serverUrl: serverUrl, + success: response.statusCode == 200, + error: + response.statusCode != 200 ? 'HTTP ${response.statusCode}' : null, + ); + } catch (e) { + return BlobDeleteResult( + serverUrl: serverUrl, + success: false, + error: e.toString(), + ); + } + } +} diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart new file mode 100644 index 00000000..e482c620 --- /dev/null +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -0,0 +1,81 @@ +import 'dart:typed_data'; + +enum UploadStrategy { + /// Upload to first server, then mirror to others + mirrorAfterSuccess, + + /// Upload to all servers simultaneously + allSimultaneous, + + /// Upload to first successful server only + firstSuccess +} + +abstract class BlossomRepository { + /// Uploads a blob using the specified strategy + Future> uploadBlob( + Uint8List data, { + String? contentType, + UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, + }); + + /// Gets a blob by trying servers sequentially until success + Future getBlob(String sha256); + + /// Lists blobs from the first successful server + Future> listBlobs(String pubkey, + {DateTime? since, DateTime? until}); + + /// Attempts to delete blob from all servers + Future> deleteBlob(String sha256); +} + +class BlobDescriptor { + final String url; + final String sha256; + final int size; + final String? type; + final DateTime uploaded; + + BlobDescriptor( + {required this.url, + required this.sha256, + required this.size, + this.type, + required this.uploaded}); + + factory BlobDescriptor.fromJson(Map json) { + return BlobDescriptor( + url: json['url'], + sha256: json['sha256'], + size: json['size'], + type: json['type'], + uploaded: DateTime.fromMillisecondsSinceEpoch(json['uploaded'] * 1000)); + } +} + +class BlobUploadResult { + final String serverUrl; + final bool success; + final BlobDescriptor? descriptor; + final String? error; + + BlobUploadResult({ + required this.serverUrl, + required this.success, + this.descriptor, + this.error, + }); +} + +class BlobDeleteResult { + final String serverUrl; + final bool success; + final String? error; + + BlobDeleteResult({ + required this.serverUrl, + required this.success, + this.error, + }); +} diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart new file mode 100644 index 00000000..4c0c0cf5 --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import '../../repositories/blossom.dart'; + +class Files { + final BlossomRepository repository; + + Files(this.repository); + + Future> uploadBlob({ + required Uint8List data, + String? contentType, + UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, + }) { + return repository.uploadBlob( + data, + contentType: contentType, + strategy: strategy, + ); + } + + Future getBlob(String sha256) { + return repository.getBlob(sha256); + } + + Future> listBlobs( + String pubkey, { + DateTime? since, + DateTime? until, + }) { + return repository.listBlobs(pubkey, since: since, until: until); + } + +// lib/domain/usecases/delete_blob_usecase.dart + + Future delteBlob(String sha256) { + return repository.deleteBlob(sha256); + } +} diff --git a/packages/ndk/pubspec.lock b/packages/ndk/pubspec.lock index 62339a40..73ea5f66 100644 --- a/packages/ndk/pubspec.lock +++ b/packages/ndk/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: "direct main" description: @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_multi_server: dependency: transitive description: @@ -285,10 +293,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" io: dependency: transitive description: @@ -434,13 +442,13 @@ packages: source: hosted version: "0.28.0" shelf: - dependency: transitive + dependency: "direct dev" description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -449,6 +457,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + shelf_router: + dependency: "direct dev" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" shelf_static: dependency: transitive description: diff --git a/packages/ndk/pubspec.yaml b/packages/ndk/pubspec.yaml index ed4d6b89..554697bd 100644 --- a/packages/ndk/pubspec.yaml +++ b/packages/ndk/pubspec.yaml @@ -37,3 +37,7 @@ dev_dependencies: flutter_lints: ^5.0.0 mockito: ^5.0.17 test: any + # shelf used for blossom mock server + shelf: ^1.4.2 + shelf_router: ^1.1.4 + diff --git a/packages/ndk/test/mocks/mock_blossom_server.dart b/packages/ndk/test/mocks/mock_blossom_server.dart new file mode 100644 index 00000000..eb7cb5ca --- /dev/null +++ b/packages/ndk/test/mocks/mock_blossom_server.dart @@ -0,0 +1,194 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:shelf_router/shelf_router.dart'; + +class MockBlossomServer { + // In-memory storage for blobs + final Map _blobs = {}; + final int port; + HttpServer? _server; + + MockBlossomServer({this.port = 3000}); + + Router _createRouter() { + final router = Router(); + + // GET / - Get Blob + router.get('/', (Request request, String sha256) { + if (!_blobs.containsKey(sha256)) { + return Response.notFound('Blob not found'); + } + return Response.ok(_blobs[sha256]!.data, + headers: {'Content-Type': _blobs[sha256]!.contentType}); + }); + + // HEAD / - Has Blob + router.head('/', (Request request, String sha256) { + if (!_blobs.containsKey(sha256)) { + return Response.notFound('Blob not found'); + } + return Response(200, headers: { + 'Content-Length': _blobs[sha256]!.data.length.toString(), + 'Content-Type': _blobs[sha256]!.contentType, + }); + }); + + // PUT /upload - Upload Blob + router.put('/upload', (Request request) async { + // Check for authorization header + final authHeader = request.headers['authorization']; + if (authHeader == null) { + return Response.forbidden('Missing authorization'); + } + + try { + final authEvent = json.decode(authHeader); + if (!_verifyAuthEvent(authEvent, 'upload')) { + return Response.forbidden('Invalid authorization event'); + } + } catch (e) { + return Response.forbidden('Invalid authorization format'); + } + + // Read the request body + final bytes = await request.read().expand((chunk) => chunk).toList(); + final data = Uint8List.fromList(bytes); + + final sha256 = _computeSha256(data); + final contentType = + request.headers['content-type'] ?? 'application/octet-stream'; + + _blobs[sha256] = _BlobEntry( + data: data, + contentType: contentType, + uploader: 'test_pubkey', + uploadedAt: DateTime.now(), + ); + + return Response.ok( + json.encode({ + 'url': 'http://localhost:$port/$sha256', + 'sha256': sha256, + 'size': data.length, + 'type': contentType, + 'uploaded': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }), + headers: {'Content-Type': 'application/json'}, + ); + }); + + // GET /list/ - List Blobs + router.get('/list/', (Request request, String pubkey) { + final since = int.tryParse(request.url.queryParameters['since'] ?? ''); + final until = int.tryParse(request.url.queryParameters['until'] ?? ''); + + final blobs = _blobs.entries + .where((entry) => entry.value.uploader == pubkey) + .where((entry) { + final timestamp = + entry.value.uploadedAt.millisecondsSinceEpoch ~/ 1000; + if (since != null && timestamp < since) return false; + if (until != null && timestamp > until) return false; + return true; + }) + .map((entry) => { + 'url': 'http://localhost:$port/${entry.key}', + 'sha256': entry.key, + 'size': entry.value.data.length, + 'type': entry.value.contentType, + 'uploaded': + entry.value.uploadedAt.millisecondsSinceEpoch ~/ 1000, + }) + .toList(); + + return Response.ok( + json.encode(blobs), + headers: {'Content-Type': 'application/json'}, + ); + }); + + // DELETE / - Delete Blob + router.delete('/', (Request request, String sha256) { + final authHeader = request.headers['authorization']; + if (authHeader == null) { + return Response.forbidden('Missing authorization'); + } + + try { + final authEvent = json.decode(authHeader); + if (!_verifyAuthEvent(authEvent, 'delete')) { + return Response.forbidden('Invalid authorization event'); + } + } catch (e) { + return Response.forbidden('Invalid authorization format'); + } + + if (!_blobs.containsKey(sha256)) { + return Response.notFound('Blob not found'); + } + + _blobs.remove(sha256); + return Response(200); + }); + + return router; + } + + Future start() async { + final handler = Pipeline() + .addMiddleware(logRequests()) + .addHandler(_createRouter().call); + + _server = await serve(handler, 'localhost', port); + print('Mock Blossom Server running on port $port'); + } + + Future stop() async { + await _server?.close(); + _server = null; + } + + // Helper methods + String _computeSha256(List data) { + return sha256.convert(data).toString(); + } + + bool _verifyAuthEvent(Map event, String type) { + // Simple verification for testing purposes + if (event['kind'] != 24242) return false; + + final tags = List>.from(event['tags']); + final hasTypeTag = + tags.any((tag) => tag.length >= 2 && tag[0] == 't' && tag[1] == type); + + return hasTypeTag; + } +} + +class _BlobEntry { + final Uint8List data; + final String contentType; + final String uploader; + final DateTime uploadedAt; + + _BlobEntry({ + required this.data, + required this.contentType, + required this.uploader, + required this.uploadedAt, + }); +} + +// Example usage in tests +void main() async { + final server = MockBlossomServer(port: 3000); + await server.start(); + + // Run your tests here + + await server.stop(); +} diff --git a/packages/ndk/test/usecases/files/files_test.dart b/packages/ndk/test/usecases/files/files_test.dart new file mode 100644 index 00000000..ed2860c4 --- /dev/null +++ b/packages/ndk/test/usecases/files/files_test.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; + +import 'package:ndk/data_layer/data_sources/http_request.dart'; +import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; +import 'package:ndk/domain_layer/usecases/files/files.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_blossom_server.dart'; + +void main() { + late MockBlossomServer server; + late Files client; + + setUp(() async { + server = MockBlossomServer(port: 3000); + await server.start(); + + final blossomRepo = BlossomRepositoryImpl( + client: HttpRequestDS(http.Client()), + serverUrls: ['http://localhost:3000'], + ); + client = Files(blossomRepo); + }); + + tearDown(() async { + await server.stop(); + }); + + group('Blossom Client Integration Tests', () { + test('Upload and retrieve blob', () async { + final testData = Uint8List.fromList(utf8.encode('Hello, Blossom!')); + final authEvent = createTestAuthEvent('upload'); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + //authorization: authEvent, + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Retrieve blob + final getResponse = await client.getBlob(sha256); + expect(utf8.decode(getResponse), equals('Hello, Blossom!')); + }); + + test('List blobs for user', () async { + // Upload some test blobs first + final testData1 = Uint8List.fromList(utf8.encode('Test 1')); + final testData2 = Uint8List.fromList(utf8.encode('Test 2')); + + await client.uploadBlob( + data: testData1, + //authorization: createTestAuthEvent('upload'), + ); + await client.uploadBlob( + data: testData2, + //authorization: createTestAuthEvent('upload'), + ); + + final listResponse = await client.listBlobs('test_pubkey'); + + expect(listResponse.length, equals(2)); + }); + }); +} + +Map createTestAuthEvent(String type) { + return { + 'kind': 24242, + 'pubkey': 'test_pubkey', + 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'tags': [ + ['t', type], + ['x', 'test_hash'], + ], + 'sig': 'test_signature', + }; +} diff --git a/packages/objectbox/pubspec.lock b/packages/objectbox/pubspec.lock index efe69acf..ba98e49a 100644 --- a/packages/objectbox/pubspec.lock +++ b/packages/objectbox/pubspec.lock @@ -401,7 +401,7 @@ packages: path: "../ndk" relative: true source: path - version: "0.2.0" + version: "0.2.4" node_preamble: dependency: transitive description: diff --git a/packages/rust_verifier/.flutter-plugins b/packages/rust_verifier/.flutter-plugins index 522dd75a..206f100c 100644 --- a/packages/rust_verifier/.flutter-plugins +++ b/packages/rust_verifier/.flutter-plugins @@ -1,8 +1,8 @@ # This is a generated file; do not edit or check into version control. -integration_test=/usr/bin/flutter/packages/integration_test/ -path_provider=/home/fmar/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ -path_provider_android=/home/fmar/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/ -path_provider_foundation=/home/fmar/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/ -path_provider_linux=/home/fmar/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ -path_provider_windows=/home/fmar/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ -rust_lib_ndk=/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/ +integration_test=C:\\Users\\Beonde\\Documents\\flutterSDK\\flutter\\packages\\integration_test\\ +path_provider=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider-2.1.5\\ +path_provider_android=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_android-2.2.12\\ +path_provider_foundation=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_foundation-2.4.0\\ +path_provider_linux=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_linux-2.2.1\\ +path_provider_windows=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_windows-2.3.0\\ +rust_lib_ndk=c:\\Users\\Beonde\\AndroidStudioProjects\\ndk\\packages\\rust_verifier\\rust_builder\\ diff --git a/packages/rust_verifier/.flutter-plugins-dependencies b/packages/rust_verifier/.flutter-plugins-dependencies index b6416a02..0be33c7f 100644 --- a/packages/rust_verifier/.flutter-plugins-dependencies +++ b/packages/rust_verifier/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/usr/bin/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/fmar/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/usr/bin/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/fmar/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/","native_build":true,"dependencies":[]}],"macos":[{"name":"path_provider_foundation","path":"/home/fmar/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/","native_build":true,"dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/home/fmar/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/","native_build":true,"dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/home/fmar/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"/home/fmar/workspace/ndk/packages/rust_verifier/rust_builder/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"rust_lib_ndk","dependencies":[]}],"date_created":"2024-12-18 00:22:24.640813","version":"3.27.0","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"C:\\\\Users\\\\Beonde\\\\Documents\\\\flutterSDK\\\\flutter\\\\packages\\\\integration_test\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"C:\\\\Users\\\\Beonde\\\\Documents\\\\flutterSDK\\\\flutter\\\\packages\\\\integration_test\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.12\\\\","native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"macos":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"rust_lib_ndk","dependencies":[]}],"date_created":"2025-01-10 11:17:04.830361","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/rust_verifier/pubspec.lock b/packages/rust_verifier/pubspec.lock index cc8561c6..7de95040 100644 --- a/packages/rust_verifier/pubspec.lock +++ b/packages/rust_verifier/pubspec.lock @@ -477,7 +477,7 @@ packages: path: "../ndk" relative: true source: path - version: "0.2.0" + version: "0.2.4" package_config: dependency: transitive description: From 35ad93941aeafbc35b5ff3260c0ea061e2fd8c1d Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:01:37 +0100 Subject: [PATCH 02/31] wip: auth in blossom repo impl --- .../repositories/blossom/blossom_impl.dart | 105 +++++++++++++----- .../domain_layer/entities/nip_01_event.dart | 5 + .../domain_layer/repositories/blossom.dart | 25 ++++- .../domain_layer/usecases/files/blossom.dart | 39 +++++++ .../domain_layer/usecases/files/files.dart | 31 +----- .../ndk/test/mocks/mock_blossom_server.dart | 4 +- 6 files changed, 142 insertions(+), 67 deletions(-) create mode 100644 packages/ndk/lib/domain_layer/usecases/files/blossom.dart diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 791d5dba..50d0ead0 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import '../../../domain_layer/entities/nip_01_event.dart'; import '../../../domain_layer/repositories/blossom.dart'; import '../../data_sources/http_request.dart'; @@ -18,82 +19,119 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future> uploadBlob( - Uint8List data, { + Future> uploadBlob({ + required Uint8List data, + required Nip01Event authorization, String? contentType, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, }) async { switch (strategy) { case UploadStrategy.mirrorAfterSuccess: - return _uploadWithMirroring(data, contentType); + return _uploadWithMirroring( + data: data, + contentType: contentType, + authorization: authorization, + ); case UploadStrategy.allSimultaneous: - return _uploadToAllServers(data, contentType); + return _uploadToAllServers( + data: data, + contentType: contentType, + authorization: authorization, + ); case UploadStrategy.firstSuccess: - return _uploadToFirstSuccess(data, contentType); + return _uploadToFirstSuccess( + data: data, + contentType: contentType, + authorization: authorization, + ); } } - Future> _uploadWithMirroring( - Uint8List data, + Future> _uploadWithMirroring({ + required Uint8List data, + required Nip01Event authorization, String? contentType, - ) async { + }) async { final results = []; // Try primary upload final primaryResult = await _uploadToServer( - serverUrls.first, - data, - contentType, + serverUrl: serverUrls.first, + data: data, + contentType: contentType, + authorization: authorization, ); results.add(primaryResult); if (primaryResult.success) { // Mirror to other servers - final mirrorResults = await Future.wait(serverUrls - .skip(1) - .map((url) => _uploadToServer(url, data, contentType))); + final mirrorResults = + await Future.wait(serverUrls.skip(1).map((url) => _uploadToServer( + serverUrl: url, + data: data, + contentType: contentType, + authorization: authorization, + ))); results.addAll(mirrorResults); } return results; } - Future> _uploadToAllServers( - Uint8List data, + Future> _uploadToAllServers({ + required Uint8List data, + required Nip01Event authorization, String? contentType, - ) async { - final results = await Future.wait( - serverUrls.map((url) => _uploadToServer(url, data, contentType))); + }) async { + final results = await Future.wait(serverUrls.map((url) => _uploadToServer( + serverUrl: url, + data: data, + contentType: contentType, + authorization: authorization, + ))); return results; } - Future> _uploadToFirstSuccess( - Uint8List data, + Future> _uploadToFirstSuccess({ + required Uint8List data, + required Nip01Event authorization, String? contentType, - ) async { + }) async { for (final url in serverUrls) { - final result = await _uploadToServer(url, data, contentType); + final result = await _uploadToServer( + serverUrl: url, + data: data, + contentType: contentType, + authorization: authorization, + ); if (result.success) { return [result]; } } // If all servers failed, return all errors - final results = await _uploadToAllServers(data, contentType); + final results = await _uploadToAllServers( + data: data, + contentType: contentType, + authorization: authorization, + ); return results; } - Future _uploadToServer( - String serverUrl, - Uint8List data, + Future _uploadToServer({ + required String serverUrl, + required Uint8List data, + Nip01Event? authorization, String? contentType, - ) async { + }) async { try { final response = await client.put( url: Uri.parse('$serverUrl/upload'), body: data, headers: { if (contentType != null) 'Content-Type': contentType, + if (authorization != null) + 'Authorization': "Nostr ${authorization.toBase64()}", 'Content-Length': '${data.length}', }, ); @@ -121,7 +159,10 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future getBlob(String sha256) async { + Future getBlob( + String sha256, { + Nip01Event? authorization, + }) async { Exception? lastError; for (final url in serverUrls) { @@ -148,6 +189,7 @@ class BlossomRepositoryImpl implements BlossomRepository { String pubkey, { DateTime? since, DateTime? until, + Nip01Event? authorization, }) async { Exception? lastError; @@ -177,7 +219,10 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future> deleteBlob(String sha256) async { + Future> deleteBlob({ + required String sha256, + required Nip01Event authorization, + }) async { final results = await Future.wait( serverUrls.map((url) => _deleteFromServer(url, sha256))); return results; diff --git a/packages/ndk/lib/domain_layer/entities/nip_01_event.dart b/packages/ndk/lib/domain_layer/entities/nip_01_event.dart index 82c9a06e..82ea3d01 100644 --- a/packages/ndk/lib/domain_layer/entities/nip_01_event.dart +++ b/packages/ndk/lib/domain_layer/entities/nip_01_event.dart @@ -93,6 +93,11 @@ class Nip01Event { }; } + /// Returns the Event object as a base64-encoded JSON string + String toBase64() { + return base64Encode(utf8.encode(json.encode(toJson()))); + } + /// sign the event with given privateKey /// [WARN] only for testing! Use [EventSigner] to sign events in production void sign(String privateKey) { diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart index e482c620..3992f5b5 100644 --- a/packages/ndk/lib/domain_layer/repositories/blossom.dart +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; +import 'package:ndk/domain_layer/entities/nip_01_event.dart'; + enum UploadStrategy { /// Upload to first server, then mirror to others mirrorAfterSuccess, @@ -13,21 +15,32 @@ enum UploadStrategy { abstract class BlossomRepository { /// Uploads a blob using the specified strategy - Future> uploadBlob( - Uint8List data, { + Future> uploadBlob({ + required Uint8List data, + required Nip01Event authorization, String? contentType, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, }); /// Gets a blob by trying servers sequentially until success - Future getBlob(String sha256); + Future getBlob( + String sha256, { + Nip01Event? authorization, + }); /// Lists blobs from the first successful server - Future> listBlobs(String pubkey, - {DateTime? since, DateTime? until}); + Future> listBlobs( + String pubkey, { + DateTime? since, + DateTime? until, + Nip01Event? authorization, + }); /// Attempts to delete blob from all servers - Future> deleteBlob(String sha256); + Future> deleteBlob({ + required String sha256, + required Nip01Event authorization, + }); } class BlobDescriptor { diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart new file mode 100644 index 00000000..e37e8b03 --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import '../../repositories/blossom.dart'; + +/// direct access usecase to blossom \ +/// use files usecase for a more convinent way to manage files +class Blossom { + final BlossomRepository repository; + + Blossom(this.repository); + + Future> uploadBlob({ + required Uint8List data, + String? contentType, + UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, + }) { + return repository.uploadBlob( + data, + contentType: contentType, + strategy: strategy, + ); + } + + Future getBlob(String sha256) { + return repository.getBlob(sha256); + } + + Future> listBlobs( + String pubkey, { + DateTime? since, + DateTime? until, + }) { + return repository.listBlobs(pubkey, since: since, until: until); + } + + Future delteBlob(String sha256) { + return repository.deleteBlob(sha256); + } +} diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index 4c0c0cf5..15fa2a63 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -2,38 +2,9 @@ import 'dart:typed_data'; import '../../repositories/blossom.dart'; +/// high level usecase to manage files on nostr class Files { final BlossomRepository repository; Files(this.repository); - - Future> uploadBlob({ - required Uint8List data, - String? contentType, - UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, - }) { - return repository.uploadBlob( - data, - contentType: contentType, - strategy: strategy, - ); - } - - Future getBlob(String sha256) { - return repository.getBlob(sha256); - } - - Future> listBlobs( - String pubkey, { - DateTime? since, - DateTime? until, - }) { - return repository.listBlobs(pubkey, since: since, until: until); - } - -// lib/domain/usecases/delete_blob_usecase.dart - - Future delteBlob(String sha256) { - return repository.deleteBlob(sha256); - } } diff --git a/packages/ndk/test/mocks/mock_blossom_server.dart b/packages/ndk/test/mocks/mock_blossom_server.dart index eb7cb5ca..fe85e3a9 100644 --- a/packages/ndk/test/mocks/mock_blossom_server.dart +++ b/packages/ndk/test/mocks/mock_blossom_server.dart @@ -119,7 +119,9 @@ class MockBlossomServer { } try { - final authEvent = json.decode(authHeader); + final authEvent = + json.decode(utf8.decode(base64Decode(authHeader.split(' ')[1]))); + if (!_verifyAuthEvent(authEvent, 'delete')) { return Response.forbidden('Invalid authorization event'); } From 6489d8426151d73bd5ea3f3a5b309a1372d3d460 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:40:46 +0100 Subject: [PATCH 03/31] wip: server urls as param --- packages/ndk/lib/config/blossom_config.dart | 3 + .../repositories/blossom/blossom_impl.dart | 27 +++--- .../domain_layer/repositories/blossom.dart | 12 ++- .../domain_layer/usecases/files/blossom.dart | 86 +++++++++++++++++-- .../domain_layer/usecases/files/files.dart | 13 +-- 5 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 packages/ndk/lib/config/blossom_config.dart diff --git a/packages/ndk/lib/config/blossom_config.dart b/packages/ndk/lib/config/blossom_config.dart new file mode 100644 index 00000000..d34ecded --- /dev/null +++ b/packages/ndk/lib/config/blossom_config.dart @@ -0,0 +1,3 @@ +// ignore_for_file: constant_identifier_names + +const Duration BLOSSOM_AUTH_EXPIRATION = Duration(minutes: 5); diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 50d0ead0..d82b55cc 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -7,40 +7,38 @@ import '../../data_sources/http_request.dart'; class BlossomRepositoryImpl implements BlossomRepository { final HttpRequestDS client; - final List serverUrls; BlossomRepositoryImpl({ required this.client, - required this.serverUrls, - }) { - if (serverUrls.isEmpty) { - throw ArgumentError('At least one server URL must be provided'); - } - } + }); @override Future> uploadBlob({ required Uint8List data, required Nip01Event authorization, String? contentType, + required List serverUrls, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, }) async { switch (strategy) { case UploadStrategy.mirrorAfterSuccess: return _uploadWithMirroring( data: data, + serverUrls: serverUrls, contentType: contentType, authorization: authorization, ); case UploadStrategy.allSimultaneous: return _uploadToAllServers( data: data, + serverUrls: serverUrls, contentType: contentType, authorization: authorization, ); case UploadStrategy.firstSuccess: return _uploadToFirstSuccess( data: data, + serverUrls: serverUrls, contentType: contentType, authorization: authorization, ); @@ -50,6 +48,7 @@ class BlossomRepositoryImpl implements BlossomRepository { Future> _uploadWithMirroring({ required Uint8List data, required Nip01Event authorization, + required List serverUrls, String? contentType, }) async { final results = []; @@ -80,6 +79,7 @@ class BlossomRepositoryImpl implements BlossomRepository { Future> _uploadToAllServers({ required Uint8List data, + required List serverUrls, required Nip01Event authorization, String? contentType, }) async { @@ -94,6 +94,7 @@ class BlossomRepositoryImpl implements BlossomRepository { Future> _uploadToFirstSuccess({ required Uint8List data, + required List serverUrls, required Nip01Event authorization, String? contentType, }) async { @@ -112,6 +113,7 @@ class BlossomRepositoryImpl implements BlossomRepository { // If all servers failed, return all errors final results = await _uploadToAllServers( data: data, + serverUrls: serverUrls, contentType: contentType, authorization: authorization, ); @@ -159,8 +161,9 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future getBlob( - String sha256, { + Future getBlob({ + required String sha256, + required List serverUrls, Nip01Event? authorization, }) async { Exception? lastError; @@ -185,8 +188,9 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future> listBlobs( - String pubkey, { + Future> listBlobs({ + required pubkey, + required List serverUrls, DateTime? since, DateTime? until, Nip01Event? authorization, @@ -221,6 +225,7 @@ class BlossomRepositoryImpl implements BlossomRepository { @override Future> deleteBlob({ required String sha256, + required List serverUrls, required Nip01Event authorization, }) async { final results = await Future.wait( diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart index 3992f5b5..5db561c9 100644 --- a/packages/ndk/lib/domain_layer/repositories/blossom.dart +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -19,18 +19,21 @@ abstract class BlossomRepository { required Uint8List data, required Nip01Event authorization, String? contentType, + required List serverUrls, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, }); /// Gets a blob by trying servers sequentially until success - Future getBlob( - String sha256, { + Future getBlob({ + required String sha256, + required List serverUrls, Nip01Event? authorization, }); /// Lists blobs from the first successful server - Future> listBlobs( - String pubkey, { + Future> listBlobs({ + required String pubkey, + required List serverUrls, DateTime? since, DateTime? until, Nip01Event? authorization, @@ -39,6 +42,7 @@ abstract class BlossomRepository { /// Attempts to delete blob from all servers Future> deleteBlob({ required String sha256, + required List serverUrls, required Nip01Event authorization, }); } diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index e37e8b03..16860adb 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -1,28 +1,81 @@ import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import '../../../config/blossom_config.dart'; +import '../../entities/nip_01_event.dart'; import '../../repositories/blossom.dart'; +import '../../repositories/event_signer.dart'; /// direct access usecase to blossom \ /// use files usecase for a more convinent way to manage files class Blossom { + /// kind for all most of blossom + static const kBlossom = 24242; + + /// kind for blossom user server list + static const kBlossomUserServerList = 10063; + final BlossomRepository repository; + final EventSigner signer; - Blossom(this.repository); + Blossom( + this.repository, + this.signer, + ); Future> uploadBlob({ required Uint8List data, String? contentType, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, - }) { + }) async { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + /// sha256 of the data + final dataSha256 = sha256.convert(data); + + final Nip01Event myAuthorization = Nip01Event( + content: "upload", + pubKey: signer.getPublicKey(), + kind: kBlossom, + createdAt: now, + tags: [ + ["t", "upload"], + ["x", dataSha256.toString()], + ["expiration", "${now + BLOSSOM_AUTH_EXPIRATION.inMilliseconds}"], + ], + ); + + await signer.sign(myAuthorization); + return repository.uploadBlob( - data, + data: data, + authorization: myAuthorization, contentType: contentType, strategy: strategy, ); } - Future getBlob(String sha256) { - return repository.getBlob(sha256); + Future getBlob(String sha256, {bool useAuth = false}) async { + late final Nip01Event myAuthorization; + + if (useAuth) { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + myAuthorization = Nip01Event( + content: "get", + pubKey: signer.getPublicKey(), + kind: kBlossom, + createdAt: now, + tags: [ + ["t", "get"], + ["x", sha256], + ["expiration", "${now + BLOSSOM_AUTH_EXPIRATION.inMilliseconds}"], + ], + ); + + await signer.sign(myAuthorization); + } + + return repository.getBlob(sha256, authorization: myAuthorization); } Future> listBlobs( @@ -33,7 +86,26 @@ class Blossom { return repository.listBlobs(pubkey, since: since, until: until); } - Future delteBlob(String sha256) { - return repository.deleteBlob(sha256); + Future> delteBlob(String sha256) async { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + final Nip01Event myAuthorization = Nip01Event( + content: "delete", + pubKey: signer.getPublicKey(), + kind: kBlossom, + createdAt: now, + tags: [ + ["t", "delete"], + ["x", sha256], + ["expiration", "${now + BLOSSOM_AUTH_EXPIRATION.inMilliseconds}"], + ], + ); + + await signer.sign(myAuthorization); + + return repository.deleteBlob( + sha256: sha256, + authorization: myAuthorization, + ); } } diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index 15fa2a63..4cd3cae7 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -1,10 +1,13 @@ -import 'dart:typed_data'; - -import '../../repositories/blossom.dart'; +import 'blossom.dart'; /// high level usecase to manage files on nostr class Files { - final BlossomRepository repository; + final Blossom blossom; + + Files(this.blossom); + + + + getFile({}) - Files(this.repository); } From d6b43ece2560b8bd46e77fbe04732a1dfe48d1a8 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:52:45 +0100 Subject: [PATCH 04/31] wip: serverUrls as optional arg --- .../domain_layer/usecases/files/blossom.dart | 85 +++++++++++++++++-- .../files/blossom_user_server_list.dart | 0 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 16860adb..247b22ea 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -23,8 +23,12 @@ class Blossom { this.signer, ); + /// upload a blob to the server + /// if [serverUrls] is null, the userServerList is fetched from nostr. \ + /// if the pukey has no UserServerList (kind: 10063), throws an error Future> uploadBlob({ required Uint8List data, + List? serverUrls, String? contentType, UploadStrategy strategy = UploadStrategy.mirrorAfterSuccess, }) async { @@ -47,15 +51,30 @@ class Blossom { await signer.sign(myAuthorization); + // todo: fetch user server list from nostr + + if (serverUrls == null) { + throw UnimplementedError(); + } + return repository.uploadBlob( data: data, + serverUrls: serverUrls, authorization: myAuthorization, contentType: contentType, strategy: strategy, ); } - Future getBlob(String sha256, {bool useAuth = false}) async { + /// downloads a blob + /// if [serverUrls] is null, the userServerList is fetched from nostr. \ + /// if the pukey has no UserServerList (kind: 10063), throws an error + Future getBlob({ + required String sha256, + bool useAuth = false, + List? serverUrls, + String? pubkeyToFetchUserServerList, + }) async { late final Nip01Event myAuthorization; if (useAuth) { @@ -75,18 +94,63 @@ class Blossom { await signer.sign(myAuthorization); } - return repository.getBlob(sha256, authorization: myAuthorization); + // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + + if (serverUrls == null) { + throw UnimplementedError(); + } + + return repository.getBlob( + sha256: sha256, + authorization: myAuthorization, + serverUrls: serverUrls, + ); } - Future> listBlobs( - String pubkey, { + Future> listBlobs({ + required String pubkey, + List? serverUrls, + bool useAuth = true, DateTime? since, DateTime? until, - }) { - return repository.listBlobs(pubkey, since: since, until: until); + }) async { + late final Nip01Event myAuthorization; + + if (useAuth) { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + myAuthorization = Nip01Event( + content: "List Blobs", + pubKey: signer.getPublicKey(), + kind: kBlossom, + createdAt: now, + tags: [ + ["t", "list"], + ["expiration", "${now + BLOSSOM_AUTH_EXPIRATION.inMilliseconds}"], + ], + ); + + await signer.sign(myAuthorization); + } + + // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + + if (serverUrls == null) { + throw UnimplementedError(); + } + + return repository.listBlobs( + pubkey: pubkey, + since: since, + until: until, + serverUrls: serverUrls, + authorization: myAuthorization, + ); } - Future> delteBlob(String sha256) async { + Future> delteBlob({ + required String sha256, + List? serverUrls, + }) async { final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; final Nip01Event myAuthorization = Nip01Event( @@ -103,9 +167,16 @@ class Blossom { await signer.sign(myAuthorization); + // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + + if (serverUrls == null) { + throw UnimplementedError(); + } + return repository.deleteBlob( sha256: sha256, authorization: myAuthorization, + serverUrls: serverUrls, ); } } diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart new file mode 100644 index 00000000..e69de29b From cec62520fe49ea6266130a52c891ffa875397815 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:20:45 +0100 Subject: [PATCH 05/31] basic blossom tests --- .../data_layer/data_sources/http_request.dart | 10 +- .../repositories/blossom/blossom_impl.dart | 19 ++- .../domain_layer/usecases/files/blossom.dart | 4 +- .../domain_layer/usecases/files/files.dart | 5 - .../ndk/test/mocks/mock_blossom_server.dart | 4 +- .../ndk/test/usecases/files/blossom_test.dart | 109 ++++++++++++++++++ .../ndk/test/usecases/files/files_test.dart | 82 ------------- 7 files changed, 136 insertions(+), 97 deletions(-) create mode 100644 packages/ndk/test/usecases/files/blossom_test.dart delete mode 100644 packages/ndk/test/usecases/files/files_test.dart diff --git a/packages/ndk/lib/data_layer/data_sources/http_request.dart b/packages/ndk/lib/data_layer/data_sources/http_request.dart index 43b84890..cf3601de 100644 --- a/packages/ndk/lib/data_layer/data_sources/http_request.dart +++ b/packages/ndk/lib/data_layer/data_sources/http_request.dart @@ -53,8 +53,14 @@ class HttpRequestDS { return response; } - Future delete(Uri url) async { - http.Response response = await _client.delete(url); + Future delete({ + required Uri url, + required headers, + }) async { + http.Response response = await _client.delete( + url, + headers: headers, + ); if (response.statusCode != 200) { throw Exception( diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index d82b55cc..6ebc5bd6 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -228,16 +228,25 @@ class BlossomRepositoryImpl implements BlossomRepository { required List serverUrls, required Nip01Event authorization, }) async { - final results = await Future.wait( - serverUrls.map((url) => _deleteFromServer(url, sha256))); + final results = await Future.wait(serverUrls.map((url) => _deleteFromServer( + serverUrl: url, + sha256: sha256, + authorization: authorization, + ))); return results; } - Future _deleteFromServer( - String serverUrl, String sha256) async { + Future _deleteFromServer({ + required String serverUrl, + required String sha256, + required Nip01Event authorization, + }) async { try { final response = await client.delete( - Uri.parse('$serverUrl/$sha256'), + url: Uri.parse('$serverUrl/$sha256'), + headers: { + 'Authorization': "Nostr ${authorization.toBase64()}", + }, ); return BlobDeleteResult( diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 247b22ea..15ae044b 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -75,7 +75,7 @@ class Blossom { List? serverUrls, String? pubkeyToFetchUserServerList, }) async { - late final Nip01Event myAuthorization; + Nip01Event? myAuthorization; if (useAuth) { final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; @@ -114,7 +114,7 @@ class Blossom { DateTime? since, DateTime? until, }) async { - late final Nip01Event myAuthorization; + Nip01Event? myAuthorization; if (useAuth) { final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index 4cd3cae7..cb1c8991 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -5,9 +5,4 @@ class Files { final Blossom blossom; Files(this.blossom); - - - - getFile({}) - } diff --git a/packages/ndk/test/mocks/mock_blossom_server.dart b/packages/ndk/test/mocks/mock_blossom_server.dart index fe85e3a9..ad4be1f6 100644 --- a/packages/ndk/test/mocks/mock_blossom_server.dart +++ b/packages/ndk/test/mocks/mock_blossom_server.dart @@ -41,12 +41,14 @@ class MockBlossomServer { router.put('/upload', (Request request) async { // Check for authorization header final authHeader = request.headers['authorization']; + if (authHeader == null) { return Response.forbidden('Missing authorization'); } try { - final authEvent = json.decode(authHeader); + final authEvent = + json.decode(utf8.decode(base64Decode(authHeader.split(' ')[1]))); if (!_verifyAuthEvent(authEvent, 'upload')) { return Response.forbidden('Invalid authorization event'); } diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart new file mode 100644 index 00000000..ed38b302 --- /dev/null +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; + +import 'package:ndk/data_layer/data_sources/http_request.dart'; +import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; +import 'package:ndk/data_layer/repositories/signers/bip340_event_signer.dart'; +import 'package:ndk/domain_layer/usecases/files/blossom.dart'; + +import 'package:ndk/shared/nips/nip01/bip340.dart'; +import 'package:ndk/shared/nips/nip01/key_pair.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_blossom_server.dart'; + +void main() { + late MockBlossomServer server; + late Blossom client; + + setUp(() async { + server = MockBlossomServer(port: 3000); + await server.start(); + + final blossomRepo = BlossomRepositoryImpl( + client: HttpRequestDS(http.Client()), + ); + + KeyPair key1 = Bip340.generatePrivateKey(); + final signer = Bip340EventSigner( + privateKey: key1.privateKey, publicKey: key1.publicKey); + client = Blossom(blossomRepo, signer); + }); + + tearDown(() async { + await server.stop(); + }); + + group('Blossom Client Integration Tests', () { + test('Upload and retrieve blob', () async { + final testData = Uint8List.fromList(utf8.encode('Hello, Blossom!')); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Retrieve blob + final getResponse = await client.getBlob( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + ); + expect(utf8.decode(getResponse), equals('Hello, Blossom!')); + }); + + test('List blobs for user', () async { + // Upload some test blobs first + final testData1 = Uint8List.fromList(utf8.encode('Test 1')); + final testData2 = Uint8List.fromList(utf8.encode('Test 2')); + + await client.uploadBlob( + data: testData1, + serverUrls: ['http://localhost:3000'], + ); + await client.uploadBlob( + data: testData2, + serverUrls: ['http://localhost:3000'], + ); + + final listResponse = await client.listBlobs( + pubkey: 'test_pubkey', + serverUrls: ['http://localhost:3000'], + ); + + expect(listResponse.length, equals(2)); + }); + + test('Delete blob', () async { + final testData = Uint8List.fromList(utf8.encode('Hello, Blossom!')); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Delete blob + final deleteResponse = await client.delteBlob( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + ); + expect(deleteResponse.first.success, true); + + // Retrieve blob + final getResponse = client.getBlob( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + ); + //check that something throws an error + expect(getResponse, throwsException); + }); + }); +} diff --git a/packages/ndk/test/usecases/files/files_test.dart b/packages/ndk/test/usecases/files/files_test.dart deleted file mode 100644 index ed2860c4..00000000 --- a/packages/ndk/test/usecases/files/files_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:http/http.dart' as http; - -import 'package:ndk/data_layer/data_sources/http_request.dart'; -import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; -import 'package:ndk/domain_layer/usecases/files/files.dart'; -import 'package:test/test.dart'; - -import '../../mocks/mock_blossom_server.dart'; - -void main() { - late MockBlossomServer server; - late Files client; - - setUp(() async { - server = MockBlossomServer(port: 3000); - await server.start(); - - final blossomRepo = BlossomRepositoryImpl( - client: HttpRequestDS(http.Client()), - serverUrls: ['http://localhost:3000'], - ); - client = Files(blossomRepo); - }); - - tearDown(() async { - await server.stop(); - }); - - group('Blossom Client Integration Tests', () { - test('Upload and retrieve blob', () async { - final testData = Uint8List.fromList(utf8.encode('Hello, Blossom!')); - final authEvent = createTestAuthEvent('upload'); - - // Upload blob - final uploadResponse = await client.uploadBlob( - data: testData, - //authorization: authEvent, - ); - expect(uploadResponse.first.success, true); - - final sha256 = uploadResponse.first.descriptor!.sha256; - - // Retrieve blob - final getResponse = await client.getBlob(sha256); - expect(utf8.decode(getResponse), equals('Hello, Blossom!')); - }); - - test('List blobs for user', () async { - // Upload some test blobs first - final testData1 = Uint8List.fromList(utf8.encode('Test 1')); - final testData2 = Uint8List.fromList(utf8.encode('Test 2')); - - await client.uploadBlob( - data: testData1, - //authorization: createTestAuthEvent('upload'), - ); - await client.uploadBlob( - data: testData2, - //authorization: createTestAuthEvent('upload'), - ); - - final listResponse = await client.listBlobs('test_pubkey'); - - expect(listResponse.length, equals(2)); - }); - }); -} - -Map createTestAuthEvent(String type) { - return { - 'kind': 24242, - 'pubkey': 'test_pubkey', - 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'tags': [ - ['t', type], - ['x', 'test_hash'], - ], - 'sig': 'test_signature', - }; -} From a09f1cbf44ea3c433be5a5bd964dff102128fe86 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:04:32 +0100 Subject: [PATCH 06/31] test with dead and unavailable data --- .../ndk/test/usecases/files/blossom_test.dart | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index ed38b302..06f8bade 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -15,11 +15,14 @@ import '../../mocks/mock_blossom_server.dart'; void main() { late MockBlossomServer server; + late MockBlossomServer server2; late Blossom client; setUp(() async { server = MockBlossomServer(port: 3000); + server2 = MockBlossomServer(port: 3001); await server.start(); + await server2.start(); final blossomRepo = BlossomRepositoryImpl( client: HttpRequestDS(http.Client()), @@ -33,6 +36,7 @@ void main() { tearDown(() async { await server.stop(); + await server2.stop(); }); group('Blossom Client Integration Tests', () { @@ -56,6 +60,30 @@ void main() { expect(utf8.decode(getResponse), equals('Hello, Blossom!')); }); + test('Upload and retrieve blob - one out of three', () async { + final testData = Uint8List.fromList(utf8.encode('Hello World!')); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Retrieve blob + final getResponse = await client.getBlob( + sha256: sha256, + serverUrls: [ + 'http://dead.example.com', + 'http://localhost:3001', + 'http://localhost:3000', + ], + ); + expect(utf8.decode(getResponse), equals('Hello World!')); + }); + test('List blobs for user', () async { // Upload some test blobs first final testData1 = Uint8List.fromList(utf8.encode('Test 1')); From d20c9bdf1125faaf5f60c39170ea811c33ca647a Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:43:45 +0100 Subject: [PATCH 07/31] basic getUserServerList --- .../domain_layer/usecases/files/blossom.dart | 49 ++++++++++++------- .../files/blossom_user_server_list.dart | 44 +++++++++++++++++ .../ndk/test/usecases/files/blossom_test.dart | 21 +++++++- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 15ae044b..0e92e08a 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -5,6 +5,7 @@ import '../../../config/blossom_config.dart'; import '../../entities/nip_01_event.dart'; import '../../repositories/blossom.dart'; import '../../repositories/event_signer.dart'; +import 'blossom_user_server_list.dart'; /// direct access usecase to blossom \ /// use files usecase for a more convinent way to manage files @@ -15,13 +16,15 @@ class Blossom { /// kind for blossom user server list static const kBlossomUserServerList = 10063; - final BlossomRepository repository; + final BlossomUserServerList userServerList; + final BlossomRepository blossomImpl; final EventSigner signer; - Blossom( - this.repository, - this.signer, - ); + Blossom({ + required this.userServerList, + required this.blossomImpl, + required this.signer, + }); /// upload a blob to the server /// if [serverUrls] is null, the userServerList is fetched from nostr. \ @@ -51,13 +54,14 @@ class Blossom { await signer.sign(myAuthorization); - // todo: fetch user server list from nostr + serverUrls ??= await userServerList + .getUserServerList(pubkeys: [signer.getPublicKey()]); if (serverUrls == null) { - throw UnimplementedError(); + throw "User has no server list"; } - return repository.uploadBlob( + return blossomImpl.uploadBlob( data: data, serverUrls: serverUrls, authorization: myAuthorization, @@ -94,13 +98,20 @@ class Blossom { await signer.sign(myAuthorization); } - // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + if (serverUrls == null) { + if (pubkeyToFetchUserServerList == null) { + throw "pubkeyToFetchUserServerList is null and serverUrls is null"; + } + + serverUrls ??= await userServerList + .getUserServerList(pubkeys: [pubkeyToFetchUserServerList]); + } if (serverUrls == null) { - throw UnimplementedError(); + throw "User has no server list"; } - return repository.getBlob( + return blossomImpl.getBlob( sha256: sha256, authorization: myAuthorization, serverUrls: serverUrls, @@ -132,13 +143,14 @@ class Blossom { await signer.sign(myAuthorization); } - // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + /// fetch user server list from nostr + serverUrls ??= await userServerList.getUserServerList(pubkeys: [pubkey]); if (serverUrls == null) { - throw UnimplementedError(); + throw "User has no server list: $pubkey"; } - return repository.listBlobs( + return blossomImpl.listBlobs( pubkey: pubkey, since: since, until: until, @@ -167,13 +179,14 @@ class Blossom { await signer.sign(myAuthorization); - // todo: fetch user server list from nostr for user [pubkeyToFetchUserServerList] + /// fetch user server list from nostr + serverUrls ??= await userServerList + .getUserServerList(pubkeys: [signer.getPublicKey()]); if (serverUrls == null) { - throw UnimplementedError(); + throw "User has no server list"; } - - return repository.deleteBlob( + return blossomImpl.deleteBlob( sha256: sha256, authorization: myAuthorization, serverUrls: serverUrls, diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart index e69de29b..80b0f0ca 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom_user_server_list.dart @@ -0,0 +1,44 @@ +import '../../entities/filter.dart'; +import '../requests/requests.dart'; +import 'blossom.dart'; + +class BlossomUserServerList { + final Requests requests; + + BlossomUserServerList( + this.requests, + ); + + /// Get user server list + /// returns list of server urls + /// returns null if the user has no server list + Future?> getUserServerList({ + required List pubkeys, + }) async { + final rsp = requests.query( + filters: [ + Filter( + authors: pubkeys, + kinds: [Blossom.kBlossomUserServerList], + ) + ], + ); + + final data = await rsp.future; + data.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + if (data.isEmpty) { + return null; + } + + final List foundServers = []; + + for (final tag in data.first.tags) { + if (tag.length > 1 && tag[0] == 'server') { + foundServers.add(tag[1]); + } + } + + return foundServers; + } +} diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index 06f8bade..104a8a59 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -4,14 +4,16 @@ import 'package:http/http.dart' as http; import 'package:ndk/data_layer/data_sources/http_request.dart'; import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; -import 'package:ndk/data_layer/repositories/signers/bip340_event_signer.dart'; import 'package:ndk/domain_layer/usecases/files/blossom.dart'; +import 'package:ndk/domain_layer/usecases/files/blossom_user_server_list.dart'; +import 'package:ndk/ndk.dart'; import 'package:ndk/shared/nips/nip01/bip340.dart'; import 'package:ndk/shared/nips/nip01/key_pair.dart'; import 'package:test/test.dart'; import '../../mocks/mock_blossom_server.dart'; +import '../../mocks/mock_event_verifier.dart'; void main() { late MockBlossomServer server; @@ -31,7 +33,22 @@ void main() { KeyPair key1 = Bip340.generatePrivateKey(); final signer = Bip340EventSigner( privateKey: key1.privateKey, publicKey: key1.publicKey); - client = Blossom(blossomRepo, signer); + + final ndk = Ndk( + NdkConfig( + eventVerifier: MockEventVerifier(), + cache: MemCacheManager(), + engine: NdkEngine.JIT, + ), + ); + + final BlossomUserServerList blossomUserServerList = + BlossomUserServerList(ndk.requests); + client = Blossom( + blossomImpl: blossomRepo, + signer: signer, + userServerList: blossomUserServerList, + ); }); tearDown(() async { From 1b52fd37579df965d5470ff74b727def417478a9 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:08:18 +0100 Subject: [PATCH 08/31] stream impl --- .../data_layer/data_sources/http_request.dart | 27 ++++- .../repositories/blossom/blossom_impl.dart | 109 +++++++++++++++++- .../domain_layer/entities/blossom_blobs.dart | 58 ++++++++++ .../domain_layer/repositories/blossom.dart | 77 +++++-------- .../domain_layer/usecases/files/blossom.dart | 3 +- .../domain_layer/usecases/files/files.dart | 4 + .../ndk/test/usecases/files/blossom_test.dart | 4 +- 7 files changed, 221 insertions(+), 61 deletions(-) create mode 100644 packages/ndk/lib/domain_layer/entities/blossom_blobs.dart diff --git a/packages/ndk/lib/data_layer/data_sources/http_request.dart b/packages/ndk/lib/data_layer/data_sources/http_request.dart index cf3601de..3d748eba 100644 --- a/packages/ndk/lib/data_layer/data_sources/http_request.dart +++ b/packages/ndk/lib/data_layer/data_sources/http_request.dart @@ -42,8 +42,31 @@ class HttpRequestDS { return response; } - Future get(Uri url) async { - http.Response response = await _client.get(url); + Future head({ + required Uri url, + headers, + }) async { + http.Response response = await _client.head( + url, + headers: headers, + ); + + if (response.statusCode != 200) { + throw Exception( + "error fetching STATUS: ${response.statusCode}, Link: $url"); + } + + return response; + } + + Future get({ + required Uri url, + headers, + }) async { + http.Response response = await _client.get( + url, + headers: headers, + ); if (response.statusCode != 200) { throw Exception( diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 6ebc5bd6..392144af 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -1,7 +1,10 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import '../../../domain_layer/entities/blossom_blobs.dart'; import '../../../domain_layer/entities/nip_01_event.dart'; +import '../../../domain_layer/entities/tuple.dart'; import '../../../domain_layer/repositories/blossom.dart'; import '../../data_sources/http_request.dart'; @@ -161,21 +164,35 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future getBlob({ + Future getBlob({ required String sha256, required List serverUrls, Nip01Event? authorization, + int? start, + int? end, }) async { Exception? lastError; for (final url in serverUrls) { try { + final headers = {}; + if (start != null) { + // Create range header in format "bytes=start-end" + // If end is null, it means "until the end of the file" + headers['range'] = 'bytes=$start-${end ?? ''}'; + } + final response = await client.get( - Uri.parse('$url/$sha256'), + url: Uri.parse('$url/$sha256'), + headers: headers, ); - if (response.statusCode == 200) { - return response.bodyBytes; + // Check for both 200 (full content) and 206 (partial content) status codes + if (response.statusCode == 200 || response.statusCode == 206) { + return BlossomBlobResponse( + data: response.bodyBytes, + mimeType: response.headers['content-type'], + ); } lastError = Exception('HTTP ${response.statusCode}'); } catch (e) { @@ -187,6 +204,87 @@ class BlossomRepositoryImpl implements BlossomRepository { 'Failed to get blob from all servers. Last error: $lastError'); } + /// first value is whether the server supports range requests \ + /// second value is the content length of the blob in bytes + @override + Future> supportsRangeRequests({ + required String sha256, + required String serverUrl, + }) async { + try { + final response = await client.head( + url: Uri.parse('$serverUrl/$sha256'), + ); + + final acceptRanges = response.headers['accept-ranges']; + final contentLength = + int.tryParse(response.headers['content-length'] ?? ''); + return Tuple(acceptRanges?.toLowerCase() == 'bytes', contentLength); + } catch (e) { + return Tuple(false, null); + } + } + + @override + Future> getBlobStream({ + required String sha256, + required List serverUrls, + Nip01Event? authorization, + int chunkSize = 1024 * 1024, // 1MB chunks + }) async { + // Find a server that supports range requests + String? supportedServer; + int? contentLength; + + for (final url in serverUrls) { + try { + final rangeResponse = await supportsRangeRequests( + sha256: sha256, + serverUrl: url, + ); + if (rangeResponse.first) { + supportedServer = url; + contentLength = rangeResponse.second; + break; + } + } catch (_) { + continue; + } + } + + if (supportedServer == null || contentLength == null) { + // Fallback to regular download if no server supports range requests + final bytes = await getBlob(sha256: sha256, serverUrls: serverUrls); + return Stream.value(bytes); + } + + // Create a stream controller to manage the chunks + final controller = StreamController(); + + // Start downloading chunks + int offset = 0; + while (offset < contentLength) { + final end = (offset + chunkSize - 1).clamp(0, contentLength - 1); + + try { + final chunk = await getBlob( + sha256: sha256, + serverUrls: [supportedServer], + start: offset, + end: end, + ); + controller.add(chunk); + offset = end + 1; + } catch (e) { + await controller.close(); + rethrow; + } + } + + await controller.close(); + return controller.stream; + } + @override Future> listBlobs({ required pubkey, @@ -205,7 +303,8 @@ class BlossomRepositoryImpl implements BlossomRepository { }; final response = await client.get( - Uri.parse('$url/list/$pubkey').replace(queryParameters: queryParams), + url: Uri.parse('$url/list/$pubkey') + .replace(queryParameters: queryParams), ); if (response.statusCode == 200) { diff --git a/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart new file mode 100644 index 00000000..254c9419 --- /dev/null +++ b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; + +class BlobDescriptor { + final String url; + final String sha256; + final int size; + final String? type; + final DateTime uploaded; + + BlobDescriptor( + {required this.url, + required this.sha256, + required this.size, + this.type, + required this.uploaded}); + + factory BlobDescriptor.fromJson(Map json) { + return BlobDescriptor( + url: json['url'], + sha256: json['sha256'], + size: json['size'], + type: json['type'], + uploaded: DateTime.fromMillisecondsSinceEpoch(json['uploaded'] * 1000)); + } +} + +class BlobUploadResult { + final String serverUrl; + final bool success; + final BlobDescriptor? descriptor; + final String? error; + + BlobUploadResult({ + required this.serverUrl, + required this.success, + this.descriptor, + this.error, + }); +} + +class BlobDeleteResult { + final String serverUrl; + final bool success; + final String? error; + + BlobDeleteResult({ + required this.serverUrl, + required this.success, + this.error, + }); +} + +class BlossomBlobResponse { + final Uint8List data; + final String? mimeType; + + BlossomBlobResponse({required this.data, this.mimeType}); +} diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart index 5db561c9..6bbf8c62 100644 --- a/packages/ndk/lib/domain_layer/repositories/blossom.dart +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -2,6 +2,9 @@ import 'dart:typed_data'; import 'package:ndk/domain_layer/entities/nip_01_event.dart'; +import '../entities/blossom_blobs.dart'; +import '../entities/tuple.dart'; + enum UploadStrategy { /// Upload to first server, then mirror to others mirrorAfterSuccess, @@ -24,10 +27,32 @@ abstract class BlossomRepository { }); /// Gets a blob by trying servers sequentially until success - Future getBlob({ + /// If [authorization] is null, the server must be public + /// If [start] and [end] are null, the entire blob is returned + /// [start] and [end] are used to download a range of bytes, @see MDN HTTP range requests + Future getBlob({ + required String sha256, + required List serverUrls, + Nip01Event? authorization, + int? start, + int? end, + }); + + /// checks if the server supports range requests, if no server supports range requests, the entire blob is returned + /// otherwise, the blob is returned in chunks. @see MDN HTTP range requests + Future> getBlobStream({ required String sha256, required List serverUrls, Nip01Event? authorization, + int chunkSize = 1024 * 1024, // 1MB chunks + }); + + /// Checks if the server supports range requests and gets the content length \ + /// first value is whether the server supports range requests \ + /// second value is the content length of the blob in bytes + Future> supportsRangeRequests({ + required String sha256, + required String serverUrl, }); /// Lists blobs from the first successful server @@ -46,53 +71,3 @@ abstract class BlossomRepository { required Nip01Event authorization, }); } - -class BlobDescriptor { - final String url; - final String sha256; - final int size; - final String? type; - final DateTime uploaded; - - BlobDescriptor( - {required this.url, - required this.sha256, - required this.size, - this.type, - required this.uploaded}); - - factory BlobDescriptor.fromJson(Map json) { - return BlobDescriptor( - url: json['url'], - sha256: json['sha256'], - size: json['size'], - type: json['type'], - uploaded: DateTime.fromMillisecondsSinceEpoch(json['uploaded'] * 1000)); - } -} - -class BlobUploadResult { - final String serverUrl; - final bool success; - final BlobDescriptor? descriptor; - final String? error; - - BlobUploadResult({ - required this.serverUrl, - required this.success, - this.descriptor, - this.error, - }); -} - -class BlobDeleteResult { - final String serverUrl; - final bool success; - final String? error; - - BlobDeleteResult({ - required this.serverUrl, - required this.success, - this.error, - }); -} diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 0e92e08a..bf9ba1ad 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart'; +import 'package:ndk/domain_layer/entities/blossom_blobs.dart'; import '../../../config/blossom_config.dart'; import '../../entities/nip_01_event.dart'; @@ -73,7 +74,7 @@ class Blossom { /// downloads a blob /// if [serverUrls] is null, the userServerList is fetched from nostr. \ /// if the pukey has no UserServerList (kind: 10063), throws an error - Future getBlob({ + Future getBlob({ required String sha256, bool useAuth = false, List? serverUrls, diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index cb1c8991..eafbf7c6 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -5,4 +5,8 @@ class Files { final Blossom blossom; Files(this.blossom); + + upload() {} + + download() {} } diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index 104a8a59..b8cb7a9c 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -74,7 +74,7 @@ void main() { sha256: sha256, serverUrls: ['http://localhost:3000'], ); - expect(utf8.decode(getResponse), equals('Hello, Blossom!')); + expect(utf8.decode(getResponse.data), equals('Hello, Blossom!')); }); test('Upload and retrieve blob - one out of three', () async { @@ -98,7 +98,7 @@ void main() { 'http://localhost:3000', ], ); - expect(utf8.decode(getResponse), equals('Hello World!')); + expect(utf8.decode(getResponse.data), equals('Hello World!')); }); test('List blobs for user', () async { From 4e3d9abad93deb9d97e3903c2c7b403f72a57b4d Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:12:53 +0100 Subject: [PATCH 09/31] include content -range -length --- .../repositories/blossom/blossom_impl.dart | 3 +++ .../domain_layer/entities/blossom_blobs.dart | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 392144af..97a038db 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -192,6 +192,9 @@ class BlossomRepositoryImpl implements BlossomRepository { return BlossomBlobResponse( data: response.bodyBytes, mimeType: response.headers['content-type'], + contentLength: + int.tryParse(response.headers['content-length'] ?? ''), + contentRange: response.headers['content-range'], ); } lastError = Exception('HTTP ${response.statusCode}'); diff --git a/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart index 254c9419..e4b8234e 100644 --- a/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart +++ b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart @@ -7,12 +7,13 @@ class BlobDescriptor { final String? type; final DateTime uploaded; - BlobDescriptor( - {required this.url, - required this.sha256, - required this.size, - this.type, - required this.uploaded}); + BlobDescriptor({ + required this.url, + required this.sha256, + required this.size, + this.type, + required this.uploaded, + }); factory BlobDescriptor.fromJson(Map json) { return BlobDescriptor( @@ -53,6 +54,13 @@ class BlobDeleteResult { class BlossomBlobResponse { final Uint8List data; final String? mimeType; + final int? contentLength; + final String? contentRange; - BlossomBlobResponse({required this.data, this.mimeType}); + BlossomBlobResponse({ + required this.data, + this.mimeType, + this.contentLength, + this.contentRange, + }); } From 820ca020582aa6fa441b6955501bf861ee2da9fc Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:43:29 +0100 Subject: [PATCH 10/31] files usecase --- .../repositories/blossom/blossom_impl.dart | 23 ++++++-- .../domain_layer/entities/blossom_blobs.dart | 4 +- .../lib/domain_layer/entities/ndk_file.dart | 15 +++++ .../domain_layer/repositories/blossom.dart | 9 ++- .../domain_layer/usecases/files/blossom.dart | 11 +++- .../domain_layer/usecases/files/files.dart | 57 ++++++++++++++++++- .../ndk/test/usecases/files/blossom_test.dart | 2 +- 7 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 packages/ndk/lib/domain_layer/entities/ndk_file.dart diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 97a038db..3c47cda4 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -164,7 +164,7 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future getBlob({ + Future getBlob({ required String sha256, required List serverUrls, Nip01Event? authorization, @@ -189,12 +189,12 @@ class BlossomRepositoryImpl implements BlossomRepository { // Check for both 200 (full content) and 206 (partial content) status codes if (response.statusCode == 200 || response.statusCode == 206) { - return BlossomBlobResponse( + return BlobResponse( data: response.bodyBytes, mimeType: response.headers['content-type'], contentLength: int.tryParse(response.headers['content-length'] ?? ''), - contentRange: response.headers['content-range'], + contentRange: response.headers['content-range'] ?? '', ); } lastError = Exception('HTTP ${response.statusCode}'); @@ -229,7 +229,7 @@ class BlossomRepositoryImpl implements BlossomRepository { } @override - Future> getBlobStream({ + Future> getBlobStream({ required String sha256, required List serverUrls, Nip01Event? authorization, @@ -262,7 +262,7 @@ class BlossomRepositoryImpl implements BlossomRepository { } // Create a stream controller to manage the chunks - final controller = StreamController(); + final controller = StreamController(); // Start downloading chunks int offset = 0; @@ -365,4 +365,17 @@ class BlossomRepositoryImpl implements BlossomRepository { ); } } + + @override + Future directDownload({ + required Uri url, + }) async { + final response = await client.get(url: url); + return BlobResponse( + data: response.bodyBytes, + mimeType: response.headers['content-type'], + contentLength: int.tryParse(response.headers['content-length'] ?? ''), + contentRange: response.headers['content-range'] ?? '', + ); + } } diff --git a/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart index e4b8234e..5f21c7ee 100644 --- a/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart +++ b/packages/ndk/lib/domain_layer/entities/blossom_blobs.dart @@ -51,13 +51,13 @@ class BlobDeleteResult { }); } -class BlossomBlobResponse { +class BlobResponse { final Uint8List data; final String? mimeType; final int? contentLength; final String? contentRange; - BlossomBlobResponse({ + BlobResponse({ required this.data, this.mimeType, this.contentLength, diff --git a/packages/ndk/lib/domain_layer/entities/ndk_file.dart b/packages/ndk/lib/domain_layer/entities/ndk_file.dart new file mode 100644 index 00000000..74e3f6d1 --- /dev/null +++ b/packages/ndk/lib/domain_layer/entities/ndk_file.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +class NdkFile { + final Uint8List data; + final String sha256; + final String? mimeType; + final int? size; + + NdkFile({ + required this.data, + this.mimeType, + this.size, + required this.sha256, + }); +} diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart index 6bbf8c62..a1466dd2 100644 --- a/packages/ndk/lib/domain_layer/repositories/blossom.dart +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -30,7 +30,7 @@ abstract class BlossomRepository { /// If [authorization] is null, the server must be public /// If [start] and [end] are null, the entire blob is returned /// [start] and [end] are used to download a range of bytes, @see MDN HTTP range requests - Future getBlob({ + Future getBlob({ required String sha256, required List serverUrls, Nip01Event? authorization, @@ -38,9 +38,14 @@ abstract class BlossomRepository { int? end, }); + /// Directly downloads a blob from the url, without blossom + Future directDownload({ + required Uri url, + }); + /// checks if the server supports range requests, if no server supports range requests, the entire blob is returned /// otherwise, the blob is returned in chunks. @see MDN HTTP range requests - Future> getBlobStream({ + Future> getBlobStream({ required String sha256, required List serverUrls, Nip01Event? authorization, diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index bf9ba1ad..9014131e 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -74,7 +74,7 @@ class Blossom { /// downloads a blob /// if [serverUrls] is null, the userServerList is fetched from nostr. \ /// if the pukey has no UserServerList (kind: 10063), throws an error - Future getBlob({ + Future getBlob({ required String sha256, bool useAuth = false, List? serverUrls, @@ -160,7 +160,7 @@ class Blossom { ); } - Future> delteBlob({ + Future> deleteBlob({ required String sha256, List? serverUrls, }) async { @@ -193,4 +193,11 @@ class Blossom { serverUrls: serverUrls, ); } + + /// Directly downloads a blob from the url, without blossom + Future directDownload({ + required Uri url, + }) { + return blossomImpl.directDownload(url: url); + } } diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index eafbf7c6..4a96ff1f 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -1,3 +1,5 @@ +import '../../entities/blossom_blobs.dart'; +import '../../entities/ndk_file.dart'; import 'blossom.dart'; /// high level usecase to manage files on nostr @@ -6,7 +8,58 @@ class Files { Files(this.blossom); - upload() {} + /// upload a file to the server(s) \ + /// if [serverUrls] is null, the userServerList is fetched from nostr. \ + /// if no serverUrls (param or nostr) are found, throws an error + Future> upload( + NdkFile file, + List? serverUrls, + ) { + return blossom.uploadBlob( + data: file.data, + serverUrls: serverUrls, + contentType: file.mimeType, + ); + } - download() {} + /// deletes a file from the server(s) \ + /// if [serverUrls] is null, the userServerList is fetched from nostr. \ + Future> delete({ + required String sha256, + List? serverUrls, + }) { + return blossom.deleteBlob( + sha256: sha256, + serverUrls: serverUrls, + ); + } + + /// download a file from the server(s) \ + /// if its a blossom url (sha256 in url), blossom is used to download \ + /// if its a public url, the file is downloaded directly \ + /// \ + /// [serverUrls] and [pubkey] are used to download from blossom \ + /// if [serverUrls] is null, the userServerList is fetched from nostr (using the pubkey). \ + /// if both [serverUrls] and [pubkey] are null, throws an error. + Future download( + String url, + List? serverUrls, + String? pubkey, + ) async { + // Regular expression to match SHA256 in URLs + final sha256Match = RegExp(r'/([a-fA-F0-9]{64})(?:/|$)').firstMatch(url); + + if (sha256Match != null) { + // This is a blossom URL, handle it using blossom protocol + final sha256 = sha256Match.group(1)!; + + // Try to download using blossom + return await blossom.getBlob( + sha256: sha256, + serverUrls: serverUrls, + pubkeyToFetchUserServerList: pubkey); + } else { + return await blossom.directDownload(url: Uri.parse(url)); + } + } } diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index b8cb7a9c..e2ee5806 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -136,7 +136,7 @@ void main() { final sha256 = uploadResponse.first.descriptor!.sha256; // Delete blob - final deleteResponse = await client.delteBlob( + final deleteResponse = await client.deleteBlob( sha256: sha256, serverUrls: ['http://localhost:3000'], ); From 1f3eafaa761c6254df9821cb66eddf14c8a3b430 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:54:28 +0100 Subject: [PATCH 11/31] signer optional --- .../domain_layer/usecases/files/blossom.dart | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 9014131e..6730c4be 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -19,7 +19,7 @@ class Blossom { final BlossomUserServerList userServerList; final BlossomRepository blossomImpl; - final EventSigner signer; + final EventSigner? signer; Blossom({ required this.userServerList, @@ -27,6 +27,12 @@ class Blossom { required this.signer, }); + _checkSigner() { + if (signer == null) { + throw "Signer is null"; + } + } + /// upload a blob to the server /// if [serverUrls] is null, the userServerList is fetched from nostr. \ /// if the pukey has no UserServerList (kind: 10063), throws an error @@ -41,9 +47,11 @@ class Blossom { /// sha256 of the data final dataSha256 = sha256.convert(data); + _checkSigner(); + final Nip01Event myAuthorization = Nip01Event( content: "upload", - pubKey: signer.getPublicKey(), + pubKey: signer!.getPublicKey(), kind: kBlossom, createdAt: now, tags: [ @@ -53,10 +61,10 @@ class Blossom { ], ); - await signer.sign(myAuthorization); + await signer!.sign(myAuthorization); serverUrls ??= await userServerList - .getUserServerList(pubkeys: [signer.getPublicKey()]); + .getUserServerList(pubkeys: [signer!.getPublicKey()]); if (serverUrls == null) { throw "User has no server list"; @@ -83,10 +91,12 @@ class Blossom { Nip01Event? myAuthorization; if (useAuth) { + _checkSigner(); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; myAuthorization = Nip01Event( content: "get", - pubKey: signer.getPublicKey(), + pubKey: signer!.getPublicKey(), kind: kBlossom, createdAt: now, tags: [ @@ -96,7 +106,7 @@ class Blossom { ], ); - await signer.sign(myAuthorization); + await signer!.sign(myAuthorization); } if (serverUrls == null) { @@ -129,10 +139,12 @@ class Blossom { Nip01Event? myAuthorization; if (useAuth) { + _checkSigner(); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; myAuthorization = Nip01Event( content: "List Blobs", - pubKey: signer.getPublicKey(), + pubKey: signer!.getPublicKey(), kind: kBlossom, createdAt: now, tags: [ @@ -141,7 +153,7 @@ class Blossom { ], ); - await signer.sign(myAuthorization); + await signer!.sign(myAuthorization); } /// fetch user server list from nostr @@ -166,9 +178,11 @@ class Blossom { }) async { final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + _checkSigner(); + final Nip01Event myAuthorization = Nip01Event( content: "delete", - pubKey: signer.getPublicKey(), + pubKey: signer!.getPublicKey(), kind: kBlossom, createdAt: now, tags: [ @@ -178,11 +192,11 @@ class Blossom { ], ); - await signer.sign(myAuthorization); + await signer!.sign(myAuthorization); /// fetch user server list from nostr serverUrls ??= await userServerList - .getUserServerList(pubkeys: [signer.getPublicKey()]); + .getUserServerList(pubkeys: [signer!.getPublicKey()]); if (serverUrls == null) { throw "User has no server list"; From a510cac87d1bd1e8cf81bebdbcc046aebaf60021 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:55:01 +0100 Subject: [PATCH 12/31] expose to outside world --- packages/ndk/lib/entities.dart | 1 + packages/ndk/lib/ndk.dart | 3 +++ packages/ndk/lib/presentation_layer/init.dart | 26 +++++++++++++++++-- packages/ndk/lib/presentation_layer/ndk.dart | 12 +++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/ndk/lib/entities.dart b/packages/ndk/lib/entities.dart index 2b3a75ae..e91bc554 100644 --- a/packages/ndk/lib/entities.dart +++ b/packages/ndk/lib/entities.dart @@ -28,3 +28,4 @@ export 'domain_layer/entities/request_response.dart'; export 'domain_layer/entities/request_state.dart'; export 'domain_layer/entities/tuple.dart'; export 'domain_layer/entities/user_relay_list.dart'; +export 'domain_layer/entities/blossom_blobs.dart'; diff --git a/packages/ndk/lib/ndk.dart b/packages/ndk/lib/ndk.dart index 0d1e69f5..17025569 100644 --- a/packages/ndk/lib/ndk.dart +++ b/packages/ndk/lib/ndk.dart @@ -35,6 +35,7 @@ export 'domain_layer/usecases/nwc/responses/pay_invoice_response.dart'; export 'domain_layer/usecases/nwc/responses/list_transactions_response.dart'; export 'domain_layer/usecases/nwc/responses/lookup_invoice_response.dart'; export 'domain_layer/usecases/nwc/nwc_connection.dart'; +export 'domain_layer/entities/blossom_blobs.dart'; /** * export classes that need to be injected @@ -70,6 +71,8 @@ export 'domain_layer/usecases/zaps/zaps.dart'; export 'domain_layer/usecases/zaps/zap_request.dart'; export 'domain_layer/usecases/zaps/zap_receipt.dart'; export 'domain_layer/usecases/zaps/invoice_response.dart'; +export 'domain_layer/usecases/files/files.dart'; +export 'domain_layer/usecases/files/blossom.dart'; /** * other stuff diff --git a/packages/ndk/lib/presentation_layer/init.dart b/packages/ndk/lib/presentation_layer/init.dart index 6c5b85b0..d30fbd60 100644 --- a/packages/ndk/lib/presentation_layer/init.dart +++ b/packages/ndk/lib/presentation_layer/init.dart @@ -1,17 +1,22 @@ import 'package:http/http.dart' as http; -import 'package:ndk/data_layer/repositories/lnurl_http_impl.dart'; -import 'package:ndk/domain_layer/repositories/lnurl_transport.dart'; import '../data_layer/data_sources/http_request.dart'; +import '../data_layer/repositories/blossom/blossom_impl.dart'; +import '../data_layer/repositories/lnurl_http_impl.dart'; import '../data_layer/repositories/nip_05_http_impl.dart'; import '../data_layer/repositories/nostr_transport/websocket_client_nostr_transport_factory.dart'; import '../domain_layer/entities/global_state.dart'; import '../domain_layer/entities/jit_engine_relay_connectivity_data.dart'; +import '../domain_layer/repositories/blossom.dart'; +import '../domain_layer/repositories/lnurl_transport.dart'; import '../domain_layer/repositories/nip_05_repo.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; import '../domain_layer/usecases/cache_read/cache_read.dart'; import '../domain_layer/usecases/cache_write/cache_write.dart'; import '../domain_layer/usecases/engines/network_engine.dart'; +import '../domain_layer/usecases/files/blossom.dart'; +import '../domain_layer/usecases/files/blossom_user_server_list.dart'; +import '../domain_layer/usecases/files/files.dart'; import '../domain_layer/usecases/follows/follows.dart'; import '../domain_layer/usecases/jit_engine/jit_engine.dart'; import '../domain_layer/usecases/lists/lists.dart'; @@ -60,6 +65,9 @@ class Initialization { late Nwc nwc; late Zaps zaps; late Lnurl lnurl; + late Files files; + late Blossom blossom; + late BlossomUserServerList blossomUserServerList; late VerifyNip05 verifyNip05; @@ -110,6 +118,10 @@ class Initialization { final Nip05Repository nip05repository = Nip05HttpRepositoryImpl(httpDS: _httpRequestDS); + final BlossomRepository blossomRepository = BlossomRepositoryImpl( + client: _httpRequestDS, + ); + /// use cases cacheWrite = CacheWrite(_ndkConfig.cache); cacheRead = CacheRead(_ndkConfig.cache); @@ -184,6 +196,16 @@ class Initialization { lnurl: lnurl, ); + blossomUserServerList = BlossomUserServerList(requests); + + blossom = Blossom( + blossomImpl: blossomRepository, + signer: _ndkConfig.eventSigner, + userServerList: blossomUserServerList, + ); + + files = Files(blossom); + /// set the user configured log level Logger.setLogLevel(_ndkConfig.logLevel); } diff --git a/packages/ndk/lib/presentation_layer/ndk.dart b/packages/ndk/lib/presentation_layer/ndk.dart index 6900da53..12cd2bde 100644 --- a/packages/ndk/lib/presentation_layer/ndk.dart +++ b/packages/ndk/lib/presentation_layer/ndk.dart @@ -4,6 +4,8 @@ import '../data_layer/repositories/cache_manager/mem_cache_manager.dart'; import '../data_layer/repositories/verifiers/bip340_event_verifier.dart'; import '../domain_layer/entities/global_state.dart'; import '../domain_layer/usecases/broadcast/broadcast.dart'; +import '../domain_layer/usecases/files/blossom.dart'; +import '../domain_layer/usecases/files/files.dart'; import '../domain_layer/usecases/follows/follows.dart'; import '../domain_layer/usecases/lists/lists.dart'; import '../domain_layer/usecases/metadatas/metadatas.dart'; @@ -92,6 +94,16 @@ class Ndk { /// Verifies NIP-05 events VerifyNip05 get nip05 => _initialization.verifyNip05; + /// manage files on nostr \ + /// upload, download, delete files \ + /// high level usecase, recommended for most users + Files get files => _initialization.files; + + /// Blossom usecase \ + /// upload, download, delete, list files \ + /// low level usecase, recommended for advanced users + Blossom get blossom => _initialization.blossom; + /// Nostr Wallet connect @experimental // needs more docs & tests Nwc get nwc => _initialization.nwc; From 4dc8d54d7a634fe44c792742fdf288fe83101388 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:03:39 +0100 Subject: [PATCH 13/31] fix imports --- packages/ndk/lib/domain_layer/repositories/blossom.dart | 3 +-- packages/ndk/lib/domain_layer/usecases/files/blossom.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ndk/lib/domain_layer/repositories/blossom.dart b/packages/ndk/lib/domain_layer/repositories/blossom.dart index a1466dd2..a2900f8e 100644 --- a/packages/ndk/lib/domain_layer/repositories/blossom.dart +++ b/packages/ndk/lib/domain_layer/repositories/blossom.dart @@ -1,8 +1,7 @@ import 'dart:typed_data'; -import 'package:ndk/domain_layer/entities/nip_01_event.dart'; - import '../entities/blossom_blobs.dart'; +import '../entities/nip_01_event.dart'; import '../entities/tuple.dart'; enum UploadStrategy { diff --git a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart index 6730c4be..a874dfac 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/blossom.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/blossom.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart'; -import 'package:ndk/domain_layer/entities/blossom_blobs.dart'; import '../../../config/blossom_config.dart'; +import '../../entities/blossom_blobs.dart'; import '../../entities/nip_01_event.dart'; import '../../repositories/blossom.dart'; import '../../repositories/event_signer.dart'; From 1489c6e58f5b18b3cd0a23ef149863b22c4588f7 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:06:46 +0100 Subject: [PATCH 14/31] config description --- packages/ndk/lib/config/blossom_config.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ndk/lib/config/blossom_config.dart b/packages/ndk/lib/config/blossom_config.dart index d34ecded..0dc42283 100644 --- a/packages/ndk/lib/config/blossom_config.dart +++ b/packages/ndk/lib/config/blossom_config.dart @@ -1,3 +1,4 @@ // ignore_for_file: constant_identifier_names +/// how long nostr auth messages are valid for const Duration BLOSSOM_AUTH_EXPIRATION = Duration(minutes: 5); From 69c1d511287ba056eee705f189973686adb9bd2d Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:23:02 +0100 Subject: [PATCH 15/31] strategy tests --- .../ndk/test/usecases/files/blossom_test.dart | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index e2ee5806..05ce5526 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart' as http; import 'package:ndk/data_layer/data_sources/http_request.dart'; import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; -import 'package:ndk/domain_layer/usecases/files/blossom.dart'; +import 'package:ndk/domain_layer/repositories/blossom.dart'; import 'package:ndk/domain_layer/usecases/files/blossom_user_server_list.dart'; import 'package:ndk/ndk.dart'; @@ -151,4 +151,114 @@ void main() { expect(getResponse, throwsException); }); }); + + group("blossom upload strategy tests", () { + test('Upload to first successful server only - firstSuccess', () async { + final testData = Uint8List.fromList(utf8.encode('strategy test')); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: [ + 'http://dead.example.com', + 'http://localhost:3001', + 'http://localhost:3000', + ], + strategy: UploadStrategy.firstSuccess, + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + final deadServer = client.getBlob(sha256: sha256, serverUrls: [ + 'http://dead.example.com', + ]); + expect(deadServer, throwsException); + + final server1 = await client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3001', + ]); + + expect(utf8.decode(server1.data), equals('strategy test')); + + final server2 = client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3000', + ]); + + expect(server2, throwsException); + }); + + test('Upload to first successful server only - mirrorAfterSuccess', + () async { + final myData = "strategy test mirrorAfterSuccess"; + final testData = Uint8List.fromList(utf8.encode(myData)); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: [ + 'http://dead.example.com', + 'http://localhost:3001', + 'http://localhost:3000', + ], + strategy: UploadStrategy.mirrorAfterSuccess, + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + final deadServer = client.getBlob(sha256: sha256, serverUrls: [ + 'http://dead.example.com', + ]); + expect(deadServer, throwsException); + + final server1 = await client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3001', + ]); + + expect(utf8.decode(server1.data), equals(myData)); + + final server2 = await client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3000', + ]); + + expect(utf8.decode(server2.data), equals(myData)); + }); + + test('Upload to first successful server only - allSimultaneous', () async { + final myData = "strategy test allSimultaneous"; + final testData = Uint8List.fromList(utf8.encode(myData)); + + // Upload blob + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: [ + 'http://dead.example.com', + 'http://localhost:3001', + 'http://localhost:3000', + ], + strategy: UploadStrategy.allSimultaneous, + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + final deadServer = client.getBlob(sha256: sha256, serverUrls: [ + 'http://dead.example.com', + ]); + expect(deadServer, throwsException); + + final server1 = await client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3001', + ]); + + expect(utf8.decode(server1.data), equals(myData)); + + final server2 = await client.getBlob(sha256: sha256, serverUrls: [ + 'http://localhost:3000', + ]); + + expect(utf8.decode(server2.data), equals(myData)); + }); + }); } From 1aacca1ac943cdc780587e73b7c489e2cae7c085 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:53:15 +0100 Subject: [PATCH 16/31] fix: mirror uploads --- .../data_layer/data_sources/http_request.dart | 19 ++++ .../repositories/blossom/blossom_impl.dart | 90 +++++++++++++++---- .../ndk/test/mocks/mock_blossom_server.dart | 81 +++++++++++++++++ .../ndk/test/usecases/files/blossom_test.dart | 4 +- 4 files changed, 176 insertions(+), 18 deletions(-) diff --git a/packages/ndk/lib/data_layer/data_sources/http_request.dart b/packages/ndk/lib/data_layer/data_sources/http_request.dart index 3d748eba..c535ab7a 100644 --- a/packages/ndk/lib/data_layer/data_sources/http_request.dart +++ b/packages/ndk/lib/data_layer/data_sources/http_request.dart @@ -42,6 +42,25 @@ class HttpRequestDS { return response; } + Future post({ + required Uri url, + required Uint8List body, + required headers, + }) async { + http.Response response = await _client.post( + url, + body: body, + headers: headers, + ); + + if (response.statusCode != 200) { + throw Exception( + "error fetching STATUS: ${response.statusCode}, Link: $url"); + } + + return response; + } + Future head({ required Uri url, headers, diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index 3c47cda4..c0a254f6 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -55,26 +55,42 @@ class BlossomRepositoryImpl implements BlossomRepository { String? contentType, }) async { final results = []; + BlobUploadResult? successfulUpload; - // Try primary upload - final primaryResult = await _uploadToServer( - serverUrl: serverUrls.first, - data: data, - contentType: contentType, - authorization: authorization, - ); - results.add(primaryResult); + // Try servers until we get a successful upload + for (final serverUrl in serverUrls) { + final result = await _uploadToServer( + serverUrl: serverUrl, + data: data, + contentType: contentType, + authorization: authorization, + ); + results.add(result); - if (primaryResult.success) { - // Mirror to other servers - final mirrorResults = - await Future.wait(serverUrls.skip(1).map((url) => _uploadToServer( + if (result.success) { + successfulUpload = result; + break; + } + } + + // If we found a working server, mirror to all other servers that haven't been tried yet + if (successfulUpload != null) { + // Get the index where we succeeded + final successIndex = serverUrls.indexOf(successfulUpload.serverUrl); + + // Mirror to remaining servers (ones we haven't tried yet) + final remainingServers = serverUrls.sublist(successIndex + 1); + if (remainingServers.isNotEmpty) { + final mirrorResults = await Future.wait( + remainingServers.map((url) => _mirrorToServer( + fileUrl: successfulUpload!.descriptor!.url, serverUrl: url, - data: data, - contentType: contentType, + sha256: successfulUpload.descriptor!.sha256, authorization: authorization, - ))); - results.addAll(mirrorResults); + )), + ); + results.addAll(mirrorResults); + } } return results; @@ -163,6 +179,48 @@ class BlossomRepositoryImpl implements BlossomRepository { } } + Future _mirrorToServer({ + required String fileUrl, + required String serverUrl, + required String sha256, + required Nip01Event authorization, + }) async { + final jsonMsg = {"url": fileUrl}; + + final Uint8List myBody = + Uint8List.fromList(utf8.encode(jsonEncode(jsonMsg))); + try { + // Mirror endpoint is POST /mirror/ + final response = await client.post( + url: Uri.parse('$serverUrl/mirror/$sha256'), + body: myBody, + headers: { + 'Authorization': "Nostr ${authorization.toBase64()}", + }, + ); + + if (response.statusCode != 200) { + return BlobUploadResult( + serverUrl: serverUrl, + success: false, + error: 'HTTP ${response.statusCode}', + ); + } + + return BlobUploadResult( + serverUrl: serverUrl, + success: true, + descriptor: BlobDescriptor.fromJson(jsonDecode(response.body)), + ); + } catch (e) { + return BlobUploadResult( + serverUrl: serverUrl, + success: false, + error: e.toString(), + ); + } + } + @override Future getBlob({ required String sha256, diff --git a/packages/ndk/test/mocks/mock_blossom_server.dart b/packages/ndk/test/mocks/mock_blossom_server.dart index ad4be1f6..c5e89276 100644 --- a/packages/ndk/test/mocks/mock_blossom_server.dart +++ b/packages/ndk/test/mocks/mock_blossom_server.dart @@ -139,6 +139,87 @@ class MockBlossomServer { return Response(200); }); + router.post('/mirror/', (Request request, String sha256) async { + // Check for authorization header + final authHeader = request.headers['authorization']; + + if (authHeader == null) { + return Response.forbidden('Missing authorization'); + } + + try { + final authEvent = + json.decode(utf8.decode(base64Decode(authHeader.split(' ')[1]))); + if (!_verifyAuthEvent(authEvent, 'upload')) { + return Response.forbidden('Invalid authorization event'); + } + } catch (e) { + return Response.forbidden('Invalid authorization format'); + } + + // Parse the request body to get the URL + final String body = await request.readAsString(); + Map requestData; + try { + requestData = json.decode(body); + if (!requestData.containsKey('url')) { + return Response.badRequest( + body: 'Request body must contain a "url" field'); + } + } catch (e) { + return Response.badRequest(body: 'Invalid JSON body'); + } + + // Download the blob from the provided URL + try { + final sourceUrl = requestData['url']; + final httpClient = HttpClient(); + final request = await httpClient.getUrl(Uri.parse(sourceUrl)); + final response = await request.close(); + + if (response.statusCode != 200) { + return Response.internalServerError( + body: + 'Failed to download from source URL: ${response.statusCode}'); + } + + // Read the response data + final bytes = await response.expand((chunk) => chunk).toList(); + final data = Uint8List.fromList(bytes); + + // Verify the SHA256 matches + final computedSha256 = _computeSha256(data); + if (computedSha256 != sha256) { + return Response.badRequest( + body: 'SHA256 mismatch: expected $sha256, got $computedSha256'); + } + + // Store the blob + _blobs[sha256] = _BlobEntry( + data: data, + contentType: response.headers.contentType?.toString() ?? + 'application/octet-stream', + uploader: 'test_pubkey', + uploadedAt: DateTime.now(), + ); + + // Return the same descriptor format as upload + return Response.ok( + json.encode({ + 'url': 'http://localhost:$port/$sha256', + 'sha256': sha256, + 'size': data.length, + 'type': _blobs[sha256]!.contentType, + 'uploaded': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: 'Failed to mirror blob: ${e.toString()}'); + } + }); + return router; } diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index 05ce5526..01408aea 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -203,9 +203,9 @@ void main() { ], strategy: UploadStrategy.mirrorAfterSuccess, ); - expect(uploadResponse.first.success, true); + expect(uploadResponse[1].success, true); - final sha256 = uploadResponse.first.descriptor!.sha256; + final sha256 = uploadResponse[1].descriptor!.sha256; final deadServer = client.getBlob(sha256: sha256, serverUrls: [ 'http://dead.example.com', From 56ef9114c45148a9440efe84045df0365fcf6050 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:55:38 +0100 Subject: [PATCH 17/31] upload all test --- packages/ndk/test/usecases/files/blossom_test.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index 01408aea..fc5b1ae8 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -203,7 +203,9 @@ void main() { ], strategy: UploadStrategy.mirrorAfterSuccess, ); + expect(uploadResponse[0].success, false); expect(uploadResponse[1].success, true); + expect(uploadResponse[2].success, true); final sha256 = uploadResponse[1].descriptor!.sha256; @@ -239,9 +241,11 @@ void main() { ], strategy: UploadStrategy.allSimultaneous, ); - expect(uploadResponse.first.success, true); + expect(uploadResponse[0].success, false); + expect(uploadResponse[1].success, true); + expect(uploadResponse[2].success, true); - final sha256 = uploadResponse.first.descriptor!.sha256; + final sha256 = uploadResponse[1].descriptor!.sha256; final deadServer = client.getBlob(sha256: sha256, serverUrls: [ 'http://dead.example.com', From 042f310379efedcf1748c43c68bbb57143638f54 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:00:22 +0100 Subject: [PATCH 18/31] comments --- .../ndk/lib/data_layer/repositories/blossom/blossom_impl.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index c0a254f6..e20a07f2 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -139,6 +139,7 @@ class BlossomRepositoryImpl implements BlossomRepository { return results; } + /// Upload a file to a server Future _uploadToServer({ required String serverUrl, required Uint8List data, @@ -179,6 +180,7 @@ class BlossomRepositoryImpl implements BlossomRepository { } } + /// Mirror a file from one server to another, based on the file URL Future _mirrorToServer({ required String fileUrl, required String serverUrl, From 967945c098a5b90852628f0c6599e076bcf643a9 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:16:34 +0100 Subject: [PATCH 19/31] wip: stream tests --- .../ndk/test/usecases/files/blossom_test.dart | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index fc5b1ae8..3f177493 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -265,4 +265,125 @@ void main() { expect(utf8.decode(server2.data), equals(myData)); }); }); + + group("stream blobs", () { + final BlossomRepository blossomRepo = BlossomRepositoryImpl( + client: HttpRequestDS(http.Client()), + ); + test('getBlobStream should properly stream large files with range requests', + () async { + // First upload a test file to the mock server + final testData = Uint8List.fromList( + List.generate(5 * 1024 * 1024, (i) => i % 256)); // 5MB test file + + // Upload the test file + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + + expect(uploadResponse.first.success, true); + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Now test the streaming download + final stream = await blossomRepo.getBlobStream( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + chunkSize: 1024 * 1024, // 1MB chunks + ); + + // Collect all chunks and verify the data + final chunks = []; + int totalSize = 0; + + await for (final response in stream) { + chunks.add(response.data); + totalSize += response.data.length; + + // Verify chunk metadata + expect(response.mimeType, isNotNull); + expect(response.contentLength, equals(testData.length)); + //expect(response.contentRange, matches(RegExp(r'bytes \d+-\d+/\d+'))); + } + + // Combine chunks and verify the complete file + final resultData = Uint8List(totalSize); + int offset = 0; + for (final chunk in chunks) { + resultData.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + + expect(resultData, equals(testData)); + expect(totalSize, equals(testData.length)); + }); + + test( + 'getBlobStream should fallback to regular download if range requests not supported', + () async { + // Upload a smaller test file + final testData = Uint8List.fromList( + List.generate(1024, (i) => i % 256)); // 1KB test file + + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + + expect(uploadResponse.first.success, true); + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Test the streaming download + final stream = await blossomRepo.getBlobStream( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + ); + + // Should receive exactly one chunk with the complete file + final chunks = await stream.toList(); + expect(chunks.length, equals(1)); + expect(chunks.first.data, equals(testData)); + }); + + test('getBlobStream should handle server errors gracefully', () async { + expect( + () => blossomRepo.getBlobStream( + sha256: 'nonexistent_sha256', + serverUrls: ['http://localhost:3000'], + ), + throwsException, + ); + }); + + test( + 'getBlobStream should try multiple servers until finding one that works', + () async { + final testData = Uint8List.fromList( + List.generate(2 * 1024 * 1024, (i) => i % 256)); // 2MB test file + + final uploadResponse = await client.uploadBlob( + data: testData, + serverUrls: ['http://localhost:3000'], + ); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // Test with multiple servers, including non-existent ones + final stream = await blossomRepo.getBlobStream( + sha256: sha256, + serverUrls: [ + 'http://nonexistent-server:3000', + 'http://localhost:3000', + 'http://another-nonexistent:3000', + ], + ); + + final receivedData = await stream + .map((response) => response.data) + .expand((chunk) => chunk) + .toList(); + + expect(Uint8List.fromList(receivedData), equals(testData)); + }); + }); } From 8e9a938b8d89e8c9615da450cba93cc8502d2c51 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:13:44 +0100 Subject: [PATCH 20/31] better exception msg --- .../ndk/lib/data_layer/repositories/blossom/blossom_impl.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart index e20a07f2..5126d992 100644 --- a/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart +++ b/packages/ndk/lib/data_layer/repositories/blossom/blossom_impl.dart @@ -264,7 +264,7 @@ class BlossomRepositoryImpl implements BlossomRepository { } throw Exception( - 'Failed to get blob from all servers. Last error: $lastError'); + 'Failed to get blob from any of the servers. Last error: $lastError'); } /// first value is whether the server supports range requests \ From 76b64c1dd11e7e34ed3d72e86f10527c7a6b6a35 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:16:16 +0100 Subject: [PATCH 21/31] move regex outside --- packages/ndk/lib/domain_layer/usecases/files/files.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index 4a96ff1f..31d3d8ab 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -6,6 +6,9 @@ import 'blossom.dart'; class Files { final Blossom blossom; + /// Regular expression to match SHA256 in URLs + static final sha256Regex = RegExp(r'/([a-fA-F0-9]{64})(?:/|$)'); + Files(this.blossom); /// upload a file to the server(s) \ @@ -47,7 +50,7 @@ class Files { String? pubkey, ) async { // Regular expression to match SHA256 in URLs - final sha256Match = RegExp(r'/([a-fA-F0-9]{64})(?:/|$)').firstMatch(url); + final sha256Match = sha256Regex.firstMatch(url); if (sha256Match != null) { // This is a blossom URL, handle it using blossom protocol From ba2bcc7dfaa78830e8073a14e611474b4b1f7cdb Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:19:08 +0100 Subject: [PATCH 22/31] use ndk for blossom in test --- .../ndk/test/usecases/files/blossom_test.dart | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/ndk/test/usecases/files/blossom_test.dart b/packages/ndk/test/usecases/files/blossom_test.dart index 3f177493..6c349d92 100644 --- a/packages/ndk/test/usecases/files/blossom_test.dart +++ b/packages/ndk/test/usecases/files/blossom_test.dart @@ -5,7 +5,7 @@ import 'package:http/http.dart' as http; import 'package:ndk/data_layer/data_sources/http_request.dart'; import 'package:ndk/data_layer/repositories/blossom/blossom_impl.dart'; import 'package:ndk/domain_layer/repositories/blossom.dart'; -import 'package:ndk/domain_layer/usecases/files/blossom_user_server_list.dart'; + import 'package:ndk/ndk.dart'; import 'package:ndk/shared/nips/nip01/bip340.dart'; @@ -26,10 +26,6 @@ void main() { await server.start(); await server2.start(); - final blossomRepo = BlossomRepositoryImpl( - client: HttpRequestDS(http.Client()), - ); - KeyPair key1 = Bip340.generatePrivateKey(); final signer = Bip340EventSigner( privateKey: key1.privateKey, publicKey: key1.publicKey); @@ -39,16 +35,11 @@ void main() { eventVerifier: MockEventVerifier(), cache: MemCacheManager(), engine: NdkEngine.JIT, + eventSigner: signer, ), ); - final BlossomUserServerList blossomUserServerList = - BlossomUserServerList(ndk.requests); - client = Blossom( - blossomImpl: blossomRepo, - signer: signer, - userServerList: blossomUserServerList, - ); + client = ndk.blossom; }); tearDown(() async { From ce3491949292a55bf26f1bcf21be37f611995dbd Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:20:17 +0100 Subject: [PATCH 23/31] remove non git files --- packages/rust_verifier/.flutter-plugins | 8 -------- packages/rust_verifier/.flutter-plugins-dependencies | 1 - 2 files changed, 9 deletions(-) delete mode 100644 packages/rust_verifier/.flutter-plugins delete mode 100644 packages/rust_verifier/.flutter-plugins-dependencies diff --git a/packages/rust_verifier/.flutter-plugins b/packages/rust_verifier/.flutter-plugins deleted file mode 100644 index 206f100c..00000000 --- a/packages/rust_verifier/.flutter-plugins +++ /dev/null @@ -1,8 +0,0 @@ -# This is a generated file; do not edit or check into version control. -integration_test=C:\\Users\\Beonde\\Documents\\flutterSDK\\flutter\\packages\\integration_test\\ -path_provider=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider-2.1.5\\ -path_provider_android=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_android-2.2.12\\ -path_provider_foundation=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_foundation-2.4.0\\ -path_provider_linux=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_linux-2.2.1\\ -path_provider_windows=C:\\Users\\Beonde\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_windows-2.3.0\\ -rust_lib_ndk=c:\\Users\\Beonde\\AndroidStudioProjects\\ndk\\packages\\rust_verifier\\rust_builder\\ diff --git a/packages/rust_verifier/.flutter-plugins-dependencies b/packages/rust_verifier/.flutter-plugins-dependencies deleted file mode 100644 index 0be33c7f..00000000 --- a/packages/rust_verifier/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"C:\\\\Users\\\\Beonde\\\\Documents\\\\flutterSDK\\\\flutter\\\\packages\\\\integration_test\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"C:\\\\Users\\\\Beonde\\\\Documents\\\\flutterSDK\\\\flutter\\\\packages\\\\integration_test\\\\","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.12\\\\","native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"macos":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.4.0\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"C:\\\\Users\\\\Beonde\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[]},{"name":"rust_lib_ndk","path":"c:\\\\Users\\\\Beonde\\\\AndroidStudioProjects\\\\ndk\\\\packages\\\\rust_verifier\\\\rust_builder\\\\","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"rust_lib_ndk","dependencies":[]}],"date_created":"2025-01-10 11:17:04.830361","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file From ddb7245ec2ed6b834deca11ac75277dc2e905cd2 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:24:49 +0100 Subject: [PATCH 24/31] fix double assigned ports --- packages/ndk/test/timeouts/network_speed_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ndk/test/timeouts/network_speed_test.dart b/packages/ndk/test/timeouts/network_speed_test.dart index d34347fd..9e04963a 100644 --- a/packages/ndk/test/timeouts/network_speed_test.dart +++ b/packages/ndk/test/timeouts/network_speed_test.dart @@ -21,8 +21,8 @@ void main() { group('low level - network faster then timeout', () { KeyPair key1 = Bip340.generatePrivateKey(); - MockRelay relay1 = MockRelay(name: "relay 1", explicitPort: 8201); - MockRelay relay2 = MockRelay(name: "relay 2", explicitPort: 8202); + MockRelay relay1 = MockRelay(name: "relay 1", explicitPort: 8221); + MockRelay relay2 = MockRelay(name: "relay 2", explicitPort: 8222); Map key1TextNotes = {key1: textNote(key1)}; From 4f1b38b3d26c9e6c30d9fa3c41e0834c5fd0e5ef Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:46:57 +0100 Subject: [PATCH 25/31] update gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4a51c218..a8d4f017 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ migrate_working_dir/ build/ /coverage/* /lcov_badge_generator -/.flutter-plugins -/.flutter-plugins-dependencies +.flutter-plugins +.flutter-plugins-dependencies /packages/ndk/coverage/* /packages/ndk/coverage.lcov From a581d6bd80028c61e77f7a7d590508963d32592f Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:31:08 +0100 Subject: [PATCH 26/31] files test --- .../lib/domain_layer/entities/ndk_file.dart | 4 +- .../domain_layer/usecases/files/files.dart | 12 +-- .../ndk/test/usecases/files/files_test.dart | 102 ++++++++++++++++++ 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 packages/ndk/test/usecases/files/files_test.dart diff --git a/packages/ndk/lib/domain_layer/entities/ndk_file.dart b/packages/ndk/lib/domain_layer/entities/ndk_file.dart index 74e3f6d1..6f8e3683 100644 --- a/packages/ndk/lib/domain_layer/entities/ndk_file.dart +++ b/packages/ndk/lib/domain_layer/entities/ndk_file.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; class NdkFile { final Uint8List data; - final String sha256; + final String? sha256; final String? mimeType; final int? size; @@ -10,6 +10,6 @@ class NdkFile { required this.data, this.mimeType, this.size, - required this.sha256, + this.sha256, }); } diff --git a/packages/ndk/lib/domain_layer/usecases/files/files.dart b/packages/ndk/lib/domain_layer/usecases/files/files.dart index 31d3d8ab..9cc0ed9f 100644 --- a/packages/ndk/lib/domain_layer/usecases/files/files.dart +++ b/packages/ndk/lib/domain_layer/usecases/files/files.dart @@ -14,10 +14,10 @@ class Files { /// upload a file to the server(s) \ /// if [serverUrls] is null, the userServerList is fetched from nostr. \ /// if no serverUrls (param or nostr) are found, throws an error - Future> upload( - NdkFile file, + Future> upload({ + required NdkFile file, List? serverUrls, - ) { + }) { return blossom.uploadBlob( data: file.data, serverUrls: serverUrls, @@ -44,11 +44,11 @@ class Files { /// [serverUrls] and [pubkey] are used to download from blossom \ /// if [serverUrls] is null, the userServerList is fetched from nostr (using the pubkey). \ /// if both [serverUrls] and [pubkey] are null, throws an error. - Future download( - String url, + Future download({ + required String url, List? serverUrls, String? pubkey, - ) async { + }) async { // Regular expression to match SHA256 in URLs final sha256Match = sha256Regex.firstMatch(url); diff --git a/packages/ndk/test/usecases/files/files_test.dart b/packages/ndk/test/usecases/files/files_test.dart new file mode 100644 index 00000000..84559b05 --- /dev/null +++ b/packages/ndk/test/usecases/files/files_test.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:ndk/domain_layer/entities/ndk_file.dart'; +import 'package:ndk/ndk.dart'; +import 'package:ndk/shared/nips/nip01/bip340.dart'; +import 'package:ndk/shared/nips/nip01/key_pair.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_blossom_server.dart'; +import '../../mocks/mock_event_verifier.dart'; + +void main() { + late MockBlossomServer server; + late MockBlossomServer server2; + late Files client; + + setUp(() async { + server = MockBlossomServer(port: 3010); + server2 = MockBlossomServer(port: 3011); + await server.start(); + await server2.start(); + + KeyPair key1 = Bip340.generatePrivateKey(); + final signer = Bip340EventSigner( + privateKey: key1.privateKey, publicKey: key1.publicKey); + + final ndk = Ndk( + NdkConfig( + eventVerifier: MockEventVerifier(), + cache: MemCacheManager(), + engine: NdkEngine.JIT, + eventSigner: signer, + ), + ); + + client = ndk.files; + }); + + tearDown(() async { + await server.stop(); + await server2.stop(); + }); + + group('File Integration Tests', () { + test('no file', () async { + // download + final getResponse = client.download( + url: 'http://localhost:3000/no_file', + serverUrls: ['http://localhost:3000'], + ); + expect(getResponse, throwsA(isA())); + }); + + test('Upload and retrieve file', () async { + final testData = Uint8List.fromList(utf8.encode('Hello, File')); + + // Upload + final uploadResponse = await client.upload( + file: NdkFile(data: testData, mimeType: 'text/plain'), + serverUrls: ['http://localhost:3000'], + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + // download + final getResponse = await client.download( + url: 'http://localhost:3000/$sha256', + serverUrls: ['http://localhost:3000'], + ); + expect(utf8.decode(getResponse.data), equals('Hello, File')); + }); + + test('Upload and delete file', () async { + final testData = Uint8List.fromList(utf8.encode('Hello, File')); + + // Upload + final uploadResponse = await client.upload( + file: NdkFile(data: testData, mimeType: 'text/plain'), + serverUrls: ['http://localhost:3000'], + ); + expect(uploadResponse.first.success, true); + + final sha256 = uploadResponse.first.descriptor!.sha256; + + final deleteResponse = await client.delete( + sha256: sha256, + serverUrls: ['http://localhost:3000'], + ); + + expect(deleteResponse.first.success, true); + + // download + final getResponse = client.download( + url: 'http://localhost:3000/$sha256', + serverUrls: ['http://localhost:3000'], + ); + expect(getResponse, throwsA(isA())); + }); + }); +} From b0374885f458a674fd740a6ad7021db9b5a31721 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:43:43 +0100 Subject: [PATCH 27/31] files, blossom show tests --- packages/ndk/example/files/blossom_test.dart | 22 ++++++++++++++++++++ packages/ndk/example/files/files_test.dart | 19 +++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/ndk/example/files/blossom_test.dart create mode 100644 packages/ndk/example/files/files_test.dart diff --git a/packages/ndk/example/files/blossom_test.dart b/packages/ndk/example/files/blossom_test.dart new file mode 100644 index 00000000..d2180b88 --- /dev/null +++ b/packages/ndk/example/files/blossom_test.dart @@ -0,0 +1,22 @@ +// ignore_for_file: avoid_print + +import 'package:ndk/ndk.dart'; +import 'package:test/test.dart'; + +void main() async { + test('download test', () async { + final ndk = Ndk.defaultConfig(); + + final downloadResult = await ndk.blossom.getBlob( + sha256: + "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553", + serverUrls: ["https://cdn.hzrd149.com"], + ); + + print( + "file of type: ${downloadResult.mimeType}, size: ${downloadResult.data.length}", + ); + + expect(downloadResult.data.length, greaterThan(0)); + }); +} diff --git a/packages/ndk/example/files/files_test.dart b/packages/ndk/example/files/files_test.dart new file mode 100644 index 00000000..ac2ddbf9 --- /dev/null +++ b/packages/ndk/example/files/files_test.dart @@ -0,0 +1,19 @@ +// ignore_for_file: avoid_print + +import 'package:ndk/ndk.dart'; +import 'package:test/test.dart'; + +void main() async { + test('download test', () async { + final ndk = Ndk.defaultConfig(); + + final downloadResult = await ndk.files.download( + url: + "https://cdn.hzrd149.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"); + + print( + "file of type: ${downloadResult.mimeType}, size: ${downloadResult.data.length}"); + + expect(downloadResult.data.length, greaterThan(0)); + }); +} From cb81927633e64717f50fcd080ba9da1e60324a8e Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:50:30 +0100 Subject: [PATCH 28/31] files non blossom test --- packages/ndk/example/files/files_test.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ndk/example/files/files_test.dart b/packages/ndk/example/files/files_test.dart index ac2ddbf9..8f73d974 100644 --- a/packages/ndk/example/files/files_test.dart +++ b/packages/ndk/example/files/files_test.dart @@ -4,7 +4,7 @@ import 'package:ndk/ndk.dart'; import 'package:test/test.dart'; void main() async { - test('download test', () async { + test('download test - blossom', () async { final ndk = Ndk.defaultConfig(); final downloadResult = await ndk.files.download( @@ -16,4 +16,16 @@ void main() async { expect(downloadResult.data.length, greaterThan(0)); }); + + test('download test - non blossom', () async { + final ndk = Ndk.defaultConfig(); + + final downloadResult = await ndk.files + .download(url: "https://camelus.app/.well-known/nostr.json"); + + print( + "file of type: ${downloadResult.mimeType}, size: ${downloadResult.data.length}"); + + expect(downloadResult.data.length, greaterThan(0)); + }); } From 3e23622c0252dc1d1ef1e86148c829e1de99e1b2 Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:53:05 +0100 Subject: [PATCH 29/31] better file names --- .../files/{blossom_test.dart => blossom_example_test.dart} | 0 .../example/files/{files_test.dart => files_example_test.dart} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/ndk/example/files/{blossom_test.dart => blossom_example_test.dart} (100%) rename packages/ndk/example/files/{files_test.dart => files_example_test.dart} (100%) diff --git a/packages/ndk/example/files/blossom_test.dart b/packages/ndk/example/files/blossom_example_test.dart similarity index 100% rename from packages/ndk/example/files/blossom_test.dart rename to packages/ndk/example/files/blossom_example_test.dart diff --git a/packages/ndk/example/files/files_test.dart b/packages/ndk/example/files/files_example_test.dart similarity index 100% rename from packages/ndk/example/files/files_test.dart rename to packages/ndk/example/files/files_example_test.dart From e40542a2581feabd8a59b1ca6df92d15861285fb Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:56:19 +0100 Subject: [PATCH 30/31] ci ports --- packages/ndk/test/usecases/files/files_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ndk/test/usecases/files/files_test.dart b/packages/ndk/test/usecases/files/files_test.dart index 84559b05..24ad8f77 100644 --- a/packages/ndk/test/usecases/files/files_test.dart +++ b/packages/ndk/test/usecases/files/files_test.dart @@ -16,8 +16,8 @@ void main() { late Files client; setUp(() async { - server = MockBlossomServer(port: 3010); - server2 = MockBlossomServer(port: 3011); + server = MockBlossomServer(port: 3008); + server2 = MockBlossomServer(port: 3009); await server.start(); await server2.start(); From 6fdc9c681e2f3c1c74e685452ecb1dba67a3945a Mon Sep 17 00:00:00 2001 From: LeoLox <58687994+leo-lox@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:04:46 +0100 Subject: [PATCH 31/31] fix file test --- .../ndk/test/usecases/files/files_test.dart | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/ndk/test/usecases/files/files_test.dart b/packages/ndk/test/usecases/files/files_test.dart index 24ad8f77..b66c781d 100644 --- a/packages/ndk/test/usecases/files/files_test.dart +++ b/packages/ndk/test/usecases/files/files_test.dart @@ -16,8 +16,8 @@ void main() { late Files client; setUp(() async { - server = MockBlossomServer(port: 3008); - server2 = MockBlossomServer(port: 3009); + server = MockBlossomServer(port: 3010); + server2 = MockBlossomServer(port: 3011); await server.start(); await server2.start(); @@ -47,18 +47,18 @@ void main() { // download final getResponse = client.download( url: 'http://localhost:3000/no_file', - serverUrls: ['http://localhost:3000'], + serverUrls: ['http://localhost:3010'], ); expect(getResponse, throwsA(isA())); }); test('Upload and retrieve file', () async { - final testData = Uint8List.fromList(utf8.encode('Hello, File')); + final testData = Uint8List.fromList(utf8.encode('Hello, File2')); // Upload final uploadResponse = await client.upload( file: NdkFile(data: testData, mimeType: 'text/plain'), - serverUrls: ['http://localhost:3000'], + serverUrls: ['http://localhost:3010'], ); expect(uploadResponse.first.success, true); @@ -66,19 +66,20 @@ void main() { // download final getResponse = await client.download( - url: 'http://localhost:3000/$sha256', - serverUrls: ['http://localhost:3000'], + url: 'http://localhost:3010/$sha256', + serverUrls: ['http://localhost:3010'], ); - expect(utf8.decode(getResponse.data), equals('Hello, File')); + + expect(utf8.decode(getResponse.data), equals('Hello, File2')); }); test('Upload and delete file', () async { - final testData = Uint8List.fromList(utf8.encode('Hello, File')); + final testData = Uint8List.fromList(utf8.encode('Hello, File2')); // Upload final uploadResponse = await client.upload( file: NdkFile(data: testData, mimeType: 'text/plain'), - serverUrls: ['http://localhost:3000'], + serverUrls: ['http://localhost:3010'], ); expect(uploadResponse.first.success, true); @@ -86,15 +87,18 @@ void main() { final deleteResponse = await client.delete( sha256: sha256, - serverUrls: ['http://localhost:3000'], + serverUrls: ['http://localhost:3010', 'http://localhost:3011'], ); expect(deleteResponse.first.success, true); // download final getResponse = client.download( - url: 'http://localhost:3000/$sha256', - serverUrls: ['http://localhost:3000'], + url: 'http://localhost:3010/$sha256', + serverUrls: [ + 'https://localhost:3011', + 'http://localhost:3010', + ], ); expect(getResponse, throwsA(isA())); });