file download actor

This commit is contained in:
randogoth 2026-06-08 12:33:37 +03:00
parent 0cdf3d7dbf
commit 0596fdec27
4 changed files with 179 additions and 0 deletions

View file

@ -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
Uploads files to a Nostr-authenticated remote storage service. Handles

View 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);
}
}

View file

@ -5,4 +5,5 @@ export 'src/nostr_fetch_actor.dart';
export 'src/nostr_signer_actor.dart';
export 'src/geo_actor.dart';
export 'src/file_upload_actor.dart';
export 'src/file_download_actor.dart';
export 'src/nostr_account_actor.dart';

View 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>()),
);
}
});
}