十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

curl 断点续传自动重试脚本:原理、实现与实战排坑

curl 断点续传自动重试脚本:原理、实现与实战排坑 做服务器运维或者经常跟 Linux 打交道的人应该都体会过这种痛一个几 GB 的安装包、模型文件或者日志备份用 curl 吭哧吭哧下载了一大半眼看着进度条快到 100%结果网络一抖、SSH 断了一下、机房半夜调整路由连接直接卡死。你 CtrlC 之后再重新执行原来那条命令curl 却像个没有记忆的打工人一样老老实实又从 0 开始拉数据。那一刻的心情基本等同于写了两小时的文档没保存然后电脑蓝屏。其实 curl 原生就支持断点续传关键参数就是-C但你光靠手工指定它又能怎样下载中断了得手动重新敲命令或者写个固定参数碰运气根本没解决自动重试、自动续传、自动处理各种异常状态的问题。这篇文章我想跟你聊的就是我一直在用的一个“curl 下载自动断点续传脚本”它做的事情很简单把断点续传做成一个可复用的 shell 函数 / 脚本下载中断后自动从头继续而不是从头再来遇到服务器不支持续传、文件大小对不上、磁盘满了这些情况时它也能自动判断是重试、放弃还是拉警报。这篇文章不是教科书式地给你贴一段代码就完事我会把设计思路、curl 断点续传的原理、脚本里每个关键判断的理由、以及我踩过的坑一次性讲清楚。适合谁看一类是经常在服务器上下载大文件、做数据同步的运维和开发另一类是刚开始学 shell 脚本、想写出点“真正能干活”脚本的朋友。看完之后你既能拿去即抄即用也能理解背后的原理遇到类似需求能自己改出一版更合适的。后面所有代码我都贴的是完整可运行版本并且给出了详细的解释你根据自己的环境稍作调整就能直接用。1. 先搞清楚curl 的断点续传到底是怎么实现的用脚本之前我建议你先理解 curl 断点续传背后的原理。不然你遇到问题的时候连从哪个方向排查都不知道。1.1 curl -C 参数的本质是 HTTP Range 请求curl 的-C参数全称是--continue-at它有两种常见的写法curl -C - -O https://example.com/bigfile.tar.gz curl -C 12345678 -O https://example.com/bigfile.tar.gz-C -的意思是让 curl 自己根据本地已下载的文件大小自动计算出从哪里继续下载。注意这里的-不是可有可无的占位符它表达的是“自动”这个动作。-C 12345678则是你手动指定一个字节偏移量告诉 curl“你别看本地文件了就从这个字节位置接着下”。为什么能实现“从中间接着下”这依赖的是 HTTP 协议中的 Range 机制。curl 在断点续传时会在请求头里加上类似这样的字段Range: bytes12345678-服务端收到这个请求后如果支持断点续传会返回206 Partial Content状态码并且带上Content-Range: bytes 12345678-...这样的响应头然后直接从指定的字节区间传输数据。如果你的服务器不支持 Range它大概率会忽略掉这个头返回200 OK和完整的文件内容。这种情况curl 的行为会非常迷惑——它不会主动告诉你“服务器不支持续传”而是可能把返回的内容直接追加到已有文件末尾最终得到一个损坏的文件。1.2 不是所有下载场景都支持断点续传这是很多新手第一个踩坑的地方。你可能会想既然 curl 这么强大为什么断点续传还会失败因为断点续传能否成功不取决于 curl而取决于服务端。我简单列一下常见的情况静态文件服务器 / Nginx / Apache / OSS / S3这些场景大多数支持 Range 请求用-C -基本没问题。动态生成的下载链接比如程序实时生成的导出文件、需要经过鉴权逻辑的临时下载接口很多并没有正确实现 Range 响应。你请求Range: bytes...它照样给你返回 200 全量内容。CDN 边缘节点部分配置不正确的 CDN 或者缓存策略比较激进的反代对 Range 请求的支持差异很大。有的 CDN 节点缓存不完整你请求后半段内容它直接从源站拉全量然后再给你切片行为很不可控。签名 URL比如某些云厂商的对象存储临时链接链接里面带了有效期和签名参数。如果你下载到一半才去续传链接可能已经过期服务端直接给你 403这时候续传逻辑再强大也没用只能重新获取新链接重新下载。所以在写自动续传脚本时不光要处理“网络断开自动重连”这个最简单的情况还得考虑“服务端不支持续传”这种隐藏问题。否则脚本会在跑了几小时后给你拼出一个坏文件你还不自知。1.3 验证服务器是否支持 Range 请求一条命令搞定在写复杂的下载脚本之前我建议你先用curl -I看一下目标服务器返回的响应头curl -I https://example.com/bigfile.tar.gz关注响应头里有没有Accept-Ranges: bytes。有这个字段基本可以确定服务端支持按字节范围续传。如果没有那就要考虑你的脚本要不要设计一个“不支持续传时自动回退为全量重新下载”的逻辑。注意有些服务器虽然不返回Accept-Ranges但实际仍然能处理 Range 请求但始终会遵循返回的作为支持标记。1.4 什么时候别用断点续传还有一个反向经验不是所有下载都应该续传。如果文件本身极小几 KB 的配置文件或者下载地址是一次性的下载完立即失效甚至文件在服务端可能持续变化比如某个实时更新的日志快照那你做断点续传的意义不大反而可能因为续传把两段不同内容拼在一起得到坏文件。我在脚本里一般会加一个文件大小阈值判断小于某个值就直接重新下载不做续传。这不是炫技就是单纯地想少踩坑。2. 脚本设计思路从“能续传”到“真正无脑可用”知道了原理我们来聊脚本设计。很多人写下载重试脚本就是套一个 for 循环或者 while 循环失败就重新执行 curl表面看没问题但实际生产环境里跑一阵子就各种幺蛾子。2.1 核心目标让脚本自己判断成功和失败先说你最容易忽略的一点curl 命令执行完不等于下载成功。curl 的退出码为 0 只能说明“传输过程没有异常”但不代表文件就是完整的。举个很典型的场景你在一个 10 GB 的文件下载到 9.5 GB 时断了然后启动续传脚本结果发现本地已经存在一个 9.5 GB 的残留文件脚本直接判断为“已存在”就退出了。这种问题怎么防核心就是校验机制。我在脚本里放了两个层面的校验第一层是 curl 自身的退出码第二层是下载完成后的文件大小检查和可选的 checksum 校验。如果文件有固定的 MD5 或 SHA256 值甚至可以把校验值写进脚本参数里下载完成后自动比对。这样才能真正做到无人值守。2.2 无限重试 vs 有限重试别让你的脚本变成僵尸进程新手最容易犯的错误是写一个 while true 无限重试。网络一旦出问题这个脚本就会永远在那里重试、失败、重试、失败消耗 CPU、网络资源还可能把远端服务器打成“恶意访问”。我的经验是给每次重试之间加一个递增的等待时间并且设置最大重试次数。具体做法其实不复杂用一个变量记录当前是第几次重试每次重试前先 sleep 一段时间时间随重试次数递增比如第一次重试等待 5 秒第二次 15 秒第三次 30 秒最多等待 300 秒超过后放弃。这种策略在运维上叫退回退避本质上很朴素的想法给网络和服务端一个恢复的时间窗口同时不把自己拖死。2.3 区分“可重试错误”和“不可重试错误”这是脚本健壮性的关键。curl 有很多退出码但不是每个退出码都值得重试。我比较常用的是关键几个curl 退出码含义是否值得重试0成功不需要7连接失败值得网络抖动常见18传输过程中文件部分传输值得最典型的续传场景28超时值得但需要确认超时参数33服务端不支持 Range不值得重试多少次都没用35SSL 连接错误看情况可能是临时性也可能是证书问题51SSL 证书验证失败不建议直接重试要先排查证书56接收数据失败值得网络问题居多63写文件失败不值得大概率是磁盘权限/空间问题在脚本里我一般是先判断退出码如果是明确“不可重试”的错误就直接退出并把错误信息打出来而不是傻傻地继续循环。否则你可能会看到脚本连着跑了一整天日志里全是同一个错误在反复刷屏。2.4 临时文件与目标文件分离还有一个我坚持的设计习惯下载到临时文件成功后再改名。你想想看如果你的下载脚本是直接写到目标文件名下载到一半被人 CtrlC 强杀了或者脚本异常退出就会留下一个残缺的目标文件。下次你或同事运行这个脚本时可能因为代码逻辑判断不完善误以为文件存在且完整直接用这个坏文件去部署、去解压、去上传那就是连环事故。所以我的脚本逻辑是下载期间写入${target_file}.part下载成功后用mv原子性地改成目标文件名。这个习惯看起来很寒酸其实就是磁盘上的小事它规避了很大一部分“假性成功”的坑。2.5 信号处理CtrlC 也不能留下脏数据你平时用 curl 下载可能习惯性 CtrlC 中断。但在脚本里如果你按了 CtrlC默认行为是直接杀掉当前进程trap 是必要的。我一般在脚本开头写一个 trap确保按下 CtrlC 或者脚本被 kill 时能记录一下当前进度、清理临时环境变量、把日志补完。虽然对断点续传来说CtrlC 中断留下的.part文件反而是下次续传的关键素材所以要格外小心不要提前把.part删了这和“下载后清理临时文件”是两个冲突的需求我会用追加写日志的方式区分状态。3. 实操阶段完整脚本拆解与实现理论聊完下面是实打实的脚本代码。我会提供一个相对完整的 bash 版本然后逐段解释关键逻辑。你可以根据自己的需求直接复制修改。3.1 基础版本curl_retry 函数适合直接嵌入其他脚本最开始我写的版本就是一个函数因为很多时候我需要的不是单独一个脚本而是能嵌入到部署脚本、数据同步脚本里的一个通用工具函数。#!/bin/bash # curl 自动断点续传下载函数 # 用法: curl_retry url output_file [--checksum md5] [--max-retries 数] [--timeout 秒] curl_retry() { local url$1 local output$2 shift 2 local checksum local max_retries10 local connect_timeout20 local max_time300 while [[ $# -gt 0 ]]; do case $1 in --checksum) checksum$2 shift 2 ;; --max-retries) max_retries$2 shift 2 ;; --timeout) connect_timeout$2 shift 2 ;; *) echo 未知参数: $1 return 2 ;; esac done local part_file${output}.part local retry_count0 local wait_time5 while [[ $retry_count -lt $max_retries ]]; do # -C - 从本地已有部分文件续传-o 写入 .part 临时文件 curl --continue-at - \ --output $part_file \ --connect-timeout $connect_timeout \ --max-time $max_time \ --fail \ --location \ $url local exit_code$? if [[ $exit_code -eq 0 ]]; then # 传输过程无异常继续做文件校验 break fi case $exit_code in 7|18|28|56) # 这些是典型的网络类错误值得重试续传 retry_count$((retry_count 1)) echo [$retry_count] 下载中断退出码 $exit_code${wait_time} 秒后尝试续传... sleep $wait_time # 简单退避最多到 300 秒 wait_time$((wait_time * 2)) if [[ $wait_time -gt 300 ]]; then wait_time300 fi ;; *) # 其他错误不重试直接退出 echo 不可重试的 curl 退出码: $exit_code return 1 ;; esac done if [[ $retry_count -ge $max_retries ]]; then echo 重试 $max_retries 次后仍然失败请手动检查网络/URL return 1 fi # 做一个简单的大小判断文件存在且大小不为 0 if [[ ! -s $part_file ]]; then echo 下载文件为空失败 return 1 fi # 可选MD5 校验 if [[ -n $checksum ]]; then local actual_md5 actual_md5$(md5sum $part_file | awk {print $1}) if [[ $actual_md5 ! $checksum ]]; then echo MD5 校验失败实际 $actual_md5期望 $checksum return 1 fi fi # 下载和校验都通过移动到最终位置 mv $part_file $output echo 下载成功: $output return 0 }这段代码的逻辑我认为已经能覆盖绝大部分下载场景了。核心的几个设计点我再强调一下。第一我用了--fail参数。加了它之后如果服务器返回 404、500 之类的 HTTP 错误码curl 会直接失败退出而不是把错误页面内容下载下来。没有这个参数你下载一个 404 页面curl 返回码可能仍然是 0脚本会以为下载成功了然后给你一个内容是 HTML 乱码的“目标文件”非常坑。第二--location参数是为了跟随重定向。很多下载链接会先 302 跳到真实的存储地址如果不加这个参数url 变了以后 curl 不会自动跟随下载会直接失败。第三--max-time是一个总超时控制整个请求的最长时间。--connect-timeout是连接超时。两者区别在于连接超时只管 TCP 握手建连而 max-time 约束的是整个传输过程。对大文件、弱网环境来说两个都要合理设置否则一个连接卡住能让你的脚本卡好几个小时。3.2 有参数解析的完整脚本支持 URL 列表与日志输出函数版本适合嵌入其他脚本但如果你的需求是独立运行建议做成一个支持参数解析的完整脚本核心能力包括支持从命令行参数传 URL、支持从文件读取 URL 列表、支持输出日志到指定文件。#!/bin/bash # # 自动续传下载脚本 # 用法: # ./download_retry.sh url output [--checksum md5] # ./download_retry.sh --list urls.txt --output-dir /data/downloads # set -Eeuo pipefail LOG_FILE log() { local msg$1 echo $(date %Y-%m-%d %H:%M:%S) $msg if [[ -n $LOG_FILE ]]; then echo $(date %Y-%m-%d %H:%M:%S) $msg $LOG_FILE fi } download_one() { local url$1 local output$2 local max_retries${MAX_RETRIES:-8} local retry_count0 local wait_time3 local part_file${output}.part if [[ -f $output -s $output ]]; then log 目标文件已存在且非空跳过: $output return 0 fi while [[ $retry_count -lt $max_retries ]]; do log 开始下载/续传: $url if [[ -f $part_file ]]; then local file_size file_size$(stat -c%s $part_file 2/dev/null || echo 0) if [[ $file_size -gt 0 ]]; then log 检测到本地已有部分文件大小 $file_size 字节将进行断点续传 fi fi curl --continue-at - \ --output $part_file \ --connect-timeout 15 \ --max-time 600 \ --retry 3 \ --retry-delay 5 \ --fail \ --location \ --silent --show-error \ $url local exit_code$? if [[ $exit_code -eq 0 ]]; then break fi case $exit_code in 7|18|28|56) retry_count$((retry_count 1)) log 网络类错误($exit_code)第 $retry_count 次重试等待 $wait_time 秒 sleep $wait_time wait_time$((wait_time * 2)) [[ $wait_time -gt 60 ]] wait_time60 ;; *) log 不可重试错误($exit_code)放弃下载: $url return 1 ;; esac done if [[ $retry_count -ge $max_retries ]]; then log 达到最大重试次数下载失败: $url return 1 fi if [[ ! -s $part_file ]]; then log 下载得到的文件为空 return 1 fi mv $part_file $output log 下载完成: $output } # 解析命令行参数 MODEsingle URL OUTPUT LIST_FILE OUTPUT_DIR./ while [[ $# -gt 0 ]]; do case $1 in --list) MODElist LIST_FILE$2 shift 2 ;; --output-dir) OUTPUT_DIR$2 shift 2 ;; --log) LOG_FILE$2 shift 2 ;; --max-retries) MAX_RETRIES$2 shift 2 ;; *) if [[ -z $URL ]]; then URL$1 elif [[ -z $OUTPUT ]]; then OUTPUT$1 fi shift ;; esac done if [[ $MODE single ]]; then if [[ -z $URL || -z $OUTPUT ]]; then echo 用法: $0 url output [--log 日志文件] [--max-retries N] exit 1 fi download_one $URL $OUTPUT elif [[ $MODE list ]]; then if [[ -z $LIST_FILE || ! -f $LIST_FILE ]]; then echo URL 列表文件不存在: $LIST_FILE exit 1 fi while IFS read -r line; do [[ -z $line || $line \#* ]] continue url$line filename$(basename $url) output${OUTPUT_DIR}/${filename} download_one $url $output done $LIST_FILE fi这个版本里我加了几个细节set -Eeuo pipefail让脚本在出错时行为更可控URL 列表文件里支持空行和#注释日志统一走log()函数方便同时输出到控制台和文件。实际使用中我经常配合 cron 跑这种脚本比如每天夜里同步几个固定的数据文件日志记录到固定目录第二天早上扫一眼日志有没有下载完成关键字就可以了。3.3 下载进度可视化别让脚本“看起来像卡死了”如果你直接跑上面的脚本会发现终端上什么进度都没有因为我在 curl 命令里加了--silent --show-error。这样设计是为了让脚本输出更干净方便记日志。但如果你是交互式使用想看进度可以去掉--silent或者用一个回调函数来解析 curl 的进度输出。还有一个折中方案是让 curl 每秒钟输出一行当前速度信息用--progress-bar参数curl --continue-at - --output $part_file --progress-bar --fail --location $url实测下来--progress-bar比默认的--progress-meter在脚本日志里更紧凑适合那种不是特别频繁、但你还是想看进度的场景。不过要注意输出到日志文件时进度条那堆字符会让日志很难看所以我通常只有交互模式下才关掉--silent定时任务里一律安静模式。3.4 多个文件顺序下载与断点管理上面脚本的--list模式已经支持多个文件了但实际运行中还得考虑这样一个问题如果第一个文件已经下载完毕第二次再跑脚本时它应该跳过已完成的文件而不是重新下载。我在download_one函数里加了这样的判断if [[ -f $output -s $output ]]; then log 目标文件已存在且非空跳过: $output return 0 fi但这个判断逻辑不能滥用。如果你要下载的文件是会更新的版本更新包那“已存在即跳过”就是不合适的。这时我一般追加一个--force参数强制删除本地文件重新下。具体实现就是把if条件外再加一层变量控制这里不展开根据自己需求改一行就行。3.5 多线程/多段下载的补充方案curl 本身不带多线程分段下载功能。如果你下载的资源非常大比如几十 GB 的机器学习数据集又想跑满带宽可以考虑结合aria2c。aria2c 支持多线程分片下载、断点续传、多协议语法也很类似aria2c -x 16 -s 16 -k 1M $url -d $dir -o $filename这里的-x 16表示单服务器最大 16 个连接-s 16表示将文件分成 16 段-k 1M是每段最小分片大小。如果你用的是这种工具断点续传脚本的思路同样适用只是把内部 curl 换成 aria2c 而已。很多场景下我不建议自己用 shell 脚本做多线程下载因为并发分片的断点状态管理非常麻烦shell 处理起来要写很多胶水代码不如直接用成熟工具。所以我的思路是小文件和一般场景用 curl 自动重试超大文件用 aria2c 自动重试脚本框架是一样的。4. 常见问题与调试经验脚本写完了接下去是真正的重头戏把脚本丢到真实环境里跑你会遇到各种古古怪怪的问题。下面这些基本上都是我一个个趟过的坑。4.1 curl: (33) HTTP server does not seem to support byte ranges这个报错提示非常明确服务器不支持 Range。可能是源站服务器配置问题也可能是中间有代理层把 Range 头滤掉了。怎么处理先用curl -I确认响应头有没有Accept-Ranges: bytes如果没有基本可以死心。如果响应头显示支持但 curl 仍报 33检查是不是走了代理或者 Nginx 没开proxy_pass_request_headers。实在不支持的话脚本策略要回退成“从头下载”同时先把本地.part文件删掉避免垃圾文件残留。我的脚本里暂时没有写这种自动回退逻辑因为实际情况太复杂。但如果你明确知道自己要下载的服务器可能不支持 Range可以单独封装一个“不支持续传就全量下载”的分支。具体实现就是在case里捕获退出码 33然后执行一个不带-C -的 curl 把文件重新拉一遍。4.2 本地文件大小大于服务器文件大小导致 416 Requested Range Not Satisfiable这个坑很有意思。你下载到一半服务端的文件其实已经被替换成了更小的新版本或者 CDN 节点缓存的分片有问题。此时你请求Range: bytes9000000-服务端发现文件总共才 8 MB就会返回 416 错误。curl 的退出码是 33和服务器不支持 Range 一样都是 33但日志里的 HTTP 状态码会显示 416。处理方式也挺直接检测到 416 后删掉本地.part文件重新下载同时输出告警说“疑似远端文件已变化”。如果远端文件经常更新你甚至应该在脚本里加上--time-cond参数做时间条件判断确保本地文件和服务端版本一致。4.3 curl: (18) transfer closed with outstanding read data remaining这是我最常见的“值得重试”的报错。它表示连接在传输中途关闭了但数据没传完。绝大多数情况下都是网络不稳定导致的直接进入重试逻辑就行。值得注意的是如果重试了十几次还在报 18那就要看看是不是本地磁盘满了或者目标服务器主动断开了持续过长的连接。之前我遇到过一个场景服务器上的 Nginx 配置里proxy_read_timeout设成了 60 秒大文件下载稍慢一点就会被强制断开这时候你客户端再重试多少次都没用要去改服务端配置。4.4 中断后重试却从头开始你可能忘了 --continue-at 的语义有些朋友说“我加了-C -呀为什么断点续传不生效每次还是从头开始下载”这时候你要检查一下你命令里的-C是不是写在了-o后面、并且作用的对象是不是同一个文件。curl 的-C -自动续传逻辑依赖本地文件的大小比如你的输出文件是/tmp/xxx.part那你-C -读取的就是/tmp/xxx.part的大小。如果你上一次下载用的输出文件是/tmp/xxx这次却写成/tmp/xxx.partcurl 会认为本地文件是 0 字节自然从头开始下载。很多“续传失败”其实不是 curl 的问题而是输出文件路径不一致或者文件被删除。所以我在脚本里特意把.part文件路径固定为一个变量并且在日志里打印出当前检测到的本地文件大小。这个日志看起来不起眼但在排查问题时非常有用你一眼就能看出脚本在续传前是否正确地识别到了当地的已有数据。4.5 断点续传后文件校验失败九成是拼包问题如果你下载完成后做 MD5 校验不过很可能就是续传时“拼包”拼错了。什么场景会产生这个问题最常见的是你在本地已经有了一个不完整的.part文件但它是在服务器端重新上传新版本文件之前下载的此时服务端文件内容已经变了Range 请求获取到的字节流和之前下载的前半段不是同一个版本的文件拼起来自然对不上。这个问题的根治方法不是脚本逻辑而是下载前主动记录服务端文件的Last-Modified时间或者etag在续传时做一致性校验。curl 的--time-cond参数可以做时间条件请求但用于断点续传的一致性校验时还得多写几行逻辑有兴趣的可以自己深挖。4.6 代理环境下的坑如果你的服务器是通过代理访问外网的curl 默认会读取http_proxy、https_proxy环境变量。这时候断点续传经常出幺蛾子因为代理服务器可能会缓冲、修改 Range 请求头导致响应看起来像是完整文件。脚本层面我能给出的建议是明确知道需要走代理时显式用--proxy指定代理地址。代理会导致连接更容易中断重试次数建议调大。如果代理本身不稳定大文件下载建议别走 HTTP 代理改用直接连接或者让代理也配置好 Range 支持。4.7 磁盘满与权限错误的识别curl 退出码 63 表示“无法写数据”通常就是磁盘满了、权限不足或者文件被其他进程占用。这种情况你再怎么重试也没用。我脚本里把它归为不可重试错误同时会在日志里打出一段提示让你去排查磁盘空间和权限节省了大家对着日志发呆的时间。4.8 如何验证下载脚本是否真的在续传调试脚本时想验证“断点续传”是否真的生效有两个办法。第一个办法是在下载大文件的中途手动杀掉进程然后重新运行脚本观察日志里是否有“检测到本地已有部分文件大小 xxx 字节”的输出再看最终文件能否通过校验。第二个办法是抓包看 HTTP 请求头直接确认是否发出了Range: bytesxxx-这个头部。用 tcpdump 的话sudo tcpdump -i any -s 0 -A tcp port 80 and dst host example.com | grep Range:这个办法比较粗暴但能非常直观地确认 curl 确实在请求从中间继续传输而不是从头开始。5. 让脚本更强大的一些扩展点基础脚本就够用但如果你把它放进更复杂的生产环境有几个方向可以扩展。5.1 接入消息通知下载完了发个消息定时任务里跑下载脚本你不能一直盯着屏幕看所以给脚本加一个通知机制会方便很多。最简单的做法就是用 shell 直接调用一个 webhook比如飞书机器人或者钉钉机器人脚本下载成功或失败后发一条 POST 请求。这个逻辑不复杂notify() { local title$1 local content$2 curl --connect-timeout 5 -s -X POST https://your-webhook-url \ -H Content-Type: application/json \ -d {\title\:\$title\,\content\:\$content\} }然后在download_one的出口处判断返回值调用notify发通知。这样下载任务跑在凌晨的服务器上早上起来你只需要看手机消息就知道昨晚的同步任务是否成功。5.2 将下载记录持久化方便追溯如果你经常用这个脚本拉数据建议把每次下载的 URL、开始时间、结束时间、文件大小、校验值、下载结果写入一个 CSV 或者 SQLite 数据库。别嫌麻烦当你在三个月后回查“某个文件到底是哪天下载成功的”就知道这招多值钱了。用 shell 写 CSV 很简单echo $(date %F_%T),$url,$output,$(stat -c%s $output),$checksum,SUCCESS download_history.csv5.3 支持断点续传 后台任务排队如果你要下载非常多的文件而且时间不敏感可以封装一个简单的“任务队列”。最简单的方式就是给脚本外面套一个循环从任务列表文件里逐行读取 URL 和目标文件名依次调用download_one。注意这里的队列策略是顺序下载不会并发。如果你需要并发下载多个文件考虑用xargs -P或者 GNU parallel但并发下载会带来磁盘 IO 和带宽竞争建议控制并发数。5.4 将校验逻辑做强: checksum 文件自动匹配手动指定--checksum有点麻烦。一种改进是当下载完文件后自动去寻找同目录下的.md5或.sha256文件如果有就自动校验。比如你下载的文件是model.tar.gz旁边还有一个model.tar.gz.md5脚本可以这样自动校验if [[ -f ${output}.md5 ]]; then expected$(awk {print $1} ${output}.md5) actual$(md5sum $output | awk {print $1}) if [[ $expected ! $actual ]]; then echo 校验失败: ${output} return 1 fi fi这种方式在下载开源软件、数据集镜像时特别实用因为大多数官方渠道都会提供校验文件。5.5 旧文件清理策略下载任务跑久了磁盘上会累积非常多.part临时文件和已完成的旧版本文件。建议在脚本里加一个简单的清理机制下载完成后只保留最近 N 个版本的目标文件其余的按时间删除。用find加-mtime参数就能实现。如果没做清理最典型的后果就是某天你发现磁盘满了然后翻日志才发现是几个下载任务攒下来的历史文件把空间吃光了。6. 我个人实际使用中的几点感悟这篇文章写到这里核心内容已经讲得差不多了。最后我想聊一点不是技术但比技术更影响长期使用的体会。第一脚本不是越复杂越好。我最开始写自动续传脚本时恨不得把所有错误码、所有异常分支都处理一遍写出来一个三百多行的“瑞士军刀”结果部署到真实环境后反而因为某个分支逻辑写错导致脚本在一个并不复杂的场景下反复报错。后来我反思了一下把脚本拆成了“基础下载逻辑”和“业务扩展逻辑”两层核心续传函数保持不到五十行业务层面的校验、通知、历史记录再往外面包。这样的结构非常利于维护出问题时也能快速定位。第二日志输出一定要克制但完整。所谓克制是不要每次重试都刷几十行无意义的输出所谓完整是必须包含时间戳、URL、当前本地文件大小、curl 退出码、等待时间这几个关键信息。断点续传脚本是典型的“跑起来就不管”的工具等你发现它出问题时唯一能帮助你还原现场的就是日志。我见过太多同事写的脚本失败后只输出一行download failed具体怎么失败的、卡在了哪里完全不知道这种日志等于没写。第三尽量让脚本在所有 Linux 发行版上都能运行。写 shell 脚本时尽量用 POSIX 兼容的语法或者明确脚本依赖bash。比如上面代码里我用了[[ ]]、局部变量、数组这些在 bash 下没问题但如果你把脚本丢到sh或者某些精简容器里执行就可能会报语法错误。建议脚本开头写上#!/bin/bash并且在依赖复杂功能前简单检测一下环境。最后再分享一个小技巧如果你要下载的文件特别大而且网络环境经常性不稳定可以先把 curl 脚本跑起来然后配合screen或tmux放到后台会话里执行这样即使你的 SSH 连接断开下载任务也不会中断。这个组合我用了很多年几乎成了服务器下载大文件时的标准操作。脚本写得再稳也要考虑外部环境对它的影响而tmux 自动续传脚本这套组合能帮你把“环境因素”这个变量也牢牢控制住。希望这篇分享对你有用。如果你在实际使用中碰到其他奇葩问题或者有更好的设计思路欢迎在评论区交流互相补补坑。
返回列表