Initial commit
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:moonwell_launcher/config.dart';
|
||||
import 'package:moonwell_launcher/core/use_case.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_exceptions.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_request.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_result.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/repositories/file_system.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/repositories/storage_reader.dart';
|
||||
|
||||
final class DownloadObjectUseCaseInput {
|
||||
final DownloadRequest request;
|
||||
|
||||
final FutureOr<bool> Function()? isCancelled;
|
||||
|
||||
final void Function(DownloadResult result)? onComplete;
|
||||
|
||||
const DownloadObjectUseCaseInput({
|
||||
required this.request,
|
||||
this.isCancelled,
|
||||
this.onComplete,
|
||||
});
|
||||
}
|
||||
|
||||
@lazySingleton
|
||||
class DownloadObjectUseCase
|
||||
implements StreamUseCase<DownloadObjectUseCaseInput, DownloadProgress> {
|
||||
final StorageReader _storage;
|
||||
|
||||
final FileSystem _fileSystem;
|
||||
|
||||
const DownloadObjectUseCase({
|
||||
required StorageReader storage,
|
||||
required FileSystem fileSystem,
|
||||
}) : _storage = storage,
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
@override
|
||||
Stream<DownloadProgress> call(DownloadObjectUseCaseInput input) async* {
|
||||
final request = input.request;
|
||||
|
||||
final meta = await _storage.stat(
|
||||
bucket: Config.bucketName,
|
||||
key: Config.objectName,
|
||||
);
|
||||
final total = meta.size;
|
||||
|
||||
final pathToFile = '${request.destinationPath}/${Config.objectName}';
|
||||
|
||||
await _fileSystem.ensureParentExists(pathToFile);
|
||||
|
||||
int offset = 0;
|
||||
if (await _fileSystem.exists(pathToFile)) {
|
||||
offset = await _fileSystem.sizeOf(pathToFile);
|
||||
if (offset > total) {
|
||||
await _fileSystem.truncate(pathToFile);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
if (offset == total) {
|
||||
input.onComplete?.call(
|
||||
DownloadResult(path: pathToFile, bytes: offset, eTag: meta.eTag),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await _fileSystem.createEmpty(pathToFile);
|
||||
}
|
||||
|
||||
final objectStream = await _storage.readFromOffset(
|
||||
bucket: Config.bucketName,
|
||||
key: Config.objectName,
|
||||
startOffset: offset,
|
||||
);
|
||||
final sink = await _fileSystem.openAppend(pathToFile);
|
||||
final controller = StreamController<DownloadProgress>();
|
||||
|
||||
final sw = Stopwatch()..start();
|
||||
int lastBytes = 0;
|
||||
int downloaded = offset;
|
||||
|
||||
Timer? ticker;
|
||||
|
||||
void tick() {
|
||||
final elapsedMs = sw.elapsedMilliseconds;
|
||||
final bps = elapsedMs == 0 ? 0 : (lastBytes * 1000) ~/ elapsedMs;
|
||||
final remaining = (total - downloaded).clamp(0, total);
|
||||
final eta = bps > 0 ? Duration(seconds: remaining ~/ bps) : Duration.zero;
|
||||
|
||||
controller.add(
|
||||
DownloadProgress(
|
||||
downloaded: downloaded,
|
||||
total: total,
|
||||
speed: bps,
|
||||
eta: eta,
|
||||
),
|
||||
);
|
||||
lastBytes = 0;
|
||||
sw
|
||||
..reset()
|
||||
..start();
|
||||
}
|
||||
|
||||
ticker = Timer.periodic(const Duration(seconds: 1), (_) => tick());
|
||||
|
||||
late StreamSubscription<List<int>> sub;
|
||||
controller.onCancel = () async {
|
||||
await sub.cancel();
|
||||
await sink.close();
|
||||
ticker?.cancel();
|
||||
};
|
||||
|
||||
sub = objectStream.listen(
|
||||
(chunk) async {
|
||||
if (input.isCancelled != null && await input.isCancelled!.call()) {
|
||||
ticker?.cancel();
|
||||
await sub.cancel();
|
||||
await sink.close();
|
||||
controller.addError(const CancelledException());
|
||||
await controller.close();
|
||||
return;
|
||||
}
|
||||
sink.add(chunk);
|
||||
final n = chunk.length;
|
||||
downloaded += n;
|
||||
lastBytes += n;
|
||||
},
|
||||
onError: (e, st) async {
|
||||
ticker?.cancel();
|
||||
await sink.close();
|
||||
await controller.close();
|
||||
throw _mapError(e);
|
||||
},
|
||||
onDone: () async {
|
||||
ticker?.cancel();
|
||||
tick(); // final snapshot
|
||||
await sink.close();
|
||||
await controller.close();
|
||||
input.onComplete?.call(
|
||||
DownloadResult(path: pathToFile, bytes: downloaded, eTag: meta.eTag),
|
||||
);
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
yield* controller.stream;
|
||||
}
|
||||
|
||||
DownloadException _mapError(Object e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('SocketException') || s.contains('HandshakeException')) {
|
||||
return NetworkException(s);
|
||||
}
|
||||
if (s.contains('Minio')) return RemoteException(s);
|
||||
return IoException(s);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user