diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index 8db113b687e..6d0d5fca033 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -2,6 +2,11 @@ * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. +## 0.3.5+3 + +* Adds `exists()` implementation. +* Adds `delete()` implementation. + ## 0.3.4+2 * Adds support for `web: ^1.0.0`. diff --git a/packages/cross_file/lib/src/types/base.dart b/packages/cross_file/lib/src/types/base.dart index b3fb82ea406..4044fae3c81 100644 --- a/packages/cross_file/lib/src/types/base.dart +++ b/packages/cross_file/lib/src/types/base.dart @@ -86,4 +86,14 @@ abstract class XFileBase { Future lastModified() { throw UnimplementedError('lastModified() has not been implemented.'); } + + /// Check whether the CrossFile exists + Future exists() { + throw UnimplementedError('exists() has not been implemented.'); + } + + /// Delete the CrossFile + Future delete() { + throw UnimplementedError('delete() has not been implemented.'); + } } diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index e8d8371f062..3a80dec3cb7 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -176,6 +176,28 @@ class XFile extends XFileBase { return readAsBytes().then(encoding.decode); } + @override + Future exists() async { + try { + await _blob; + return true; + } catch (_) { + return false; + } + } + + @override + Future delete() async { + // On web, deleting a file is not possible. + // However, we can revoke the ObjectUrl to free up memory. + if (_browserBlob != null) { + URL.revokeObjectURL(_path); + _browserBlob = null; + return true; + } + return false; + } + // TODO(dit): https://github.com/flutter/flutter/issues/91867 Implement openRead properly. @override Stream openRead([int? start, int? end]) async* { diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart index 74ca1806cc3..aa9ed59f960 100644 --- a/packages/cross_file/lib/src/types/io.dart +++ b/packages/cross_file/lib/src/types/io.dart @@ -138,4 +138,18 @@ class XFile extends XFileBase { .map((List chunk) => Uint8List.fromList(chunk)); } } + + @override + // ignore: avoid_slow_async_io + Future exists() => _file.exists(); + + @override + Future delete() async { + try { + await _file.delete(); + return true; + } catch (_) { + return false; + } + } } diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index 13e5474fbf9..4a70eef0791 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -2,7 +2,7 @@ name: cross_file description: An abstraction to allow working with files across multiple platforms. repository: https://github.com/flutter/packages/tree/main/packages/cross_file issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+cross_file%22 -version: 0.3.4+2 +version: 0.3.5+3 environment: sdk: ^3.7.0 diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index 43449035b92..ec611d42a39 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -63,6 +63,23 @@ void main() { test('Stream can be sliced', () async { expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); }); + + test('exists() returns true when blob exists', () async { + expect(await file.exists(), isTrue); + }); + + test('delete() revokes ObjectURL and returns true', () async { + final XFile fileToDelete = XFile.fromData(bytes); + expect(await fileToDelete.exists(), isTrue); + + final bool deleted = await fileToDelete.delete(); + expect(deleted, isTrue); + + // After delete, the blob should not be accessible + expect(() async { + await fileToDelete.readAsBytes(); + }, throwsException); + }); }); group('Blob backend', () { @@ -85,6 +102,23 @@ void main() { await file.readAsBytes(); }, throwsException); }); + + test('exists() returns false after ObjectURL is revoked', () async { + final XFile fileToRevoke = XFile(textFileUrl); + expect(await fileToRevoke.exists(), isTrue); + + html.URL.revokeObjectURL(fileToRevoke.path); + expect(await fileToRevoke.exists(), isFalse); + }); + + test('delete() returns false when blob is not cached', () async { + final XFile fileWithoutBlob = XFile(textFileUrl); + // Read to ensure we're testing the uncached path + await fileWithoutBlob.readAsBytes(); + + final bool deleted = await fileWithoutBlob.delete(); + expect(deleted, isFalse); + }); }); group('saveTo(..)', () { diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart index a6de36568d1..4639fee53da 100644 --- a/packages/cross_file/test/x_file_io_test.dart +++ b/packages/cross_file/test/x_file_io_test.dart @@ -75,6 +75,37 @@ void main() { test('nullability is correct', () async { expect(_ensureNonnullPathArgument('a/path'), isNotNull); }); + + test('exists() returns true for existing file', () async { + final XFile file = XFile(textFilePath); + expect(await file.exists(), isTrue); + }); + + test('exists() returns false for non-existing file', () async { + final XFile file = XFile('/non/existent/path.txt'); + expect(await file.exists(), isFalse); + }); + + test('delete() removes file and returns true', () async { + final Directory tempDir = Directory.systemTemp.createTempSync(); + final File targetFile = File('${tempDir.path}/deleteMe.txt'); + targetFile.writeAsStringSync('Delete this'); + + final XFile file = XFile(targetFile.path); + expect(await file.exists(), isTrue); + + final bool deleted = await file.delete(); + expect(deleted, isTrue); + expect(targetFile.existsSync(), isFalse); + + await tempDir.delete(recursive: true); + }); + + test('delete() returns false for non-existing file', () async { + final XFile file = XFile('/non/existent/path.txt'); + final bool deleted = await file.delete(); + expect(deleted, isFalse); + }); }); group('Create with data', () {