file download actor
This commit is contained in:
parent
0cdf3d7dbf
commit
0596fdec27
4 changed files with 179 additions and 0 deletions
37
README.md
37
README.md
|
|
@ -208,6 +208,43 @@ print((nb as NeighborsResult).neighbors['NORTH']); // u09tunu
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## FileDownloadActor
|
||||||
|
|
||||||
|
Downloads a file from a URL and saves it to disk via an injected `FileActor`.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final actor = FileDownloadActor(
|
||||||
|
fileActor: FileActor(),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `fileActor` | `FileActor` | required | Actor used to write the downloaded bytes to disk |
|
||||||
|
| `httpClient` | `http.Client?` | new client | Injectable HTTP client; provide a `MockClient` in tests |
|
||||||
|
|
||||||
|
Call `actor.close()` when done to release the HTTP client.
|
||||||
|
|
||||||
|
| Message | Result | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `DownloadFile(url, savePath)` | `FileDownloaded(savePath)` | GETs the URL and writes the response bytes to `savePath`. Throws if the response status is not 2xx. |
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final actor = FileDownloadActor(fileActor: FileActor());
|
||||||
|
|
||||||
|
final result = await actor.handle(
|
||||||
|
const DownloadFile(
|
||||||
|
url: 'https://example.com/image.jpg',
|
||||||
|
savePath: '/tmp/image.jpg',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print((result as FileDownloaded).savePath); // /tmp/image.jpg
|
||||||
|
|
||||||
|
await actor.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## FileUploadActor
|
## FileUploadActor
|
||||||
|
|
||||||
Uploads files to a Nostr-authenticated remote storage service. Handles
|
Uploads files to a Nostr-authenticated remote storage service. Handles
|
||||||
|
|
|
||||||
57
lib/src/file_download_actor.dart
Normal file
57
lib/src/file_download_actor.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:actors/actors.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import 'file_actor.dart';
|
||||||
|
|
||||||
|
sealed class FileDownloadMessage {
|
||||||
|
const FileDownloadMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DownloadFile extends FileDownloadMessage {
|
||||||
|
final String url;
|
||||||
|
final String savePath;
|
||||||
|
const DownloadFile({required this.url, required this.savePath});
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class FileDownloadResult {
|
||||||
|
const FileDownloadResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class FileDownloaded extends FileDownloadResult {
|
||||||
|
final String savePath;
|
||||||
|
const FileDownloaded(this.savePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDownloadActor
|
||||||
|
with Handler<FileDownloadMessage, FileDownloadResult> {
|
||||||
|
final FileActor _fileActor;
|
||||||
|
final http.Client _httpClient;
|
||||||
|
|
||||||
|
FileDownloadActor({
|
||||||
|
required FileActor fileActor,
|
||||||
|
http.Client? httpClient,
|
||||||
|
}) : _fileActor = fileActor,
|
||||||
|
_httpClient = httpClient ?? http.Client();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<FileDownloadResult> handle(FileDownloadMessage message) =>
|
||||||
|
switch (message) {
|
||||||
|
DownloadFile(:final url, :final savePath) => _download(url, savePath),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async => _httpClient.close();
|
||||||
|
|
||||||
|
Future<FileDownloaded> _download(String url, String savePath) async {
|
||||||
|
final response = await _httpClient.get(Uri.parse(url));
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
throw Exception('Download failed: HTTP ${response.statusCode}');
|
||||||
|
}
|
||||||
|
await _fileActor.handle(
|
||||||
|
SaveBinaryFile(savePath, Uint8List.fromList(response.bodyBytes)),
|
||||||
|
);
|
||||||
|
return FileDownloaded(savePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,4 +5,5 @@ export 'src/nostr_fetch_actor.dart';
|
||||||
export 'src/nostr_signer_actor.dart';
|
export 'src/nostr_signer_actor.dart';
|
||||||
export 'src/geo_actor.dart';
|
export 'src/geo_actor.dart';
|
||||||
export 'src/file_upload_actor.dart';
|
export 'src/file_upload_actor.dart';
|
||||||
|
export 'src/file_download_actor.dart';
|
||||||
export 'src/nostr_account_actor.dart';
|
export 'src/nostr_account_actor.dart';
|
||||||
|
|
|
||||||
84
test/file_download_actor_test.dart
Normal file
84
test/file_download_actor_test.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/testing.dart';
|
||||||
|
import 'package:swarm/swarm.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late Directory tmpDir;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
tmpDir = await Directory.systemTemp.createTemp('file_download_test_');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await tmpDir.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
FileDownloadActor makeActor(http.Client client) => FileDownloadActor(
|
||||||
|
fileActor: FileActor(),
|
||||||
|
httpClient: client,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('returns FileDownloaded with correct savePath', () async {
|
||||||
|
final client = MockClient(
|
||||||
|
(_) async => http.Response.bytes([1, 2, 3], 200),
|
||||||
|
);
|
||||||
|
final path = '${tmpDir.path}/output.bin';
|
||||||
|
final result = await makeActor(client).handle(
|
||||||
|
DownloadFile(url: 'https://example.com/file', savePath: path),
|
||||||
|
);
|
||||||
|
expect(result, isA<FileDownloaded>());
|
||||||
|
expect((result as FileDownloaded).savePath, equals(path));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saves correct bytes to disk', () async {
|
||||||
|
final bytes = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]);
|
||||||
|
final client = MockClient(
|
||||||
|
(_) async => http.Response.bytes(bytes, 200),
|
||||||
|
);
|
||||||
|
final path = '${tmpDir.path}/data.bin';
|
||||||
|
await makeActor(client).handle(
|
||||||
|
DownloadFile(url: 'https://example.com/file', savePath: path),
|
||||||
|
);
|
||||||
|
expect(await File(path).readAsBytes(), equals(bytes));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends GET request to the correct URL', () async {
|
||||||
|
http.Request? captured;
|
||||||
|
final client = MockClient((request) async {
|
||||||
|
captured = request;
|
||||||
|
return http.Response.bytes([0], 200);
|
||||||
|
});
|
||||||
|
await makeActor(client).handle(
|
||||||
|
DownloadFile(
|
||||||
|
url: 'https://example.com/assets/image.png',
|
||||||
|
savePath: '${tmpDir.path}/image.png',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(captured!.method, equals('GET'));
|
||||||
|
expect(
|
||||||
|
captured!.url.toString(),
|
||||||
|
equals('https://example.com/assets/image.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-2xx response throws', () async {
|
||||||
|
for (final status in [400, 403, 404, 500]) {
|
||||||
|
final client = MockClient(
|
||||||
|
(_) async => http.Response.bytes([], status),
|
||||||
|
);
|
||||||
|
await expectLater(
|
||||||
|
makeActor(client).handle(
|
||||||
|
DownloadFile(
|
||||||
|
url: 'https://example.com/file',
|
||||||
|
savePath: '${tmpDir.path}/out.bin',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue