No description
  • PHP 72.9%
  • TypeScript 26.4%
  • CSS 0.5%
Find a file
2026-09-28 17:57:09 +08:00
docker Initial commit 2026-09-28 17:54:40 +08:00
docs Initial commit 2026-09-28 17:54:40 +08:00
server Initial commit 2026-09-28 17:54:40 +08:00
web Initial commit 2026-09-28 17:54:40 +08:00
.editorconfig Initial commit 2026-09-28 17:54:40 +08:00
.gitignore chore:移除编译产物 2026-09-28 17:57:09 +08:00
AGENTS.md Initial commit 2026-09-28 17:54:40 +08:00
README.md Initial commit 2026-09-28 17:54:40 +08:00

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

关键设计决定

  1. 字节流不经过应用进程。 视频、图片、大文件统统交给 nginx 直接发;应用只做鉴权、目录索引和 URL 签发。这是弱机方案里最重要的一条 —— 它决定了看视频时 NAS 的 CPU 占用是不是零。
  2. 不做转码。 800MHz 双核转 1080p 是幻想。库里是 HEVC/MKV 就只能下载,不能在网页里播。
  3. 上传必须分片。 既绕开群晖反向代理的 client_max_body_size 限制,也避免把整个文件读进内存(512MB 的机器上这就是 OOM)。
  4. 库根目录在页面上选,不写死在配置里。 配置只放一个「允许浏览的根目录白名单」,这是路径穿越防护的边界。
  5. 不用 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 上第一次部署时完全一样):

  1. 进初始化流程 —— 设置管理员密码(至少 8 位);
  2. 进「书库」→ 新建 → 选择文件夹(选择器只能在 paths.browse_roots 白名单里活动, 本地开发时白名单是 /app/var/demo,所以先跑一次 scripts/seed-demo.sh);
  3. 点建立索引 —— 扫描是显式动作:几千本书的扫描不该挂在建库请求上;
  4. 进书架读书。

开发库(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 落地。大致路径:

  1. Web Station 建一个 PHP 8.2 站点指向 server/public —— 这就是应用的 API 站点;

  2. 让 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;

  3. 反向代理加 HTTPS(用群晖自带的证书),不要直接暴露 PHP-FPM;

  4. 部署后第一件事:打开 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 部署