流式 tar 归档下载服务器,支持随机访问和 HTTP Range 请求。
在工作中遇到了一个业务场景:需要提供一个将整个目录一次性打包下载的 HTTP 下载链接。这个场景面临以下挑战:
- 文件体积巨大: 目录下的文件非常大,传统的压缩方式需要占用双倍的存储空间(源目录 + 压缩文件)
- 必须保持原始目录结构: 不能简单地打平文件结构,需要完整保留目录层级
- 内容频繁更新: 源目录内容会不定期更新,如果采用预先压缩的方式,每次更新后都需要重新压缩,耗时耗力
- 临时压缩延迟高: 如果在用户请求时临时压缩,用户需要等待很长时间才能开始下载,体验极差
- 大文件下载可靠性: 由于文件非常大,下载过程中很容易因网络中断而失败,需要支持断点续传和多线程下载
基于这些实际需求,开发了这个项目。
RAAS (Random Access Archive Stream) 是一个高性能的流式归档下载服务器,支持 tar 与 zip 两种归档格式,其核心功能是将整个目录打包成一个可下载的归档文件,解决了传统 HTTP 下载只能一次性下载单个资源的局限性。
与传统方案不同,RAAS 无需预先创建完整的 tar 文件即可实现流式传输。当客户端请求下载某个目录时,服务器会实时扫描目录结构、计算文件偏移量、构建虚拟索引,然后通过 HTTP 响应流式生成并传输 tar 归档数据。整个过程无需在磁盘上创建临时文件,内存占用极低,即使对于 GB 级别的大型目录也能高效处理。
RAAS 最具创新性的特性是支持对 tar 压缩流的随机访问。这意味着:
- 不只是打包下载: 系统不仅能将整个目录打包成 tar 格式供客户端下载,更重要的是实现了对生成的 tar 流的任意位置访问能力
- HTTP Range 协议完整支持: 基于随机访问能力,系统完整实现了 HTTP Range 协议,使客户端可以:
- 多线程下载: 同时发起多个 Range 请求,并行下载归档的不同部分,显著提升下载速度
- 断点续传: 网络中断后可以从断点位置继续下载,无需重新开始,对大文件下载尤为重要
- 部分下载: 只下载归档文件中需要的特定字节范围,无需获取完整归档,节省带宽和时间
- 随机访问压缩流: 实现了对 tar 流的任意位置访问,这是区别于传统 tar 服务器的核心能力
- 完全流式处理: 无需预创建完整归档文件,无需临时文件,边计算边传输,内存占用与目录大小无关
- 缓存机制:
- 文件句柄缓存: 使用 RefCell 缓存当前文件句柄,避免重复打开文件的系统调用开销,请求结束后自动释放
- 归档索引缓存: 缓存已扫描的目录结构和预计算的偏移量信息,减少重复扫描和计算
- tar 头部缓存: 预计算并缓存每个文件的 tar 头部(包括 GNU LongLink 扩展),避免重复生成
- 自动失效: 通过文件系统监听实时感知源目录变更,自动失效受影响的缓存条目,保证缓存与源目录一致
- tar 格式规范:
- 支持 GNU LongPath 扩展,处理超过 100 字符的长路径
- 严格的 512 字节对齐处理
- 正确的目录项和文件项区分
- 保留源文件的权限位与修改时间,并以规范要求的两个 512 字节零块结束归档
- 路径安全验证: 防止路径穿越攻击,确保访问路径在 DATA_ROOT 范围内
- 不阻塞异步运行时: 归档扫描与文件读取都是同步 I/O,统一放在阻塞线程池执行;流式响应体通过有界通道把数据块交回异步流,客户端断开时任务自动退出
- 多归档格式: 通过
format参数在tar与zip之间切换,两种格式都完整支持随机访问与 HTTP Range
当客户端请求一个目录的归档下载时,系统执行以下流程:
- 递归扫描目标目录,收集所有文件和子目录的元数据信息
- 获取每个文件的相对路径、实际大小、类型(文件/目录)
- 为每个文件创建 tar 头部,确定是否需要 GNU LongLink 扩展
- 计算每个文件在 tar 流中的精确位置偏移量,包括:
- tar 头部大小(标准 512 字节或带 LongLink 的扩展头部)
- 文件内容大小
- 内容后的 512 字节对齐填充
- 所有偏移量存储在
file_indexHashMap 中,形成虚拟索引
- 构建包含所有文件元数据和偏移量的索引结构
- 预计算并缓存每个文件的 tar 头部数据(包括 GNU LongLink 头部)
- 计算归档文件的总大小(
total_size)
- 当收到 Range 请求(如
Range: bytes=1024-2048)时,系统通过file_index快速定位到对应偏移量 - 使用二分查找算法(
find_file_by_position)确定指定位置属于哪个文件的哪个部分(头部/内容/填充) - 无需按顺序读取,可直接跳转到任意位置开始传输
RangeStreamWriter实现了std::io::Readtrait,支持按需读取指定范围的数据- 根据当前位置判断需要读取的数据类型:
- 头部区域: 直接从
header_cache读取预计算的 tar 头部 - 内容区域: 通过文件句柄缓存打开源文件,seek 到指定位置读取文件内容
- 填充区域: 生成零字节填充数据以满足 512 字节对齐
- 头部区域: 直接从
- 使用异步流(
async_stream)将数据分块传输给 HTTP 响应体 - 整个过程按需进行,内存中只保留当前读取的数据块
Zip 归档复用与 tar 相同的「扫描 → 计算偏移 → 随机访问」流程,差异在于格式本身:
- STORED 存储: 条目数据不压缩,与 tar 一样避免占用双份存储;因此单个文件的内容区间可直接映射到源文件偏移,天然可随机访问
- CRC-32 可控: Zip 规范要求每个条目记录 CRC-32。默认(未启用
zip-crc32feature)下 CRC 字段直接写 0,完全不读取文件内容,与 tar 一样只依赖元数据;启用zip-crc32后,CRC 在首次生成该条目头部时按需计算并缓存,且仅访问文件内容区间不会触发 - 区间划分: 整个归档被切分为「本地头 / 文件数据 / 中央目录项 / Zip64 EOCD / Zip64 定位器 / EOCD」等连续区间,给定偏移量通过二分查找定位所属区间并按需生成或读取
- Zip64: 当大小、偏移或条目数超出 Zip32 上限时,自动切换为 Zip64 记录,支持超过 4GiB 的归档
- UTF-8 文件名: 设置通用标志位 bit 11,正确处理中文等多字节路径
兼容性/性能权衡: 默认关闭 CRC 时,生成的是「结构合法但校验值为 0」的 zip,标准解压工具在读取内容时会报告 CRC 校验失败(
unzip -t、Pythonzipfile等);文件结构与中央目录仍可正常解析与列举。若需要严格校验,请启用zip-crc32feature(代价:首次生成头部时会额外读取一次文件内容)。
cargo build --release# 设置数据根目录(Windows PowerShell)
$env:DATA_ROOT="C:\your\data\directory"
# 设置监听地址(可选)
$env:BIND_ADDR="0.0.0.0:8080"
# 启动服务器
cargo run --releasecurl -O http://127.0.0.1:8080/api/archive/download?path=my_foldercurl -H "Range: bytes=0-1023" http://127.0.0.1:8080/api/archive/download?path=my_folder# 完整下载
curl -O "http://127.0.0.1:8080/api/archive/download?path=my_folder&format=zip"
# Range 请求同样适用于 zip
curl -H "Range: bytes=0-1023" "http://127.0.0.1:8080/api/archive/download?path=my_folder&format=zip"| 变量名 | 说明 | 默认值 |
|---|---|---|
DATA_ROOT |
数据根目录 | . |
BIND_ADDR |
监听地址 | 0.0.0.0:8080 |
MAX_CONCURRENT_REQUESTS |
最大并发请求数 | 100 |
THREAD_POOL_SIZE |
线程池大小 | 4 |
STREAM_READ_BUFFER_SIZE |
流式读取块大小(字节,低于 65536 会被提升) | 131072 |
ARCHIVE_CACHE_MAX_CAPACITY |
存档缓存最大容量 | 100 |
ENABLE_FILE_WATCHER |
是否启用文件系统监听以自动失效缓存 | true |
FILE_WATCHER_DEBOUNCE_MS |
文件系统事件抖动合并窗口(毫秒) | 200 |
服务使用 tracing 输出结构化日志,每个请求会记录方法、路径、状态码与耗时。默认级别随构建模式变化:release 构建为 info,debug(cargo run/cargo test)构建为 debug。
可通过环境变量 RUST_LOG 覆盖默认级别:
# 强制输出缓存命中/未命中、归档构建与流式传输统计等
export RUST_LOG=debug
# 含缓存失效明细与 zip CRC 计算等更细粒度信息
export RUST_LOG=raas=trace
# release 下只想看错误
export RUST_LOG=error| Feature | 说明 | 默认 |
|---|---|---|
zip-crc32 |
启用 Zip 归档的 CRC-32 计算。关闭时 CRC 字段写 0、不额外读取文件;开启后按需计算,标准工具可严格校验内容 | 关闭 |
启用方式:
cargo build --release --features zip-crc32健康检查,返回 {"status":"ok"}。
流式下载目录为归档文件(默认 tar,可通过 format 选择 zip)。
查询参数:
path(必需): 相对于 DATA_ROOT 的路径format(可选): 归档格式,支持tar(默认)与zip
响应头:
Content-Type: application/x-tar(tar)或application/zip(zip)Accept-Ranges: bytesContent-Disposition: attachment; filename="<name>.<ext>"
支持 Range 请求:
- 请求头:
Range: bytes=start-end - 支持的区间形式:
bytes=start-end、bytes=start-(到结尾)、bytes=-N(最后 N 字节);多段区间仅取第一段 - 结束位置超出归档大小时自动裁剪到末尾;起始位置超出范围时返回
416 Range Not Satisfiable - 语法非法的
Range头按 HTTP 规范忽略,返回完整内容(200) - 响应状态:
206 Partial Content - 响应头:
Content-Range: bytes start-end/total
目标: 实现文件系统变更自动检测,确保缓存数据与源目录保持一致
实现:
- 基于
notify递归监听DATA_ROOT,在源目录内容发生变更时自动失效受影响的存档缓存 - 每个事件只失效「自身及所有祖先路径」的缓存条目,例如
root/a/b.txt变更时失效root/a/b.txt、root/a、root的缓存,避免无关缓存被误伤 - 通过抖动窗口(debounce)合并连续事件,避免批量写入时反复重扫目录
- 失效操作与正在进行的请求相互隔离:已获取的存档由
Arc持有,不受缓存移除影响,新请求则会重新扫描 - 监听器生命周期与
AppState绑定,Drop时自动停止监听并回收线程 - 可通过
ENABLE_FILE_WATCHER=false关闭监听;监听启动失败时自动降级为不启用,不影响服务主流程
目标: 集成操作系统级零拷贝机制,大幅提升大文件传输性能
目标: 扩展归档格式选择,支持 Zip 格式同时保持随机访问特性
实现:
- 新增统一的归档抽象
Archivetrait,tar与zip各自实现format/total_size/stream_range,缓存与 HTTP 处理器不感知具体格式 - 通过查询参数
format=tar|zip选择格式(默认tar);缓存以「源路径 + 格式」为键,两种格式互不影响、可同时缓存,并在源目录变更时一并失效 - Zip 采用 STORED(不压缩) 存储,索引构建阶段只读取目录元数据、不读取文件内容,与 tar 一样保持低内存、低首包延迟
- CRC-32 由
zip-crc32feature 控制(默认关闭):关闭时字段写 0、不读取文件;启用后按需在生成条目头部时计算并缓存,仅做 Range 下载或仅访问文件内容区间不会触发额外读取 - 随机访问:将归档切分为本地头、文件数据、中央目录项、EOCD 等连续区间,基于偏移量二分查找即可定位任意位置,完整支持 HTTP Range 与多线程下载
- 支持 Zip64 扩展,自动处理超过 4GiB 的归档与超过 65535 个条目的目录
- 文件名统一使用 UTF-8,正确处理中文等多字节路径
MIT