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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Fixes

* Fixed a partial cache file deleted mid-download (by the device's cache management, say) failing every request for what it held until the download ended: the download wrote on into the deleted file. A request now resets the cache, and waits for its range to download again.

## 0.3.1

### Fixes
Expand Down
18 changes: 17 additions & 1 deletion lib/src/cache_stream/http_cache_stream.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ class HttpCacheStream {
}
}

//Deleted while it downloads (by the device's cache management, say), the
//partial cache file takes the download's writes into a file no response can
//open, until the download ends. Reset, the download starts again, and the
//request waits for its range.
final activeDownloader = _cacheDownloader;
final cacheDeleted = activeDownloader != null &&
!activeDownloader.isClosed &&
activeDownloader.filePosition > 0 &&
!await files.activeCacheFile().exists();
if (cacheDeleted) {
await _resetCache(InvalidCacheSizeException(
sourceUrl, -1, responseHeaders?.sourceLength ?? 0));
}

final rangeThreshold = config.rangeRequestSplitThreshold;
if (rangeThreshold != null &&
range.start >= rangeThreshold &&
Expand All @@ -132,7 +146,9 @@ class HttpCacheStream {
final streamRequest = StreamRequest.construct(range);
final downloader = _cacheDownloader;

if (downloader != null && downloader.processRequest(streamRequest)) {
if (!cacheDeleted &&
downloader != null &&
downloader.processRequest(streamRequest)) {
return streamRequest.response; //Request was processed immediately
} else {
_queuedRequests
Expand Down
70 changes: 70 additions & 0 deletions test/e2e/partial_deleted_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:http_cache_stream/http_cache_stream.dart';
import 'package:http_cache_stream/src/etc/extensions/file_extensions.dart';

import '../support/harness.dart';
import '../support/payload.dart';

/// The partial cache file deleted while it downloads (by the device's cache
/// management, say): the download writes on into the deleted file, where no
/// response can open what it holds. A request resets the cache, and its range
/// downloads again.
void main() {
late CacheTestHarness h;

setUp(() async {
h = CacheTestHarness();
await h.setUp();
});

tearDown(() => h.tearDown());

Future<Uint8List> read(HttpCacheStream stream, int start, int end) async {
final response = await stream.request(start: start, end: end);
final builder = BytesBuilder(copy: false);
await for (final chunk in response.stream) {
builder.add(chunk);
}
return builder.takeBytes();
}

test('a range it had downloaded is read again after the file was deleted',
() async {
final half = h.origin.payload.length ~/ 2;
final bodyGate = Completer<void>();
h.origin
..responseBodyGate = bodyGate
..responseBodyGateAfterBytes = half;
final stream = h.manager.createStream(h.origin.url('/deleted.mp3'));
final reachedHalf = Completer<void>();
final errors = <Object>[];
final subscription = stream.cacheStateStream.listen((state) {
if (state.position >= half && !reachedHalf.isCompleted) {
reachedHalf.complete();
}
}, onError: errors.add);
final download = stream.download();
await reachedHalf.future.timeout(const Duration(seconds: 5));
final partial = stream.files.partial;
for (var i = 0; i < 100 && (partial.lengthSyncOrNull() ?? 0) < half; i++) {
await Future<void>.delayed(const Duration(milliseconds: 10));
}
await partial.delete();

final bytes =
await read(stream, 1000, 9000).timeout(const Duration(seconds: 10));
expect(bytes, Uint8List.sublistView(h.origin.payload, 1000, 9000));
expect(errors.whereType<InvalidCacheSizeException>().map((e) => e.size),
contains(-1),
reason: 'the deletion is reported');

bodyGate.complete();
final file = await download.timeout(const Duration(seconds: 15));
expect(Payload.hash(await file.readAsBytes()), h.payloadHash);
await subscription.cancel();
await stream.dispose(force: true);
});
}
Loading