流式 tar 归档下载服务器,支持随机访问和 HTTP Range 请求。
在工作中遇到了一个业务场景:需要提供一个将整个目录一次性打包下载的 HTTP 下载链接。这个场景面临以下挑战:
- 文件体积巨大: 目录下的文件非常大,传统的压缩方式需要占用双倍的存储空间(源目录 + 压缩文件)
- 必须保持原始目录结构: 不能简单地打平文件结构,需要完整保留目录层级
- 内容频繁更新: 源目录内容会不定期更新,如果采用预先压缩的方式,每次更新后都需要重新压缩,耗时耗力
- 临时压缩延迟高: 如果在用户请求时临时压缩,用户需要等待很长时间才能开始下载,体验极差
- 大文件下载可靠性: 由于文件非常大,下载过程中很容易因网络中断而失败,需要支持断点续传和多线程下载
基于这些实际需求,开发了这个项目。
RAAS (Random Access Archive Stream) 是一个高性能的流式 tar 归档下载服务器,其核心功能是将整个目录打包成一个可下载的归档文件,解决了传统 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 字节对齐处理
- 正确的目录项和文件项区分
- 路径安全验证: 防止路径穿越攻击,确保访问路径在 DATA_ROOT 范围内
当客户端请求一个目录的归档下载时,系统执行以下流程:
- 递归扫描目标目录,收集所有文件和子目录的元数据信息
- 获取每个文件的相对路径、实际大小、类型(文件/目录)
- 为每个文件创建 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 响应体 - 整个过程按需进行,内存中只保留当前读取的数据块
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| 变量名 | 说明 | 默认值 |
|---|---|---|
DATA_ROOT |
数据根目录 | . |
BIND_ADDR |
监听地址 | 0.0.0.0:8080 |
MAX_CONCURRENT_REQUESTS |
最大并发请求数 | 100 |
THREAD_POOL_SIZE |
线程池大小 | 4 |
STREAM_READ_BUFFER_SIZE |
流式读取缓冲区大小(字节) | 8192 |
ARCHIVE_CACHE_MAX_CAPACITY |
存档缓存最大容量 | 100 |
流式下载目录为 tar 归档。
查询参数:
path(必需): 相对于 DATA_ROOT 的路径
响应头:
Content-Type: application/x-tarAccept-Ranges: bytesContent-Disposition: attachment; filename="<name>.tar"
支持 Range 请求:
- 请求头:
Range: bytes=start-end - 响应状态:
206 Partial Content - 响应头:
Content-Range: bytes start-end/total
目标: 实现文件系统变更自动检测,确保缓存数据与源目录保持一致
目标: 集成操作系统级零拷贝机制,大幅提升大文件传输性能
目标: 扩展归档格式选择,支持 Zip 格式同时保持随机访问特性
MIT