WeKan 在 Sandstorm 上的 Meteor 3.5 / Node.js 24 构建与 MongoDB 3 → FerretDB 迁移方案
2026/9/14 4:25:00 网站建设 项目流程

WeKan 在 Sandstorm 上的 Meteor 3.5 / Node.js 24 构建与 MongoDB 3 → FerretDB 迁移方案

【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

本文档对应仓库中的 docs/Platforms/FOSS/Container/Sandstorm/Meteor3/Migration.md,该方案目前处于design / not yet implemented状态,文中标注TEST的步骤都必须在真实 Sandstorm 实例(理想情况是现有 WeKan grain 的副本)上验证后才可发布。

导读

WeKan 的 Sandstorm.spk构建长期依赖 2021 年的 meteor-spk 0.6.0 打包器,其内部捆绑的是 2015 年设计的 Node 14 / MongoDB 3.0 运行时。随着 WeKan 主版本升级到 Meteor 3.5(要求 Node.js 24),旧构建方式已经无法复用。本篇技术指南完整讲解如何打造一个现代化的 WeKan.spk:用 Meteor 3.5 + Node.js 24 替代旧运行时,将打包的 MongoDB 3.0 数据库替换为FerretDB v1(内嵌 SQLite),并在 grain 首次启动时完成存量数据的自动迁移——直接复用 WeKan snap 已随附的迁移逻辑。读完本文,你将掌握:Sandstorm grain 的运行时替换与 seccomp 兼容性评估、meteor-spk.deps载荷(payload)的现代化改造、两阶段迁移(niscu 2.x → MongoDB 3.0 → FerretDB)的完整启动器设计、迁移导入器(importer)的内部机制,以及管理面板中"迁移状态 / 磁盘占用 / 删除旧数据"的 Sandstorm 专属 UI 实现。

1. 为什么需要重写 Sandstorm 构建

当前 Sandstorm 构建使用meteor-spk 0.6.0https://dl.sandstorm.io/meteor-spk-0.6.0.tar.xz,构建于 2021-10-23)。其meteor-spk.deps载荷是一个 2015 年时代的设计,与 WeKan 现在所需的运行时存在巨大落差:

组件meteor-spk 0.6.0 自带WeKan 需要
bin/nodeNode14.17.5(nodejs.org 官方原版)Node 24.x(Meteor 3.5 要求)
bin/mongodMongoDB3.0.7(WiredTiger)保留——仅作为只读迁移数据源
bin/niscudMongoDB2.x(Kenton 的 Niscu fork)保留(用于 niscu→3.0 迁移)
lib/glibc2.31(Ubuntu 20.04)在 22.04(glibc 2.35)上重新生成,以支撑 node24/ferretdb
start.js启动 Mongo 3.0 + niscu→3.0 迁移重写为面向 FerretDB

0.6.0 中的start.js与 0.5.1 功能上完全相同(仅做了重新排版)。它仍然以--nohttpinterface启动 mongod 3.0——该参数在 MongoDB 3.6+ 中已被移除,因此即使不修改start.js,也无法直接塞入更新的 mongod。

关于 Node fork 的说明sandstorm-io/nodefork并不需要。它冻结在 Node 8.11.4(2018 年),其唯一的实质补丁是为node-fibers做的 V8 线程表优化——而 Meteor 3 已彻底移除 fibers。meteor-spk 本身已经打包了上游原版Node(0.6.0 的bin/node正是官方 nodejs.org 14.17.5 构建)。因此直接使用原版 Node 24即可。

2. 目标架构

迁移完成后的稳态:WeKan(Node 24)通过MongoDB wire 协议FerretDB v1(SQLite)通信——整个 grain 中不再运行任何 MongoDB 服务器。mongod 3.0 仅在一次性的迁移过程中被临时使用一次,用于读取存量 grain 的旧数据。

grain(单一沙箱,可写目录 = /var) ├─ /var/wiredTigerDb ← 现有 MongoDB 3.0 数据(旧 grain 才有;只读迁移源) ├─ /var/files/ ← WRITABLE_PATH(新增) │ ├─ attachments/ ← GridFS 附件在此落盘(Meteor-Files 存于文件系统) │ ├─ avatars/ ← GridFS 头像在此落盘 │ └─ db/ ← FerretDB SQLite 目录(wekan.sqlite)← 稳态数据库 └─ /var/.migration-to-ferretdb-done ← 幂等性标记文件

grain 内环回端口

端口角色
4000应用 HTTP(位于sandstorm-http-bridge之后)
4001FerretDB v1(稳态)——MONGO_URL=mongodb://127.0.0.1:4001/wekan
4003临时 mongod 3.0(迁移只读源)

从 sandstorm-src/start.js 的实现看,实际布局与文档略有差异但保持一致:新增了NISCU_PORT = '4004'专供 niscud(niscu→3.0 阶段的数据源),以及STATE_DIR = '/var/ferretdb'(FerretDB 进程状态state.json的存放位置)。这些细节在下面的启动器分析中会展开。

3. Sandbox 兼容性(基于 sandstorm 源码验证)

文档作者检查了sandstorm/src/sandstorm/seccomp-bpf/filter.s。grain 的 seccomp 过滤器是一个白名单,其默认动作是ENOSYS而非 SIGSYS 杀进程,因此 Go/glibc 遇到缺失的系统调用时会优雅地回退。相关结论:

  • 允许(Node 24 / Go FerretDB 所需):getrandomstatfs/fstatfssched_getaffinityclonefutexepoll_*eventfd2forkmmapmprotect
  • 拒绝 → ENOSYS(通过回退容忍):membarrierrseqclone3restart_syscall

结论:grain 可以同时运行 Node 24 + FerretDB(Go)+ mongod 3.0——这与当前构建中 mongod+node 的多进程模型完全相同。无需从 sandstorm 平台仓库额外索取任何东西。

TEST(最高风险项):Node 24 的 glibc 在创建线程时使用clone3,依赖 ENOSYS→clone回退(glibc ≥ 2.34)。在投入其余工作之前,务必先验证一个最简单的 Node 24 grain 能否成功创建线程。

4.meteor-spk.deps载荷变更

以 0.6.0 为基础,然后:

  • 替换bin/nodeNode 24——使用 Meteor 3.5 dev bundle 中的精确构建(通过meteor node -e "console.log(process.version)"确认版本),确保原生插件(native addon)ABI 与 WeKan bundle 匹配。
  • 保留bin/mongod(3.0.7)——作为只读迁移数据源。
  • 保留bin/niscud(MongoDB 2.x)以及旧的捆绑node_modules/{mongodb,bson,mongodb-core,es6-promise,readable-stream}——供start.jsniscu → MongoDB 3.0阶段(Stage 1)处理非常古老的 grain。对旧版本的迁移支持是永久性的,不会被移除。(0.6.0 已同时携带二者;无需额外添加。)
  • 新增migratemongo/bin/{mongoexport,mongo}+migratemongo/lib/(这些 3.x CLI 所需的旧 glibc)。导入器用这些工具读取 3.0 数据——现代的 Node 驱动无法与 3.0/3.2 服务器对话。复用与 snap 的migratemongo(amd64、x86_64)相同的工具。
  • 新增ferretdb——直接下载ferretdb-amd64二进制(取 wekan/FerretDB 的最新发布;与release-all.yml内嵌的ferretdb-amd64资产一致)。
  • 新增migrate-mongo3-to-ferretdb.mjs——从 snap-src/bin/migrate-mongo3-to-ferretdb.mjs原样复制;该脚本完全由环境变量/路径驱动,无需任何修改。
  • 导入器(Stage 2)通过NODE_PATH=<bundle>/programs/server/node_modules从 WeKan bundle 解析其mongodb/bson(现代驱动,独立进程,与 Stage 1 使用的旧驱动不冲突)。
  • 在 Ubuntu 22.04/24.04(glibc 2.35)上为 node24 + ferretdb重新生成lib/lib64/usr/lib。将库单独保留在migratemongo/lib下,供 mongod3/mongoexport 使用(双库模式,与 snap 用MM_LIB的做法完全一致)。

0.5.1 fork 的lib/x86_64-linux-gnu/*指向/home/user/Latauket/...(下载文件夹)的悬空符号链接。不要继承这些——依赖树必须自包含,并在干净的现代基础环境上用gather-deps重新生成。

仅支持 amd64(Sandstorm 的 arm64 不在本文范围内;migratemongo 3.x 二进制是 x86_64 的)。

5. 重写后的启动器start.js

规范启动器已提交在 sandstorm-src/start.js;spk 构建会将其复制到meteor-spk.deps/start.js。它是一个 CJS 启动器,由 pkgdef 以... node start.js形式派生。首次启动时,它会针对 grain 持有的数据执行迁移链——niscu(2.x)→ MongoDB 3.0(Stage 1,保留的遗留路径,供非常古老的 grain 使用),然后MongoDB 3.0 → FerretDB(Stage 2)——之后在 FerretDB 上运行 WeKan。全新 grain 和已迁移的 grain 直接跳过迁移,直接在 FerretDB 上运行。

迁移逻辑是 snap 的migration-control+migrate-mongo3-to-ferretdb.mjs的重新定向(re-pathed)移植。所有应用载荷路径都从__dirname(grain 挂载根)解析,并且在运行导入器之前会等待 mongod 3.0 回应 ping。核心结构(精简版,完整文件见 sandstorm-src/start.js):

// meteor-spk.deps/start.js (完整文件见 sandstorm-src/start.js) const { spawn, spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const APP_PORT = process.env.PORT || '4000'; const DB_PORT = '4001'; // FerretDB 稳态端口 const SRC_PORT = '4003'; // 临时 mongod 3.0(迁移只读) const FILES_DIR = '/var/files'; const SQLITE_DIR = path.join(FILES_DIR, 'db'); const OLD_MONGO = '/var/wiredTigerDb'; const MARKER = '/var/.migration-to-ferretdb-done'; const STATUS = path.join(FILES_DIR, 'db', 'migration-status.json'); const APPROOT = __dirname; // .meteor-spk/bundle 挂载处 const MM_LIB = path.join(APPROOT, 'migratemongo/lib'); for (const d of [FILES_DIR, SQLITE_DIR, path.join(FILES_DIR, 'attachments'), path.join(FILES_DIR, 'avatars')]) { fs.mkdirSync(d, { recursive: true }); } function startFerret(port) { return spawn(path.join(APPROOT, 'ferretdb'), ['--handler=sqlite', `--sqlite-url=file:${SQLITE_DIR}/`, `--listen-addr=127.0.0.1:${port}`, '--telemetry=disable'], { stdio: 'inherit', env: { ...process.env, DO_NOT_TRACK: '1', FERRETDB_TELEMETRY: 'disable' } }); } function migrateIfNeeded() { const hasOld = fs.existsSync(path.join(OLD_MONGO, 'WiredTiger')); if (fs.existsSync(MARKER) || !hasOld) return; // 全新安装或已迁移 console.log('** Migrating MongoDB 3 -> FerretDB (one-time) ...'); const oldLibEnv = { ...process.env, LD_LIBRARY_PATH: `${MM_LIB}:${process.env.LD_LIBRARY_PATH || ''}` }; // 1. 旧 mongod 3.0 服务现有数据(只读迁移源) spawnSync('/bin/mongod', ['--dbpath', OLD_MONGO, '--bind_ip', '127.0.0.1', '--port', SRC_PORT, '--storageEngine', 'wiredTiger', '--fork', '--logpath', '/var/migration-mongod.log'], { stdio: 'inherit', env: oldLibEnv }); // 2. FerretDB 目标(永久 SQLite 目录——无需临时切换) const ferret = startFerret(DB_PORT); // 3. 导入器:CLI 读 3.0 -> 驱动写 FerretDB;GridFS -> /var/files/{attachments,avatars} const rc = spawnSync(path.join(APPROOT, 'node'), [path.join(APPROOT, 'migrate-mongo3-to-ferretdb.mjs')], { stdio: 'inherit', env: { ...process.env, NODE_PATH: path.join(APPROOT, 'programs/server/node_modules'), MONGO_BIN_DIR: path.join(APPROOT, 'migratemongo/bin'), MONGO_LIB: MM_LIB, SRC_PORT, SRC_DB: 'wekan', TARGET_MONGO_URL: `mongodb://127.0.0.1:${DB_PORT}/wekan`, FILES_DIR, MIGRATION_PORT: APP_PORT, // 进度仪表盘显示在 grain URL 上 STATUS_FILE: STATUS }, // 导入器将最终状态写入此处(见 §6) }).status; try { ferret.kill(); } catch {} spawnSync('/bin/mongo', ['--port', SRC_PORT, '--quiet', '--eval', 'db.getSiblingDB("admin").shutdownServer()'], { env: oldLibEnv }); if (rc === 0) { fs.writeFileSync(MARKER, new Date().toISOString()); // 绝不重新迁移;旧数据保留 } else { console.error('** Migration failed; will retry on next grain start. Old data kept.'); process.exit(1); } } function runApp() { const ferret = startFerret(DB_PORT); // 稳态数据库 process.env.MONGO_URL = `mongodb://127.0.0.1:${DB_PORT}/wekan`; process.env.ROOT_URL = process.env.ROOT_URL || `http://127.0.0.1:${APP_PORT}`; process.env.PORT = APP_PORT; process.env.WRITABLE_PATH = FILES_DIR; process.on('exit', () => { try { ferret.kill(); } catch {} }); setTimeout(() => require('./main.js'), 1500); // 等待 FerretDB 开始监听 } migrateIfNeeded(); runApp();

与 snap 相比的设计决策

  • 不做临时目录再切换(no temp-then-switch):迁移直接写入永久 FerretDB SQLite 目录(/var/files/db),因为在 grain 中从此只有一个数据库。
  • 幂等:标记文件阻止重复迁移;旧/var/wiredTigerDb永远不会被自动删除——由管理员在 UI 中显式删除(§7)。

实际仓库实现中的增强(相对设计文档)

对照 sandstorm-src/start.js 的完整实现,可以确认设计文档中描述的骨架已经落地,并补充了若干关键细节:

niscu → MongoDB 3.0 的 WiredTiger 缓存问题:在 grain 沙箱中 mongod 3.0 的 RAM 探测返回 0,导致其按cache_size=0G计算并触发 WiredTiger 中止("Fatal Assertion 28561" / "Value too small for key 'cache_size'")。两个迁移阶段都用--wiredTigerCacheSizeGB 1将缓存钉在 1 GB——mongod 3.0.7 将该选项解析为整数 GB(小数如 0.25 会解析失败)。它只是缓存上限而非预分配,短时迁移不会真正用到这么多,但从此不再依赖 RAM 探测。

LD_LIBRARY_PATH双库隔离oldLibEnv()migratemongo/lib(旧 glibc)前置到LD_LIBRARY_PATH,只用于 Mongo 3.x CLI 工具(mongoexport/mongo);node24/ferretdb 始终使用重新生成的新 glibc 2.35 库,互不污染。

cpu-exec特征安全启动([#6458]):当 bundle 中有bin/cpu-exec时,所有捆绑二进制都经它启动,因此落在 CPU 缺少某些特性(如 AVX 被 QEMU/KVM/Proxmox 等虚拟化屏蔽)的主机上的 grain,会透明回退到捆绑的 qemu-user,而不是以 SIGILL(退出码 132)瞬间死亡。这些二进制当前都不声明必需特性,所以 cpu-exec 目前是零开销直执行,但启动路径从此具备"特性安全"。

migration-bridge.js等待页:Sandstorm 通过/sandstorm-http-bridge把 grain 嵌入 iframe 并代理到 APP_PORT。在首次启动迁移期间以及向 WeKan 交接的窗口内,若该端口上没有任何服务,浏览器会显示 "This page can not be displayed embedded in another page"。因此 sandstorm-src/migration-bridge.js 作为一个子进程HTTP 服务器(仅用node:http,无外部依赖)占用 APP_PORT,向所有 URL 返回带自动刷新的 "please wait" 页面(HTTP 503),保证 grain 始终保持可嵌入状态。之所以必须是子进程,是因为start.jsspawnSync阻塞执行迁移,无法在进程内服务监听——杀掉子进程才能确定性地释放端口。

FerretDB 的--state-dir:即使关闭遥测,FerretDB 也会通过其 state provider 持久化state.json;其默认--state-dir.(在 grain 中是只读的/),若不指向可写目录会报 "open /state.json: read-only file system" 并导致 grain 崩溃循环。因此启动器显式传入--state-dir=/var/ferretdb并设置FERRETDB_STATE_DIR

OpLog 与轮询策略([#6503/#6480/#6481]):FerretDB v1 可以 tail OpLog,但在 SQLite 后端上 tailable+awaitData 的 tail 会让 CPU 空转保持在约 190–390%,所以默认采用仅轮询(polling-only)。通过WEKAN_FERRETDB_OPLOG=true显式启用 OpLog tail;启用后设置MONGO_OPLOG_URLMETEOR_REACTIVITY_ORDER=oplog,polling,否则删除MONGO_OPLOG_URL并把 reactivity order 设为polling([#6498]:仅仅设置了该变量就会让 Meteor 在启动时持续轮询 FerretDB 造成高 CPU)。迁移期临时 FerretDB 一律不启用 OpLog(不带--repl-set-name),以免批量插入被 oplog 记录拖慢。

Sandstorm 认证环境变量process.env.SANDSTORM = '1'必须在require('./main.js')之前设置——packages/wekan-accounts-sandstorm 只在process.env.SANDSTORM存在时才把__meteor_runtime_config__.SANDSTORM注入运行时配置(触发客户端基于 header 的自动登录);否则 WeKan 能启动但每个页面都显示 "Must be logged in"。

SRC_DB关键细节:Sandstorm WeKan grain 的数据存放在 Meteor 默认数据库meteor中(niscu→3.0 阶段读写的就是meteor,现有 grain 的 WiredTiger catalog 也显示meteor.*集合)。因此启动器给导入器传的是SRC_DB: 'meteor',只有 FerretDB目标才是wekan——若按设计文档早期版本传SRC_DB='wekan',真实 grain 会迁移到零文档。

6. 迁移导入器做了什么

migrate-mongo3-to-ferretdb.mjs(与 snap 版本一致):

  • 文本集合——逐个用mongoexport(Extended JSON)从 mongod 3.0 导出,再通过现代 Node 驱动insertMany写入 FerretDB(批次 200,冲突时 upsert)。
  • 附件 + 头像——来自 CollectionFS GridFS(cfs_gridfs.<bucket>.{files,chunks}+cfs.<bucket>.filerecord):每个文件从 chunks 重组后写入/var/files/attachments/var/files/avatars,并在 FerretDB 中插入一条指向该文件系统路径的Meteor-Files记录。这本质上就是 WeKan 的"MongoDB → 文件系统"附件迁移,在此内联完成。
  • Schema 原样保留——由 WeKan 自身的 Board Settings / Migrations 在首次使用时升级。
  • 进度通过 HTTP 在MIGRATION_PORT(即应用端口)上提供服务,迁移期间打开 grain 的用户可以看到实时仪表盘。

对照 snap-src/bin/migrate-mongo3-to-ferretdb.mjs 的实现,还能看到更多细节:

为何不能用 mongodump/mongorestore:现代 Node MongoDB 驱动无法连接 3.2 服务器,因此导入器用遗留 MongoDB CLI(mongoexport能与 3.2 对话)读取源,用现代 Node 驱动写入 FerretDB——不需要 mongodump/mongorestore,也不需要中间的 MongoDB 7。

CJS require 锚定(anchor):MongoDB 和 bson 在 WeKan bundle 中是 CommonJS;在 Node 24 的 ESM loader 下import x from 'cjs'的默认互操作会丢失 EJSON(EJSON.parse unavailable)。因此用createRequire锚定 require 解析,并构建多个根候选($SNAP/BUNDLE_ROOT/NODE_PATH/脚本父目录),按现代优先顺序遍历已知 bundle 子路径(npm-mongo嵌套的 mongodb v6 排在普通 node_modules 之前),确保绝不误用古老的 mongodb v2 驱动(旧驱动走 legacy OP_QUERY,insert 会报 "Unsupported OP_QUERY command: update")。

进度状态对象state对象包含startedAtphasecollectionsfiles(attachments/avatars 的 done/bytes)、current(正在提取的单文件实时进度,用于大附件的进度条)以及diskFree(文件卷实时剩余空间)。磁盘空间不足时会中止迁移——磁盘写满会损坏 MongoDB;中止时会回滚已部分迁移的文件以归还空间。文档建议的STATUS_FILE写入({ startedAt, finishedAt, phase, success, collections, files, errors })正是管理面板读取的状态 JSON。脚本还支持FILES_ONLY=true([#6473])做增量附件/头像修复:不动文本集合,验证并跳过已迁移文件,只补缺失的二进制与记录。

7. 管理面板 / 附件 / Sandstorm(新增,仅isSandstorm

已实现——服务端:server/methods/sandstormMigration.js;客户端:位于 client/components/settings/attachments.jade / attachments.js 的Sandstorm标签页;文案在 imports/i18n/data/en.i18n.json。导入器写入的migration-status.json(STATUS_FILE)由面板读取。

isSandstorm === trueMeteor.settings.public.sandstorm,见 models/settings.js)时,管理面板 / 附件中会出现新的Sandstorm分区(位于 Backup 旁边),展示:

  1. 迁移状态——成功 / 待处理 / 失败 + 时间戳与按集合 / 文件统计,从/var/files/db/migration-status.json和标记文件读取。
  2. 原始 MongoDB 磁盘占用——/var/wiredTigerDb下旧 MongoDB 3.0 原始文件当前占用的字节数(可回收空间),以及 FerretDB SQLite 大小和附件/头像大小作参考。
  3. 删除原始 MongoDB 文件——一个有防护的按钮,删除/var/wiredTigerDb以释放 grain 磁盘。仅当迁移成功(标记存在且success:true)且 FerretDB 确实持有数据时才启用。

服务端方法

对照 server/methods/sandstormMigration.js 的完整实现(与 server/methods/backup.js 相同的管理员门控方式:ReactiveCache.getCurrentUser()+isAdmin):

const OLD_MONGO = '/var/wiredTigerDb'; const STATUS = (process.env.WRITABLE_PATH || '/var/files') + '/db/migration-status.json'; const MARKER = '/var/.migration-to-ferretdb-done'; function dirBytes(dir) { // 递归统计大小;目录缺失时容错 let total = 0; const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else { try { total += fs.statSync(p).size; } catch {} } } }; try { if (fs.existsSync(dir)) walk(dir); } catch {} return total; } Meteor.methods({ async sandstormMigrationStatus() { const user = await ReactiveCache.getCurrentUser(); if (!user || !user.isAdmin) return false; let status = null; try { status = JSON.parse(fs.readFileSync(STATUS, 'utf8')); } catch {} return { isSandstorm: true, migrationDone: fs.existsSync(MARKER), status, rawMongoBytes: dirBytes(OLD_MONGO), rawMongoExists: fs.existsSync(OLD_MONGO), ferretBytes: dirBytes(path.dirname(STATUS)), }; }, async sandstormDeleteRawMongo() { const user = await ReactiveCache.getCurrentUser(); if (!user || !user.isAdmin) throw new Meteor.Error('not-authorized'); // 安全防护:仅在确认迁移成功之后 if (!fs.existsSync(MARKER)) throw new Meteor.Error('migration-not-done'); let ok = false; try { ok = JSON.parse(fs.readFileSync(STATUS, 'utf8')).success === true; } catch {} if (!ok) throw new Meteor.Error('migration-not-successful'); const freed = dirBytes(OLD_MONGO); fs.rmSync(OLD_MONGO, { recursive: true, force: true }); return { deleted: true, freedBytes: freed }; }, });

实际实现比设计文档还多返回了migrationSuccess(仅在记录的迁移以success:true结束时才为 true)、attachmentsBytesavatarsBytes;非 Sandstorm 环境返回{ isSandstorm: false };删除方法在非 Sandstorm 下抛not-sandstorm,并支持通过SANDSTORM_RAW_MONGO_PATHSANDSTORM_MIGRATION_MARKER等环境变量覆盖路径以便在 grain 外测试。删除原始文件的操作不可逆,其门控条件为:迁移success:true+ 显式确认 +(推荐)输入型确认。

客户端

client/components/settings/attachments.js 中新增的 tab 仅在isSandstorm时渲染:轮询sandstormMigrationStatus(类似 Backup 标签页轮询backupStatus),渲染状态 + 人类可读的大小(filesize),以及一个在确认对话框之后调用sandstormDeleteRawMongo并刷新的Delete raw MongoDB files按钮。从当前仓库快照看,该 tab 相关代码大部分以注释形式保留在 attachments.js 中(sandstormStatussandstormDeleteDisabledclick .js-sandstorm-delete-raw-mongodb等),可推断为客户端集成仍在按此设计落地中。

8. pkgdef / 环境变量变更(sandstorm-pkgdef.capnp)

  • WRITABLE_PATH/var/wekan-uploads/var/files
  • 不要environ中设置MONGO_URL(由 start.js 设为 FerretDB 端口)。
  • 递增appVersionappMarketingVersion
  • 保留argv = ["/sandstorm-http-bridge", "4000", "--", "node", "start.js"]
  • SANDSTORM=1METEOR_SETTINGS={"public":{"sandstorm":true}}已存在——驱动新管理面板的isSandstorm

对照当前 sandstorm-pkgdef.capnp 的environ,可看到迁移方案已部分落地:WRITABLE_PATH=/var/files(注释明确说明 files root 服务于 Node24/FerretDB 构建,附件/头像与 FerretDB SQLitedb/都在/var/files下)、SANDSTORM=1METEOR_SETTINGS={"public": {"sandstorm": true}}均已就位;同时还保留了DDP_TRANSPORT=sockjs——该值使 grain 通过 sockjs 走 DDP,从而允许 bundle 裁剪(releases/bundle-trim.mjs)移除全部 121 MB 的 uWebSockets.js 预编译二进制,这是让包体积符合 Sandstorm 1 GiB 限制的重要一环,tests/bundleTrim.test.cjs 将二者绑定防止漂移。注意当前 capnp 的argv仍是./start-memory.sh(旧启动路径),与设计文档目标node start.js存在差异,这属于"设计未完全落地"清单的一部分。

9. 构建 / CI(.github/workflows/sandstorm.yml)

  • 基础 = 上游 meteor-spk 0.6.0,从https://dl.sandstorm.io/meteor-spk-0.6.0.tar.xz下载。它已包含bin/mongod(3.0.7)、bin/niscud(2.x)以及 niscu→3.0 阶段所需的旧node_modules不需要旧的projects.7z(它只含 0.4.1/0.5.0/0.5.1,且其替换的 node 过于古老——反正我们要装 Node 24)。死掉的releases.wekan.team/dev/meteor-spk/projects.7z拉取被移除。
  • 装配脚本(sandstorm-src/build-deps.sh)现代化meteor-spk.deps:换入 Node 24,添加ferretdb-amd64(来自 wekan/FerretDB 发布的按架构资产),添加migratemongo/{bin,lib}mongoexport+mongo+ 旧库),复制 sandstorm-src/start.js →meteor-spk.deps/start.js、snap-src/bin/migrate-mongo3-to-ferretdb.mjs →meteor-spk.deps/保留niscud,并在 ubuntu-24.04 上用gather-deps重新生成库树。
  • 额外的二进制(Node 24、ferretdb-amd64、migratemongo CLI)从GitHub release 资产获取——releases.wekan.team已不存在,所以任何所需构建文件都放在 GitHub releases 上。

对照 .github/workflows/sandstorm.yml 的实际 CI:工作流在ubuntu-24.04上运行,需要显式放宽 unprivileged user namespaces(Ubuntu 24.04 默认kernel.apparmor_restrict_unprivileged_userns=1,会阻断 Sandstorm 安装与 spk supervisor 使用的沙箱;这也是文档中"fragile part"的出处)。装配步骤注释明确写明了"Base = upstream meteor-spk 0.6.0 ... 无依赖已退役的 releases.wekan.team 或旧 projects.7z",且build-deps.sh的 [bridge] 步骤会用/opt/sandstorm/latest/bin/sandstorm-http-bridge覆盖捆绑的古老桥接二进制,因此Sandstorm 必须先于装配步骤安装。打包阶段以meteor-spk pack生成wekan-sandstorm-YYYY_MM_DD-HH_MM_SS.spk,并警告:超过 Cloudflare 100 MB 上传上限时应通过 DNS-only 主机 / SSH 隧道安装或继续裁剪 bundle。签名密钥来自SANDSTORM_KEYRINGsecret(写入$HOME/.sandstorm-keyring)。

10. 风险与测试清单

  • TESTNode 24 在 seccomp 下的线程创建(clone3→clone ENOSYS 回退)。最高风险。
  • TESTmongod 3.0.7 在旧库环境下于 grain 内打开/var/wiredTigerDb
  • TESTFerretDB(Go)在 grain 内启动并开始监听。
  • TEST真实旧 WeKan grain 的副本上跑完整迁移(文本 + 附件 + 头像)。
  • TEST全新安装(无/var/wiredTigerDb):跳过迁移,直接在空 FerretDB 上运行。
  • TESTgrain 磁盘余量——迁移期间会同时持有旧 Mongo + 新 SQLite + 提取出的文件,需在 grain 配额内。
  • TESTLD_LIBRARY_PATH隔离——node24/ferretdb 用新的 2.35 库;mongo-3.0 CLI 用旧库;无交叉污染。
  • TEST管理面板 / 附件 / Sandstorm:状态准确性、磁盘数字、受防护的删除按钮。

11. 开放问题

  • FerretDB v1 SQLite 文件名:已确认——对于wekan库,后端写入<sqlite-dir>/wekan.sqlite(外加 SQLite WAL 伴生文件wekan.sqlite-shm/wekan.sqlite-wal)。因此对db/目录的磁盘用量求和即可覆盖全部。
  • Bundle 在 grain 中的挂载根:sandstorm-src/start.js 从__dirname解析应用载荷路径(因此bin/mongodferretdbmigratemongo/main.js都相对于启动器)。需要从打包后的 spk 确认新增二进制落在start.js期望的位置(例如migratemongo/ferretdb在挂载根、CLI 在migratemongo/bin)。
  • 旧版本(niscu 2.x 和 MongoDB 3.0)的迁移支持是永久性的——不会移除,因此niscud、mongod 3.0 和基于 CLI 的导入器会一直留在包里。

总结

这份设计方案的核心价值在于:它没有为 Sandstorm 平台"另起炉灶",而是最大化复用 WeKan 生态已有的迁移资产——migrate-mongo3-to-ferretdb.mjs导入器与migratemongo工具直接从 snap 移植,start.js采用"双库 + 幂等标记 + 失败重试"的模式,管理面板提供受防护的旧数据回收入口。当文档中标注的TEST项全部通过后,Sandstorm 上的 WeKan grain 将告别 2015 年的 Node 14 / MongoDB 3.0 组合,平稳过渡到 Meteor 3.5 / Node 24 / FerretDB(SQLite) 的现代架构,同时保持对最古老 grain(niscu 2.x 时代)的永久迁移兼容。

【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询