Initial commit

This commit is contained in:
gasaichandesu
2025-08-30 19:47:42 +04:00
commit 58d7808a8d
100 changed files with 6186 additions and 0 deletions
@@ -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);
}
}