Filebox 项目开发记录:从零到上线的 PDF 压缩工具
Filebox 项目开发记录:从零到上线的 PDF 压缩工具

Filebox 项目开发记录:从零到上线的 PDF 压缩工具

项目地址: filebox.fanyang.me


项目背景

起因很简单——想把本地已经安装好的 Ghostscript 做成一个可以通过浏览器访问的在线工具,方便自己和他人压缩 PDF 文件,后期计划扩展到图片、视频、文本等多种文件格式的压缩处理。


技术选型

层级 技术
压缩引擎 Ghostscript
后端框架 Python + FastAPI
前端 原生 HTML / CSS / JavaScript
服务器 Ubuntu + LNMP(Nginx 反向代理)
HTTPS Let’s Encrypt(LNMP 内置工具申请)

选择 FastAPI 的原因是它原生支持异步处理,对于 I/O 密集型的文件压缩任务性能较好,且部署简单。前端没有使用任何框架,保持轻量。


功能列表

核心功能

  • 拖拽或点击上传 PDF 文件
  • 四档压缩质量选择:极限(72 dpi)、均衡(150 dpi)、高清(300 dpi)、印刷级(300+ dpi)
  • 支持批量上传,每次最多 10 个文件,单文件最大 50 MB
  • 压缩完成后显示压缩前后文件大小对比及压缩率
  • 即时下载压缩结果

安全与隐私

  • 速率限制:每个 IP 每 60 秒最多发送 20 次请求,防止刷接口
  • 文件内容校验:检查文件头魔数(%PDF),防止伪装成 PDF 的恶意文件上传
  • 自动清理:后台任务每 10 分钟扫描一次,压缩文件超过 1 小时自动删除,原始上传文件压缩完成后立即删除
  • 手动删除:用户可在下载后主动点击删除按钮,立即清除服务器上的文件
  • 路径过滤:对文件 ID 做字符过滤,防止路径穿越攻击
  • Ghostscript 超时保护:单个压缩任务最长执行 300 秒,防止异常 PDF 卡死进程

并发处理

使用 asyncio.to_thread() 将同步的 Ghostscript 调用放入线程池执行,多个压缩请求可以真正并发处理,不互相阻塞。批量上传的多个文件通过 asyncio.gather() 并发压缩。

国际化

  • 中英文双语界面,右上角一键切换
  • 所有文案(界面文字、错误提示、按钮、结果标签)均支持双语

合规

  • GDPR 合规:服务器位于欧盟境内(德国),数据不出欧盟
  • 页面顶部展示数据处理告知横幅
  • 完整隐私声明弹窗(中英双语),包含数据控制者说明、处理目的与法律依据(GDPR 第 6(1)(b) 条)、数据保留期限、用户权利(删除权、访问权、投诉权)等
  • 监管机构:爱尔兰数据保护委员会(DPC)

其他细节

  • Beta 阶段标注(顶部徽章 + 横幅说明)
  • 反馈邮箱(混淆处理防爬虫,用户点击正常打开邮件客户端)
  • 页脚个人网站链接

前端设计风格

极简科技风。白色背景、大量留白、精准的几何线条,使用 IBM Plex Sans + IBM Plex Mono 字体组合,蓝色(#0066ff)作为主色调。整体风格参考工程师审美,避免通用的 AI 设计模板感。


部署架构

用户(HTTPS)
    ↓
Nginx(LNMP,443 端口)
    ↓ 反向代理
uvicorn(127.0.0.1:8000)
    ↓
FastAPI + Ghostscript

WordPress 和 Filebox 运行在同一台服务器上,通过不同域名(fanyang.mefilebox.fanyang.me)由 Nginx 分发,互不干扰。

Filebox 通过 systemd 管理,配置开机自启和自动重启。


遇到的问题与解决

问题 1:忘记在公网服务器上安装 Ghostscript

本地虚拟机已安装,上线后才发现公网服务器没装。

sudo apt update && sudo apt install ghostscript -y

公网服务器(Ubuntu 22.04)安装的是 9.55.0,比本地虚拟机(Ubuntu 24.04)的 10.02.1 版本低,但对于项目使用的基础压缩功能完全没有影响。

问题 2:LNMP 虚拟主机默认配置不是反向代理

LNMP vhost add 生成的配置是静态文件服务模式,需要手动将 HTTPS server 块替换为 proxy_pass 反向代理配置,同时保留 LNMP 已自动配置好的 SSL 证书路径和 HTTP→HTTPS 重定向。

问题 3:Ghostscript 是同步阻塞调用

FastAPI 是异步框架,但 subprocess.run() 是同步阻塞的。直接调用会导致多个请求排队等待。解决方案是用 asyncio.to_thread() 将其放入线程池:

async def compress_pdf_async(input_path, output_path, quality):
    return await asyncio.to_thread(_run_ghostscript, input_path, output_path, quality)

项目文件结构

/opt/www/filebox/
├── main.py              # FastAPI 后端
├── requirements.txt     # Python 依赖
├── venv/                # Python 虚拟环境
├── static/
│   └── index.html       # 前端页面
├── uploads/             # 临时上传目录(自动创建)
└── compressed/          # 压缩输出目录(自动创建)

后续计划

  • 加入磁盘用量监控,防止服务器空间被耗尽
  • 扩展支持图片压缩
  • 扩展支持视频压缩
  • 扩展支持文本文件处理
  • 域名独立:目前作为 fanyang.me 的二级域名运行,后期考虑注册独立域名 filebox.xxx

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注