FileGate 是一个用 Go 和 Gin 写的文件处理中间件。
它负责把文件请求转发到不同的存储后端。后端可以是本地文件系统、S3 兼容存储,也可以是另一个 HTTP 服务。文件访问本身之外,还可以按配置做访问控制、后端切换以及图片转换。
一个普通的文件请求大致会经过下面这条链路:
flowchart LR
Client([客户端]) --> Step1
subgraph FileGate [FileGate 网关]
Step1[解析 namespace / class] --> Step2[Referer / 签名 / 路径检查]
Step2 --> Step3{解析图片转换参数}
Step3 -->|有转换| ImgProxy[imgproxy]
ImgProxy--> |回源拉原图| Strategy
Step3 -->|无转换| Strategy[后端策略]
Strategy --> Circuit[熔断 / 重试]
end
Circuit --> Storage[(fs / s3 / http)]
启动时,FileGate会把配置中的 namespace、class 和后端策略整理好,请求进来后直接根据路由找到对应配置。
如果请求带了图片转换参数,则交给 imgproxy;否则直接从后端读取文件。后端发生错误时,根据配置的策略决定是否换下一个后端。
主要代码大致分为几块:
| 组件 | 作用 |
|---|---|
Router |
根据 namespace / class 找到对应配置,预编译路径过滤器 |
PolicyEngine |
决定请求使用哪些后端,并处理熔断 |
Processor |
解析请求路径和图片转换参数 |
Backend |
统一 fs、s3、http 三种后端的访问方式 |
Middleware |
Referer、签名和路径过滤 |
imgproxyClient |
生成imgproxy请求并把结果转回客户端 |
主要接口:
GET /fs/{namespace}/{class}/{objectPath}
HEAD /fs/{namespace}/{class}/{objectPath}
不携带转换参数时,直接按 objectPath 从后端拉取原文件:
/fs/namespace1/class1/images/photo.jpg
在 objectPath 末尾追加以 @ 分隔的转换后缀:
/fs/{namespace}/{class}/{objectPath}@[<参数>...][.<格式>]
参数之间使用 _ 分隔,顺序没有要求,也可以只写其中一部分。
| 参数 | 含义 | 示例 |
|---|---|---|
!{rule} |
使用哪个转换规则 | !png_conversion |
<数字>w |
宽度,单位像素 | 320w |
<数字>h |
高度,单位像素 | 240h |
<数字>b |
模糊程度,数值除以 10 后作为 sigma | 5b |
<数字>q |
图片质量,1–100 | 80q |
.{格式} |
输出格式 | .webp |
其中转换规则是可选的。宽度、高度等参数可以省略,省略时依次使用规则 params、类别 default_params 中的默认值。
例如:
# 使用 png_conversion,宽 320,质量 70,输出 webp
/fs/namespace1/class1/images/photo.jpg@!png_conversion_320w_70q.webp
# 只指定高度和输出格式
/fs/namespace1/class1/avatars/u1.png@!png_conversion_600h.avif
# 不指定规则,仅覆盖宽度,其余参数取类别 default_params
/fs/namespace1/class1/images/photo.jpg@320w
也可以使用查询参数:
?rule=&width=&height=&quality=&blur=&format=
如果路径后缀和查询参数同时指定了同一个字段,并且值不同,会返回 400。
enable_request_params 没有开启的参数会被忽略。不存在的转换规则会返回 400。
需要注意,图片转换依赖 service.imgproxy.url。如果没有配置 imgproxy,带转换参数的请求不会报错,而是直接回源读取原文件。
需要 Go 1.25 或更新版本。
go build -o filegate ./cmd/server/运行:
./filegate -config config.yaml配置文件默认使用当前目录下的 config.yaml,也可以通过 -config 指定其他文件。
版本号可以在构建时注入:
go build -ldflags="-s -w -X github.com/thun888/filegate/internal/server.Version=v1.2.3" ./cmd/server/先准备配置文件
mv config.example.yaml config.yaml一个最简单的本地文件配置如下:
backends:
- name: "local"
type: "fs"
config:
root_path: "/data/files"
backend_policy:
- name: "default"
strategy: single
backends:
- local
namespaces:
- name: "ns1"
backend_policy: "default"
class:
- name: "images"
system:
server:
host: 127.0.0.1
port: 8080启动:
./filegate -config config.yaml然后可以测试:
curl http://127.0.0.1:8080/ping
curl http://127.0.0.1:8080/fs/ns1/images/photo.jpg完整配置可以参考 config.example.yaml。
配置中的 backend、policy、rule、namespace 等名称匹配时不区分大小写。
backends[]
└─ 后端存储(fs / s3 / http)
└─ timeout / retries / retry_delay / circuit_breaker
backend_policy[]
└─ 后端调度策略
└─ backends[] → 引用 backends[].name
namespaces[]
└─ namespace
└─ backend_policy → 引用 backend_policy[].name
└─ class
├─ security
├─ file_conversion
└─ response_headers
file_conversion_rules[]
└─ 图片转换规则
service.imgproxy
└─ imgproxy 配置
system
├─ server
├─ logging
└─ metrics
三种后端的配置方式略有不同,但重试、超时和熔断配置是共用的。
backends:
- name: "backend1"
type: "http" # fs | s3 | http
config:
# http
url_prefix: "http://upstream:8080"
extra_headers:
X-Example: "value"
# s3
endpoint: "s3.amazonaws.com"
region: "us-east-1"
bucket: "my-bucket"
access_key: ""
secret_key: ""
# fs
root_path: "/data/files"
timeout: 5s
retries: 3
retry_delay: 1s
circuit_breaker:
failure_threshold: 5
recovery_timeout: 30s
half_open_timeout: 10s各后端需要的配置:
fs:必须有config.root_pathhttp:必须有config.url_prefixs3:必须有config.buckets3使用认证时,access_key和secret_key必须同时配置
重试只发生在单个后端内部。比如一个后端配置了 retries: 3,三次都失败后,才会把这个后端判定为失败,然后根据策略尝试下一个后端。
熔断器只有在 failure_threshold > 0 时才会启用。
backend_policy:
- name: "policy1"
strategy: fallback
backends:
- backend1
- backend2backends 的顺序就是优先级。
| strategy | 行为 |
|---|---|
single |
只使用第一个后端 |
fallback |
按顺序尝试后端,成功后停止 |
priority |
和 fallback 相同 |
round_robin |
在多个后端之间轮流使用 |
random |
随机决定后端顺序 |
round_robin 的状态保存在进程内,服务重启后会重新开始。
namespace 和 class 最终会直接出现在 URL 中:
/fs/{namespace}/{class}/{objectPath}
例如:
namespaces:
- name: "namespace1" # 命名空间名称
backend_policy: "policy1" # 使用的后端策略
class:
- name: "class1" # 子类别
security:
# Referer 检查:按 Referer 提取的域名匹配(忽略协议、端口、路径)
refer_check:
enabled: true
allowed_referers:
- "example.com" # 精确域名
- "*.another.com" # 泛域名,匹配所有子域名(不含基域名本身)
- "*" # 单独的 * 放行所有域名
# 注意:enabled 为 true 但列表为空时,所有请求都会被拒绝(403)
# URL 签名(HMAC-SHA256),客户端通过 ?exp=<秒>&sign=<值> 传递,见“安全机制 - URL 签名”
signature:
enabled: true
secret: "xxxx" # 签名密钥,启用时必填
expire: 300 # exp 允许的最大超前窗口(秒),0 表示不限制
# 路径过滤,检查顺序:deny_patterns → allow_paths → allow_extensions
path_filter:
deny_patterns: # 字面量子串匹配(非正则),路径包含任一条目即拒绝
- "../"
- ".git"
allow_paths: # 非空时,路径必须命中其中一个前缀
- "images/"
- "avatars/"
allow_extensions: # 非空时,文件扩展名必须命中其中之一
- "jpg"
- "png"
- "webp"
# 三项全为空表示全部放行
# 图片转换配置
file_conversion:
rules: # 可用转换规则白名单,引用 file_conversion_rules[].name
- "png_conversion"
# 类别级默认参数,规则 params 未设置对应字段时生效(0 / 空串视为未设置)
default_params:
width: 800 # 默认宽度(像素)
height: 600 # 默认高度(像素)
blur: 0.5 # 高斯模糊 sigma,0 表示不模糊
quality: 80 # 图片质量,1–100
format: "avif" # 输出格式
# 允许通过请求(query / 路径后缀)覆盖的字段开关及取值范围
enable_request_params:
width: { enabled: true, min: 1, max: 8192 }
height: { enabled: true, min: 1, max: 8192 }
quality: { enabled: true, min: 10, max: 95 }
blur: true # blur 与 format 为 bool 开关
format: true
# 未启用的字段会被静默忽略;取值超出 min/max 时返回 400
# 自定义响应头,文件访问成功时随响应一起返回
response_headers:
Cache-Control: "public,max-age=3600"转换规则定义图片转换时的默认参数以及一些额外处理。
file_conversion_rules:
- name: "png_conversion" # 规则名称
# 源文件大小上限(如 "100MB"),用于 imgproxy 侧处理
# 不填写单位默认为字节
max_file_size: "100MB"
# 规则级默认参数,覆盖类别 default_params 中已设置的对应字段(0 / 空串视为未设置)
params:
width: 800 # 默认宽度(像素)
height: 600 # 默认高度(像素)
blur: 0.5 # 高斯模糊 sigma,0 表示不模糊
quality: 80 # 图片质量,1–100
format: "avif" # 输出格式
# 附加处理参数:按 "/" 拆分为选项段,原样前置到 imgproxy 处理 URL 开头(先于其他处理参数生效)
# 段内仅允许字母、数字与 _ : . - 字符,如 "strip_exif/w:600/q:50/f:avif"
extra_params: ""
# 水印,需要在 imgproxy 端配置对应的水印图片
watermark:
enabled: true
opacity: 0.5 # 透明度,范围 [0,1](启动时校验)
position: "soea" # ce(居中), no(顶部), so(底部), ea(右边), we(左边), noea(右上), nowe(左上), soea(右下), sowe(左下), re(平铺), ch(棋盘格平铺)
x_offset: 0 # X 偏移:>=1 或 <=-1 为绝对像素值,(-1,1) 为相对值;position 为 re/ch 时表示瓦片间距
y_offset: 0 # Y 偏移:>=1 或 <=-1 为绝对像素值,(-1,1) 为相对值;position 为 re/ch 时表示瓦片间距
scale: 0 # 水印大小相对结果图片的比例,0 表示不改变大小参数取值优先级:请求参数(query / 路径后缀)> 规则 params > 类别 default_params。
规则 params 只覆盖类别 default_params 中已设置的对应字段(0 / 空视为未设置,沿用类别默认值)。
extra_params 会按照 / 拆分成参数段,并原样放到 imgproxy 处理 URL 中。
水印需要同时在 imgproxy 端配置对应的水印图片。
service:
imgproxy:
url: "" # imgproxy 服务地址,未配置时图片转换不生效,直接回源读原文件
timeout: 20s # imgproxy 请求超时,<=0 时按默认 20s
# imgproxy URL 签名,enabled 时 key 与 salt 必填(启动校验),且须与 imgproxy 服务端一致
signature:
enabled: false
key: ""
salt: ""
system:
server:
base_url: "http://127.0.0.1:8080" # imgproxy 回源使用的地址,未配置时由 host:port 推导
host: 0.0.0.0 # 监听地址
port: 8080 # 监听端口
debug: false # true 用 gin.DebugMode,false 用 gin.ReleaseMode
logging:
level: info # 日志级别:debug, info, warn, error(非法值启动失败)
access_log: true # 是否输出 Gin 访问日志(独立于 level)
metrics:
prometheus: true # 是否启用 /metrics 端点system.server.base_url 是 imgproxy 回源时使用的地址。如果 FileGate 在容器或反向代理后面运行,这里应该填写 imgproxy 能访问到的地址,而不是随便填写一个客户端地址。
system.server.debug 控制 Gin 使用 DebugMode 还是 ReleaseMode。
system.metrics.prometheus 控制 /metrics 是否启用。
| 方法 | 路径 | 说明 |
|---|---|---|
GET / HEAD |
/fs/:namespace/:class/*objectPath |
文件访问,可选图片转换 |
GET / HEAD |
/origin/:namespace/:class/*objectPath |
给 imgproxy 回源使用 |
GET |
/ping |
测试并返回版本信息 |
GET |
/healthz |
健康检查 |
GET |
/metrics |
Prometheus 指标 |
/origin/ 是给 imgproxy 回源用的接口。它只进行路径过滤,不检查 Referer 和 URL 签名,应在反向代理层限制访问来源。
常见响应头:
| 响应头 | 说明 |
|---|---|
X-Request-Id |
请求 ID,可以由客户端提供,也可以由 FileGate 自动生成 |
X-FileGate-Policy |
当前请求使用的后端策略 |
X-FileGate-Transform |
实际使用的图片转换参数 |
错误响应方面:
403、404和5xx会返回对应的错误图片(PNG)- 其他错误返回 JSON,例如
{"error":"..."} HEAD请求不会返回响应体
可以根据 Referer 限制文件的访问来源。
支持:
example.com 精确域名
*.example.com 子域名
* 全部允许
FileGate 支持 HMAC-SHA256 URL 签名。
请求中通过:
?exp=<秒>&sign=<值>
传递签名和过期时间。
签名使用的内容为:
METHOD
PATH
QUERY
EXPIRE
然后使用配置中的 secret 计算 HMAC-SHA256。
路径过滤依次检查:
deny_patterns
↓
allow_paths
↓
allow_extensions
检查的是去掉图片转换后缀之后的真实文件路径。
例如:
photo.jpg@100w.jpg
不会因为最后的 .jpg 而绕过原始文件路径的扩展名限制。
路径还会经过 URL 解码和清洗,用来处理双重编码、.. 等路径穿越情况。
如果开启 imgproxy 签名,FileGate 会给发送给 imgproxy 的处理 URL 加上 HMAC 签名。
key 和 salt 必须和 imgproxy 服务端保持一致。
/origin/不执行 Referer 和 URL 签名校验,只执行路径过滤。部署时请务必限制这个接口的访问来源。
FileGate 没有单独的日志框架,统一使用标准库 log/slog 向 stderr 输出文本日志,
日志的收集、落盘和切割交给部署层(Docker / systemd / K8s)。
级别由 system.logging.level 控制,量化指标另行通过 Prometheus /metrics 端点暴露。
system:
logging:
level: info # debug, info, warn, error各级别对 FileGate 应用日志的作用:
| 级别 | 输出内容 |
|---|---|
debug |
全部:每请求排障日志、启动配置转储、4xx/5xx 错误留痕 |
info |
4xx/5xx 错误留痕(默认级别) |
warn |
4xx/5xx 错误留痕 |
error |
仅 5xx 错误留痕 |
其中 4xx 按 warn 记录、5xx 按 error 记录。
级别值非法(例如 trace)时服务会直接启动失败。
每个请求都会有一个 X-Request-Id。
如果上游服务已经提供,就直接使用;否则由 FileGate 自身生成。
日志会带上这个 ID,方便把同一个请求经过不同处理阶段的日志串起来。
设置:
export FILEGATE_DEBUG=1启动后,会额外输出当前实际生效的路由、后端和 imgproxy 配置,请求进入处理流程时也会打印更多信息。
这个模式主要用于排查配置和请求问题。
可以通过:
system:
logging:
access_log: true控制 Gin 的访问日志。
开启:
system:
metrics:
prometheus: true之后可以通过:
GET /metrics
获取指标。
除了 Prometheus 默认指标之外,还会提供熔断器相关指标,例如:
filegate_circuit_breaker_*
.
├── cmd/server/main.go # 程序入口
├── config/ # 配置加载、校验和解析
│ ├── config.go
│ ├── loader.go
│ └── extra_params.go
├── internal/
│ ├── backend/ # fs / s3 / http 后端和重试
│ ├── engine/ # Router / PolicyEngine / Processor
│ ├── middleware/ # Referer / 签名 / 路径过滤
│ ├── server/ # Gin 服务、路由、Handler、imgproxy
│ └── utils/ # 路径处理、字节大小换算等
├── config.example.yaml # 配置示例
└── go.mod