mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-10 21:11:08 +08:00
按产品设计 v0.2 落地服务端,覆盖第一版范围中的扫描、元数据、权限与同步。 核心决策: - 书籍主键用 dc:identifier + 内容哈希,不用文件路径。 重命名/移动不丢进度,副本自动合并,重新编码的文件保持独立。 - 库目录只读挂载,写操作全部落到 DATA_DIR。 启动时断言 DATA_DIR 不在 BOOKS_DIR 内,路径解析统一走 resolveInside 拦截逃逸,扫描不跟随符号链接。 - 元数据优先级:手动修改 > 内嵌 > provider > 文件名解析。 手动修改独立成表,扫描永远碰不到,可单字段撤销。 - 中文文件名解析采取保守策略,无证据时不猜测作者方向。 - 离线合并用 LWW,笔记删除写墓碑,批次单事务。 - 无原生编译依赖:node:sqlite 与 scrypt 均为内置模块。 交付: - Fastify + TypeScript 服务端,Docker 单容器 - 27 个自动化测试用例全部通过 - AGPL-3.0,README 做到一条命令跑起来 - CNB 流水线:typecheck / test / build
34 lines
1.3 KiB
TypeScript
34 lines
1.3 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { AppError } from '../lib/errors.ts';
|
|
|
|
/**
|
|
* Single error serialiser. Responses intentionally keep the same shape across
|
|
* every endpoint so old clients keep working as the API grows (§8.3).
|
|
*/
|
|
export function registerErrorHandler(app: FastifyInstance): void {
|
|
app.setErrorHandler((error, request, reply) => {
|
|
if (error instanceof AppError) {
|
|
reply.status(error.statusCode).send({ error: { code: error.code, message: error.message } });
|
|
return;
|
|
}
|
|
const fastifyError = error as { statusCode?: number; message?: string };
|
|
const status = fastifyError.statusCode ?? 500;
|
|
if (status >= 500) {
|
|
request.log.error({ err: error }, 'unhandled error');
|
|
reply.status(status).send({ error: { code: 'INTERNAL', message: 'internal server error' } });
|
|
return;
|
|
}
|
|
// Validation and content-type failures land here; they are the client's
|
|
// fault, so the message is passed through verbatim.
|
|
reply.status(status).send({
|
|
error: { code: 'BAD_REQUEST', message: fastifyError.message || 'bad request' },
|
|
});
|
|
});
|
|
|
|
app.setNotFoundHandler((request, reply) => {
|
|
reply.status(404).send({
|
|
error: { code: 'NOT_FOUND', message: `no route for ${request.method} ${request.url}` },
|
|
});
|
|
});
|
|
}
|