Files
book-comic-library/docs/CHANGELOG.md
XingfenD 8305af9d5c
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
docs: changelog + README backend structure for batch C (Task 29)
- CHANGELOG [Unreleased]: consolidated the duplicated Added/Changed/Fixed
  groups left by earlier batches into one group each (no entry dropped);
  added batch C entries — port-based restructure, media/upload domain
  packages, sweep moved to scanner ticker (B16), router contract test,
  portsfake unit-test layer. Upload-sweep wording no longer promises the
  old 24h opportunistic request-path behaviour.
- README.md / README_zh.md: new 'Backend structure' section documenting
  the port/fake layout (cmd/webui composition root, handlers as HTTP-only,
  internal/ports + portsfake, media/upload/store/scanner/bookfile
  responsibilities) and the two-tier testing approach (real PG+Redis
  integration vs fake-injected unit, route table pinned by contract test)

Gate: gofmt clean, go vet clean, go test -p 1 all pass (0 skip),
scripts/smoke.sh ALL SMOKE TESTS PASSED against a live webui on :18080
2026-09-14 23:37:36 +08:00

11 KiB
Raw Permalink Blame History

Changelog / 更新日志

All notable non-WebUI user-visible changes should be documented in this file, newest version on top. 本文件记录所有非 WebUI 的重要用户可见变更,最新版本在最上方。

The format loosely follows Keep a Changelog and can be adapted to the team's habits. 本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。

[Unreleased]

Added / 新增

  • API: resumable chunked upload protocol for large files — POST /api/libraries/:id/upload/init (fingerprint-derived deterministic uploadId, rejects totals over UPLOAD_MAX_MB with 413 too_large), PUT /api/uploads/:uid/parts/:index (parts ≤ 32MB), GET /api/uploads/:uid (received parts, for resume), POST /api/uploads/:uid/complete (assemble + atomic land, same path contract as single-POST upload). Sessions persist under BOOKS_DIR/.uploads/ with a periodic sweep. UPLOAD_MAX_MB is now wired through both compose stacks/.env; .env.example sets 2048 and drops NGINX_CLIENT_MAX_BODY_SIZE to 32m (nginx only ever sees one chunk).

  • API:新增大文件可续传分片上传协议——POST /api/libraries/:id/upload/init(按指纹派生确定性 uploadId,总量超 UPLOAD_MAX_MB 返回 413 too_large)、PUT /api/uploads/:uid/parts/:index(单片 ≤32MB)、GET /api/uploads/:uid(查询已传分片以续传)、POST /api/uploads/:uid/complete(拼接后原子落盘,返回与单发上传一致的 path)。会话存于 BOOKS_DIR/.uploads/,定期清理。UPLOAD_MAX_MB 已接入两份 compose/.env;.env.example 调至 2048 并将 NGINX_CLIENT_MAX_BODY_SIZE 降为 32m(nginx 只见单个分片)。

  • API: per-user bookmarks — GET/POST /api/books/:id/bookmarks (locator+percent snapshot with optional ≤500-char note; list ordered by percent) and PATCH/DELETE /api/bookmarks/:id; not-yours uniformly 404. New bookmarks table keyed like progress, cleaned up with the user (no cascade on book delete, same precedent).

  • API:新增按用户隔离的书签——GET/POST /api/books/:id/bookmarks(存当前 locator+percent,备注可选、≤500 字,列表按进度升序)与 PATCH/DELETE /api/bookmarks/:id;不属于自己的一律 404。新 bookmarks 表与进度同款定位键,随用户删除而清(删书不级联,沿用既有先例)。

  • API: CBZ page indexing now skips macOS packaging junk (__MACOSX/… and ._* AppleDouble files), which used to land in the page list as ~163-byte black "pages"; GET /api/books/:id/pages additionally returns chapters:[{title,start}] derived from the archive's folder structure (e.g. 第1話…), so per-folder comics expose their real organization.

  • API:CBZ 页索引现会跳过 macOS 打包垃圾(__MACOSX/… 与 ._* 资源叉文件),此前它们以 ~163 字节黑页混入页列表;GET /api/books/:id/pages 新增 chapters:[{title,start}],按压缩包内目录结构(如 第1話…)给出真实章节。

  • Tests: router contract test (TestRouterContract) pins the full route table — any added, removed or renamed route fails the test until the expectation is updated deliberately.

  • 测试:新增路由契约测试(TestRouterContract),锁定全量路由表——任何路由的增删改名都会使该测试失败,必须显式更新预期。

  • Tests: hand-written in-memory fakes (internal/ports/portsfake) cover all port interfaces, enabling handler unit tests with no PG or Redis. Error semantics mirror the real store (pgx.ErrNoRows, store.ErrLastAdmin, *pgconn.PgError{Code:23505}), so the fakes exercise the same 404/409/400 branches as the database.

  • 测试:新增手写内存 fake(internal/ports/portsfake),覆盖全部 port 接口,使 handler 单测无需 PG/Redis 即可运行。错误语义与真实 store 一致(pgx.ErrNoRows、store.ErrLastAdmin、*pgconn.PgError{Code:23505}),因此 fake 走到的是与真库相同的 404/409/400 分支。

  • CI workflow (.github/workflows/ci.yml) compatible with both GitHub Actions and Gitea Actions.

  • CI 工作流(.github/workflows/ci.yml),兼容 GitHub Actions 和 Gitea Actions。

Changed / 变更

  • Backend restructured around hexagonal ports: HTTP handlers now depend only on small consumer-side interfaces (internal/ports) instead of concrete *store.Store / *redispkg.R / *scanner.Scanner. Domain logic moved out of handlers into internal/media (cover/page extraction, page index cache, atomic cache writes) and internal/upload (chunked session lifecycle). cmd/webui/main.go is the composition root; api.NewRouter accepts pure interfaces.

  • 后端按六边形端口重构:HTTP handler 现在只依赖 internal/ports 中的小口径消费端接口,不再直接持有 *store.Store / *redispkg.R / *scanner.Scanner 等具体类型。域逻辑从 handler 下沉到 internal/media(封面/页抽取、页索引缓存、缓存原子写)与 internal/upload(分片会话生命周期)。cmd/webui/main.go 作为装配根,api.NewRouter 只收接口。

  • Upload session sweep moved off the request path onto the scanner's ticker cycle (B16), so POST /upload/init no longer pays for a directory walk.

  • 上传会话清理从请求路径移到扫描器的定时周期(B16),POST /upload/init 不再顺带付出一次目录遍历的开销。

  • Add ordered migration system with schema_migrations tracking and pg advisory lock for safe multi-replica schema evolution. Existing databases are auto-baselined. To change the schema, add a new NNNN_description.sql file under backend/internal/db/migrations/; never modify an already-applied file. No down migrations — rollback via backup, fix-forward.

  • 新增有序迁移系统,通过 schema_migrations 表和 pg advisory lock 实现安全的多副本 schema 演进,已有数据库自动基线化。修改 schema 时在 backend/internal/db/migrations/ 下新增 NNNN_description.sql,已应用的文件不可修改。不支持 down 迁移——回滚靠备份,fix-forward。

  • Scanner: an image-list-less archive (.zip/.cbz with no page images — video packs, document dumps) is now recorded as state=error ("no images in archive") instead of registering as an empty CBZ with a blank reader.

  • 扫描器:不含任何图片条目的 .zip/.cbz(视频包、文档包)现记录为 state=error("no images in archive"),不再注册成空 CBZ 留下一个白板阅读器。

  • API: upload over UPLOAD_MAX_MB now returns 413 too_large with the limit in the message; previously the size abort was misreported as 400 bad_request "multipart field 'file' required".

  • API:超过 UPLOAD_MAX_MB 的上传现在返回 413 too_large 并在消息中带上限额;此前体积超限被误报为 400 bad_request "multipart field 'file' required"。

  • API: POST /api/libraries now takes only {name}; root_path is generated server-side as BOOKS_DIR/<sanitized name> (no client-supplied paths, validated at creation).

  • API:POST /api/libraries 只需 {name};root_path 由服务端生成为 BOOKS_DIR/<清洗后的库名>(不再接受客户端指定路径,创建时即校验)。

  • Repo structure conformed to AGENTS.md: web/ renamed to frontend/; backend HTTP layer moved from internal/api to cmd/webui/{api,handlers} (cmd/server → cmd/webui); README/CHANGELOGs relocated under docs/ (README_zh.md added as Chinese mirror); module-level .gitignores added (backend/, deploy/); stray root library/ removed (book files live in deploy/api/storage/); debug binaries untracked.

  • Docs: docs/README.md is now the English primary; previous Chinese README mirrored to docs/README_zh.md.

Fixed / 修复

  • Rate limiter IncrWindow uses atomic Lua script for INCR+EXPIRE, preventing permanent IP lockout on EXPIRE failure (B1).
  • 限流器 IncrWindow 改用 Lua 脚本原子执行 INCR+EXPIRE,防止 EXPIRE 失败导致 IP 永久锁定(B1)。
  • Distributed lock Lock handles rand.Read failure by degrading to no-lock instead of using a zero token (B2).
  • 分布式锁 Lock 在 rand.Read 失败时降级为无锁模式,而非使用全零 token(B2)。
  • Lock unlock uses context.WithoutCancel to survive caller cancellation (B3).
  • 锁的解锁改用 context.WithoutCancel,在调用方上下文取消后仍能正常释放(B3)。
  • Upload part writes to .tmp then renames, preventing truncated parts from being reported as received (B4).
  • 分片上传先写 .tmp 再 rename,防止崩溃截断的分片被误报为已接收(B4)。
  • DeleteUser last-admin check is now transactional, eliminating TOCTOU race (B5).
  • DeleteUser 的最后管理员检查改为事务内执行,消除 TOCTOU 竞态(B5)。
  • Single-file upload io.Copy errors other than MaxBytesError return 500 instead of 413 (B6).
  • 单文件上传中非 MaxBytesError 的 io.Copy 错误返回 500 而非 413(B6)。
  • /auth/me distinguishes no rows (401) from database errors (503) (B7).
  • /auth/me 区分无记录(401)和数据库错误(503)(B7)。
  • Library creation rejects reserved names (cache, .uploads) with 400 reserved_name (B8).
  • 创建书库时拒绝保留名(cache、.uploads),返回 400 reserved_name(B8)。
  • Scanner lock auto-renews every TTL/2 during long scans; per-library single-flight prevents concurrent scans (B9).
  • 扫描锁每 TTL/2 自动续期;库级 single-flight 防止并发扫描(B9)。
  • Scanner SetBookState errors are now logged instead of silently discarded (B10).
  • 扫描器 SetBookState 的错误现在会记录日志而非静默丢弃(B10)。
  • Cover write errors fully checked; orphan .tmp files cleaned only on failure (B11).
  • 封面写入错误全部检查;孤儿 .tmp 文件仅在失败路径清理(B11)。
  • Upload handler retries on O_EXCL collision for concurrent same-name uploads (B12).
  • 上传处理器在 O_EXCL 冲突时重试,处理并发同名上传(B12)。
  • Bookmark methods check err before RowsAffected to avoid invalid reads on query failure (B13).
  • 书签方法先检查 err 再读 RowsAffected,避免查询失败时的无效读取(B13)。
  • Serve goroutine log.Fatalf replaced with channel-based shutdown to preserve graceful teardown (B14).
  • 服务 goroutine 中的 log.Fatalf 改为 channel 通知方式,确保优雅关停流程不被绕过(B14)。
  • DATABASE_URL is now validated at startup (required, parseable); empty REDIS_URL logs a clear "redis disabled" message (B15).
  • DATABASE_URL 在启动时校验(必填、可解析);空 REDIS_URL 记录明确的 "redis disabled" 日志(B15)。
  • scripts/smoke.sh aligned with current API contract, removed ignored root_path field (B17).
  • scripts/smoke.sh 对齐当前 API 契约,移除被忽略的 root_path 字段(B17)。