breakPointDownload method

void breakPointDownload({
  1. required String savePath,
  2. ProgressCallback? onReceiveProgress,
  3. Success? success,
  4. Failure? failure,
  5. Completed? completed,
  6. dynamic cancelCallback()?,
})

断点下载

Implementation

void breakPointDownload({
  required String savePath,
  adapter.ProgressCallback? onReceiveProgress,
  Success? success,
  Failure? failure,
  Completed? completed,
  Function()? cancelCallback,
}) async {
  if (!(await _checkNetWork())) {
    return;
  }

  final file = File(savePath);

  try {
    final url = _buildFinalUrl();
    final payload = _resolveRequestPayload();

    if (!file.parent.existsSync()) {
      file.parent.createSync(recursive: true);
    }

    final requestedStart = file.existsSync() ? file.lengthSync() : 0;
    final headers = _buildHeaders();
    if (requestedStart > 0) {
      headers[HttpHeaders.rangeHeader] = 'bytes=$requestedStart-';
    }

    // 构建 AdapterRequest,内部会自动使用 stream 响应类型
    final adapterRequest = _buildAdapterRequest(
      url: url,
      queryParams: payload.queryParams,
      data: payload.body,
      headers: headers,
      contentType: payload.contentType,
      responseType: rxnet_plus.ResponseType.stream,
    );

    // 使用适配器发送请求
    final adapter = _requireAdapter();
    final response = await adapter.request(adapterRequest);
    onResponse?.call(response);

    final stream = _extractByteStream(response.data);
    if (stream == null) {
      throw NetworkException(
          'Breakpoint download requires a byte stream response', null);
    }

    final contentRange = response.getHeader(HttpHeaders.contentRangeHeader);
    final isResumed = requestedStart > 0 &&
        response.statusCode == HttpStatus.partialContent &&
        _parseContentRangeStart(contentRange) == requestedStart;
    var downloaded = isResumed ? requestedStart : 0;
    final total = _resolveBreakpointDownloadTotal(
      response,
      isResumed: isResumed,
      downloaded: downloaded,
    );

    final raf = file.openSync(
      mode: isResumed ? FileMode.append : FileMode.write,
    );
    try {
      await for (final chunk in stream) {
        raf.writeFromSync(chunk);
        downloaded += chunk.length;
        onReceiveProgress?.call(
          downloaded,
          total > 0 ? total : downloaded,
        );
      }
    } catch (error) {
      if (_isCancelledStreamError(error)) {
        cancelCallback?.call();
        return;
      }
      rethrow;
    } finally {
      await raf.close();
    }

    if (total > 0 && downloaded > total) {
      onReceiveProgress?.call(total, total);
    }
    success?.call(file, SourcesType.net);
  } on AdapterException catch (error) {
    if (error.type == AdapterExceptionType.cancel) {
      cancelCallback?.call();
    } else if (error.type == AdapterExceptionType.response &&
        error.statusCode == HttpStatus.requestedRangeNotSatisfiable) {
      final localLength = file.existsSync() ? file.lengthSync() : 0;
      final total = _parseContentRangeTotal(
        error.response?.getHeader(HttpHeaders.contentRangeHeader),
      );
      if (total != null && total == localLength) {
        onReceiveProgress?.call(total, total);
        success?.call(file, SourcesType.net);
      } else {
        failure?.call(error);
      }
    } else {
      failure?.call(error);
    }
  } catch (e) {
    failure?.call(e);
  } finally {
    completed?.call();
  }
}