初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
// ignore_for_file: deprecated_member_use
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import '../../../hj_utils/text_util.dart';
import 'e_data.dart';
final _defaultOptions = BaseOptions(
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
// dio 原生会加上 request header:accept-encoding gzip,导致部分请求失败
// headers: {HttpHeaders.acceptEncodingHeader: "*"},
validateStatus: (int? status) => (status ?? 600) < 600,
);
/// 创建一个支持http/http2和兼容tls证书错误的dio层
/// cur => http not http2后台暂时不支持
/// [mainThread] 默认httpclient在主线程中
Dio createDio({BaseOptions? options, bool mainThread = true}) {
options ??= _defaultOptions;
options.headers[HttpHeaders.acceptEncodingHeader] = "*";
var dio = Dio(options);
var adapter = DefaultHttpClientAdapter();
// var adapter = DefaultHttpClientAdapter();
adapter.onHttpClientCreate = (client) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
};
dio.httpClientAdapter = adapter;
return dio;
}
/// 获取一个请求的rangeStart
/// range bytes=677636-
int getRangeStart(Map<String, String>? reqHeaders) {
var rangeStart = 0;
if (null != reqHeaders && reqHeaders.containsKey(HttpHeaders.rangeHeader)) {
// HttpHeaders.contentRangeHeader
// HttpHeaders
var rangeStr = reqHeaders[HttpHeaders.rangeHeader];
if (TextUtil.isNotEmpty(rangeStr)) {
var arr = rangeStr?.split("=");
if (arr?.isNotEmpty == true && arr!.length > 1) {
var arr2 = arr[1].split("-");
if (arr2.isNotEmpty == true) {
rangeStart = int.parse(arr2[0]);
}
}
}
}
return rangeStart;
}
/// dio 帮助类
class DioCli {
// final BaseOptions options;
late Dio _dio;
DioCli({BaseOptions? options}) {
_dio = createDio(options: options);
}
/// 获取文本
Future<EData<Response<String>>> getStr(String url, {Options? options, Map<String, dynamic>? headers, CancelToken? cancelToken}) {
final opt = options ?? Options();
opt.responseType = ResponseType.plain;
opt.headers = headers ?? {};
return asyncCall(() => _dio.get<String>(url, options: opt, cancelToken: cancelToken));
}
// 获取二进制数据
Future<EData<Response<List<int>>>> getBytes(String url, {Options? options, Map<String, dynamic>? headers, CancelToken? cancelToken}) {
final opt = options ?? Options();
opt.responseType = ResponseType.bytes;
opt.headers = headers ?? {};
return asyncCall(() => _dio.get<List<int>>(url, options: opt, cancelToken: cancelToken));
}
/// 获取数据流
Future<EData<Response<ResponseBody>>> getStream(String url, {Options? options, Map<String, dynamic>? headers, CancelToken? cancelToken}) {
final opt = options ?? Options();
opt.responseType = ResponseType.stream;
opt.headers = headers ?? {};
return asyncCall(() => _dio.get<ResponseBody>(url, options: opt, cancelToken: cancelToken));
}
/// 获取json数据
Future<EData<Response<Map<String, dynamic>>>> getJSON(String url,
{Options? options, Map<String, dynamic>? headers, CancelToken? cancelToken}) {
final opt = options ?? Options();
opt.responseType = ResponseType.json;
opt.headers = headers ?? {};
return asyncCall(() => _dio.get<Map<String, dynamic>>(url, options: opt, cancelToken: cancelToken));
}
/// 获取头
Future<EData<Response>> getHeader(String url, {Options? options, Map<String, dynamic>? headers, CancelToken? cancelToken}) {
final opt = options ?? Options();
opt.responseType = ResponseType.bytes;
opt.headers = headers ?? {};
return asyncCall(() => _dio.head(url, options: opt, cancelToken: cancelToken));
}
}
@@ -0,0 +1,354 @@
// ignore_for_file: unnecessary_null_comparison, constant_identifier_names, depend_on_referenced_packages
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'package:dio/dio.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:path/path.dart' as p;
import 'dio_cli.dart';
import 'e_data.dart';
const String _rangeHeader = "Range";
const String _acceptsRangesHeader = "Accept-Ranges";
const String _etagHeader = "ETag";
const String _contentRangeHeader = "Content-Range";
const int _M = 1024 * 1024;
const int _sliceStep = 2 * _M;
const Duration _readSliceTimeout = Duration(seconds: 60);
class _Chunk {
final Uint8List data;
_Chunk(this.data);
}
class DLError extends Error {
final Object err;
DLError(this.err);
@override
String toString() => err.toString();
}
//校对错误
class CheckSizeError extends DLError {
CheckSizeError() : super("Check Size failed");
}
//网络错误
class NetworkError extends DLError {
NetworkError(err) : super(err);
}
// 使用Dio.download时的错误
class DioDLError extends DLError {
DioDLError(err) : super(err);
}
// 文件系统错误
class FileSystemError extends DLError {
FileSystemError(err) : super(err);
}
_ddPrint(Object msg) => debugLog("oldLog", "[Dio-Downloader] $msg");
class DioSliceDownloader {
final DioCli cli;
final String? url;
final String? saveDirectory;
final String? dioSaveName;
final ProgressCallback? onReceiveProgress;
final Map<String, dynamic>? headers;
final Queue<_Chunk> _cached = Queue();
bool _isWriting = false;
RandomAccessFile? _raf;
String? _remoteETag = "";
int _remoteTotalLength = 0;
int _localInitSavedLength = 0;
int _localCurrentSavedLength = 0;
bool _remoteDownloadFinish = false;
final Completer<String> _downloadCompleter = Completer();
DioSliceDownloader(
this.cli, this.url, this.headers, this.saveDirectory, this.dioSaveName,
{this.onReceiveProgress});
Map<String, dynamic> _fillRangeHeader(int start, int end) {
final Map<String, dynamic> rangeHeader = {
_rangeHeader: "bytes=$start-$end"
};
if (headers != null) rangeHeader.addAll(headers!);
return rangeHeader;
}
// 返回 true OR false 表示是否支持分片下载
Future<EData<bool>> _fetchRemoteTotalLength() async {
final v = await cli.getHeader(url ?? "", headers: _fillRangeHeader(0, 0));
if (v.err != null) return EData(v.err, null);
final statusCode = v.data?.statusCode;
final remoteHeaders = v.data?.headers;
if (remoteHeaders == null) return EData(null, false); //不支持
final acceptRanges = remoteHeaders[_acceptsRangesHeader];
if (acceptRanges == null ||
acceptRanges.isEmpty ||
acceptRanges.first != "bytes") return EData(null, false);
if (statusCode != HttpStatus.partialContent) {
return EData(null, false); //不支持
}
final etags = remoteHeaders[_etagHeader];
if (etags == null || etags.isEmpty) {
return EData(null, false); //没有etag 认为不支持
}
final etag = etags.first;
if (etag.isEmpty) return EData(null, false); // etag为空,认为不支持
final contentRangeHeaders = remoteHeaders[_contentRangeHeader];
if (contentRangeHeaders == null || contentRangeHeaders.isEmpty) {
return EData(null, false); //不支持
}
final contentRangeHeader = contentRangeHeaders.first;
if (contentRangeHeader == null) return EData(null, false); //不支持
final temp = contentRangeHeader.split("/");
if (temp.length != 2) return EData(null, false); //不支持
final totalLengthStr = temp[1];
final totalLength = int.tryParse(totalLengthStr);
if (totalLength == null || totalLength == 0) {
return EData(null, false); //不支持
}
_remoteETag = hex.encode(utf8.encode(etag)); //etag中可能有 " 符号,这里统一处理下
_remoteTotalLength = totalLength;
_ddPrint("远端etag $etag, 标准化后的etag: $_remoteETag");
return EData(null, true);
}
// 使用断点续传的文件名
String _getETagSavePath() => p.join(saveDirectory ?? "", "$_remoteETag.apk");
// 使用原始DIO下载的文件名
String _getDioSavePath() => p.join(saveDirectory ?? "", dioSaveName);
void _closeRAFSync() {
if (_raf == null) return;
_raf?.closeSync();
_raf = null;
}
_clearAndEnsureApkDirectorySync() {
final dir = Directory(saveDirectory ?? "");
if (dir.existsSync()) dir.deleteSync(recursive: true);
dir.createSync(recursive: true);
}
int _getLocalCurrentLengthSync() {
final file = File(_getETagSavePath());
int length = 0;
if (file.existsSync()) {
length = file.lengthSync();
} else {
_clearAndEnsureApkDirectorySync();
}
_raf = file.openSync(mode: FileMode.writeOnlyAppend);
return length;
}
void _completeDownload({String? savePath, DLError? error}) {
syncCall(() => _closeRAFSync());
if (_downloadCompleter.isCompleted) return;
if (error == null) {
assert(savePath != null || savePath != "");
_downloadCompleter.complete(savePath);
} else {
_downloadCompleter.completeError(error);
}
}
Future<String> download() async {
_ddPrint("开始下载 $url");
_fetchRemoteTotalLength().then((support) {
if (support.err != null) {
_ddPrint("获取远端大小失败 ${support.err}");
_completeDownload(error: NetworkError(support.err));
return;
}
if (support.data == true) {
final mb = (_remoteTotalLength / _M).toStringAsFixed(2);
_ddPrint(
"远端支持断点下载 文件总大小:${_remoteTotalLength}B(${mb}MB) 文件标识:$_remoteETag");
try {
final localCurrentLength = _getLocalCurrentLengthSync();
_localInitSavedLength = localCurrentLength;
_localCurrentSavedLength = localCurrentLength;
_ddPrint("获取本地文件大小成功 已下载:$_localCurrentSavedLength");
} catch (e) {
_ddPrint("获取本地文件大小失败 错误 $e");
_completeDownload(error: FileSystemError(e));
return;
}
_progress();
if (_localCurrentSavedLength == _remoteTotalLength) {
_ddPrint("本地文件大小和远端文件大小相等,直接完成下载");
_completeDownload(savePath: _getETagSavePath());
} else if (_localCurrentSavedLength > _remoteTotalLength) {
_ddPrint("本地文件大小大于远端文件大小,清除本地目录");
syncCall(() => _clearAndEnsureApkDirectorySync());
_completeDownload(error: CheckSizeError());
} else {
_fetchSlice();
}
} else {
_ddPrint("远端不支持断点下载 使用Dio直接下载 ${_getDioSavePath()}");
//直接调用dio.
final savePath = _getDioSavePath();
Dio()
.download(url ?? "", savePath, onReceiveProgress: onReceiveProgress)
.then((_) {
_completeDownload(savePath: savePath);
}).catchError((err) {
_completeDownload(error: DioDLError(err));
});
}
});
return _downloadCompleter.future;
}
void _progress() {
if (onReceiveProgress == null) return;
syncCall(
() => onReceiveProgress!(_localCurrentSavedLength, _remoteTotalLength));
}
Future _fetch(int rangeStart, int rangeEnd) async {
final v = await cli.getStream(url ?? "",
headers: _fillRangeHeader(rangeStart, rangeEnd));
if (v.err != null) {
_ddPrint("Dio fetch 获取失败 range:$rangeStart-$rangeEnd err: ${v.err}");
return EData(v.err, null);
}
final completer = Completer();
final stream = v.data?.data?.stream;
stream?.timeout(_readSliceTimeout, onTimeout: (sink) {
sink.addError("Read Stream Timeout");
sink.close();
}).listen((data) {
if (data.isNotEmpty) _cached.add(_Chunk(data));
Future.microtask(() => _tryWrite());
}, onDone: () {
completer.complete();
}, onError: (err) {
_ddPrint("Dio fetch 获取流失败 range:$rangeStart-$rangeEnd err: $err");
completer.completeError(err);
}, cancelOnError: true);
return completer.future;
}
void _fetchSlice() async {
int currentStart = _localInitSavedLength;
while (true) {
if (_downloadCompleter.isCompleted) return;
final start = currentStart;
final end = start + _sliceStep;
if (start >= _remoteTotalLength) {
_ddPrint("片段下载起始位置已大于总长度, 网络下载完成");
_remoteDownloadFinish = true;
_tryWrite();
break;
}
_ddPrint("开始下载 $start-$end, 总共$_remoteTotalLength");
final v = await asyncCall(() => _fetch(start, end));
if (v.err != null) {
_ddPrint("错误下载 $start-$end, 总共$_remoteTotalLength err:${v.err}");
_completeDownload(error: NetworkError(v.err));
return;
}
currentStart = end + 1; //闭区间
}
}
void _tryWrite() async {
if (_isWriting || _cached.isEmpty || _downloadCompleter.isCompleted) return;
_isWriting = true;
final first = _cached.removeFirst();
final list = List<int>.from(first.data);
final writeRet = await asyncCall(() => _raf?.writeFrom(list));
if (writeRet.err != null) {
_ddPrint("写入错误: ${writeRet.err}");
_completeDownload(error: FileSystemError(writeRet.err));
} else {
_localCurrentSavedLength += first.data.length;
//_ddPrint("写入大小 ${list.length} 当前实际大小:${_raf.lengthSync()} 内存计算大小: $_localCurrentSavedLength");
if (_remoteDownloadFinish && _cached.isEmpty) {
try {
_ddPrint("写入完成 开始刷新磁盘缓存");
_raf?.flushSync();
_ddPrint("刷新磁盘缓存完成,开始关闭RAF");
_closeRAFSync();
_ddPrint("关闭RAF完成,开始检查文件大小");
//做最后的检查
final localLength = File(_getETagSavePath()).lengthSync();
if (localLength != _remoteTotalLength) {
_ddPrint("检查文件大小失败 local:$localLength, Target:$_remoteTotalLength");
_clearAndEnsureApkDirectorySync();
_completeDownload(error: CheckSizeError());
} else {
_ddPrint("检查文件大小完成, 下载完成:${_getETagSavePath()} 大小: $localLength");
_completeDownload(savePath: _getETagSavePath());
}
} catch (e) {
_ddPrint("网络下载完成,磁盘刷新错误 $e");
_completeDownload(error: FileSystemError(e));
}
}
_progress();
}
_isWriting = false;
_tryWrite();
}
}
typedef OnRetry = void Function(int retryCount);
class DioSliceRetryDownloader {
final DioCli cli;
final String url;
final Map<String, dynamic>? headers;
final String saveDirectory;
final String dioSaveName;
final ProgressCallback? onReceiveProgress;
final int retry;
final Duration retryInterval;
final OnRetry? onRetry;
DioSliceRetryDownloader(
this.cli, this.url, this.headers, this.saveDirectory, this.dioSaveName,
{this.onReceiveProgress,
this.retry = 3,
this.retryInterval = const Duration(seconds: 3),
this.onRetry});
Future<String> _download() {
return DioSliceDownloader(cli, url, headers, saveDirectory, dioSaveName,
onReceiveProgress: onReceiveProgress)
.download();
}
Future<String> download() async {
int tryCount = 0;
while (true) {
tryCount++;
try {
return await _download();
} catch (e) {
if (tryCount >= retry) rethrow;
syncCall(() => onRetry!(tryCount));
await Future.delayed(retryInterval);
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
/// 同步和异步调用,
/// 错误和正确数据
class EData<T> {
final dynamic _e;
final T? _d;
EData(dynamic e, T? d)
: _e = e,
_d = d;
get err => _e;
T? get data => (_d is T) ? _d : null;
}
/// 同步调用
EData<T> syncCall<T>(Function f) {
try {
return EData(null, f() as T);
} catch (e) {
return EData(e, null);
}
}
//异步调用
Future<EData<T>> asyncCall<T>(Function f) async {
try {
return EData(null, (await f()) as T);
} catch (e) {
return EData(e, null);
}
}