- PHP 72.9%
- TypeScript 26.4%
- CSS 0.5%
| docker | ||
| docs | ||
| server | ||
| web | ||
| .editorconfig | ||
| .gitignore | ||
| AGENTS.md | ||
| README.md | ||
SynologyFrontend
群晖 NAS 上的个人书库 / 视频库 / 文件中转站。单用户自用。
目标设备是 DS120j(Marvell Armada 88F3720 双核 800MHz / 512MB 板载内存 / 单盘)。 所有设计取舍都是为这台机器服务的 —— 它不是一台能跑容器编排的服务器。
功能
| # | 功能 | 说明 |
|---|---|---|
| 1 | 书库 | 选一个文件夹,下面的每个子文件夹当成一本书;在网页里翻页阅读 |
| 2 | 视频库 | 选一个文件夹作为视频库;网页里播放(依赖 nginx 直发字节流) |
| 3 | 上传 / 下载 | 分片上传 + 断点续传;下载直连 |
| 4 | 文件快传 | 取件码 + 有效期 + 下载次数限制,作临时中转 |
| 5 | 布局 | 左侧导航 + 顶部标题 + 中间内容区 |
技术栈
- 后端:PHP 8.2,零第三方依赖(不用 Composer,没有 vendor 目录)
- 数据库:SQLite(PDO,进程内,零常驻内存);所有数据都能从文件系统重建
- 前端:React + Vite + TypeScript + Tailwind + shadcn/ui + Zustand + React Router(Phase 8 落地)
- Web 服务器:群晖 Web Station 自带的 nginx + PHP-FPM
关键设计决定
- 字节流不经过应用进程。 视频、图片、大文件统统交给 nginx 直接发;应用只做鉴权、目录索引和 URL 签发。这是弱机方案里最重要的一条 —— 它决定了看视频时 NAS 的 CPU 占用是不是零。
- 不做转码。 800MHz 双核转 1080p 是幻想。库里是 HEVC/MKV 就只能下载,不能在网页里播。
- 上传必须分片。 既绕开群晖反向代理的
client_max_body_size限制,也避免把整个文件读进内存(512MB 的机器上这就是 OOM)。 - 库根目录在页面上选,不写死在配置里。 配置只放一个「允许浏览的根目录白名单」,这是路径穿越防护的边界。
- 不用 MariaDB。 再起一个数据库服务,内存预算就没了。
本地开发
本机不需要装 PHP(用 Docker 里的 php:8.2-cli)。先确认 Docker Desktop 在运行,否则所有命令都会失败:
scripts/dev.sh # 起服务,默认 http://127.0.0.1:18080
scripts/check.sh # 语法检查 + 单元测试(当前 115 组断言)
scripts/check.sh PathGuard # 只跑名字含关键字的用例
scripts/seed-demo.sh # 生成本地演示数据:3 本图片书 + 视频占位文件
scripts/build-php.sh # 构建带 GD/exif 的本地 PHP 镜像(缩略图功能需要)
首次运行 dev.sh 会自动生成 server/config/config.php(已被 .gitignore 排除)。
第一次打开应用的完整流程(和你在 NAS 上第一次部署时完全一样):
- 进初始化流程 —— 设置管理员密码(至少 8 位);
- 进「书库」→ 新建 → 选择文件夹(选择器只能在
paths.browse_roots白名单里活动, 本地开发时白名单是/app/var/demo,所以先跑一次scripts/seed-demo.sh); - 点建立索引 —— 扫描是显式动作:几千本书的扫描不该挂在建库请求上;
- 进书架读书。
开发库(server/var/app.sqlite)不入版本库,删掉它就是干净的「初次部署」状态。
常用接口:
| 接口 | 说明 |
|---|---|
GET /api/ping |
存活探测(永远免登录) |
GET /api/health |
环境体检(未设密码时可访问,设完密码后要求登录) |
GET/POST /api/auth/* |
会话状态 / 初始化 / 登录 / 登出 / 改密码 |
GET /api/browse/roots、GET /api/browse?path= |
文件夹选择器 |
GET/POST/DELETE /api/libraries、POST /api/libraries/{id}/scan |
库的增删查与建立索引 |
GET /api/libraries/{id}/books |
书架(分页 + 关键字搜索) |
GET /api/books/{id} |
书籍详情(含册页列表) |
GET /api/books/{id}/cover、GET /api/books/{id}/pages/{index}/raw |
图片字节(支持 Range 断点续传) |
GET /probe.php |
部署前环境探测页(默认停用,需要 server/var/probe.enabled) |
目录结构
server/ PHP 后端(部署时整个目录就是 Web Station 站点根)
├── public/ 唯一入口 index.php + 部署探测 probe.php
├── config/ config.example.php / config.php / php.dev.ini / routes.php(路由表)
├── migrations/ 迁移,一文件一版本(文件名即版本号)
│ 010 设置 / 020-021 鉴权 / 030-031 书库 / 040 视频
│ 050 限流 / 051 快传取件码
├── src/
│ ├── Controller/ HTTP 端点(薄)
│ ├── Service/ 业务用例
│ ├── Repository/ 数据访问(唯一碰 SQL 的一层)
│ ├── Storage/ 文件系统数据访问(书页 / 目录列举)
│ ├── Entity/ 持久化行 Dto/ 入参 XxxInput + 出参 XxxResponse
│ ├── ValueObject/ 模块内不可变值
│ ├── Http/ Media/ Fs/ Db/ Enum/ Exception/ Support/
│ └── Support/container.php 唯一装配处(显式 new,无反射)
├── vendor/ Composer 生成的自动加载文件(几十 KB,随代码部署)
├── tests/ 零依赖测试器 + 用例
└── var/ SQLite / 日志 / 缩略图缓存(不入库)
web/ React 前端(Vite + TS + Tailwind + shadcn/ui)
docker/ 本地开发镜像(给官方 PHP 镜像补 GD/exif)
scripts/ 本地开发、检查、演示数据脚本
后端分层是 Symfony / Laravel 那套技术分层:Controller → Service → Repository → PDO,
文件系统由 Service 直接调 Fs\* / Storage\*。
自动加载交给 Composer(composer.json 里只有 autoload.psr-4,没有任何第三方运行时依赖),
改了目录结构或类名后要重跑 composer dump-autoload --optimize。
为什么这条是硬要求:macOS 文件系统大小写不敏感,目录名写成小写也能跑; 但群晖是 Linux(大小写敏感),大小写不一致 = 所有类加载失败、整站白屏。
tests/cases/140_psr4_conformance_test.php会把这件事钉死。
部署到群晖
首次部署请直接看 docs/deploy.md(Web Station 逐步操作 + 体检 + 排查表), 打包用
scripts/package-deploy.sh。
Phase 9 落地。大致路径:
-
Web Station 建一个 PHP 8.2 站点指向
server/public—— 这就是应用的 API 站点; -
让 nginx 负责发字节流(可选但强烈建议,见「关键设计决定」第 1 条)。 在 nginx 里加一条内部 location:
location /_media/ { internal; # 只能被应用通过 X-Accel-Redirect 触发,外部访问不到 alias /; # /_media/volume1/books/x/01.png → /volume1/books/x/01.png }然后把
config.php里的media.driver改成x_accel。 不改也能跑(默认php驱动自己流式发送),只是会占一个 PHP-FPM worker; -
反向代理加 HTTPS(用群晖自带的证书),不要直接暴露 PHP-FPM;
-
部署后第一件事:打开
probe.php做环境体检 (需要在server/var/下临时创建probe.enabled,用完删掉)。
细节见 AGENTS.md 与后续的 deploy/。
进度
- Phase 1 骨架与核心(Result / 异常 / Http / 日志 / PathGuard / SQLite / 测试 / 探测页)
- Phase 2 登录鉴权(单用户密码、HttpOnly 会话、CSRF 同源校验、防爆破锁定)
- Phase 3 书库后端(文件夹选择器、库管理、懒扫描索引、书架/详情/册页、Range 媒体发送)
- Phase 4 前端骨架(设计 token、三态、Result 解析、登录/初始化页、左导航+顶栏+内容区、书架、基础阅读器)
- Phase 5 封面缩略图缓存(GD + 落盘缓存 + 失败回退原图 + 缓存上限)
- 结构改造:按行业标准重排为技术分层 + Composer 自动加载 + 显式装配 + 路由集中 (顺带修掉一个致命缺陷:目录名小写在群晖上会导致所有类加载失败)
- 错误模型收敛:去掉
Result包装层,服务失败一律抛类型化异常,入口统一翻译成错误信封 - 数据边界收敛:datasource 层合并进 repository,跨表组合上移到 service
- 迁移改成
migrations/一文件一版本,文件名即版本号(加迁移不用改既有代码) - Phase 6 视频库(递归扫描 + Range 流式播放 + 不能播的格式只给下载)
- Phase 7 文件管理(分片 + 断点续传上传、下载、新建文件夹、删除)
- Phase 8 文件快传(取件码 + 有效期 + 次数上限 + 免登录取件 + 防穷举)
- Phase 9 NAS 部署
- Phase 4 书库前端阅读器
- Phase 5 视频库
- Phase 6 上传下载
- Phase 7 文件快传
- Phase 8 前端骨架与布局
- Phase 9 NAS 部署