start method

Future<void> start()

Implementation

Future<void> start() async {
  _dio ??= initDio();
  FileDownloader config = this;
  String url = config.url;
  String? filePath = config.filePath;
  bool forceRedownload = config.forceRedownload ?? false;
  bool notAcceptRanges = config.notAcceptRanges ?? false;
  int? fileSizeAlreadyKnown = config.fileSizeAlreadyKnown;

  config.onStartReal?.call(url, filePath ?? "");

  if (_runningTask.contains(url)) {
    print('该url已经在下载中 $url');
    config.onFailed?.call(url, filePath ?? "", '', '该url已经在下载中', null);
    return;
  }

  try {
    filePath = await FileAndDirUtil.dealFilePath(config);
    config.filePath = filePath;
  } catch (e) {
    print('下载失败0 $url \n $e');
    config.onFailed?.call(url, filePath ?? "", '', e.toString(), null);
    return;
  }
  try {
    File file = File(filePath);

    bool isRangeRequest = false;

    if (file.existsSync() && file.lengthSync() > 0 && forceRedownload) {
      file.deleteSync();
    }

    _runningTask.add(url);

    Map<String, dynamic> headers = {};
    headers.addAll(config.headers);

    if (file.existsSync() && file.lengthSync() > 0) {
      Response headResponse = await _dio!.head(url,options: Options(headers: config.headers));

      if (fileSizeAlreadyKnown == null || fileSizeAlreadyKnown == 0) {
        fileSizeAlreadyKnown =
            int.tryParse(headResponse.headers.value('content-length') ?? '0');
      }

      bool supportRanges =
          headResponse.headers.value('accept-ranges') == 'bytes';


      if (fileSizeAlreadyKnown != null && fileSizeAlreadyKnown > 0) {
        if (file.lengthSync() == fileSizeAlreadyKnown) {
          print('文件已存在并且大小与服务器匹配 $filePath');
          _runningTask.remove(url);
          config.onSuccess(url, filePath);
          return;
        } else if (supportRanges && !notAcceptRanges) {
          headers['Range'] = 'bytes=${file.lengthSync()}-';
          isRangeRequest = true;
        } else {
          file.deleteSync();
        }
      }
    }

    CancelToken cancelToken = CancelToken();
    _tokenMap[url] = cancelToken;
    Response<ResponseBody> response = await _dio!.get<ResponseBody>(url,
        options: Options(responseType: ResponseType.stream,headers: headers),
    cancelToken: cancelToken);

    if (response.statusCode != 200 && response.statusCode != 206) {
      print('下载失败: ${response.statusCode}, $url');
      _runningTask.remove(url);
      _tokenMap.remove(url);
      config.onFailed?.call(url, filePath, response.statusCode.toString(),
          '下载失败: ${response.statusMessage}', null);
      return;
    }

    var contentLength =
        int.tryParse(response.headers.value('content-length') ?? '0');

    fileSizeAlreadyKnown = fileSizeAlreadyKnown ??contentLength ;

    var totalBytesReceived = file.existsSync() ? file.lengthSync() : 0;

    var sink = isRangeRequest
        ? file.openWrite(mode: FileMode.append)
        : file.openWrite();
    int lastReceived = 0;
    int lastProgressTime = 0;
    await response.data!.stream.listen(
      (data) {
        sink.add(data);
        totalBytesReceived += data.length;
        if(lastProgressTime ==0){
          lastReceived = data.length;
          lastProgressTime = DateTime.now().millisecondsSinceEpoch;
          config.onProgress?.call(url, filePath!, fileSizeAlreadyKnown ?? 0,
              totalBytesReceived, 0);
        }else{
          if(DateTime.now().millisecondsSinceEpoch - lastProgressTime >= config.progressCallbackIntervalMills){
            int received = totalBytesReceived - lastReceived;
            int speed = (received * 1000 /(DateTime.now().millisecondsSinceEpoch - lastProgressTime)).round();
            lastProgressTime = DateTime.now().millisecondsSinceEpoch;
            lastReceived = totalBytesReceived;
            config.onProgress?.call(url, filePath!, fileSizeAlreadyKnown ?? 0,
                totalBytesReceived, speed);
            int? size = fileSizeAlreadyKnown;
            if(fileSizeAlreadyKnown == null || fileSizeAlreadyKnown ==0){
              print("download-progress: unknown%, $url"
                  "\n${(totalBytesReceived/1024).toStringAsFixed(0)}KB, speed: ${(speed/1024).toStringAsFixed(0)}KB/s");
            }else{
              print("download-progress: ${(totalBytesReceived*100.0/(fileSizeAlreadyKnown)).toStringAsFixed(1)}%, $url"
                  "\n${(totalBytesReceived/1024).toStringAsFixed(0)}KB, speed: ${(speed/1024).toStringAsFixed(0)}KB/s");
            }

          }
        }

        /*if(!_runningTask.contains(url)){
          throw HttpException("canceled", uri: Uri.tryParse(url));
        }*/
      },
      onDone: () async {
        await sink.close();

        _runningTask.remove(url);
        _tokenMap.remove(url);
        if (fileSizeAlreadyKnown != null && fileSizeAlreadyKnown>0 &&
            file.lengthSync() != fileSizeAlreadyKnown) {
          print('下载失败: 文件大小不匹配 $url  ${file.lengthSync()}, $fileSizeAlreadyKnown');

          config.onFailed?.call(
            url,
            filePath!,
            'size not same',
            '文件大小不匹配: ${file.lengthSync()}, $fileSizeAlreadyKnown',
            null,
          );
          file.deleteSync();
        } else {
          config.onSuccess(url, filePath!);
          print('下载成功 $url  -> $filePath');
        }
      },
      onError: (e,s) {
        //HttpException: Connection closed while receiving data
        String text = e.toString();
        if(text.startsWith("HttpException: Connection closed while receiving data")){
          if(!_tokenMap.containsKey(url)){
            //为取消的请求:
            print('请求被手动取消: $url \n-> $e ->\n$s');
            config.onCancel?.call(url, filePath!);
            return;
          }
        }
        _runningTask.remove(url);
        _tokenMap.remove(url);
        config.onFailed?.call(url, filePath!, "", e.toString(), null);
        print('下载失败2: $url \n-> $e ->\n$s');

        //HttpException: Connection closed while receiving data
      },
      cancelOnError: true,
    );
  } catch (e) {
    _runningTask.remove(url);
    _tokenMap.remove(url);
    String msg = e.toString();
    if (e is DioError) {
      DioError error = e;
      msg = error.message;
    }
    config.onFailed?.call(url, filePath, "", msg, e as Exception);
    print('下载失败1: $url \n-> $msg');
  }
}