Nginx 文件服务器实战:FancyIndex 长文件名显示优化(Nginx 系列第 3 期)
Nginx 系列:第 1 期:源码编译安装 · 第 2 期:JSON 日志格式
1. 需求背景:当一个文件名太长
1.1 问题复现
汽车智能座舱团队需要在内网搭建一个 Android 代码发包平台。OTA 升级包的命名规则是:
IMX8QM_MCU_AGL_2026_Q3_Release_Candidate_3_UserDebug_eng_build_20260715_signed.zip或华为鸿蒙座舱的:
HarmonyOS_Cockpit_4.0.0.126_SP6C00E125R3P6_car_release_20260630_full_ota_update.app六七十个字符。文件名中包含硬件平台、系统版本、构建类型、日期、签名信息——这些是工程师识别正确包的刚需。
当你直接用 Nginx 的 autoindex on 开启目录浏览时,效果是这样的:
[IMAGE: autoindex-truncated-demo.png]
文件名在 50 个字符左右时被粗暴截断,最后面是一串 ..>。工程师只能靠文件名前半段(如 IMX8QM_MCU_AGL_2026_Q3_Rele...)去猜「这个是不是我要的包?」。猜错了就下载一个 2 GB 的镜像回来,解压完发现不对——这种体验在生产环境中是不可接受的。
1.2 为什么 Nginx 设计成截断?
这不是 Nginx 的 bug——是 autoindex 模块的刻意设计。autoindex 生成了一个 HTML 表格,每一行中有两个 <a> 标签(文件名链接 + 父目录/子目录链接),CSS 是硬编码在 ngx_http_autoindex_module.c 源文件中的。
截断的具体算法(来自 Nginx 源码):
#define NGX_HTTP_AUTOINDEX_NAME_LEN 50
// ...
if (entry[i].name.len > NGX_HTTP_AUTOINDEX_NAME_LEN) {
entry[i].name.len = NGX_HTTP_AUTOINDEX_NAME_LEN - 3;
// 追加 "..>" 标记截断
}直接把 C 字符串 truncate 到 47 个字符再加 ..>。服务器端截断,不是 CSS text-overflow——浏览器收到 HTML 时就已经是截断后的短文件名了。你没办法通过前端 CSS 来恢复,因为数据已经丢了。
2. 实验环境
| 项目 | 规格 |
|---|---|
| 操作系统 | Ubuntu 24.04 LTS |
| CPU | 2 vCPU (Intel Xeon E5-2680 v4 @ 2.40 GHz) |
| 内存 | 1 GB |
| Nginx 版本 | 1.30.3 Stable(源码编译安装) |
| 编译路径 | /usr/sbin/nginx、/etc/nginx/nginx.conf |
| 文件存储路径 | /data/releases/android/ |
| 域名 | filebox.example.com |
3. 方案对比:sub_filter 注入 CSS vs FancyIndex 编译安装
面对上面那个硬编码限制,有两条路可以走。
3.1 方案一:sub_filter 注入 CSS(零停机,轻量)
Nginx 的 sub_filter 能力(在 第 1 期 编译时通过 --with-http_sub_module 启用)可以修改 HTTP 响应体。利用它,在 autoindex 生成的 HTML 返回给客户端之前,注入一段 CSS 来绕过截断。
原理:autoindex 输出的是一个 HTML 表格。我们通过 sub_filter 在 </head> 前注入 CSS,让文件名列的宽度放开,再用 CSS overflow-wrap 处理超长文件名。
location /releases/ {
alias /data/releases/android/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
charset utf-8;
# 注入 CSS 前
sub_filter '</head>' '
<style>
/* 覆盖默认表格样式 */
pre {
white-space: pre-wrap;
word-break: break-all;
}
/* 扩大文件名列宽 */
table tr td:first-child {
min-width: 600px;
word-break: break-all;
}
</style>
</head>';
sub_filter_once on;
sub_filter_types text/html;
}优点:
- 不需要重新编译 Nginx,
nginx -s reload即可生效 - 零停机,不影响已有用户
缺点:
- 文件名仍被截断到 50 字符——数据已经在服务端丢失了。CSS 能做的是让显示区域更大,但
HarmonyOS_Cockpit_4.0.0.126_SP6C00E125...永远不会显示完整 sub_filter对每个 HTML 页面做正则替换,有一定的 CPU 开销(小,但存在)- 不能实现排序、搜索、主题等高级功能
3.2 方案二:FancyIndex 编译安装(完整方案,推荐)
FancyIndex(ngx-fancyindex,GitHub: aperezdc/ngx-fancyindex)是一个第三方 Nginx 模块,完全替代内置的 autoindex 模块。它的核心能力:
- 不截断文件名——这是本文的核心需求
- 自定义 CSS 样式(通过
fancyindex_css_href引入外部样式表) - 自定义页头/页脚(PHP/HTML 任意内容)
- 按文件名/大小/修改时间排序,用户点击表头实时切换
- 支持隐藏指定文件(正则匹配,如隐藏
.gitkeep、*.md) - 显示人类可读的文件大小(
1.2 GB而不是1234567890) - 支持本地时区显示
FancyIndex 也有一个局限:它不单独计算目录大小(不递归统计)。如果需要显示「这个目录有多大」(GitHub 风格的目录大小显示),需要另写脚本。但对我这种代码发包场景——目录都是空的或只放 README——这完全不影响。
以下是一个真实的 FancyIndex 对比 autoindex 的效果对比:
| 特性 | autoindex |
sub_filter 方案 |
FancyIndex |
|---|---|---|---|
| 长文件名显示 | ❌ 截断到 50 字符 | ❌ 仍被截断(CSS 只优化排版) | ✅ 完整显示,不截断 |
| 自定义 CSS | ❌ | ✅(注入) | ✅(fancyindex_css_href) |
| 自定义页头/页脚 | ❌ | ❌ | ✅(fancyindex_header/footer) |
| 用户排序 | ❌ | ❌ | ✅(点击表头切换) |
| 搜索过滤 | ❌ | ❌ | ✅(配合自定义主题的 JS) |
| 隐藏文件 | ❌ | ❌ | ✅(正则匹配 fancyindex_ignore) |
| 文件大小人性化 | ✅(autoindex_exact_size off) |
✅ | ✅(fancyindex_exact_size off) |
| 需要重编译 Nginx | ❌ | ❌ | ✅(首次) |
| 后续维护 | 零 | 零 | 升级 Nginx 版本时需重新编译模块 |
对于生产环境的代码发包服务器,FancyIndex 是唯一正确的选择。
4. FancyIndex 源码编译安装
以下是完整的编译安装流程。虽然思路与 第 1 期 类似,但本篇是完全独立、可直接复制的教程——不需要跨文章跳转。
4.1 安装编译依赖
apt update -y
apt install -y build-essential libpcre3-dev zlib1g-dev libssl-dev4.2 下载 Nginx 与 FancyIndex
cd /usr/src
# Nginx 1.30.3 (截至 2026 年 7 月最新 Stable)
wget https://nginx.org/download/nginx-1.30.3.tar.gz
tar xzf nginx-1.30.3.tar.gz
# FancyIndex 模块(v0.5.2 兼容版本)
wget https://github.com/aperezdc/ngx-fancyindex/archive/refs/tags/v0.5.2.tar.gz \
-O ngx-fancyindex-0.5.2.tar.gz
tar xzf ngx-fancyindex-0.5.2.tar.gz截至 2026 年 7 月,FancyIndex 最新版本为 v0.6.0。如果你的环境下 v0.5.2 的 configure 脚本与新版 Nginx 不一致,可以用 master 分支:
git clone https://github.com/aperezdc/ngx-fancyindex.git
cd nginx-1.30.3
./configure ... --add-module=../ngx-fancyindexmaster 分支一般向后兼容 Nginx 新版本。
4.3 Configure + 编译
cd nginx-1.30.3
./configure \
--prefix=/usr/share/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/run/nginx.pid \
--lock-path=/run/nginx.lock \
--http-client-body-temp-path=/var/lib/nginx/client_body \
--http-proxy-temp-path=/var/lib/nginx/proxy \
--http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
--user=www-data \
--group=www-data \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_sub_module \
--with-http_addition_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--with-stream \
--with-stream_ssl_module \
--add-module=../ngx-fancyindex-0.5.2
make -j$(nproc) && make install关键参数说明(FancyIndex 相关的两个):
| 参数 | 用途 |
|---|---|
--add-module=../ngx-fancyindex-0.5.2 |
将 FancyIndex 编译到最终的 Nginx 二进制中(静态链接,无需 load_module) |
--with-http_addition_module |
FancyIndex 的自定义页头/页脚功能依赖此模块 |
编译后验证 FancyIndex 是否编译成功:
/usr/sbin/nginx -V 2>&1 | grep -o 'fancyindex'
# 输出:fancyindex如果有输出,说明 FancyIndex 已经编译进二进制了。
4.4 创建目录结构
mkdir -p /data/releases/android
mkdir -p /etc/nginx/conf.d
chown -R www-data:www-data /data/releases4.5 生成 Systemd 服务
cat > /etc/systemd/system/nginx.service << 'EOF'
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -c /etc/nginx/nginx.conf
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now nginx5. FancyIndex 配置示例
5.1 最小可用配置
server {
listen 80;
server_name filebox.example.com;
location / {
root /data/releases;
fancyindex on;
fancyindex_exact_size off;
fancyindex_localtime on;
}
}配置后平滑重载:
nginx -t && systemctl reload nginx5.2 完整生产配置
以下是我在汽车座舱团队中实际使用的配置——包含自定义页头、CSS 主题、搜索结果过滤(使用 Naereen/Nginx-Fancyindex-Theme 社区的搜索功能):
server {
listen 80;
server_name filebox.example.com;
# 文件下载基础路径
root /data/releases;
# --- 访问日志(JSON 格式,详见第 2 期)---
access_log /var/log/nginx/filebox.access.log json;
error_log /var/log/nginx/filebox.error.log;
# --- 文件下载优化 ---
sendfile on;
tcp_nopush on;
aio threads;
directio 512;
output_buffers 2 1m;
location / {
# ===== FancyIndex 核心配置 =====
fancyindex on;
fancyindex_exact_size off; # 显示 "1.2 GB" 而非原始字节
fancyindex_localtime on; # 显示本地时间(Asia/Shanghai)
fancyindex_default_sort date_desc; # 默认按修改时间倒序(最新在前)
fancyindex_directories_first on; # 目录排在文件前面
fancyindex_show_dotfiles off; # 不显示 .hidden 文件
fancyindex_case_sensitive off; # 文件名排序不区分大小写
# ===== 隐藏不展示的文件(Nginx 内置的正则匹配)=====
fancyindex_ignore "README.md";
fancyindex_ignore "\.gitkeep";
fancyindex_ignore "\.DS_Store";
# ===== 自定义 CSS(长文件名完整显示)=====
fancyindex_css_href /fancyindex/style.css;
# ===== 自定义页头 =====
fancyindex_header "/fancyindex/header.html";
# ===== 自定义页脚 =====
fancyindex_footer "/fancyindex/footer.html";
}
# ===== FancyIndex 静态资源 =====
location /fancyindex/ {
alias /etc/nginx/fancyindex/;
}
}5.3 自定义 CSS(长文件名核心)
/* /etc/nginx/fancyindex/style.css */
/* 让文件名列足够宽,不截断 */
table {
width: 100%;
table-layout: auto;
}
/* 文件名列 */
td:first-of-type, th:first-of-type {
white-space: nowrap;
}
/* 长文件名处理 */
td:first-of-type a {
word-break: break-all;
overflow-wrap: break-word;
}
/* 整体字距 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
color: #333;
background: #f8f9fa;
}
/* 表格行悬停效果 */
tr:hover {
background: #e9ecef;
}
/* 链接样式 */
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}5.4 自定义页头(团队信息)
<!-- /etc/nginx/fancyindex/header.html -->
<div style="background:#1a1a2e;color:#eee;padding:15px 20px;margin-bottom:20px;border-radius:6px;">
<h1 style="margin:0;font-size:24px;">📦 Android 代码发包平台</h1>
<p style="margin:5px 0 0;font-size:14px;color:#aaa;">
最后更新:<span id="update-time"></span> |
管理员:devops@example.com
</p>
</div>
<script>
// 自动更新最后修改时间
document.getElementById('update-time').textContent = new Date().toLocaleString('zh-CN');
</script>页脚可以放公司合规声明。部署静态文件:
mkdir -p /etc/nginx/fancyindex
# 将以上 3 个文件写入 /etc/nginx/fancyindex/6. FancyIndex 指令速查表
以下是我从 aperezdc/ngx-fancyindex README 中提取的完整指令表——按使用频率排序:
| 指令 | 语法 | 默认值 | 说明 |
|---|---|---|---|
fancyindex |
on | off |
off |
启用/禁用 FancyIndex |
fancyindex_exact_size |
on | off |
on |
on=精确字节,off=人类可读(KB/MB/GB) |
fancyindex_localtime |
on | off |
off |
off=UTC 时间,on=本地时区 |
fancyindex_default_sort |
name | size | date | name_desc | size_desc | date_desc |
name |
默认排序字段和方向(_desc 后缀 = 降序) |
fancyindex_directories_first |
on | off |
on |
目录排在文件前 |
fancyindex_css_href |
URI | "" |
引入外部 CSS |
fancyindex_header |
path [local] |
"" |
页头模板(需要 ngx_http_addition_module) |
fancyindex_footer |
path [local] |
"" |
页脚模板(同上) |
fancyindex_ignore |
string1 [string2 …] | — | 隐藏匹配的文件名(支持 PCRE 正则) |
fancyindex_show_dotfiles |
on | off |
off |
是否显示 . 开头的隐藏文件 |
fancyindex_hide_symlinks |
on | off |
off |
是否在列表中隐藏符号链接 |
fancyindex_hide_parent_dir |
on | off |
off |
是否隐藏父目录链接 ../ |
fancyindex_case_sensitive |
on | off |
on |
文件名排序是否区分大小写 |
fancyindex_show_path |
on | off |
on |
是否在页头显示当前路径(配合自定义 header 使用) |
fancyindex_time_format |
string | %Y-%b-%d %H:%M |
时间格式(strftime 格式,不依赖 locale) |
7. 最佳实践:Nginx 文件服务器的生产调优
7.1 大文件下载的 TCP 栈优化
Android OTA 包通常是 1-3 GB 的单个文件。默认的 Nginx 配置发送大文件时效率偏低——以下配置来自 第 1 期 的 TCP 调优思路:
# 在 http 或 server 块中
sendfile on; # 内核级零拷贝传输
sendfile_max_chunk 2m; # 每次 sendfile 调用的最大数据量(防止单个 worker 被大文件占满)
tcp_nopush on; # 在 sendfile 时,合并 TCP 数据包减少网络往返
aio threads; # 启用线程池异步 I/O
directio 512; # 超过 512 字节的文件使用 direct I/O(绕过 OS 页缓存)
output_buffers 2 1m; # 输出缓冲区:2 个 1 MB 的缓冲区directio 512 需要特别说明:对于大于 512 字节的文件(即几乎所有实际文件),Nginx 使用 O_DIRECT 打开文件——数据直接从磁盘到 socket,不经过操作系统的页缓存(page cache)。这意味着:
- ✅ 优点:不会因为一个 2 GB 的 OTA 文件读取而把整个 OS 的页缓存冲掉(缓存污染)
- ❌ 缺点:每次请求都是真实磁盘 I/O,没有缓存命中
如果你的服务器内存足够大(16 GB+),可以不使用 directio——让 OS 的页缓存用 LRU 算法自动管理。如果内存紧张(像我这种 1 GB 的 VPS),directio 512 是必需的。
7.2 防盗链与速率限制
代码发包服务器通常只在公司内网使用,但如果你需要暴露到公网:
# 限制并发连接数(每个 IP 最多 2 个连接同时下载)
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn perip 2;
# 限制下载速率(每个连接最多 50 MB/s)
limit_rate 50m;
# 禁止外部站点盗用下载链接
valid_referers none blocked *.example.com;
if ($invalid_referer) {
return 403;
}7.3 日志审计
使用 第 2 期 的 JSON 日志格式记录每一次文件下载。把各访问者的下载量汇总到 Grafana,一周 5 TB → 突然涨到 20 TB → 查日志确认是否有外部 IP 在扫你的下载目录。
8. FAQ
Q1: sub_filter 注入 CSS 方案和 FancyIndex 哪个更适合我?
用 sub_filter 如果:你的文件名不超过 40 个字符,只是格式不好看。不需要排序和搜索。
用 FancyIndex 如果:文件名超过 45 个字符(我的场景)、需要用户排序和搜索、或者你想自定义页面的视觉效果。
sub_filter 只是遮丑——数据已经丢了。如果你的文件名真的很长,只有一个选择:FancyIndex。
Q2: FancyIndex 编译失败——ngx_http_addition_module 相关错误?
FancyIndex 的 fancyindex_header 和 fancyindex_footer 指令依赖 ngx_http_addition_module。在 configure 时加上 --with-http_addition_module 即可。
如果你不需要自定义页头/页脚,也可以不加这个模块——FancyIndex 的核心功能(文件名完整显示、CSS 主题、排序)不依赖它。
Q3: 升级 Nginx 版本时 FancyIndex 怎么处理?
FancyIndex 不需要单独升级——它是静态编译进 Nginx 二进制的。升级 Nginx 时只需在 configure 中保留 --add-module 参数重新编译即可。
# 升级流程(零停机的平滑升级):
cd /usr/src/nginx-1.30.2
./configure ... --add-module=../ngx-fancyindex-0.5.2
make -j$(nproc)
cp objs/nginx /usr/sbin/nginx
kill -s USR2 $(cat /run/nginx.pid)
kill -s QUIT $(cat /run/nginx.pid.oldbin)这是 第 1 期 第 5.1 节的平滑升级流程。
Q4: FancyIndex 可以显示文件预览或缩略图吗?
FancyIndex 本身不支持图片预览或缩略图。但这不意味着不能实现——你可以通过自定义 fancyindex_header 注入前端 JS 库(如 Lightbox2)来实现图片预览。
总结
这篇文章解决了两个层次的同一个问题:
- 短平快:用
sub_filter注入 CSS,5 分钟搞定,文件名稍长时能用(但数据仍被截断) - 根本解决:编译 FancyIndex,一步到位完整显示文件名,还附送排序、搜索、自定义主题
选择取决于你的文件名到底有多长:Package v1.0.0.tar.gz 用方案一够,IMX8QM_MCU_AGL_2026_Q3_Release_Candidate_3_UserDebug_eng_build_20260715_signed.zip 只有方案二能救你。
推荐阅读
- 📄 Nginx 第 1 期:源码编译安装 — FHS 规范、configure 参数解析、Systemd 管理
- 📄 Nginx 第 2 期:JSON 日志格式改造 — Combined → 结构化 JSON,ELK/Loki 适配
- 🔧 Linux 服务器初始化完全指南 — 安全加固与网络优化
- 🌐 中国三大运营商精品线路指南 — CN2 GIA / 9929 / CMIN2 一篇搞懂
三篇文章形成了 Nginx 从安装到监控到特定场景的完整链路。如果你有其他的 Nginx 实战场景需要分析,欢迎评论或邮件联系。