Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/file_picker_web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Fixed picking files on Safari (macOS and iOS) never returning the selection. [#2222](https://github.com/vicajilau/flutter_file_picker/issues/2222)
- Fixed a selection being reported as cancelled when the window regained focus before the `change` event.
- Fixed `readAsBytes()` and `readAsByteStream()` hiding read failures. A failed stream now emits an error instead of ending early with a truncated file. [#2229](https://github.com/vicajilau/flutter_file_picker/issues/2229)

## 4.0.0

Expand Down
98 changes: 47 additions & 51 deletions packages/file_picker_web/lib/src/platform_file_web_fetch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,40 @@ extension type _ReadResult(JSObject _) implements JSObject {
external JSUint8Array? get value;
}

/// Attempts to fetch bytes from a web-only path (`blob:` or `data:` URL).
/// Fetches the bytes of a web-only path (`blob:` or `data:` URL).
///
/// Returns the full file bytes (`Uint8List`) using `fetch(...).arrayBuffer()`
/// for `blob:` URLs, or parses `data:` URIs. Returns `null` if fetching fails
/// or if [path] is empty or not a valid web URL.
Future<Uint8List?> fetchBytesFromWebPath(String? path) async {
if (path == null || path.isEmpty) return null;

try {
if (path.startsWith('data:')) {
return Uri.parse(path).data?.contentAsBytes();
}
/// for `blob:` URLs, or parses `data:` URIs. Returns `null` if [path] is not a
/// web URL, so the caller can fall back to another source. Throws if the
/// content of a web URL cannot be read.
Future<Uint8List?> fetchBytesFromWebPath(String path) async {
if (!_isWebPath(path)) return null;
Comment thread
vicajilau marked this conversation as resolved.
Resolved

if (path.startsWith('data:')) {
final uriData = Uri.parse(path).data;

if (path.startsWith('blob:')) {
final response = _Response(await _fetchJs(path.toJS).toDart);
final buffer = await response.arrayBuffer().toDart;
return buffer.toDart.asUint8List();
if (uriData == null) {
return null;
}
} catch (_) {
return null;

return uriData.contentAsBytes();
}

return null;
final response = _Response(await _fetchJs(path.toJS).toDart);
final buffer = await response.arrayBuffer().toDart;
return buffer.toDart.asUint8List();
}

bool _isWebPath(String path) =>
path.startsWith('blob:') || path.startsWith('data:');

/// Attempts to create a streaming `Stream<Uint8List>` from a web-only path
/// (`blob:` or `data:` URL).
///
/// Returns `null` when streaming isn't possible and the caller should
/// fall back to [fetchBytesFromWebPath].
Stream<Uint8List>? fetchStreamFromWebPath(String? path) {
if (path == null || path.isEmpty) return null;
/// Returns `null` if [path] is not a web URL, so the caller can fall back to
/// another source. Read failures are emitted as errors on the stream.
Stream<Uint8List>? fetchStreamFromWebPath(String path) {
if (!_isWebPath(path)) return null;
Comment thread
vicajilau marked this conversation as resolved.
Resolved

return _streamFromWebPath(path);
}
Expand All @@ -67,43 +69,37 @@ Stream<Uint8List>? fetchStreamFromWebPath(String? path) {
/// Uses `Response.body` (`ReadableStream`) when available; otherwise falls
/// back to a single in-memory `arrayBuffer()` chunk.
Stream<Uint8List> _streamFromWebPath(String path) async* {
try {
if (path.startsWith('data:')) {
final bytes = Uri.parse(path).data?.contentAsBytes();
if (bytes != null) {
yield bytes;
}
return;
if (path.startsWith('data:')) {
final uriData = Uri.parse(path).data;

if (uriData == null) {
throw FormatException('Invalid data: URL', path);
}

if (path.startsWith('blob:')) {
final jsResponse = await _fetchJs(path.toJS).toDart;
yield uriData.contentAsBytes();
return;
}

final response = _Response(jsResponse);
final body = response.body;
final response = _Response(await _fetchJs(path.toJS).toDart);
final body = response.body;

// If there's no streaming body, fallback to arrayBuffer()
if (body == null) {
final buffer = await response.arrayBuffer().toDart;
yield buffer.toDart.asUint8List();
return;
}
// If there's no streaming body, fallback to arrayBuffer()
if (body == null) {
final buffer = await response.arrayBuffer().toDart;
yield buffer.toDart.asUint8List();
return;
}

final readable = _ReadableStream(body);
final reader = _Reader(readable.getReader());
final readable = _ReadableStream(body);
final reader = _Reader(readable.getReader());

while (true) {
final jsResultObj = await reader.read().toDart;
final result = _ReadResult(jsResultObj);
if (result.done) break;
while (true) {
final result = _ReadResult(await reader.read().toDart);
if (result.done) break;

final arr = result.value;
if (arr == null) break;
final arr = result.value;
if (arr == null) break;

yield arr.toDart;
}
}
} catch (_) {
return;
yield arr.toDart;
}
}
62 changes: 62 additions & 0 deletions packages/file_picker_web/test/web_platform_file_read_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@TestOn('browser')
library;

import 'dart:convert';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:file_picker_web/file_picker_web.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:web/web.dart';

void main() {
final content = Uint8List.fromList(utf8.encode('hello file_picker'));

String createBlobUrl() => URL.createObjectURL(Blob([content.toJS].toJS));

WebPlatformFile fileAt(String url) =>
WebPlatformFile(name: 'a.txt', uri: Uri.parse(url));

test('readAsBytes() reads a blob URL', () async {
final url = createBlobUrl();
addTearDown(() => URL.revokeObjectURL(url));

expect(await fileAt(url).readAsBytes(), content);
});

test('readAsBytes() throws when the blob URL cannot be read', () async {
final url = createBlobUrl();
URL.revokeObjectURL(url);

await expectLater(fileAt(url).readAsBytes(), throwsA(anything));
});

test('readAsByteStream() reads a blob URL', () async {
final url = createBlobUrl();
addTearDown(() => URL.revokeObjectURL(url));

final chunks = await fileAt(url).readAsByteStream().toList();
expect(chunks.expand((chunk) => chunk).toList(), content);
});

test(
'readAsByteStream() emits an error when the blob URL cannot be read',
() async {
final url = createBlobUrl();
URL.revokeObjectURL(url);

await expectLater(
fileAt(url).readAsByteStream().toList(),
throwsA(anything),
);
},
);

test('readAsBytes() and readAsByteStream() read a data URL', () async {
final url = Uri.dataFromBytes(content).toString();

expect(await fileAt(url).readAsBytes(), content);
final chunks = await fileAt(url).readAsByteStream().toList();
expect(chunks.expand((chunk) => chunk).toList(), content);
});
}
Loading