← 中文资源

简体中文版本

范围请求文件服务

在限定根目录内提供单文件下载,借助 ServeContent 处理分段请求、条件请求与范围响应头。

阅读英文原文

功能说明

视频拖动进度、断点续传和大文件下载都依赖 HTTP 范围请求。手写 Range 解析容易在开放区间、多个区间和非法范围上出错,标准库的 http.ServeContent 已经实现了完整语义:校验 Range、返回 206 与 Content-Range、处理 If-Modified-Since 和 If-None-Match,并在范围非法时返回 416。

示例把文件访问限制在指定根目录内,先把请求路径规范化,再拼接到根目录,最后确认结果仍在根目录下,防止路径穿越读取任意文件。

关键代码

path.Clean 把双点和多余斜杠消解,filepath.FromSlash 转换为当前系统分隔符。前缀检查使用根目录加路径分隔符,避免相似前缀的兄弟目录被误放行。os.Open 之后再次确认不是目录,因为 ServeContent 需要可寻址的文件。ServeContent 自动设置 Content-Length、Accept-Ranges,并根据请求头生成 206 或 304 响应。main 用临时目录和测试服务器发起 bytes=6-10 的请求。

使用注意

ServeContent 依赖文件实现 io.ReadSeeker,压缩包装或加密流需要额外处理。路径检查只做字符串前缀,若根目录本身是符号链接,应先用 filepath.EvalSymlinks 解析。生产环境还应限制文件大小、设置缓存头和内容类型,并对下载做鉴权。多区间请求会返回 multipart 响应,客户端需要按边界解析。HEAD 请求由 ServeContent 自动处理。

验证方式

文件内容为 hello range world,共十七字节。请求第六到第十字节返回状态 206,Content-Range 为 bytes 6-10/17,响应体为 range。把 Range 改为超出文件长度会得到 416;不传 Range 则返回 200 和完整内容,并带有 Accept-Ranges 响应头。

代码示例

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"path/filepath"
	"strings"
)

func fileHandler(root string) http.HandlerFunc {
	absoluteRoot, err := filepath.Abs(root)
	if err != nil {
		panic(err)
	}
	return func(w http.ResponseWriter, r *http.Request) {
		cleaned := path.Clean("/" + r.URL.Path)
		full := filepath.Join(absoluteRoot, filepath.FromSlash(strings.TrimPrefix(cleaned, "/")))
		if !strings.HasPrefix(full, absoluteRoot+string(os.PathSeparator)) {
			http.Error(w, "invalid path", http.StatusBadRequest)
			return
		}
		file, err := os.Open(full)
		if err != nil {
			http.Error(w, "not found", http.StatusNotFound)
			return
		}
		defer file.Close()
		info, err := file.Stat()
		if err != nil || info.IsDir() {
			http.Error(w, "not found", http.StatusNotFound)
			return
		}
		http.ServeContent(w, r, info.Name(), info.ModTime(), file)
	}
}

func main() {
	root, err := os.MkdirTemp("", "range-server-*")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(root)
	if err := os.WriteFile(filepath.Join(root, "report.txt"), []byte("hello range world"), 0o644); err != nil {
		panic(err)
	}

	server := httptest.NewServer(fileHandler(root))
	defer server.Close()

	request, err := http.NewRequest(http.MethodGet, server.URL+"/report.txt", nil)
	if err != nil {
		panic(err)
	}
	request.Header.Set("Range", "bytes=6-10")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println("status:", response.StatusCode)
	fmt.Println("content-range:", response.Header.Get("Content-Range"))
	fmt.Println("body:", string(body))
}