作者:Karl_wei
链接:https://juejin.cn/post/7123965772132515877
来源:稀土掘金
在项目开发中,特别是C端的产品,资源下载实现断点续传是非常有必要的。今天我们不讲过多原理的知识,分享下简单实用的资源断点续传。
一般情况下,下载的功能模块,至少需要提供如下基础功能:资源下载、取消当前下载、资源是否下载成功、资源文件的大小、清除缓存文件。
而断点续传主要体现在取消当前下载后,再次下载时能在之前已下载的基础上继续下载。这个能极大程度的减少我们服务器的带宽损耗,而且还能为用户减少流量,避免重复下载,提高用户体验。
前置条件:资源必须支持断点续传。如何确定可否支持?看看你的服务器是否支持Range请求即可。
import 'package:dio/dio.dart';typedef ProgressCallBack = void Function(int count, int total);typedef CancelTokenProvider = void Function(CancelToken cancelToken);abstract class AssetRepositoryProtocol { /// 下载单一资源 Future downloadAsset(String url, {String? subDir, ProgressCallBack? onReceiveProgress, CancelTokenProvider? cancelTokenProvider, Function(String)? done, Function(Exception)? failed}); /// 取消下载,Dio中通过CancelToken可控制 void cancelDownload(CancelToken cancelToken); /// 获取文件的缓存地址 Future filePathForAsset(String url, {String? subDir}); /// 检查文件是否缓存成功,简单对比md5 Future checkCachedSuccess(String url, {String? md5Str}); /// 查看缓存文件的大小 Future cachedFileSize({String? subDir}); /// 清除缓存 Future clearCache({String? subDir});} class AssetRepository implements AssetRepositoryProtocol { AssetRepository(this.httpManager); final HttpManagerProtocol httpManager; @override Future downloadAsset(String url, {String? subDir, ProgressCallBack? onReceiveProgress, CancelTokenProvider? cancelTokenProvider, Function(String)? done, Function(Exception)? failed}) async { CancelToken cancelToken = CancelToken(); if (cancelTokenProvider != null) { cancelTokenProvider(cancelToken); } final savePath = await _getSavePath(url, subDir: subDir); try { httpManager.downloadFile( url: url, savePath: savePath + '.temp', onReceiveProgress: onReceiveProgress, cancelToken: cancelToken, done: () { done?.call(savePath); }, failed: (e) { print(e); failed?.call(e); }); return savePath; } catch (e) { print(e); rethrow; } } @override void cancelDownload(CancelToken cancelToken) { try { if (!cancelToken.isCancelled) { cancelToken.cancel(); } } catch (e) { print(e); } } @override Future filePathForAsset(String url, {String? subDir}) async { final path = await _getSavePath(url, subDir: subDir); final file = File(path); if (!(await file.exists())) { return null; } return path; } @override Future checkCachedSuccess(String url, {String? md5Str}) async { String? path = await _getSavePath(url, subDir: FileType.video.dirName); bool isCached = await File(path).exists(); if (isCached && (md5Str != null && md5Str.isNotEmpty)) { // 存在但是md5验证不通过 File(path).readAsBytes().then((Uint8List str) { if (md5.convert(str).toString() != md5Str) { path = null; } }); } else if (isCached) { return path; } else { path = null; } return path; } @override Future cachedFileSize({String? subDir}) async { final dir = await _getDir(subDir: subDir); if (!(await dir.exists())) { return 0; } int totalSize = 0; await for (var entity in dir.list(recursive: true)) { if (entity is File) { try { totalSize += await entity.length(); } catch (e) { print('Get size of $entity failed with exception: $e'); } } } return totalSize; } @override Future clearCache({String? subDir}) async { final dir = await _getDir(subDir: subDir); if (!(await dir.exists())) { return; } dir.deleteSync(recursive: true); } Future _getSavePath(String url, {String? subDir}) async { final saveDir = await _getDir(subDir: subDir); if (!saveDir.existsSync()) { saveDir.createSync(recursive: true); } final uri = Uri.parse(url); final fileName = uri.pathSegments.last; return saveDir.path + fileName; } Future _getDir({String? subDir}) async { final cacheDir = await getTemporaryDirectory(); late final Directory saveDir; if (subDir == null) { saveDir = cacheDir; } else { saveDir = Directory(cacheDir.path + '/$subDir/'); } return saveDir; }} final downloadDio = Dio();Future downloadFile({ required String url, required String savePath, required CancelToken cancelToken, ProgressCallback? onReceiveProgress, void Function()? done, void Function(Exception)? failed,}) async { int downloadStart = 0; File f = File(savePath); if (await f.exists()) { // 文件存在时拿到已下载的字节数 downloadStart = f.lengthSync(); } print("start: $downloadStart"); try { var response = await downloadDio.get( url, options: Options( /// Receive response data as a stream responseType: ResponseType.stream, followRedirects: false, headers: { /// 加入range请求头,实现断点续传 "range": "bytes=$downloadStart-", }, ), ); File file = File(savePath); RandomAccessFile raf = file.openSync(mode: FileMode.append); int received = downloadStart; int total = await _getContentLength(response); Stream stream = response.data!.stream; StreamSubscription? subscription; subscription = stream.listen( (data) { /// Write files must be synchronized raf.writeFromSync(data); received += data.length; onReceiveProgress?.call(received, total); }, onDone: () async { file.rename(savePath.replaceAll('.temp', '')); await raf.close(); done?.call(); }, onError: (e) async { await raf.close(); failed?.call(e); }, cancelOnError: true, ); cancelToken.whenCancel.then((_) async { await subscription?.cancel(); await raf.close(); }); } on DioError catch (error) { if (CancelToken.isCancel(error)) { print("Download cancelled"); } else { failed?.call(error); } }} 这篇文章确实没有技术含量,水一篇,但其实是实用的。这个断点续传的实现有几个注意的点:
| 留言与评论(共有 0 条评论) “” |