CYBERSWAT / 开发部自由练习 · v0.4.1

U11 · 笔记拿走以后,还能被别的程序读吗

v0.4 · 先修:U01 文件读写与 U06 笔记;工作台。产品参考:flatnotes。本节用 TS 做独立文件存储练习,不要求切换到 Python 后端。

把笔记保存为普通 Markdown 文件,试着用编辑器在程序外修改,再回程序读取。你会看到文件存储的好处,也会遇到路径、覆盖、索引和恢复这些新的问题。

动手路线:每一步都有文件和结果

先完成 统一启动与恢复手册 的首次准备。以下命令均在自己的练习根目录执行;原文中的 api/web/desktop 概念对应本节列出的现成文件,不需要猜测如何搭空项目。

  1. 打开 u11/starter/model.ts,按 noteFilename → selectTag → mergeImport 的顺序逐个实现。下面第1节起解释规则;每完成一个函数就重跑样例/测试。
  2. 执行以下三条,分别核对样例、行为和类型。未完成的 TODO 应失败,全部实现后测试应为 fail 0:
node tools/try.mjs u11
node tools/check.mjs u11
npm run check
  1. 打开 runtime/src/u11.ts,找到对自己规则函数的调用。先用一句话记录输入从哪里来、结果写到哪里;读不懂的语法回到本节先修对应章节。

  2. 构建并启动自己的作品:

node tools/build-app.mjs u11
node tools/lab.mjs start u11
  1. 实际操作:保存 demo-note 后修改一次,读取内容已更新且 backups 多一份旧文件;导入包含重复slug的数组全部拒绝;../outside 为400。

在第二个终端运行 node tools/probe.mjs u11。脚本会添加合成数据并报告每组检查;不清空自己的数据。 首次通过后停止并重新启动服务,再运行 node tools/probe.mjs u11 --resume;数据库单元同时按统一手册重启数据库。

  1. 解释与再做一次:允许的文件名与符号链接分别解决哪种路径问题?备份在哪一步产生? 在自己的 LEARNING.md 先写答案,再选本节末尾的变式,列一个新失败例子后修改。保持原用例通过。仅跑通配套界面、没有完成规则与解释,记录为“参考演示已观察”,不记录为自己完成作品。

卡在规则时按“函数输入→分支条件→返回值→参考实现”逐级看提示;卡在运行时按 故障表 定位。若想单独观察参考成品,在构建命令后加 --reference;页面会明确标出参考演示。观察完再去掉该参数重建,继续自己的 starter。

1. 区分标题与文件身份

打开 u11/starter/model.ts。slug 是受限文件身份,title 可以以后独立增加。noteFilename 只允许小写字母开头、后续小写字母/数字/连字符,总长不超过 40;拒绝路径分隔符与 Windows 保留名。返回 slug 加 .md。

运行 node tools/try.mjs u11,demo-note 得到 demo-note.md。../x、a/b、con 都应失败。这个函数只验证名称,尚未证明实际磁盘路径安全。

2. 先把一批导入作为整体检查

完成 mergeImport:先验证所有新旧项和重复 slug,再返回新列表;任何冲突都失败,旧笔记不能被部分覆盖。完成 selectTag,返回匹配项而不修改原数组。运行核心测试,并给“第二条冲突时第一条也不应写入”先写一段预期。

3. 建专用文件目录和读写入口

本版启动后自动建立 runtime/data/file-notes,所有笔记仅从这里读写;自定义恢复目录通过 LAB_FILES 指定。Node 外层先通过 noteFilename 得到文件名,再与已确认的根目录组合。启动时确认根目录不是符号链接;读取条目用 lstat,只接受普通文件,拒绝符号链接。实验目录由本人独占,不声称能防御其他进程同时替换目录造成的竞态。

本版“保存文件”对已有slug执行修改:先备份旧文件,再用 wx 临时文件与重命名替换;导入入口则拒绝已有slug。查看 runtime/src/u11.ts 中 write 的 replace 参数,不能混淆两种行为。不要用传来的 title 直接拼路径,也不要随意覆盖已存在笔记。

4. 让程序外修改变得可观察

先在页面输入slug one保存,生成one.md,关闭当前编辑页面,在文本编辑器中改正文,再重新读列表与内容,预期看到新文字。若页面仍显示旧文,判断是浏览器缓存、服务缓存还是搜索索引。

第一版每次读取从文件系统加载,不加全文索引。本版 .md 第一行是 {"tags":["js"]} 元数据,其余行完整保留正文。打开一个样例核对这一约定,不把正文任何一个 # 都当成标签。之后才引入缓存,并给失效条件。

5. 导出、恢复和批量冲突

将笔记目录复制为独立备份,再将备份复制到另一个空恢复目录;原目录和备份都保留,不覆盖唯一副本。先确认备份中确有本次笔记,再在练习根目录运行以下命令,两个引号中的路径分别替换为自己的备份目录和已复制内容的恢复目录:

node tools/verify-u11-restore.mjs "备份目录" "恢复目录"

工具只读检查两个目录已存在、不是符号链接,笔记数量、文件名和逐文件内容一致,且元数据可读取;源或恢复目录为空、缺失、损坏或不一致都不会报恢复通过。两次运行只重复检查,不复制或覆盖文件。它只比较已有备份与恢复副本:如果备份本身已缺少笔记,还须对照备份前记录的笔记清单与数量。

检查通过后,在 runtime/.env 增加 LAB_FILES=恢复目录的绝对路径(把中文占位替换成自己的路径,不加引号);重启 U11、读取并与备份前记录逐条比较,再运行带 --resume 的验收。单凭空列表或 --resume 不能证明全部笔记已恢复。恢复确认后删除该配置行并重启,即回到原目录。原有自定义新目录首次写入时仍可自动创建;该行为不能用来证明恢复路径正确。

本版批量导入先全量调用 mergeImport,再在单进程串行队列中写新文件;普通写入失败会删除本批已写文件,旧文件不覆盖。它不是断电原子事务:若进程突然中断,先对照导入清单检查已写文件并备份,再处理残留,不能盲目重试。完整 staging 目录原子切换作为进阶。

6. 自检与变式

检查非法 slug、保留名、重复导入、符号链接条目、外部编辑可见、中文/换行、独立目录恢复和读取失败不清空旧资料。核心测试仅覆盖名称、标签与内存冲突,不覆盖磁盘链接、竞态或恢复。

独立变式:增加“只读模式”,所有写入入口都拒绝,直接调用接口也不能绕过。暂停前记录数据根目录和备份位置。阅读 flatnotes 时比较它的原文与索引分工;其前端占位 test 命令不替代你的自检。