dio中的download,在使用过程中,并不能很好地支持.从源码实现上看是有问题的.
headers: { "range": "bytes=$processed-", },
这是传的参数 processed是已经下载过的字节,-后面可以跟上总的. 也可以不跟.
await DioClient().dio.download(
url,
file.path,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
options: Options(
headers: {"range": "bytes=$processed-"}, //指定请求的内容区间
),
);
这段代码的问题:
不能断点,因为file使用的是write,可以取消,只有下载完了才能看到文件,如果失败了,会删除已下载的.
这在源码中可以看到实现.
另一段:
try {
var response = await DioClient().dio.get(
cancelToken: cancelToken,
url,
options: Options(
responseType: ResponseType.stream,
followRedirects: false,
headers: {
"range": "bytes=$processed-",
},
//"start": downloadStart,
),
);
RandomAccessFile raf = file.openSync(mode: FileMode.append);
Stream stream = response.data!.stream;
StreamSubscription? subscription;
subscription = stream.listen(
(data) {
/// 写入文件必须同步
raf.writeFromSync(data);
processed += data.length;
onReceiveProgress?.call(processed, total);
},
onDone: () async {
await raf.close();
},
onError: (e) async {
Log.i("download.onError.retry:$retry");
await raf.close();
retry++;
if (retry > 2) {
showToast("下载出错");
return false;
}
},
cancelOnError: true,
);
cancelToken?.whenCancel.then((_) async {
Log.i("download.cancelToken:");
await subscription?.cancel();
await raf.close();
});
} on DioException catch (error) {
/// 请求已发出,服务器用状态代码响应它不在200的范围内
Log.i("download.DioException:$retry, error:$error");
if (CancelToken.isCancel(error)) {
return false;
} else {
retry++;
if (retry > 2) {
showToast("下载出错");
return false;
}
}
}
这段代码,在我这边是取消不了.文件是有的.
打开dio的download代码一看,就是第一段的默认实现方式,只要修改两处,就可以实现断点续传与取消了.
要注意的是
int retry = 0;
do {
int processed = 0;
if (await file.exists()) {
processed = file.lengthSync();
}
url = "$url?start=$processed";
Log.i("download.start:$processed, total:$total, :$url");
try {
Response res = await Net.dioDownload(
url,
file,
onReceiveProgress: onReceiveProgress,
cancelToken: cancelToken,
);
Log.i("download.processed:$processed, res:$res");
return res.statusCode == 200 || res.statusCode == 206;
} catch (e) {
Log.i("download.error:$processed, e:$e");
if (e is DioException) {
if (CancelToken.isCancel(e)) {
Log.i("download.error:isCancel");
return false;
}
}
retry++;
if (retry > 2) {
showToast("下载出错");
return false;
}
}
} while (retry <= 2);
这个res是一个responsebody对象,里面的code=200,是非断点.=206,是断点续传.判断这两个值可以得到结果是成功了还是失败了.
对于取消,它会抛出异常来,可以捕获时得到是取消的原因.这里设置了重试,避免一些网络抖动导致的下载失败.canceltoken由外部传入就可以了,要取消的时候调用cancel就能取消了.
///这是dio包里面取的方法,原方法如果下载失败,默认删除文件.默认是从头下载的.
///所以网上的方法在下面,两个都无法断点,也可能是因为我们的接口不一样
///file参数,必须,外部传入
///使用示例可以看FileViewModel._doDownload2()
static Future dioDownload(
String urlPath,
File file, {
ProgressCallback? onReceiveProgress,
Map? queryParameters,
CancelToken? cancelToken,
bool deleteOnError = true,
String lengthHeader = Headers.contentLengthHeader,
Object? data,
Options? options,
}) async {
options ??= DioMixin.checkOptions('GET', options);
// Receive data with stream.
options.responseType = ResponseType.stream;
Response response;
try {
response = await DioClient().dio.request(
urlPath,
data: data,
options: options,
queryParameters: queryParameters,
cancelToken: cancelToken ?? CancelToken(),
);
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
e.response!.data as ResponseBody;
} else {
e.response!.data = null;
}
}
rethrow;
}
RandomAccessFile raf = file.openSync(mode: FileMode.append);
final completer = Completer();
Future future = completer.future;
int received = 0;
// Stream
final stream = response.data!.stream;
bool compressed = false;
int total = 0;
final contentEncoding = response.headers.value(
Headers.contentEncodingHeader,
);
if (contentEncoding != null) {
compressed = ['gzip', 'deflate', 'compress'].contains(contentEncoding);
}
if (lengthHeader == Headers.contentLengthHeader && compressed) {
total = -1;
} else {
total = int.parse(response.headers.value(lengthHeader) ?? '-1');
}
Future? asyncWrite;
bool closed = false;
Future closeAndDelete() async {
if (!closed) {
closed = true;
await asyncWrite;
await raf.close().catchError((_) => raf);
/*if (deleteOnError && file.existsSync()) {
await file.delete().catchError((_) => file);
}*/
}
}
late StreamSubscription subscription;
subscription = stream.listen(
(data) {
subscription.pause();
// Write file asynchronously
asyncWrite = raf.writeFrom(data).then((result) {
// Notify progress
received += data.length;
onReceiveProgress?.call(received, total);
raf = result;
if (cancelToken == null || !cancelToken.isCancelled) {
subscription.resume();
}
}).catchError((Object e) async {
try {
await subscription.cancel();
closed = true;
await raf.close().catchError((_) => raf);
if (deleteOnError && file.existsSync()) {
await file.delete().catchError((_) => file);
}
} finally {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
});
},
onDone: () async {
try {
await asyncWrite;
closed = true;
await raf.close().catchError((_) => raf);
completer.complete(response);
} catch (e) {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
},
onError: (e) async {
try {
await closeAndDelete();
} finally {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
},
cancelOnError: true,
);
cancelToken?.whenCancel.then((_) async {
await subscription.cancel();
await closeAndDelete();
});
final timeout = response.requestOptions.receiveTimeout;
if (timeout != null) {
future = future.timeout(timeout).catchError(
(dynamic e, StackTrace s) async {
await subscription.cancel();
await closeAndDelete();
if (e is TimeoutException) {
throw DioException.receiveTimeout(
timeout: timeout,
requestOptions: response.requestOptions,
error: e,
);
} else {
throw e;
}
},
);
}
return DioMixin.listenCancelForAsyncTask(cancelToken, future);
}
关键点就是closeAndDelete,这个方法意思就是关闭流且删除未下载完的文件.我把删除这段注释了,需要外部保证下载的字节.file的写入方法从write改为append.
由于我上传的服务器支持的方式不是header传参数 ,是在url后面处理,原理是一样的.
这是从dio源码中找到的下载方法,去除了删除文件,修改write为append,也就是说,原来的方法是可以从头开始下载,但中间不能停,停了会失败.但现在文件下载了多少,需要自己去保证.否则下载的文件是从现有的追加,有可能导致文件不正确.
可以看到,与版本2不同的是
raf.writeFrom(data).然后下面的cancel才有效.我不知道别人用了上面两个版本的能不能真正实现断点续传,没有实践过就到处转发是个毛病.污染太严重了.