fix(editor): 删文章 EXDEV 修正 + 订阅口令不再阻断登录

1) 删除文章报 EXDEV(cross-device link not permitted)
   · 根因一:容器内 /blog 与 /app/trash 是两个独立 bind mount,
     rename(2) 跨挂载点必然 EXDEV —— 哪怕宿主机上它们同在一块盘。
   · 根因二:退化成 fs.cpSync 后,Windows 上遇到**中文目录名**会把
     Node 进程直接干崩(exit 127,不抛异常,stdout 缓冲丢失)。
     而文章目录名几乎必然含中文(<日期>-<中文标题>-<slug>)。
   · 修法:改用 readdirSync/copyFileSync 自己递归复制(宽字符路径,中文 OK);
     bootstrap.sh / compose 改成挂 BLOG_DIR 的父目录,让仓库与回收站
     落在**同一个挂载点**内,正常情况下 rename 直接成功、不再复制。

2) 登录页「订阅口令」填错会阻断整个登录
   · 它只是可选订阅模块的口令,不该 throw 掉登录流程。
   · rssApi() 无条件 searchParams.set('token', RSS_TOKEN),会用浏览器里
     记住的旧口令覆盖调用方刚传的值 → 正确口令也验不过。
   · 现在:填错只 toast 提示 + 清掉失效旧口令;rssApi 尊重调用方传的值。

3) wrangler.toml:EDITOR_API_BASE 改指国内机新域名 writeapi.usj.cc
   (原指向即将到期的境外机 post.usj.cc,且那台从未部署过 editor-api)

验证:api-e2e 44/44;front matter 往返 130/130;保存往返 130/130;
中文路径删除实测通过(目录 + 中文子目录 + 图片全部移入回收站,进程不崩)。
This commit is contained in:
zqlit committed 2026-10-04 22:11:02 +08:00
1 parent e8d13b9a6e
commit 96751b8143
5 files changed
+98 -18

No files matched your search

+44 -1
View File
@@ -292,6 +292,49 @@ export function makePosts(cfg) {
}
/** 删除 = 移到仓库外的回收目录(不进 git,也不误伤版本历史) */
/**
* 递归复制目录树。
*
* ★ 为什么不用 fs.cpSync:它在 Windows 上遇到**非 ASCII 目录名**会直接把 Node
* 进程干崩(实测 exit 127,连异常都不抛,日志停在调用前一行,stdout 缓冲丢失)。
* 而文章目录名几乎必然含中文(`<日期>-<中文标题>-<slug>`),所以自己走一遍 ——
* readdirSync / copyFileSync / mkdirSync 用的是宽字符路径,中文没问题。
*/
function copyTree(src, dst) {
const st = fs.lstatSync(src);
if (st.isDirectory()) {
fs.mkdirSync(dst, { recursive: true });
for (const name of fs.readdirSync(src)) {
copyTree(path.join(src, name), path.join(dst, name));
}
return;
}
if (st.isSymbolicLink()) {
fs.symlinkSync(fs.readlinkSync(src), dst);
return;
}
fs.copyFileSync(src, dst);
}
/**
* 把文章目录挪进回收站。
*
* ★ 不能直接用 rename:仓库挂载点(容器内 /blog)和回收站挂载点(/app/trash)
* 是**两个独立的 bind mount**,rename(2) 跨挂载点必然返回 EXDEV
* —— 哪怕在宿主机上它们同在一块盘上。
* (bootstrap.sh 已改成把两者放进同一个挂载点,正常情况下走的是 rename 分支,
* 这里的复制分支只是兜底,比如有人手动改了挂载方式。)
*/
function moveToTrash(src, dst) {
try {
fs.renameSync(src, dst);
} catch (e) {
if (e.code !== 'EXDEV') throw e;
copyTree(src, dst);
fs.rmSync(src, { recursive: true, force: true });
}
}
function deletePost(key) {
const post = getPost(key);
if (!post) return null;
@@ -299,7 +342,7 @@ export function makePosts(cfg) {
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const dest = path.join(trashDir, stamp + '-' + path.basename(dir));
fs.mkdirSync(trashDir, { recursive: true });
fs.renameSync(dir, dest);
moveToTrash(dir, dest);
return { id: post.dirName, slug: post.slug, movedTo: dest };
}