release: 1.3.4
This commit is contained in:
@@ -48,9 +48,9 @@ pnpm build:next
|
|||||||
|
|
||||||
## 无 Docker 安装(systemd)
|
## 无 Docker 安装(systemd)
|
||||||
|
|
||||||
安装器正式支持 **Linux x86_64(x64)**,脚本和运行时也支持在对应原生 runner 上发布 **aarch64(arm64)**;当前仓库内置 workflow 只生成 x64,arm64 需要在原生 ARM64 runner 上单独构建并发布。ARMv7/ARM32 仅实验性支持;Linux x86 32 位(`i386`、`i686`、`ia32`)明确不支持,因为 Node.js 24 和项目原生依赖没有可维护的官方构建。不要在 32 位系统上强行安装。
|
安装器正式支持 **Linux x86_64(x64,glibc)**,应用包也可以在匹配的原生 runner 上发布 **aarch64(arm64)**;当前仓库内置 workflow 只生成 x64,arm64 需要在原生 ARM64 runner 上单独构建并发布。安装器托管的 Node.js 24.20.0 仅覆盖 Node.js 官方提供的 x64/arm64 glibc 归档;musl 或 ARMv7 主机必须预先提供可用的系统 Node.js 24+,否则安装器会明确拒绝。Linux x86 32 位(`i386`、`i686`、`ia32`)明确不支持。
|
||||||
|
|
||||||
发布包必须包含 `dist/`(包括 `dist/server/cli/admin-init.js`)、生产依赖、匹配架构的 Node runtime、systemd 单元、`bin/tallynote-admin-init`、`uninstall.sh`,以及 `SHA256SUMS`。签名文件 `SHA256SUMS.sig` 是可选增强校验,不需要为普通安装准备公钥。安装器默认直接安装最新版本:
|
发布包只包含 `dist/`(包括 `dist/server/cli/admin-init.js`)、CI 在目标 Linux 架构上预编译的生产依赖、systemd 单元、`bin/tallynote-admin-init`、`uninstall.sh` 和 `SHA256SUMS`,不再携带 Node.js 二进制、源码或开发依赖。安装器检查系统 Node.js 是否为 24+;符合要求时直接复用,不符合时从 `nodejs.org` 下载并校验一次,后续应用更新不会重复下载运行时。签名文件 `SHA256SUMS.sig` 是可选增强校验,不需要为普通安装准备公钥。安装器默认直接安装最新版本:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh | sudo bash
|
curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/raw/branch/main/install.sh | sudo bash
|
||||||
@@ -115,7 +115,7 @@ tallynote installer: 查看服务状态:systemctl status tallynote.service
|
|||||||
|
|
||||||
已有安装默认拒绝降级到不高于当前版本;确需回退时显式使用 `--allow-downgrade`,正常更新不会覆盖当前或更高版本。
|
已有安装默认拒绝降级到不高于当前版本;确需回退时显式使用 `--allow-downgrade`,正常更新不会覆盖当前或更高版本。
|
||||||
|
|
||||||
安装布局为 `/opt/tallynote/releases/<version>` 加 `/opt/tallynote/current` 符号链接;切换通过临时链接和原子重命名完成。root 更新器使用前缀下独立的 `/opt/tallynote/.update-work`(`0700 root:root`)和 `.update-state` 恢复标记,不会把 root 解包工作区放进应用可写暂存目录。SQLite 数据、附件、暂存、导出和更新队列始终在外置 `/var/lib/tallynote`,不会随版本包删除。服务单元位于 `/etc/systemd/system/tallynote.service`,配置文件为 `/etc/tallynote/tallynote.env`;监听地址、端口和公开 Origin 由该环境文件控制,默认仍是 `127.0.0.1:3000`。
|
安装布局为 `/opt/tallynote/releases/<version>` 加 `/opt/tallynote/current` 符号链接;没有系统 Node.js 24+ 时,安装器会额外创建带管理标记的 `/opt/tallynote/nodejs/`。切换通过临时链接和原子重命名完成。root 更新器使用前缀下独立的 `/opt/tallynote/.update-work`(`0700 root:root`)和 `.update-state` 恢复标记,不会把 root 解包工作区放进应用可写暂存目录。SQLite 数据、附件、暂存、导出和更新队列始终在外置 `/var/lib/tallynote`,不会随版本包删除。服务单元位于 `/etc/systemd/system/tallynote.service`,配置文件为 `/etc/tallynote/tallynote.env`;监听地址、端口和公开 Origin 由该环境文件控制,默认仍是 `127.0.0.1:3000`。
|
||||||
|
|
||||||
升级有两种方式:
|
升级有两种方式:
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -1,8 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
|
export PATH
|
||||||
|
|
||||||
ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||||
NODE="$ROOT/runtime/bin/node"
|
NODE=${TALLYNOTE_NODE:-}
|
||||||
[[ -x "$NODE" ]] || NODE=$(command -v node || true)
|
node_is_usable() {
|
||||||
[[ -n "$NODE" ]] || { printf 'TallyNote: Node.js runtime not found\n' >&2; exit 127; }
|
local candidate=$1 major
|
||||||
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
major=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
||||||
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 24 ]]
|
||||||
|
}
|
||||||
|
node_is_usable "$NODE" || NODE=$(command -v node || true)
|
||||||
|
node_is_usable "$NODE" || { printf 'TallyNote: Node.js 24+ not found; run the installer again\n' >&2; exit 127; }
|
||||||
exec "$NODE" "$ROOT/dist/server/index.js" "$@"
|
exec "$NODE" "$ROOT/dist/server/index.js" "$@"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -Eeuo pipefail
|
|||||||
# Production entry point for first-admin setup. The installer keeps the
|
# Production entry point for first-admin setup. The installer keeps the
|
||||||
# EnvironmentFile root-readable only, so parse simple KEY=VALUE assignments
|
# EnvironmentFile root-readable only, so parse simple KEY=VALUE assignments
|
||||||
# without sourcing arbitrary shell code.
|
# without sourcing arbitrary shell code.
|
||||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
export PATH
|
export PATH
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -15,6 +15,13 @@ SERVICE_NAME=${TALLYNOTE_SERVICE_NAME:-tallynote.service}
|
|||||||
|
|
||||||
die() { printf 'tallynote admin-init: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'tallynote admin-init: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
node_is_usable() {
|
||||||
|
local candidate=$1 major
|
||||||
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
major=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
||||||
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 24 ]]
|
||||||
|
}
|
||||||
|
|
||||||
load_environment_file() {
|
load_environment_file() {
|
||||||
[[ -e "$CONFIG_FILE" ]] || return 0
|
[[ -e "$CONFIG_FILE" ]] || return 0
|
||||||
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] || die '环境文件不是安全的普通文件'
|
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] || die '环境文件不是安全的普通文件'
|
||||||
@@ -105,9 +112,9 @@ main() {
|
|||||||
[[ "$argument" == "--check" ]] && check_only=1
|
[[ "$argument" == "--check" ]] && check_only=1
|
||||||
done
|
done
|
||||||
root=$(resolve_release_root)
|
root=$(resolve_release_root)
|
||||||
node="$root/runtime/bin/node"
|
node=${TALLYNOTE_NODE:-}
|
||||||
[[ -x "$node" ]] || node=$(command -v node || true)
|
node_is_usable "$node" || node=$(command -v node || true)
|
||||||
[[ -n "$node" && -x "$node" ]] || die '找不到 Node.js runtime'
|
node_is_usable "$node" || die '找不到 Node.js 24+'
|
||||||
cli="$root/dist/server/cli/admin-init.js"
|
cli="$root/dist/server/cli/admin-init.js"
|
||||||
[[ -f "$cli" && ! -L "$cli" ]] || die '管理员初始化程序不存在'
|
[[ -f "$cli" && ! -L "$cli" ]] || die '管理员初始化程序不存在'
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -1,8 +1,10 @@
|
|||||||
# Release、安装与更新
|
# Release、安装与更新
|
||||||
|
|
||||||
TallyNote 的发布包必须在目标 Linux 架构上构建。`better-sqlite3`、`argon2`、`sharp` 和 Node runtime 都包含原生代码,不能在 macOS 上交叉打包后冒充 Linux。
|
TallyNote 的发布包必须在目标 Linux 架构上构建。`better-sqlite3`、`argon2` 和 `sharp` 都包含原生代码,不能在 macOS 上交叉打包后冒充 Linux。Node.js 不再进入每个发布包;安装器负责复用系统 Node.js 24+,或从 Node.js 官方 HTTPS 归档下载并校验一次。
|
||||||
|
|
||||||
正式支持:Linux x86_64/amd64;脚本和安装器也支持在原生 runner 上提供 Linux aarch64/arm64(glibc 或 musl)。当前仓库 workflow 只生成 x64,arm64 必须使用对应 runner 单独构建发布。ARMv7/ARM32 只在你拥有对应 runner 和完整依赖构建结果时实验使用。Linux x86 32 位(i386、i686、ia32)明确不支持,Node.js 24 及原生依赖没有可维护的正式构建,因此安装器会拒绝它。
|
正式支持:Linux x86_64/amd64(glibc);发布脚本也可在原生 Linux aarch64/arm64 runner 上构建对应应用包。当前仓库 workflow 只生成 x64,arm64 需要在匹配的 runner 上单独构建发布。安装器托管的 Node.js 24.20.0 仅覆盖 Node.js 官方提供的 x64/arm64 glibc 归档;musl 或 ARMv7 主机必须预先提供可用的系统 Node.js 24+,否则安装器会明确拒绝,而不会请求不存在的官方归档。Linux x86 32 位(i386、i686、ia32)明确不支持。
|
||||||
|
|
||||||
|
首次安装按 `TALLYNOTE_NODE`、系统 `node`、安装器托管运行时的顺序选择 Node.js。没有满足 24+ 的系统 Node.js 时,安装器从 `https://nodejs.org/dist/v24.20.0/` 下载匹配架构的归档和 `SHASUMS256.txt`,通过 SHA-256 校验后放入 `/opt/tallynote/nodejs/`,并把路径写入 `/etc/tallynote/tallynote.env`。发布包和应用更新都不会再次携带或下载 Node.js;卸载器只删除带 TallyNote 管理标记的运行时目录。
|
||||||
|
|
||||||
## 自动发布
|
## 自动发布
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ GITEA_TOKEN=... \
|
|||||||
./scripts/publish-gitea-release.sh v1.1.2 ./release
|
./scripts/publish-gitea-release.sh v1.1.2 ./release
|
||||||
```
|
```
|
||||||
|
|
||||||
发布资产名称必须包含当前平台,例如 `tallynote-1.1.2-linux-x64-glibc.tar.gz`。构建脚本会同时生成完整安装包和轻量更新包:`tallynote-1.1.2-linux-x64-glibc.tar.gz` 用于首次安装,`tallynote-1.1.2-linux-x64-glibc.update-<锁文件 SHA256>.tar.gz` 仅用于复用现有运行时的后台更新。同一个 Release 只保留一个 `SHA256SUMS`;有签名时再保留一个 `SHA256SUMS.sig`,签名覆盖清单完整原文。
|
发布资产名称必须包含当前平台,例如 `tallynote-1.1.2-linux-x64-glibc.tar.gz`。当前发布流程只生成这一份完整生产包:包内包含构建后的 `dist/`、目标 Linux 架构上预编译的生产 `node_modules/`、迁移文件、systemd 单元和安装/更新/卸载辅助脚本,但不包含 Node.js 二进制、源码或开发依赖。首次安装和后台应用更新都使用同一份完整包;主机上的 Node.js 24+ 由安装器一次性准备并在后续更新中复用。同一个 Release 只保留一个 `SHA256SUMS`;有签名时再保留一个 `SHA256SUMS.sig`,签名覆盖清单完整原文。
|
||||||
|
|
||||||
## curl 安装
|
## curl 安装
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ tallynote installer: 访问地址:http://127.0.0.1:<端口>
|
|||||||
```text
|
```text
|
||||||
/opt/tallynote/releases/<version>/ # 只读发布代码
|
/opt/tallynote/releases/<version>/ # 只读发布代码
|
||||||
/opt/tallynote/current -> releases/<version>
|
/opt/tallynote/current -> releases/<version>
|
||||||
|
/opt/tallynote/nodejs/ # 主机没有 Node.js 24+ 时由安装器管理
|
||||||
/opt/tallynote/.update-work/ # 0700 root:root,root 更新器临时工作区
|
/opt/tallynote/.update-work/ # 0700 root:root,root 更新器临时工作区
|
||||||
/opt/tallynote/.update-state # root 更新状态标记,异常中断后用于恢复
|
/opt/tallynote/.update-state # root 更新状态标记,异常中断后用于恢复
|
||||||
/var/lib/tallynote/ # SQLite、附件、暂存和导出
|
/var/lib/tallynote/ # SQLite、附件、暂存和导出
|
||||||
@@ -106,7 +109,7 @@ sudo /usr/local/sbin/tallynote-uninstall
|
|||||||
|
|
||||||
将环境文件中的 `TALLYNOTE_UPDATE_STRATEGY=systemd`、`TALLYNOTE_UPDATE_METADATA_URL` 和 `TALLYNOTE_UPDATE_ALLOWED_HOSTS` 配好后,后台“系统更新”会读取 Gitea 的 `/api/v1/repos/<owner>/<repo>/releases/latest`。检查结果只显示当前平台匹配且通过 SHA-256 校验的资产;如果配置了 `TALLYNOTE_UPDATE_PUBLIC_KEY_FILE` 并启用签名要求,再额外验证 Ed25519 签名。
|
将环境文件中的 `TALLYNOTE_UPDATE_STRATEGY=systemd`、`TALLYNOTE_UPDATE_METADATA_URL` 和 `TALLYNOTE_UPDATE_ALLOWED_HOSTS` 配好后,后台“系统更新”会读取 Gitea 的 `/api/v1/repos/<owner>/<repo>/releases/latest`。检查结果只显示当前平台匹配且通过 SHA-256 校验的资产;如果配置了 `TALLYNOTE_UPDATE_PUBLIC_KEY_FILE` 并启用签名要求,再额外验证 Ed25519 签名。
|
||||||
|
|
||||||
后台更新分为两个明确阶段。管理员先在“系统更新”读取最新 Release 的版本号、发布时间和更新说明,点击“下载更新包”;当前安装如果存在匹配的锁文件指纹,更新器会自动选择轻量 `update-<锁文件 SHA256>` 资产,仅下载 `dist`、迁移和版本元数据,并复用当前版本的 Node 与生产依赖;如果运行时指纹不匹配或轻量包不可用,则自动选择完整安装包。root 更新器会在主服务继续运行时下载、校验 SHA-256、解包并暂存。页面显示“下载完成,等待应用”后,管理员再点击“立即更新”。应用阶段才会短暂停止服务、备份数据、切换 release、启动并执行健康检查;页面显示重启倒计时并自动重试连接。浏览器只提交版本号、任务 ID 和确认标志,不能提交 URL 或文件路径。
|
后台更新分为两个明确阶段。管理员先在“系统更新”读取最新 Release 的版本号、发布时间和更新说明,点击“下载更新包”;更新器下载同一份完整生产包,流式校验 SHA-256、解包并暂存,成功后只显示“已下载,等待应用”,不会自动重启。管理员点击“立即更新”后才写入 root 更新请求,应用阶段才会短暂停止服务、备份数据、切换 release、启动并执行健康检查;页面显示重启倒计时并自动重试连接。浏览器只提交版本号、任务 ID 和确认标志,不能提交 URL 或文件路径。
|
||||||
|
|
||||||
Web 进程把受保护的任务文件交给 root 的 `tallynote-update.path`/`tallynote-update.service`,root runner 会重新读取配置源并验证 metadata、清单和暂存目录,不信任队列文件中的 URL 或摘要。切换失败或健康检查失败会恢复旧版本;手动回滚:
|
Web 进程把受保护的任务文件交给 root 的 `tallynote-update.path`/`tallynote-update.service`,root runner 会重新读取配置源并验证 metadata、清单和暂存目录,不信任队列文件中的 URL 或摘要。切换失败或健康检查失败会恢复旧版本;手动回滚:
|
||||||
|
|
||||||
|
|||||||
+200
-17
@@ -3,7 +3,7 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
# TallyNote native installer. Installs the latest release by default; use
|
# TallyNote native installer. Installs the latest release by default; use
|
||||||
# --dry-run to preview without changing the host.
|
# --dry-run to preview without changing the host.
|
||||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
export PATH
|
export PATH
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -34,6 +34,11 @@ MAX_TIME=${TALLYNOTE_INSTALL_MAX_TIME_SECONDS:-300}
|
|||||||
RELEASE_ALLOWED_HOSTS=${TALLYNOTE_RELEASE_ALLOWED_HOSTS:-}
|
RELEASE_ALLOWED_HOSTS=${TALLYNOTE_RELEASE_ALLOWED_HOSTS:-}
|
||||||
OPENSSL_BIN=${TALLYNOTE_OPENSSL_BIN:-openssl}
|
OPENSSL_BIN=${TALLYNOTE_OPENSSL_BIN:-openssl}
|
||||||
UNAME_BIN=${TALLYNOTE_UNAME_BIN:-uname}
|
UNAME_BIN=${TALLYNOTE_UNAME_BIN:-uname}
|
||||||
|
NODE_MIN_MAJOR=24
|
||||||
|
NODE_VERSION=${TALLYNOTE_NODE_VERSION:-24.20.0}
|
||||||
|
NODE_PATH=${TALLYNOTE_NODE:-}
|
||||||
|
NODE_INSTALL_ROOT=$PREFIX/nodejs
|
||||||
|
NODE_VERSION_DETECTED=''
|
||||||
# Service network settings are written to the systemd EnvironmentFile on a
|
# Service network settings are written to the systemd EnvironmentFile on a
|
||||||
# fresh install. Existing values are preserved unless the corresponding
|
# fresh install. Existing values are preserved unless the corresponding
|
||||||
# TALLYNOTE_* variable is explicitly supplied to the installer.
|
# TALLYNOTE_* variable is explicitly supplied to the installer.
|
||||||
@@ -57,6 +62,7 @@ INSTALL_PREVIOUS_TARGET=''
|
|||||||
INSTALL_NEW_RELEASE=''
|
INSTALL_NEW_RELEASE=''
|
||||||
INSTALL_WORK_DIR=''
|
INSTALL_WORK_DIR=''
|
||||||
INSTALL_BACKUP_DIR=''
|
INSTALL_BACKUP_DIR=''
|
||||||
|
INSTALL_UNIT_TMP=''
|
||||||
INSTALL_BACKUP_COMPLETE=0
|
INSTALL_BACKUP_COMPLETE=0
|
||||||
INSTALL_WAS_ACTIVE=0
|
INSTALL_WAS_ACTIVE=0
|
||||||
INSTALL_PATH_WAS_ACTIVE=0
|
INSTALL_PATH_WAS_ACTIVE=0
|
||||||
@@ -65,6 +71,12 @@ INSTALL_WAS_ENABLED=0
|
|||||||
INSTALL_PATH_WAS_ENABLED=0
|
INSTALL_PATH_WAS_ENABLED=0
|
||||||
INSTALL_UPDATE_WAS_ENABLED=0
|
INSTALL_UPDATE_WAS_ENABLED=0
|
||||||
INSTALL_SYSTEMD_TOUCHED=0
|
INSTALL_SYSTEMD_TOUCHED=0
|
||||||
|
INSTALL_NODE_CREATED=0
|
||||||
|
INSTALL_NODE_TARGET=''
|
||||||
|
INSTALL_NODE_MARKER_CREATED=0
|
||||||
|
INSTALL_NODE_MARKER=''
|
||||||
|
INSTALL_NODE_ROOT_CREATED=0
|
||||||
|
NODE_INSTALL_TMP=''
|
||||||
ADMIN_INIT_PATH=/usr/local/sbin/tallynote-admin-init
|
ADMIN_INIT_PATH=/usr/local/sbin/tallynote-admin-init
|
||||||
INSTALL_FIRST_INSTALL=0
|
INSTALL_FIRST_INSTALL=0
|
||||||
DATA_DIR_TEMP_ROOT=0
|
DATA_DIR_TEMP_ROOT=0
|
||||||
@@ -402,6 +414,7 @@ configure_network_interactively() {
|
|||||||
[[ "$REQUIRE_SIGNATURE" == true || "$REQUIRE_SIGNATURE" == false ]] || die 'TALLYNOTE_INSTALL_REQUIRE_SIGNATURE 必须是 true 或 false'
|
[[ "$REQUIRE_SIGNATURE" == true || "$REQUIRE_SIGNATURE" == false ]] || die 'TALLYNOTE_INSTALL_REQUIRE_SIGNATURE 必须是 true 或 false'
|
||||||
[[ "$ALLOW_DOWNGRADE" == true || "$ALLOW_DOWNGRADE" == false ]] || die 'TALLYNOTE_ALLOW_DOWNGRADE 必须是 true 或 false'
|
[[ "$ALLOW_DOWNGRADE" == true || "$ALLOW_DOWNGRADE" == false ]] || die 'TALLYNOTE_ALLOW_DOWNGRADE 必须是 true 或 false'
|
||||||
[[ "$SIGNATURE_FORMAT" == ed25519 || "$SIGNATURE_FORMAT" == gpg ]] || die '签名格式必须是 ed25519 或 gpg'
|
[[ "$SIGNATURE_FORMAT" == ed25519 || "$SIGNATURE_FORMAT" == gpg ]] || die '签名格式必须是 ed25519 或 gpg'
|
||||||
|
[[ "$NODE_VERSION" =~ ^24\.[0-9]+\.[0-9]+$ ]] || die 'TALLYNOTE_NODE_VERSION 必须是 24.x.y 版本号'
|
||||||
[[ "$MAX_RELEASE_MB" =~ ^[1-9][0-9]*$ && "$MAX_EXTRACT_MB" =~ ^[1-9][0-9]*$ && "$MAX_ARCHIVE_ENTRIES" =~ ^[1-9][0-9]*$ ]] || die '安装资源限制必须是正整数'
|
[[ "$MAX_RELEASE_MB" =~ ^[1-9][0-9]*$ && "$MAX_EXTRACT_MB" =~ ^[1-9][0-9]*$ && "$MAX_ARCHIVE_ENTRIES" =~ ^[1-9][0-9]*$ ]] || die '安装资源限制必须是正整数'
|
||||||
[[ "$CONNECT_TIMEOUT" =~ ^[1-9][0-9]*$ && "$MAX_TIME" =~ ^[1-9][0-9]*$ ]] || die '安装超时配置必须是正整数'
|
[[ "$CONNECT_TIMEOUT" =~ ^[1-9][0-9]*$ && "$MAX_TIME" =~ ^[1-9][0-9]*$ ]] || die '安装超时配置必须是正整数'
|
||||||
|
|
||||||
@@ -461,6 +474,146 @@ detect_platform() {
|
|||||||
export TALLYNOTE_ARCH TALLYNOTE_LIBC
|
export TALLYNOTE_ARCH TALLYNOTE_LIBC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node_major_version() {
|
||||||
|
local candidate=$1 value
|
||||||
|
[[ -x "$candidate" ]] || return 1
|
||||||
|
value=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
||||||
|
[[ "$value" =~ ^[0-9]+$ ]] || return 1
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
node_is_legacy_embedded() {
|
||||||
|
local candidate=$1 resolved
|
||||||
|
[[ -n "$candidate" ]] || return 1
|
||||||
|
resolved=$(readlink -f -- "$candidate" 2>/dev/null || realpath "$candidate" 2>/dev/null || printf '%s' "$candidate")
|
||||||
|
[[ "$resolved" == "$PREFIX/current/runtime/"* || "$resolved" == "$PREFIX/releases/"*/runtime/* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
node_is_usable() {
|
||||||
|
local candidate=$1 major resolved uid mode_bits
|
||||||
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
resolved=$(readlink -f -- "$candidate" 2>/dev/null || realpath "$candidate" 2>/dev/null || printf '%s' "$candidate")
|
||||||
|
[[ -x "$resolved" ]] || return 1
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
uid=$(stat_uid "$resolved")
|
||||||
|
mode_bits=$(stat_mode_bits "$resolved")
|
||||||
|
[[ "$uid" == 0 && $((mode_bits & 18)) -eq 0 ]] || return 1
|
||||||
|
fi
|
||||||
|
major=$(node_major_version "$candidate") || return 1
|
||||||
|
(( major >= NODE_MIN_MAJOR )) || return 1
|
||||||
|
NODE_VERSION_DETECTED=$("$candidate" -p 'process.versions.node' 2>/dev/null || true)
|
||||||
|
[[ -n "$NODE_VERSION_DETECTED" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
node_archive_name() {
|
||||||
|
local platform
|
||||||
|
case "${TALLYNOTE_LIBC}:${TALLYNOTE_ARCH}" in
|
||||||
|
glibc:x64) platform=linux-x64 ;;
|
||||||
|
glibc:arm64) platform=linux-arm64 ;;
|
||||||
|
*) die "Node.js 官方未提供当前平台的 ${NODE_MIN_MAJOR}+ 归档(${TALLYNOTE_ARCH}/${TALLYNOTE_LIBC});请先安装可用的系统 Node.js 24+ 后重试" ;;
|
||||||
|
esac
|
||||||
|
printf 'node-v%s-%s.tar.xz' "$NODE_VERSION" "$platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_managed_node() {
|
||||||
|
local archive checksum archive_name expected actual tmp extracted target marker
|
||||||
|
archive_name=$(node_archive_name)
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
NODE_INSTALL_TMP=$tmp
|
||||||
|
archive="$tmp/$archive_name"
|
||||||
|
checksum="$tmp/SHASUMS256.txt"
|
||||||
|
stage "系统未找到 Node.js ${NODE_MIN_MAJOR}+,下载官方运行时 ${NODE_VERSION}"
|
||||||
|
append_allowed_host nodejs.org
|
||||||
|
download "https://nodejs.org/dist/v${NODE_VERSION}/${archive_name}" "$archive" $((256 * 1024 * 1024))
|
||||||
|
download "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" "$checksum" $((4 * 1024 * 1024))
|
||||||
|
expected=$(awk -v name="$archive_name" '$2 == name { print $1; exit }' "$checksum")
|
||||||
|
[[ "$expected" =~ ^[A-Fa-f0-9]{64}$ ]] || die 'Node.js 官方校验清单中没有匹配归档'
|
||||||
|
actual=$(sha256sum "$archive" | awk '{print $1}')
|
||||||
|
[[ "${actual,,}" == "${expected,,}" ]] || die 'Node.js 官方归档 SHA-256 校验失败'
|
||||||
|
if [[ ! -e "$NODE_INSTALL_ROOT" && ! -L "$NODE_INSTALL_ROOT" ]]; then
|
||||||
|
INSTALL_NODE_ROOT_CREATED=1
|
||||||
|
fi
|
||||||
|
ensure_root_directory "$NODE_INSTALL_ROOT" 755
|
||||||
|
tar -xJf "$archive" -C "$tmp"
|
||||||
|
extracted="$tmp/${archive_name%.tar.xz}"
|
||||||
|
[[ -d "$extracted" && -x "$extracted/bin/node" ]] || die 'Node.js 官方归档结构无效'
|
||||||
|
target="$NODE_INSTALL_ROOT/${archive_name%.tar.xz}"
|
||||||
|
[[ ! -e "$target" && ! -L "$target" ]] || die "Node.js 目标目录已存在:$target"
|
||||||
|
mv -- "$extracted" "$target"
|
||||||
|
INSTALL_NODE_CREATED=1
|
||||||
|
INSTALL_NODE_TARGET=$target
|
||||||
|
marker="$NODE_INSTALL_ROOT/.tallynote-managed"
|
||||||
|
if [[ -e "$marker" || -L "$marker" ]]; then
|
||||||
|
[[ -f "$marker" && ! -L "$marker" && "$(sed -n '1p' "$marker" 2>/dev/null)" == tallynote-managed-node-v1 ]] || die 'Node.js 管理目录标记无效'
|
||||||
|
else
|
||||||
|
printf 'tallynote-managed-node-v1\n' > "$marker"
|
||||||
|
chmod 600 "$marker"
|
||||||
|
INSTALL_NODE_MARKER_CREATED=1
|
||||||
|
INSTALL_NODE_MARKER=$marker
|
||||||
|
fi
|
||||||
|
chown -R root:root "$target"
|
||||||
|
chown root:root "$marker"
|
||||||
|
NODE_PATH="$target/bin/node"
|
||||||
|
rm -rf -- "$tmp"
|
||||||
|
NODE_INSTALL_TMP=''
|
||||||
|
node_is_usable "$NODE_PATH" || die '已安装的 Node.js 运行时无法通过版本检查'
|
||||||
|
stage_done "Node.js ${NODE_VERSION_DETECTED} 已安装并记录为共享运行时"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_node_install_if_needed() {
|
||||||
|
local result=$? marker
|
||||||
|
if [[ -n "$NODE_INSTALL_TMP" && -d "$NODE_INSTALL_TMP" ]]; then
|
||||||
|
rm -rf -- "$NODE_INSTALL_TMP" 2>/dev/null || true
|
||||||
|
NODE_INSTALL_TMP=''
|
||||||
|
fi
|
||||||
|
if (( INSTALL_COMMITTED == 0 && INSTALL_NODE_CREATED == 1 )); then
|
||||||
|
if [[ -n "$INSTALL_NODE_TARGET" && -d "$INSTALL_NODE_TARGET" && ! -L "$INSTALL_NODE_TARGET" ]]; then
|
||||||
|
rm -rf -- "$INSTALL_NODE_TARGET" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
marker=$INSTALL_NODE_MARKER
|
||||||
|
if (( INSTALL_NODE_MARKER_CREATED == 1 )) && [[ -n "$marker" && -f "$marker" && ! -L "$marker" ]]; then
|
||||||
|
rm -f -- "$marker" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if (( INSTALL_NODE_ROOT_CREATED == 1 )) && [[ -d "$NODE_INSTALL_ROOT" && ! -L "$NODE_INSTALL_ROOT" ]]; then
|
||||||
|
rmdir -- "$NODE_INSTALL_ROOT" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
INSTALL_NODE_CREATED=0
|
||||||
|
fi
|
||||||
|
return "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_node_runtime() {
|
||||||
|
local candidate='' managed_node marker
|
||||||
|
[[ "$NODE_INSTALL_ROOT" == "$PREFIX"/* ]] || die 'Node.js 管理目录必须位于 TallyNote 安装目录内'
|
||||||
|
if [[ -n "$NODE_PATH" ]] && ! node_is_legacy_embedded "$NODE_PATH" && node_is_usable "$NODE_PATH"; then
|
||||||
|
stage_done "复用已配置的 Node.js ${NODE_VERSION_DETECTED}:$NODE_PATH"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
candidate=$(command -v node || true)
|
||||||
|
if [[ -n "$candidate" ]] && node_is_usable "$candidate"; then
|
||||||
|
NODE_PATH=$candidate
|
||||||
|
stage_done "复用系统 Node.js ${NODE_VERSION_DETECTED}:$NODE_PATH"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
marker="$NODE_INSTALL_ROOT/.tallynote-managed"
|
||||||
|
if [[ -f "$marker" && ! -L "$marker" && "$(sed -n '1p' "$marker" 2>/dev/null)" == tallynote-managed-node-v1 ]]; then
|
||||||
|
while IFS= read -r managed_node; do
|
||||||
|
[[ -n "$managed_node" ]] || continue
|
||||||
|
if node_is_usable "$managed_node"; then
|
||||||
|
NODE_PATH=$managed_node
|
||||||
|
stage_done "复用已安装的 Node.js ${NODE_VERSION_DETECTED}:$NODE_PATH"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(find "$NODE_INSTALL_ROOT" -mindepth 3 -maxdepth 3 -type f -path '*/bin/node' -print 2>/dev/null | sort -V -r)
|
||||||
|
fi
|
||||||
|
if (( ! APPLY )); then
|
||||||
|
log "dry-run: 当前主机需要 Node.js ${NODE_MIN_MAJOR}+;正式安装时将从 nodejs.org 下载并校验"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[[ $EUID -eq 0 ]] || die '安装 Node.js 运行时必须以 root 运行'
|
||||||
|
install_managed_node
|
||||||
|
}
|
||||||
|
|
||||||
require_https() {
|
require_https() {
|
||||||
local value=$1
|
local value=$1
|
||||||
case "$value" in https://*) ;; *) die "release endpoints must use HTTPS: $value" ;; esac
|
case "$value" in https://*) ;; *) die "release endpoints must use HTTPS: $value" ;; esac
|
||||||
@@ -711,7 +864,7 @@ normalize_release_tree() {
|
|||||||
fi
|
fi
|
||||||
find "$root" -type d -exec chmod 755 {} +
|
find "$root" -type d -exec chmod 755 {} +
|
||||||
find "$root" -type f -exec chmod 644 {} +
|
find "$root" -type f -exec chmod 644 {} +
|
||||||
for item in "$root/bin"/* "$root/scripts"/*.sh "$root/runtime/bin"/* "$root/uninstall.sh"; do
|
for item in "$root/bin"/* "$root/scripts"/*.sh "$root/uninstall.sh"; do
|
||||||
[[ -f "$item" && ! -L "$item" ]] || continue
|
[[ -f "$item" && ! -L "$item" ]] || continue
|
||||||
chmod 755 "$item"
|
chmod 755 "$item"
|
||||||
done
|
done
|
||||||
@@ -868,6 +1021,10 @@ stop_existing_services() {
|
|||||||
|
|
||||||
rollback_install_if_needed() {
|
rollback_install_if_needed() {
|
||||||
local result=$? rollback_tmp
|
local result=$? rollback_tmp
|
||||||
|
# This helper intentionally returns the original exit status when used as
|
||||||
|
# the early EXIT trap. Once called from this rollback trap, swallow that
|
||||||
|
# status so errexit cannot skip restoration of the previous installation.
|
||||||
|
cleanup_node_install_if_needed || true
|
||||||
if (( INSTALL_COMMITTED == 0 && INSTALL_SYSTEMD_TOUCHED == 1 )) && command -v systemctl >/dev/null 2>&1; then
|
if (( INSTALL_COMMITTED == 0 && INSTALL_SYSTEMD_TOUCHED == 1 )) && command -v systemctl >/dev/null 2>&1; then
|
||||||
# The failed install may have started units that were inactive before the
|
# The failed install may have started units that were inactive before the
|
||||||
# attempt. Stop them before restoring files so systemd never keeps running
|
# attempt. Stop them before restoring files so systemd never keeps running
|
||||||
@@ -928,6 +1085,10 @@ rollback_install_if_needed() {
|
|||||||
if [[ -n "$INSTALL_WORK_DIR" && -d "$INSTALL_WORK_DIR" ]]; then
|
if [[ -n "$INSTALL_WORK_DIR" && -d "$INSTALL_WORK_DIR" ]]; then
|
||||||
rm -rf -- "$INSTALL_WORK_DIR" 2>/dev/null || true
|
rm -rf -- "$INSTALL_WORK_DIR" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$INSTALL_UNIT_TMP" && -d "$INSTALL_UNIT_TMP" && ! -L "$INSTALL_UNIT_TMP" ]]; then
|
||||||
|
rm -rf -- "$INSTALL_UNIT_TMP" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
INSTALL_UNIT_TMP=''
|
||||||
return "$result"
|
return "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,7 +1238,7 @@ validate_existing_env() {
|
|||||||
mode_bits=$(stat_mode_bits "$file")
|
mode_bits=$(stat_mode_bits "$file")
|
||||||
(( (mode_bits & 18) == 0 )) || die '环境文件不能被组或其他用户写入'
|
(( (mode_bits & 18) == 0 )) || die '环境文件不能被组或其他用户写入'
|
||||||
local key key_count
|
local key key_count
|
||||||
for key in TALLYNOTE_INSTALL_PREFIX TALLYNOTE_DATA_DIR TALLYNOTE_HOST TALLYNOTE_PORT TALLYNOTE_PUBLIC_ORIGIN TALLYNOTE_ALLOWED_ORIGINS TALLYNOTE_ALLOW_INSECURE_HTTP TALLYNOTE_COOKIE_SECURE TALLYNOTE_UPDATE_REQUIRE_SIGNATURE TALLYNOTE_UPDATE_METADATA_URL TALLYNOTE_UPDATE_ALLOWED_HOSTS TALLYNOTE_UPDATE_PUBLIC_KEY_FILE; do
|
for key in TALLYNOTE_INSTALL_PREFIX TALLYNOTE_DATA_DIR TALLYNOTE_CONFIG_DIR TALLYNOTE_NODE TALLYNOTE_HOST TALLYNOTE_PORT TALLYNOTE_PUBLIC_ORIGIN TALLYNOTE_ALLOWED_ORIGINS TALLYNOTE_ALLOW_INSECURE_HTTP TALLYNOTE_COOKIE_SECURE TALLYNOTE_UPDATE_REQUIRE_SIGNATURE TALLYNOTE_UPDATE_METADATA_URL TALLYNOTE_UPDATE_ALLOWED_HOSTS TALLYNOTE_UPDATE_PUBLIC_KEY_FILE; do
|
||||||
key_count=$(env_key_count "$file" "$key")
|
key_count=$(env_key_count "$file" "$key")
|
||||||
[[ "$key_count" =~ ^[0-9]+$ && "$key_count" -le 1 ]] || die "环境文件包含重复配置:$key"
|
[[ "$key_count" =~ ^[0-9]+$ && "$key_count" -le 1 ]] || die "环境文件包含重复配置:$key"
|
||||||
done
|
done
|
||||||
@@ -1085,6 +1246,12 @@ validate_existing_env() {
|
|||||||
[[ -z "$value" || "${value%/}" == "${PREFIX%/}" ]] || die '环境文件中的安装目录与本次安装不一致'
|
[[ -z "$value" || "${value%/}" == "${PREFIX%/}" ]] || die '环境文件中的安装目录与本次安装不一致'
|
||||||
value=$(read_env_value "$file" TALLYNOTE_DATA_DIR)
|
value=$(read_env_value "$file" TALLYNOTE_DATA_DIR)
|
||||||
[[ -z "$value" || "${value%/}" == "${DATA_DIR%/}" ]] || die '环境文件中的数据目录与本次安装不一致'
|
[[ -z "$value" || "${value%/}" == "${DATA_DIR%/}" ]] || die '环境文件中的数据目录与本次安装不一致'
|
||||||
|
value=$(read_env_value "$file" TALLYNOTE_CONFIG_DIR)
|
||||||
|
[[ -z "$value" || "${value%/}" == "${CONFIG_DIR%/}" ]] || die '环境文件中的配置目录与本次安装不一致'
|
||||||
|
value=$(read_env_value "$file" TALLYNOTE_NODE)
|
||||||
|
if [[ -n "$value" ]]; then
|
||||||
|
validate_env_value "$value" '环境文件中的 Node.js 路径'
|
||||||
|
fi
|
||||||
value=$(read_env_value "$file" TALLYNOTE_UPDATE_REQUIRE_SIGNATURE)
|
value=$(read_env_value "$file" TALLYNOTE_UPDATE_REQUIRE_SIGNATURE)
|
||||||
[[ -z "$value" || "$value" == true || "$value" == false ]] || die '环境文件中的签名校验配置必须是 true 或 false'
|
[[ -z "$value" || "$value" == true || "$value" == false ]] || die '环境文件中的签名校验配置必须是 true 或 false'
|
||||||
if (( $(env_key_count "$file" TALLYNOTE_HOST) )); then
|
if (( $(env_key_count "$file" TALLYNOTE_HOST) )); then
|
||||||
@@ -1161,6 +1328,7 @@ install_release() {
|
|||||||
safe_extract "$archive" "$tmp/unpacked"
|
safe_extract "$archive" "$tmp/unpacked"
|
||||||
normalize_release_tree "$tmp/unpacked"
|
normalize_release_tree "$tmp/unpacked"
|
||||||
[[ -d "$tmp/unpacked/dist" ]] || die 'release archive must contain dist/ at its root'
|
[[ -d "$tmp/unpacked/dist" ]] || die 'release archive must contain dist/ at its root'
|
||||||
|
[[ ! -e "$tmp/unpacked/runtime" ]] || die 'release archive must not contain an embedded Node.js runtime'
|
||||||
[[ -x "$tmp/unpacked/bin/tallynote" ]] || die 'release archive must contain executable bin/tallynote'
|
[[ -x "$tmp/unpacked/bin/tallynote" ]] || die 'release archive must contain executable bin/tallynote'
|
||||||
[[ -f "$tmp/unpacked/package.json" && -f "$tmp/unpacked/dist/server/index.js" && -f "$tmp/unpacked/dist/server/cli/admin-init.js" && -f "$tmp/unpacked/dist/web/index.html" ]] || die 'release archive is incomplete'
|
[[ -f "$tmp/unpacked/package.json" && -f "$tmp/unpacked/dist/server/index.js" && -f "$tmp/unpacked/dist/server/cli/admin-init.js" && -f "$tmp/unpacked/dist/web/index.html" ]] || die 'release archive is incomplete'
|
||||||
[[ -f "$tmp/unpacked/systemd/tallynote.service" && -f "$tmp/unpacked/systemd/tallynote-update.service" && -f "$tmp/unpacked/systemd/tallynote-update.path" ]] || die 'release archive is missing systemd units'
|
[[ -f "$tmp/unpacked/systemd/tallynote.service" && -f "$tmp/unpacked/systemd/tallynote-update.service" && -f "$tmp/unpacked/systemd/tallynote-update.path" ]] || die 'release archive is missing systemd units'
|
||||||
@@ -1224,6 +1392,9 @@ main() {
|
|||||||
fi
|
fi
|
||||||
detect_platform
|
detect_platform
|
||||||
configure_network_interactively
|
configure_network_interactively
|
||||||
|
if [[ -z "$NODE_PATH" && -f "$CONFIG_DIR/tallynote.env" ]]; then
|
||||||
|
NODE_PATH=$(read_env_value "$CONFIG_DIR/tallynote.env" TALLYNOTE_NODE 2>/dev/null || true)
|
||||||
|
fi
|
||||||
validate_listen_host "$INSTALL_HOST"
|
validate_listen_host "$INSTALL_HOST"
|
||||||
validate_listen_port "$INSTALL_PORT"
|
validate_listen_port "$INSTALL_PORT"
|
||||||
if (( APPLY && NETWORK_INTERACTIVE )); then
|
if (( APPLY && NETWORK_INTERACTIVE )); then
|
||||||
@@ -1310,6 +1481,11 @@ main() {
|
|||||||
for command_name in curl sha256sum tar install sed awk find systemctl; do
|
for command_name in curl sha256sum tar install sed awk find systemctl; do
|
||||||
command -v "$command_name" >/dev/null 2>&1 || die "$command_name is required"
|
command -v "$command_name" >/dev/null 2>&1 || die "$command_name is required"
|
||||||
done
|
done
|
||||||
|
# Install the cleanup trap before downloading a managed Node.js runtime. A
|
||||||
|
# failed runtime download or extraction must not leave a partial toolchain
|
||||||
|
# behind even when release acquisition has not started yet.
|
||||||
|
trap cleanup_node_install_if_needed EXIT
|
||||||
|
ensure_node_runtime
|
||||||
if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" || -n "$UPDATE_PUBLIC_KEY_FILE" ]]; then
|
if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" || -n "$UPDATE_PUBLIC_KEY_FILE" ]]; then
|
||||||
command -v "$OPENSSL_BIN" >/dev/null 2>&1 || die 'openssl is required when signature verification is enabled'
|
command -v "$OPENSSL_BIN" >/dev/null 2>&1 || die 'openssl is required when signature verification is enabled'
|
||||||
fi
|
fi
|
||||||
@@ -1394,24 +1570,26 @@ main() {
|
|||||||
[[ -f "$release_dir/systemd/tallynote.env.example" && -f "$release_dir/scripts/tallynote-update-runner.sh" && -x "$release_dir/uninstall.sh" && -x "$release_dir/bin/tallynote-admin-init" && -f "$release_dir/dist/server/cli/admin-init.js" ]] || die 'release package is missing update/uninstall/admin-init support files'
|
[[ -f "$release_dir/systemd/tallynote.env.example" && -f "$release_dir/scripts/tallynote-update-runner.sh" && -x "$release_dir/uninstall.sh" && -x "$release_dir/bin/tallynote-admin-init" && -f "$release_dir/dist/server/cli/admin-init.js" ]] || die 'release package is missing update/uninstall/admin-init support files'
|
||||||
stage '安装 systemd 单元、更新辅助程序和卸载器'
|
stage '安装 systemd 单元、更新辅助程序和卸载器'
|
||||||
install -d -m 755 /usr/local/sbin /usr/local/libexec /etc/systemd/system
|
install -d -m 755 /usr/local/sbin /usr/local/libexec /etc/systemd/system
|
||||||
local unit_tmp
|
INSTALL_UNIT_TMP=$(mktemp -d)
|
||||||
unit_tmp=$(mktemp -d)
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/systemd/tallynote.service" > "$INSTALL_UNIT_TMP/tallynote.service"
|
||||||
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g" "$release_dir/systemd/tallynote.service" > "$unit_tmp/tallynote.service"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote-backups#$(dirname -- "$DATA_DIR")/tallynote-backups#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/systemd/tallynote-update.service" > "$INSTALL_UNIT_TMP/tallynote-update.service"
|
||||||
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/var/lib/tallynote-backups#$(dirname -- "$DATA_DIR")/tallynote-backups#g" "$release_dir/systemd/tallynote-update.service" > "$unit_tmp/tallynote-update.service"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g" "$release_dir/systemd/tallynote-update.path" > "$INSTALL_UNIT_TMP/tallynote-update.path"
|
||||||
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g" "$release_dir/systemd/tallynote-update.path" > "$unit_tmp/tallynote-update.path"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/bin/tallynote-admin-init" > "$INSTALL_UNIT_TMP/tallynote-admin-init"
|
||||||
sed "s#/opt/tallynote#$PREFIX#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/bin/tallynote-admin-init" > "$unit_tmp/tallynote-admin-init"
|
install -o root -g root -m 644 "$INSTALL_UNIT_TMP/tallynote.service" /etc/systemd/system/tallynote.service
|
||||||
install -o root -g root -m 644 "$unit_tmp/tallynote.service" /etc/systemd/system/tallynote.service
|
install -o root -g root -m 644 "$INSTALL_UNIT_TMP/tallynote-update.service" /etc/systemd/system/tallynote-update.service
|
||||||
install -o root -g root -m 644 "$unit_tmp/tallynote-update.service" /etc/systemd/system/tallynote-update.service
|
install -o root -g root -m 644 "$INSTALL_UNIT_TMP/tallynote-update.path" /etc/systemd/system/tallynote-update.path
|
||||||
install -o root -g root -m 644 "$unit_tmp/tallynote-update.path" /etc/systemd/system/tallynote-update.path
|
install -o root -g root -m 755 "$INSTALL_UNIT_TMP/tallynote-admin-init" "$ADMIN_INIT_PATH"
|
||||||
install -o root -g root -m 755 "$unit_tmp/tallynote-admin-init" "$ADMIN_INIT_PATH"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/scripts/tallynote-update.sh" > "$INSTALL_UNIT_TMP/tallynote-update.sh"
|
||||||
rm -rf "$unit_tmp"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/scripts/tallynote-update-runner.sh" > "$INSTALL_UNIT_TMP/tallynote-update-runner.sh"
|
||||||
install -o root -g root -m 755 "$release_dir/scripts/tallynote-update.sh" /usr/local/sbin/tallynote-update
|
install -o root -g root -m 755 "$INSTALL_UNIT_TMP/tallynote-update.sh" /usr/local/sbin/tallynote-update
|
||||||
install -o root -g root -m 755 "$release_dir/scripts/tallynote-update-runner.sh" /usr/local/libexec/tallynote-update-runner
|
install -o root -g root -m 755 "$INSTALL_UNIT_TMP/tallynote-update-runner.sh" /usr/local/libexec/tallynote-update-runner
|
||||||
|
rm -rf -- "$INSTALL_UNIT_TMP"
|
||||||
|
INSTALL_UNIT_TMP=''
|
||||||
install -o root -g root -m 755 "$release_dir/uninstall.sh" /usr/local/sbin/tallynote-uninstall
|
install -o root -g root -m 755 "$release_dir/uninstall.sh" /usr/local/sbin/tallynote-uninstall
|
||||||
ensure_root_directory "$(dirname -- "$DATA_DIR")/tallynote-backups" 700
|
ensure_root_directory "$(dirname -- "$DATA_DIR")/tallynote-backups" 700
|
||||||
local env_created=0
|
local env_created=0
|
||||||
if [[ ! -f "$CONFIG_DIR/tallynote.env" ]]; then
|
if [[ ! -f "$CONFIG_DIR/tallynote.env" ]]; then
|
||||||
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g" "$release_dir/systemd/tallynote.env.example" > "$CONFIG_DIR/tallynote.env"
|
sed "s#/opt/tallynote#$PREFIX#g; s#/var/lib/tallynote#$DATA_DIR#g; s#/etc/tallynote#$CONFIG_DIR#g" "$release_dir/systemd/tallynote.env.example" > "$CONFIG_DIR/tallynote.env"
|
||||||
chown root:root "$CONFIG_DIR/tallynote.env"
|
chown root:root "$CONFIG_DIR/tallynote.env"
|
||||||
chmod 640 "$CONFIG_DIR/tallynote.env"
|
chmod 640 "$CONFIG_DIR/tallynote.env"
|
||||||
env_created=1
|
env_created=1
|
||||||
@@ -1463,6 +1641,11 @@ main() {
|
|||||||
if [[ -n "${TALLYNOTE_ALLOW_INSECURE_HTTP+x}" ]]; then set_env_key TALLYNOTE_ALLOW_INSECURE_HTTP "$INSTALL_ALLOW_INSECURE_HTTP"; fi
|
if [[ -n "${TALLYNOTE_ALLOW_INSECURE_HTTP+x}" ]]; then set_env_key TALLYNOTE_ALLOW_INSECURE_HTTP "$INSTALL_ALLOW_INSECURE_HTTP"; fi
|
||||||
ensure_env_key TALLYNOTE_INSTALL_PREFIX "$PREFIX"
|
ensure_env_key TALLYNOTE_INSTALL_PREFIX "$PREFIX"
|
||||||
ensure_env_key TALLYNOTE_DATA_DIR "$DATA_DIR"
|
ensure_env_key TALLYNOTE_DATA_DIR "$DATA_DIR"
|
||||||
|
ensure_env_key TALLYNOTE_CONFIG_DIR "$CONFIG_DIR"
|
||||||
|
configured_node=$(read_env_value "$CONFIG_DIR/tallynote.env" TALLYNOTE_NODE 2>/dev/null || true)
|
||||||
|
if [[ -z "$configured_node" ]] || node_is_legacy_embedded "$configured_node" || ! node_is_usable "$configured_node"; then
|
||||||
|
set_env_key TALLYNOTE_NODE "$NODE_PATH"
|
||||||
|
fi
|
||||||
ensure_env_key TALLYNOTE_UPDATE_STRATEGY systemd
|
ensure_env_key TALLYNOTE_UPDATE_STRATEGY systemd
|
||||||
ensure_env_key TALLYNOTE_UPDATE_METADATA_URL "$RELEASE_API_URL"
|
ensure_env_key TALLYNOTE_UPDATE_METADATA_URL "$RELEASE_API_URL"
|
||||||
ensure_env_key TALLYNOTE_UPDATE_ALLOWED_HOSTS "$RELEASE_ALLOWED_HOSTS"
|
ensure_env_key TALLYNOTE_UPDATE_ALLOWED_HOSTS "$RELEASE_ALLOWED_HOSTS"
|
||||||
|
|||||||
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tallynote",
|
"name": "tallynote",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
@@ -29,30 +29,21 @@
|
|||||||
"@fastify/helmet": "^13.0.2",
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@fastify/multipart": "^9.2.1",
|
"@fastify/multipart": "^9.2.1",
|
||||||
"@fastify/static": "^10.1.3",
|
"@fastify/static": "^10.1.3",
|
||||||
"@fontsource-variable/plus-jakarta-sans": "5.3.0",
|
|
||||||
"@reduxjs/toolkit": "2.12.0",
|
|
||||||
"archiver": "^8.0.0",
|
"archiver": "^8.0.0",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"echarts": "6.1.0",
|
|
||||||
"echarts-for-react": "3.0.6",
|
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"fast-xml-parser": "^5.2.5",
|
"fast-xml-parser": "^5.2.5",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"less": "4.4.1",
|
|
||||||
"lucide-react": "^0.542.0",
|
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"react": "^19.1.1",
|
|
||||||
"react-dom": "^19.1.1",
|
|
||||||
"react-redux": "9.2.0",
|
|
||||||
"react-router-dom": "7.18.3",
|
|
||||||
"sharp": "^0.35.4",
|
"sharp": "^0.35.4",
|
||||||
"tdesign-react": "1.18.2",
|
|
||||||
"yauzl": "^3.2.0",
|
"yauzl": "^3.2.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fontsource-variable/plus-jakarta-sans": "5.3.0",
|
||||||
|
"@reduxjs/toolkit": "2.12.0",
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
"@types/archiver": "^8.0.0",
|
"@types/archiver": "^8.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
@@ -63,6 +54,15 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
|
"echarts": "6.1.0",
|
||||||
|
"echarts-for-react": "3.0.6",
|
||||||
|
"less": "4.4.1",
|
||||||
|
"lucide-react": "^0.542.0",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-redux": "9.2.0",
|
||||||
|
"react-router-dom": "7.18.3",
|
||||||
|
"tdesign-react": "1.18.2",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.3",
|
"vite": "^7.1.3",
|
||||||
|
|||||||
Generated
+33
-33
@@ -23,12 +23,6 @@ importers:
|
|||||||
'@fastify/static':
|
'@fastify/static':
|
||||||
specifier: ^10.1.3
|
specifier: ^10.1.3
|
||||||
version: 10.1.3
|
version: 10.1.3
|
||||||
'@fontsource-variable/plus-jakarta-sans':
|
|
||||||
specifier: 5.3.0
|
|
||||||
version: 5.3.0
|
|
||||||
'@reduxjs/toolkit':
|
|
||||||
specifier: 2.12.0
|
|
||||||
version: 2.12.0(react-redux@9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)
|
|
||||||
archiver:
|
archiver:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
@@ -41,12 +35,6 @@ importers:
|
|||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.2
|
specifier: ^0.45.2
|
||||||
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)
|
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.11.1)
|
||||||
echarts:
|
|
||||||
specifier: 6.1.0
|
|
||||||
version: 6.1.0
|
|
||||||
echarts-for-react:
|
|
||||||
specifier: 3.0.6
|
|
||||||
version: 3.0.6(echarts@6.1.0)(react@19.2.8)
|
|
||||||
exceljs:
|
exceljs:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
@@ -56,33 +44,12 @@ importers:
|
|||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.12.1
|
version: 5.12.1
|
||||||
less:
|
|
||||||
specifier: 4.4.1
|
|
||||||
version: 4.4.1
|
|
||||||
lucide-react:
|
|
||||||
specifier: ^0.542.0
|
|
||||||
version: 0.542.0(react@19.2.8)
|
|
||||||
pdf-lib:
|
pdf-lib:
|
||||||
specifier: ^1.17.1
|
specifier: ^1.17.1
|
||||||
version: 1.17.1
|
version: 1.17.1
|
||||||
react:
|
|
||||||
specifier: ^19.1.1
|
|
||||||
version: 19.2.8
|
|
||||||
react-dom:
|
|
||||||
specifier: ^19.1.1
|
|
||||||
version: 19.2.8(react@19.2.8)
|
|
||||||
react-redux:
|
|
||||||
specifier: 9.2.0
|
|
||||||
version: 9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)
|
|
||||||
react-router-dom:
|
|
||||||
specifier: 7.18.3
|
|
||||||
version: 7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.35.4
|
specifier: ^0.35.4
|
||||||
version: 0.35.4(@types/node@24.13.3)
|
version: 0.35.4(@types/node@24.13.3)
|
||||||
tdesign-react:
|
|
||||||
specifier: 1.18.2
|
|
||||||
version: 1.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
|
||||||
yauzl:
|
yauzl:
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
@@ -90,9 +57,15 @@ importers:
|
|||||||
specifier: ^4.1.5
|
specifier: ^4.1.5
|
||||||
version: 4.4.3
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@fontsource-variable/plus-jakarta-sans':
|
||||||
|
specifier: 5.3.0
|
||||||
|
version: 5.3.0
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: ^1.55.0
|
specifier: ^1.55.0
|
||||||
version: 1.62.1
|
version: 1.62.1
|
||||||
|
'@reduxjs/toolkit':
|
||||||
|
specifier: 2.12.0
|
||||||
|
version: 2.12.0(react-redux@9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)
|
||||||
'@types/archiver':
|
'@types/archiver':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
@@ -120,6 +93,33 @@ importers:
|
|||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.4
|
specifier: ^0.31.4
|
||||||
version: 0.31.10
|
version: 0.31.10
|
||||||
|
echarts:
|
||||||
|
specifier: 6.1.0
|
||||||
|
version: 6.1.0
|
||||||
|
echarts-for-react:
|
||||||
|
specifier: 3.0.6
|
||||||
|
version: 3.0.6(echarts@6.1.0)(react@19.2.8)
|
||||||
|
less:
|
||||||
|
specifier: 4.4.1
|
||||||
|
version: 4.4.1
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.542.0
|
||||||
|
version: 0.542.0(react@19.2.8)
|
||||||
|
react:
|
||||||
|
specifier: ^19.1.1
|
||||||
|
version: 19.2.8
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.1.1
|
||||||
|
version: 19.2.8(react@19.2.8)
|
||||||
|
react-redux:
|
||||||
|
specifier: 9.2.0
|
||||||
|
version: 9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: 7.18.3
|
||||||
|
version: 7.18.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
|
tdesign-react:
|
||||||
|
specifier: 1.18.2
|
||||||
|
version: 1.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.20.5
|
specifier: ^4.20.5
|
||||||
version: 4.23.12
|
version: 4.23.12
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
# Build a self-contained release on the target Linux architecture. Native
|
# Build a production release on the target Linux architecture. Native addons
|
||||||
# addons (SQLite, Argon2 and image processing) must be installed on the same
|
# (SQLite, Argon2 and image processing) must be installed on the same
|
||||||
# architecture/libc as the artifact.
|
# architecture/libc as the artifact. Node.js itself is deliberately managed
|
||||||
|
# by the installer outside each release so application updates stay small.
|
||||||
ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||||
VERSION=${1:-}
|
VERSION=${1:-}
|
||||||
OUT_DIR=${2:-$ROOT/release}
|
OUT_DIR=${2:-$ROOT/release}
|
||||||
@@ -28,7 +29,7 @@ cd "$ROOT"
|
|||||||
pnpm build
|
pnpm build
|
||||||
stage=$(mktemp -d)
|
stage=$(mktemp -d)
|
||||||
trap 'rm -rf "$stage"' EXIT
|
trap 'rm -rf "$stage"' EXIT
|
||||||
mkdir -p "$stage/dist" "$stage/migrations" "$stage/bin" "$stage/scripts" "$stage/systemd" "$stage/runtime/bin"
|
mkdir -p "$stage/dist" "$stage/migrations" "$stage/bin" "$stage/scripts" "$stage/systemd"
|
||||||
# Copy only the production build outputs. In particular, do not carry a
|
# Copy only the production build outputs. In particular, do not carry a
|
||||||
# stale dist/web-next directory from a previous local preview build.
|
# stale dist/web-next directory from a previous local preview build.
|
||||||
cp -a dist/server "$stage/dist/"
|
cp -a dist/server "$stage/dist/"
|
||||||
@@ -40,9 +41,7 @@ cp -a bin/. "$stage/bin/"
|
|||||||
cp -a scripts/tallynote-update.sh scripts/tallynote-update-runner.sh "$stage/scripts/"
|
cp -a scripts/tallynote-update.sh scripts/tallynote-update-runner.sh "$stage/scripts/"
|
||||||
cp uninstall.sh "$stage/uninstall.sh"
|
cp uninstall.sh "$stage/uninstall.sh"
|
||||||
cp -a systemd/tallynote.service systemd/tallynote-update.service systemd/tallynote-update.path systemd/tallynote.env.example "$stage/systemd/"
|
cp -a systemd/tallynote.service systemd/tallynote-update.service systemd/tallynote-update.path systemd/tallynote.env.example "$stage/systemd/"
|
||||||
node_path=$(command -v node)
|
chmod 755 "$stage/bin/tallynote" "$stage/bin/tallynote-admin-init" "$stage/scripts"/*.sh "$stage/uninstall.sh"
|
||||||
cp -L "$node_path" "$stage/runtime/bin/node"
|
|
||||||
chmod 755 "$stage/bin/tallynote" "$stage/bin/tallynote-admin-init" "$stage/scripts"/*.sh "$stage/runtime/bin/node" "$stage/uninstall.sh"
|
|
||||||
|
|
||||||
# pnpm's default linker creates symlinks. A release archive is deliberately
|
# pnpm's default linker creates symlinks. A release archive is deliberately
|
||||||
# symlink-free so the installer can reject traversal links deterministically.
|
# symlink-free so the installer can reject traversal links deterministically.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ set -Eeuo pipefail
|
|||||||
# always generated; an Ed25519 detached signature is added when a signing key
|
# always generated; an Ed25519 detached signature is added when a signing key
|
||||||
# is supplied. The script remains separate from the workflow so operators can
|
# is supplied. The script remains separate from the workflow so operators can
|
||||||
# dry-run the exact same asset selection locally without exposing a key.
|
# dry-run the exact same asset selection locally without exposing a key.
|
||||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
export PATH
|
export PATH
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -205,7 +205,6 @@ fi
|
|||||||
command -v "$CURL_BIN" >/dev/null 2>&1 || die 'curl is required'
|
command -v "$CURL_BIN" >/dev/null 2>&1 || die 'curl is required'
|
||||||
|
|
||||||
full_assets=()
|
full_assets=()
|
||||||
update_assets=()
|
|
||||||
for file in "$ASSET_DIR"/*.tar.gz; do
|
for file in "$ASSET_DIR"/*.tar.gz; do
|
||||||
[[ -f "$file" && ! -L "$file" ]] || continue
|
[[ -f "$file" && ! -L "$file" ]] || continue
|
||||||
name=$(basename -- "$file")
|
name=$(basename -- "$file")
|
||||||
@@ -214,13 +213,11 @@ for file in "$ASSET_DIR"/*.tar.gz; do
|
|||||||
asset_version=${asset_version%%-linux-*}
|
asset_version=${asset_version%%-linux-*}
|
||||||
[[ "$asset_version" == "${TAG#v}" ]] || die "release asset version does not match tag: $name"
|
[[ "$asset_version" == "${TAG#v}" ]] || die "release asset version does not match tag: $name"
|
||||||
if [[ "$name" =~ \.update-[a-f0-9]{64}\.tar\.gz$ ]]; then
|
if [[ "$name" =~ \.update-[a-f0-9]{64}\.tar\.gz$ ]]; then
|
||||||
update_assets+=("$file")
|
die "不再发布轻量更新资产:$name;请只保留完整生产包"
|
||||||
else
|
|
||||||
full_assets+=("$file")
|
|
||||||
fi
|
fi
|
||||||
|
full_assets+=("$file")
|
||||||
done
|
done
|
||||||
assets=("${full_assets[@]}")
|
assets=("${full_assets[@]}")
|
||||||
if ((${#update_assets[@]})); then assets+=("${update_assets[@]}"); fi
|
|
||||||
(( ${#assets[@]} > 0 )) || die 'no .tar.gz release asset found'
|
(( ${#assets[@]} > 0 )) || die 'no .tar.gz release asset found'
|
||||||
(( ${#full_assets[@]} > 0 )) || die 'no full release asset found'
|
(( ${#full_assets[@]} > 0 )) || die 'no full release asset found'
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
export PATH
|
export PATH
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}
|
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}
|
||||||
DATA_DIR=${TALLYNOTE_DATA_DIR:-/var/lib/tallynote}
|
DATA_DIR=${TALLYNOTE_DATA_DIR:-/var/lib/tallynote}
|
||||||
|
CONFIG_DIR=${TALLYNOTE_CONFIG_DIR:-/etc/tallynote}
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/tallynote.env"
|
||||||
REQUEST_FILE="$DATA_DIR/update-request.json"
|
REQUEST_FILE="$DATA_DIR/update-request.json"
|
||||||
CURRENT_LINK="$PREFIX/current"
|
CURRENT_LINK="$PREFIX/current"
|
||||||
STATE_FILE="$PREFIX/.update-state"
|
STATE_FILE="$PREFIX/.update-state"
|
||||||
@@ -22,6 +24,27 @@ if [[ "$HEALTH_HOST" == *:* && "$HEALTH_HOST" != \[* ]]; then HEALTH_HOST="[$HEA
|
|||||||
|
|
||||||
die() { printf 'tallynote update runner: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'tallynote update runner: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
node_is_usable() {
|
||||||
|
local candidate=$1 major
|
||||||
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
major=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
||||||
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 24 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_node() {
|
||||||
|
local candidate=${TALLYNOTE_NODE:-}
|
||||||
|
if [[ -z "$candidate" && -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]]; then
|
||||||
|
candidate=$(sed -n 's/^TALLYNOTE_NODE=//p' "$CONFIG_FILE" | head -n 1)
|
||||||
|
fi
|
||||||
|
if node_is_usable "$candidate"; then
|
||||||
|
printf '%s' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
candidate=$(command -v node || true)
|
||||||
|
node_is_usable "$candidate" || return 1
|
||||||
|
printf '%s' "$candidate"
|
||||||
|
}
|
||||||
|
|
||||||
# The runner may exit during any of the checks below. Install its EXIT cleanup
|
# The runner may exit during any of the checks below. Install its EXIT cleanup
|
||||||
# before doing privileged preflight so a partial invocation never leaves a
|
# before doing privileged preflight so a partial invocation never leaves a
|
||||||
# heartbeat or lock behind.
|
# heartbeat or lock behind.
|
||||||
@@ -202,9 +225,7 @@ if [[ "$request_operation" == download ]]; then
|
|||||||
trap 'exit 143' TERM
|
trap 'exit 143' TERM
|
||||||
trap 'exit 130' INT
|
trap 'exit 130' INT
|
||||||
start_heartbeat
|
start_heartbeat
|
||||||
node_bin="$CURRENT_LINK/runtime/bin/node"
|
node_bin=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$node_bin" ]] || node_bin=$(command -v node || true)
|
|
||||||
[[ -n "$node_bin" ]] || die 'node runtime not found'
|
|
||||||
cli="$CURRENT_LINK/dist/server/cli/update.js"
|
cli="$CURRENT_LINK/dist/server/cli/update.js"
|
||||||
[[ -f "$cli" ]] || die 'update CLI not found in current release'
|
[[ -f "$cli" ]] || die 'update CLI not found in current release'
|
||||||
set +e
|
set +e
|
||||||
@@ -243,8 +264,7 @@ restore_initial_service() {
|
|||||||
return "$result"
|
return "$result"
|
||||||
}
|
}
|
||||||
trap restore_initial_service EXIT
|
trap restore_initial_service EXIT
|
||||||
old_node="$CURRENT_LINK/runtime/bin/node"
|
old_node=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$old_node" ]] || old_node=$(command -v node || true)
|
|
||||||
handled=0
|
handled=0
|
||||||
|
|
||||||
write_update_state() { write_recovery_state "$1"; }
|
write_update_state() { write_recovery_state "$1"; }
|
||||||
@@ -287,8 +307,7 @@ recover_stale_state() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ "$state_phase" == finalizing && "$current_target" != "$state_old" ]]; then
|
if [[ "$state_phase" == finalizing && "$current_target" != "$state_old" ]]; then
|
||||||
recovery_node="$CURRENT_LINK/runtime/bin/node"
|
recovery_node=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$recovery_node" ]] || recovery_node=$(command -v node || true)
|
|
||||||
for _ in 1 2 3; do
|
for _ in 1 2 3; do
|
||||||
if finalize_state_job "$recovery_node" completed "$state_job"; then
|
if finalize_state_job "$recovery_node" completed "$state_job"; then
|
||||||
rm -f -- "$REQUEST_FILE" 2>/dev/null || true
|
rm -f -- "$REQUEST_FILE" 2>/dev/null || true
|
||||||
@@ -304,8 +323,7 @@ recover_stale_state() {
|
|||||||
# that case the old link is already safe to serve, but the database row
|
# that case the old link is already safe to serve, but the database row
|
||||||
# can still be `applying`; finish it as failed before clearing recovery
|
# can still be `applying`; finish it as failed before clearing recovery
|
||||||
# markers so the UI does not poll forever.
|
# markers so the UI does not poll forever.
|
||||||
recovery_node="$CURRENT_LINK/runtime/bin/node"
|
recovery_node=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$recovery_node" ]] || recovery_node=$(command -v node || true)
|
|
||||||
if finalize_state_job "$recovery_node" failed "$state_job"; then
|
if finalize_state_job "$recovery_node" failed "$state_job"; then
|
||||||
rm -f -- "$REQUEST_FILE" 2>/dev/null || true
|
rm -f -- "$REQUEST_FILE" 2>/dev/null || true
|
||||||
clear_update_state || true
|
clear_update_state || true
|
||||||
@@ -328,8 +346,7 @@ recover_stale_state() {
|
|||||||
rm -f -- "$rollback_link" 2>/dev/null || true
|
rm -f -- "$rollback_link" 2>/dev/null || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
recovery_node="$CURRENT_LINK/runtime/bin/node"
|
recovery_node=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$recovery_node" ]] || recovery_node=$(command -v node || true)
|
|
||||||
if ! finalize_state_job "$recovery_node" failed "$state_job"; then
|
if ! finalize_state_job "$recovery_node" failed "$state_job"; then
|
||||||
# If the original queue is still present, retry it from the restored old
|
# If the original queue is still present, retry it from the restored old
|
||||||
# release; a crash before the CLI wrote its job row is recoverable this
|
# release; a crash before the CLI wrote its job row is recoverable this
|
||||||
@@ -439,9 +456,7 @@ cleanup_after_update() {
|
|||||||
}
|
}
|
||||||
trap cleanup_after_update EXIT
|
trap cleanup_after_update EXIT
|
||||||
|
|
||||||
node_bin="$CURRENT_LINK/runtime/bin/node"
|
node_bin=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$node_bin" ]] || node_bin=$(command -v node || true)
|
|
||||||
[[ -n "$node_bin" ]] || die 'node runtime not found'
|
|
||||||
cli="$CURRENT_LINK/dist/server/cli/update.js"
|
cli="$CURRENT_LINK/dist/server/cli/update.js"
|
||||||
[[ -f "$cli" ]] || die 'update CLI not found in current release'
|
[[ -f "$cli" ]] || die 'update CLI not found in current release'
|
||||||
|
|
||||||
@@ -483,8 +498,7 @@ if (( was_active == 0 )); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
write_update_state finalizing || exit 1
|
write_update_state finalizing || exit 1
|
||||||
final_node="$CURRENT_LINK/runtime/bin/node"
|
final_node=$(resolve_node) || die 'Node.js 24+ not found'
|
||||||
[[ -x "$final_node" ]] || final_node=$(command -v node || true)
|
|
||||||
if [[ "$job_id" =~ ^[0-9a-f-]{36}$ ]]; then
|
if [[ "$job_id" =~ ^[0-9a-f-]{36}$ ]]; then
|
||||||
finalized=0
|
finalized=0
|
||||||
for _ in 1 2 3; do
|
for _ in 1 2 3; do
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
export PATH
|
export PATH
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
@@ -10,9 +10,22 @@ umask 077
|
|||||||
# extraction and atomic release switching.
|
# extraction and atomic release switching.
|
||||||
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-${TALLYNOTE_PREFIX:-/opt/tallynote}}
|
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-${TALLYNOTE_PREFIX:-/opt/tallynote}}
|
||||||
DATA_DIR=${TALLYNOTE_DATA_DIR:-/var/lib/tallynote}
|
DATA_DIR=${TALLYNOTE_DATA_DIR:-/var/lib/tallynote}
|
||||||
|
CONFIG_DIR=${TALLYNOTE_CONFIG_DIR:-/etc/tallynote}
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/tallynote.env"
|
||||||
REQUEST_FILE=${TALLYNOTE_UPDATE_REQUEST_FILE:-$DATA_DIR/update-request.json}
|
REQUEST_FILE=${TALLYNOTE_UPDATE_REQUEST_FILE:-$DATA_DIR/update-request.json}
|
||||||
NODE=${TALLYNOTE_NODE:-}
|
NODE=${TALLYNOTE_NODE:-}
|
||||||
|
|
||||||
|
node_is_usable() {
|
||||||
|
local candidate=$1 major
|
||||||
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
major=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
||||||
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 24 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$NODE" && -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]]; then
|
||||||
|
NODE=$(sed -n 's/^TALLYNOTE_NODE=//p' "$CONFIG_FILE" | head -n 1)
|
||||||
|
fi
|
||||||
|
|
||||||
die() { printf 'tallynote update: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'tallynote update: %s\n' "$*" >&2; exit 1; }
|
||||||
version_sort_desc() {
|
version_sort_desc() {
|
||||||
if sort -V </dev/null >/dev/null 2>&1; then
|
if sort -V </dev/null >/dev/null 2>&1; then
|
||||||
@@ -71,10 +84,9 @@ if [[ -x /usr/local/libexec/tallynote-update-runner ]]; then
|
|||||||
exec /usr/local/libexec/tallynote-update-runner
|
exec /usr/local/libexec/tallynote-update-runner
|
||||||
fi
|
fi
|
||||||
if [[ -z "$NODE" ]]; then
|
if [[ -z "$NODE" ]]; then
|
||||||
NODE="$PREFIX/current/runtime/bin/node"
|
NODE=$(command -v node || true)
|
||||||
[[ -x "$NODE" ]] || NODE=$(command -v node || true)
|
|
||||||
fi
|
fi
|
||||||
[[ -n "$NODE" ]] || die 'node runtime not found'
|
node_is_usable "$NODE" || die 'Node.js 24+ not found'
|
||||||
CLI="$PREFIX/current/dist/server/cli/update.js"
|
CLI="$PREFIX/current/dist/server/cli/update.js"
|
||||||
[[ -f "$CLI" ]] || die 'update CLI not found'
|
[[ -f "$CLI" ]] || die 'update CLI not found'
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -173,23 +173,23 @@ runner_root="$tmp/runner"
|
|||||||
runner_prefix="$runner_root/prefix"
|
runner_prefix="$runner_root/prefix"
|
||||||
runner_data="$runner_root/data"
|
runner_data="$runner_root/data"
|
||||||
runner_tools="$runner_root/tools"
|
runner_tools="$runner_root/tools"
|
||||||
mkdir -p "$runner_prefix/releases/1.0.0/runtime/bin" "$runner_prefix/releases/1.0.0/dist/server/cli" "$runner_data" "$runner_tools"
|
mkdir -p "$runner_prefix/releases/1.0.0/dist/server/cli" "$runner_data" "$runner_tools"
|
||||||
ln -s "$runner_prefix/releases/1.0.0" "$runner_prefix/current"
|
ln -s "$runner_prefix/releases/1.0.0" "$runner_prefix/current"
|
||||||
printf '%s\n' '{"jobId":"00000000-0000-4000-8000-000000000001","operation":"apply"}' > "$runner_data/update-request.json"
|
printf '%s\n' '{"jobId":"00000000-0000-4000-8000-000000000001","operation":"apply"}' > "$runner_data/update-request.json"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'printf "%s\\n" "$*" >> "$TALLYNOTE_NODE_TRACE"' 'exit 0' > "$runner_prefix/releases/1.0.0/runtime/bin/node"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-p" ]]; then printf "%s\n" "24"; else printf "%s\n" "$*" >> "$TALLYNOTE_NODE_TRACE"; fi' 'exit 0' > "$runner_tools/node"
|
||||||
printf '%s\n' cli > "$runner_prefix/releases/1.0.0/dist/server/cli/update.js"
|
printf '%s\n' cli > "$runner_prefix/releases/1.0.0/dist/server/cli/update.js"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'case "${1:-}" in is-active) exit 0;; *) exit 0;; esac' > "$runner_tools/systemctl"
|
printf '%s\n' '#!/usr/bin/env bash' 'case "${1:-}" in is-active) exit 0;; *) exit 0;; esac' > "$runner_tools/systemctl"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-f" ]]; then shift; [[ "${1:-}" == "--" ]] && shift; /bin/realpath "$1"; else /usr/bin/readlink "$@"; fi' > "$runner_tools/readlink"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-f" ]]; then shift; [[ "${1:-}" == "--" ]] && shift; /bin/realpath "$1"; else /usr/bin/readlink "$@"; fi' > "$runner_tools/readlink"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-Tf" ]]; then shift; /bin/mv -f "$@"; else /bin/mv "$@"; fi' > "$runner_tools/mv"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-Tf" ]]; then shift; /bin/mv -f "$@"; else /bin/mv "$@"; fi' > "$runner_tools/mv"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$runner_tools/curl"
|
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$runner_tools/curl"
|
||||||
chmod 755 "$runner_prefix/releases/1.0.0/runtime/bin/node" "$runner_tools/systemctl" "$runner_tools/readlink" "$runner_tools/mv" "$runner_tools/curl"
|
chmod 755 "$runner_tools/node" "$runner_tools/systemctl" "$runner_tools/readlink" "$runner_tools/mv" "$runner_tools/curl"
|
||||||
runner_script="$runner_root/runner.sh"
|
runner_script="$runner_root/runner.sh"
|
||||||
runner_path="$runner_tools:/usr/sbin:/usr/bin:/sbin:/bin"
|
runner_path="$runner_tools:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
sed "s#PATH=/usr/sbin:/usr/bin:/sbin:/bin#PATH=$runner_path#" "$root/scripts/tallynote-update-runner.sh" > "$runner_script"
|
sed "s#PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin#PATH=$runner_path#" "$root/scripts/tallynote-update-runner.sh" > "$runner_script"
|
||||||
chmod 755 "$runner_script"
|
chmod 755 "$runner_script"
|
||||||
runner_prefix_physical=$(cd "$runner_prefix" && pwd -P)
|
runner_prefix_physical=$(cd "$runner_prefix" && pwd -P)
|
||||||
runner_data_physical=$(cd "$runner_data" && pwd -P)
|
runner_data_physical=$(cd "$runner_data" && pwd -P)
|
||||||
env EUID=0 TALLYNOTE_INSTALL_PREFIX="$runner_prefix_physical" TALLYNOTE_DATA_DIR="$runner_data_physical" TALLYNOTE_NODE_TRACE="$runner_root/node.log" bash "$runner_script"
|
env EUID=0 TALLYNOTE_INSTALL_PREFIX="$runner_prefix_physical" TALLYNOTE_DATA_DIR="$runner_data_physical" TALLYNOTE_NODE="$runner_tools/node" TALLYNOTE_NODE_TRACE="$runner_root/node.log" bash "$runner_script"
|
||||||
grep -q -- '--request-file' "$runner_root/node.log"
|
grep -q -- '--request-file' "$runner_root/node.log"
|
||||||
grep -q -- '--finalize-job' "$runner_root/node.log"
|
grep -q -- '--finalize-job' "$runner_root/node.log"
|
||||||
[[ ! -e "$runner_data/update-request.json" ]]
|
[[ ! -e "$runner_data/update-request.json" ]]
|
||||||
@@ -202,14 +202,14 @@ download_runner_root="$tmp/download-runner"
|
|||||||
download_runner_prefix="$download_runner_root/prefix"
|
download_runner_prefix="$download_runner_root/prefix"
|
||||||
download_runner_data="$download_runner_root/data"
|
download_runner_data="$download_runner_root/data"
|
||||||
download_runner_tools="$download_runner_root/tools"
|
download_runner_tools="$download_runner_root/tools"
|
||||||
mkdir -p "$download_runner_prefix/releases/1.0.0/runtime/bin" "$download_runner_prefix/releases/1.0.0/dist/server/cli" "$download_runner_data" "$download_runner_tools"
|
mkdir -p "$download_runner_prefix/releases/1.0.0/dist/server/cli" "$download_runner_data" "$download_runner_tools"
|
||||||
ln -s "$download_runner_prefix/releases/1.0.0" "$download_runner_prefix/current"
|
ln -s "$download_runner_prefix/releases/1.0.0" "$download_runner_prefix/current"
|
||||||
# The request has already been consumed; only the stale download marker is
|
# The request has already been consumed; only the stale download marker is
|
||||||
# left, which is the narrow recovery window covered by this fixture.
|
# left, which is the narrow recovery window covered by this fixture.
|
||||||
download_runner_prefix_physical=$(cd "$download_runner_prefix" && pwd -P)
|
download_runner_prefix_physical=$(cd "$download_runner_prefix" && pwd -P)
|
||||||
download_runner_data_physical=$(cd "$download_runner_data" && pwd -P)
|
download_runner_data_physical=$(cd "$download_runner_data" && pwd -P)
|
||||||
printf '%s\n' 'job_id=00000000-0000-4000-8000-000000000002' "old_target=$download_runner_prefix_physical/releases/1.0.0" 'phase=download' > "$download_runner_prefix/.update-state"
|
printf '%s\n' 'job_id=00000000-0000-4000-8000-000000000002' "old_target=$download_runner_prefix_physical/releases/1.0.0" 'phase=download' > "$download_runner_prefix/.update-state"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'printf "%s\n" "$*" >> "$TALLYNOTE_DOWNLOAD_NODE_TRACE"' 'exit 0' > "$download_runner_prefix/releases/1.0.0/runtime/bin/node"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-p" ]]; then printf "%s\n" "24"; else printf "%s\n" "$*" >> "$TALLYNOTE_DOWNLOAD_NODE_TRACE"; fi' 'exit 0' > "$download_runner_tools/node"
|
||||||
printf '%s\n' cli > "$download_runner_prefix/releases/1.0.0/dist/server/cli/update.js"
|
printf '%s\n' cli > "$download_runner_prefix/releases/1.0.0/dist/server/cli/update.js"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'case "${1:-}" in is-active) exit 0;; *) exit 0;; esac' > "$download_runner_tools/systemctl"
|
printf '%s\n' '#!/usr/bin/env bash' 'case "${1:-}" in is-active) exit 0;; *) exit 0;; esac' > "$download_runner_tools/systemctl"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-f" ]]; then shift; [[ "${1:-}" == "--" ]] && shift; /bin/realpath "$1"; else /usr/bin/readlink "$@"; fi' > "$download_runner_tools/readlink"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-f" ]]; then shift; [[ "${1:-}" == "--" ]] && shift; /bin/realpath "$1"; else /usr/bin/readlink "$@"; fi' > "$download_runner_tools/readlink"
|
||||||
@@ -221,11 +221,11 @@ case "$*" in
|
|||||||
*) /usr/bin/stat "$@" ;;
|
*) /usr/bin/stat "$@" ;;
|
||||||
esac
|
esac
|
||||||
EOF
|
EOF
|
||||||
chmod 755 "$download_runner_prefix/releases/1.0.0/runtime/bin/node" "$download_runner_tools/systemctl" "$download_runner_tools/readlink" "$download_runner_tools/stat"
|
chmod 755 "$download_runner_tools/node" "$download_runner_tools/systemctl" "$download_runner_tools/readlink" "$download_runner_tools/stat"
|
||||||
download_runner_script="$download_runner_root/runner.sh"
|
download_runner_script="$download_runner_root/runner.sh"
|
||||||
sed "s#PATH=/usr/sbin:/usr/bin:/sbin:/bin#PATH=$download_runner_tools:/usr/sbin:/usr/bin:/sbin:/bin#" "$root/scripts/tallynote-update-runner.sh" > "$download_runner_script"
|
sed "s#PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin#PATH=$download_runner_tools:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin#" "$root/scripts/tallynote-update-runner.sh" > "$download_runner_script"
|
||||||
chmod 755 "$download_runner_script"
|
chmod 755 "$download_runner_script"
|
||||||
env EUID=0 TALLYNOTE_INSTALL_PREFIX="$download_runner_prefix_physical" TALLYNOTE_DATA_DIR="$download_runner_data_physical" TALLYNOTE_DOWNLOAD_NODE_TRACE="$download_runner_root/node.log" bash "$download_runner_script"
|
env EUID=0 TALLYNOTE_INSTALL_PREFIX="$download_runner_prefix_physical" TALLYNOTE_DATA_DIR="$download_runner_data_physical" TALLYNOTE_NODE="$download_runner_tools/node" TALLYNOTE_DOWNLOAD_NODE_TRACE="$download_runner_root/node.log" bash "$download_runner_script"
|
||||||
[[ ! -e "$download_runner_root/node.log" ]]
|
[[ ! -e "$download_runner_root/node.log" ]]
|
||||||
[[ ! -e "$download_runner_prefix/.update-state" ]]
|
[[ ! -e "$download_runner_prefix/.update-state" ]]
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ env EUID=0 TALLYNOTE_INSTALL_PREFIX="$download_runner_prefix_physical" TALLYNOTE
|
|||||||
# temporary variables still exist; otherwise set -u fails at the end of main.
|
# temporary variables still exist; otherwise set -u fails at the end of main.
|
||||||
release_fixture="$tmp/release-fixture"
|
release_fixture="$tmp/release-fixture"
|
||||||
mkdir -p "$release_fixture/dist/server/cli" "$release_fixture/dist/web" "$release_fixture/bin" \
|
mkdir -p "$release_fixture/dist/server/cli" "$release_fixture/dist/web" "$release_fixture/bin" \
|
||||||
"$release_fixture/scripts" "$release_fixture/runtime/bin" "$release_fixture/systemd"
|
"$release_fixture/scripts" "$release_fixture/systemd"
|
||||||
printf '%s\n' '{"version":"1.0.0"}' > "$release_fixture/package.json"
|
printf '%s\n' '{"version":"1.0.0"}' > "$release_fixture/package.json"
|
||||||
printf '%s\n' server > "$release_fixture/dist/server/index.js"
|
printf '%s\n' server > "$release_fixture/dist/server/index.js"
|
||||||
printf '%s\n' cli > "$release_fixture/dist/server/cli/admin-init.js"
|
printf '%s\n' cli > "$release_fixture/dist/server/cli/admin-init.js"
|
||||||
@@ -282,16 +282,16 @@ grep -Fxq "PathChanged=$tmp/custom-prefix" "$rendered_path"
|
|||||||
# The production admin wrapper must load a release-relative runtime, change to
|
# The production admin wrapper must load a release-relative runtime, change to
|
||||||
# the release root, and forward CLI arguments without requiring pnpm.
|
# the release root, and forward CLI arguments without requiring pnpm.
|
||||||
wrapper_prefix="$tmp/wrapper-prefix"
|
wrapper_prefix="$tmp/wrapper-prefix"
|
||||||
mkdir -p "$wrapper_prefix/releases/1.0.0/runtime/bin" "$wrapper_prefix/releases/1.0.0/dist/server/cli"
|
mkdir -p "$wrapper_prefix/releases/1.0.0/dist/server/cli" "$tmp/wrapper-tools"
|
||||||
ln -s "$wrapper_prefix/releases/1.0.0" "$wrapper_prefix/current"
|
ln -s "$wrapper_prefix/releases/1.0.0" "$wrapper_prefix/current"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'pwd -P > "$TALLYNOTE_WRAPPER_LOG"' 'printf "%s\n" "$@" >> "$TALLYNOTE_WRAPPER_LOG"' > "$wrapper_prefix/releases/1.0.0/runtime/bin/node"
|
printf '%s\n' '#!/usr/bin/env bash' 'if [[ "${1:-}" == "-p" ]]; then printf "%s\n" "24"; else pwd -P > "$TALLYNOTE_WRAPPER_LOG"; printf "%s\n" "$@" >> "$TALLYNOTE_WRAPPER_LOG"; fi' > "$tmp/wrapper-tools/node"
|
||||||
chmod 755 "$wrapper_prefix/releases/1.0.0/runtime/bin/node"
|
chmod 755 "$tmp/wrapper-tools/node"
|
||||||
printf '%s\n' cli > "$wrapper_prefix/releases/1.0.0/dist/server/cli/admin-init.js"
|
printf '%s\n' cli > "$wrapper_prefix/releases/1.0.0/dist/server/cli/admin-init.js"
|
||||||
# This fixture verifies release-relative execution and argument forwarding.
|
# This fixture verifies release-relative execution and argument forwarding.
|
||||||
# Force the wrapper's non-root branch so the root CI runner does not need a
|
# Force the wrapper's non-root branch so the root CI runner does not need a
|
||||||
# real `tallynote` service account or a privileged runuser hand-off; that
|
# real `tallynote` service account or a privileged runuser hand-off; that
|
||||||
# privilege boundary is validated by the production checks themselves.
|
# privilege boundary is validated by the production checks themselves.
|
||||||
env EUID=1000 TALLYNOTE_INSTALL_PREFIX="$wrapper_prefix" TALLYNOTE_CONFIG_DIR="$tmp/no-config" TALLYNOTE_WRAPPER_LOG="$tmp/wrapper.log" \
|
env EUID=1000 TALLYNOTE_INSTALL_PREFIX="$wrapper_prefix" TALLYNOTE_CONFIG_DIR="$tmp/no-config" TALLYNOTE_NODE="$tmp/wrapper-tools/node" TALLYNOTE_WRAPPER_LOG="$tmp/wrapper.log" \
|
||||||
bash "$root/bin/tallynote-admin-init" --generate
|
bash "$root/bin/tallynote-admin-init" --generate
|
||||||
wrapper_expected_root=$(cd "$wrapper_prefix/releases/1.0.0" && pwd -P)
|
wrapper_expected_root=$(cd "$wrapper_prefix/releases/1.0.0" && pwd -P)
|
||||||
grep -Fxq "$wrapper_expected_root" "$tmp/wrapper.log"
|
grep -Fxq "$wrapper_expected_root" "$tmp/wrapper.log"
|
||||||
@@ -590,13 +590,12 @@ env -u TALLYNOTE_PUBLIC_ORIGIN -u TALLYNOTE_ALLOW_INSECURE_HTTP \
|
|||||||
# A release archive is extracted under umask 077, then explicitly normalized
|
# A release archive is extracted under umask 077, then explicitly normalized
|
||||||
# so the tallynote system user can traverse and execute the shipped tree.
|
# so the tallynote system user can traverse and execute the shipped tree.
|
||||||
source_tmp="$tmp/source"
|
source_tmp="$tmp/source"
|
||||||
mkdir -p "$source_tmp/dist/server" "$source_tmp/bin" "$source_tmp/scripts" "$source_tmp/runtime/bin"
|
mkdir -p "$source_tmp/dist/server" "$source_tmp/bin" "$source_tmp/scripts"
|
||||||
printf '%s\n' 'server' > "$source_tmp/dist/server/index.js"
|
printf '%s\n' 'server' > "$source_tmp/dist/server/index.js"
|
||||||
printf '%s\n' '#!/bin/sh' > "$source_tmp/uninstall.sh"
|
printf '%s\n' '#!/bin/sh' > "$source_tmp/uninstall.sh"
|
||||||
printf '%s\n' '#!/bin/sh' > "$source_tmp/bin/tallynote"
|
printf '%s\n' '#!/bin/sh' > "$source_tmp/bin/tallynote"
|
||||||
printf '%s\n' '#!/bin/sh' > "$source_tmp/scripts/runner.sh"
|
printf '%s\n' '#!/bin/sh' > "$source_tmp/scripts/runner.sh"
|
||||||
printf '%s\n' 'node' > "$source_tmp/runtime/bin/node"
|
chmod 755 "$source_tmp/bin/tallynote" "$source_tmp/scripts/runner.sh"
|
||||||
chmod 755 "$source_tmp/bin/tallynote" "$source_tmp/scripts/runner.sh" "$source_tmp/runtime/bin/node"
|
|
||||||
chmod 755 "$source_tmp/uninstall.sh"
|
chmod 755 "$source_tmp/uninstall.sh"
|
||||||
archive_tmp="$tmp/release.tar.gz"
|
archive_tmp="$tmp/release.tar.gz"
|
||||||
tar -C "$source_tmp" -czf "$archive_tmp" .
|
tar -C "$source_tmp" -czf "$archive_tmp" .
|
||||||
@@ -612,6 +611,7 @@ bash -c '
|
|||||||
[[ "$(stat_mode "$destination/dist/server/index.js")" == 644 ]]
|
[[ "$(stat_mode "$destination/dist/server/index.js")" == 644 ]]
|
||||||
[[ "$(stat_mode "$destination/bin/tallynote")" == 755 ]]
|
[[ "$(stat_mode "$destination/bin/tallynote")" == 755 ]]
|
||||||
[[ "$(stat_mode "$destination/uninstall.sh")" == 755 ]]
|
[[ "$(stat_mode "$destination/uninstall.sh")" == 755 ]]
|
||||||
|
[[ ! -e "$destination/runtime" ]]
|
||||||
' _ "$installer_lib" "$archive_tmp" "$tmp/unpacked"
|
' _ "$installer_lib" "$archive_tmp" "$tmp/unpacked"
|
||||||
|
|
||||||
# A normal public-release install only needs the detached SHA-256 manifest;
|
# A normal public-release install only needs the detached SHA-256 manifest;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ make_fixture() {
|
|||||||
done
|
done
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/sbin/tallynote-update"
|
printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/sbin/tallynote-update"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/libexec/tallynote-update-runner"
|
printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/libexec/tallynote-update-runner"
|
||||||
printf '%s\n' '#!/usr/bin/env bash' 'exec /opt/tallynote/current/runtime/bin/node /opt/tallynote/current/dist/server/cli/admin-init.js' > "$fixture/usr/local/sbin/tallynote-admin-init"
|
printf '%s\n' '#!/usr/bin/env bash' 'exec /usr/bin/node /opt/tallynote/current/dist/server/cli/admin-init.js' > "$fixture/usr/local/sbin/tallynote-admin-init"
|
||||||
cp "$root/uninstall.sh" "$fixture/usr/local/sbin/tallynote-uninstall"
|
cp "$root/uninstall.sh" "$fixture/usr/local/sbin/tallynote-uninstall"
|
||||||
chmod 755 "$fixture/usr/local/sbin/tallynote-update" "$fixture/usr/local/libexec/tallynote-update-runner" "$fixture/usr/local/sbin/tallynote-admin-init" "$fixture/usr/local/sbin/tallynote-uninstall"
|
chmod 755 "$fixture/usr/local/sbin/tallynote-update" "$fixture/usr/local/libexec/tallynote-update-runner" "$fixture/usr/local/sbin/tallynote-admin-init" "$fixture/usr/local/sbin/tallynote-uninstall"
|
||||||
printf '%s\n' 'sqlite' > "$fixture/var/lib/tallynote/tallynote.db"
|
printf '%s\n' 'sqlite' > "$fixture/var/lib/tallynote/tallynote.db"
|
||||||
|
|||||||
+4
-22
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { cp, lstat, mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
|
import { lstat, mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import type Database from "better-sqlite3";
|
import type Database from "better-sqlite3";
|
||||||
@@ -10,7 +10,6 @@ import { writeAudit } from "../audit.js";
|
|||||||
import {
|
import {
|
||||||
atomicSwitchDirectory,
|
atomicSwitchDirectory,
|
||||||
atomicSwitchRelease,
|
atomicSwitchRelease,
|
||||||
applicationUpdateRuntimeHash,
|
|
||||||
compareSemver,
|
compareSemver,
|
||||||
createSafeArchive,
|
createSafeArchive,
|
||||||
detectPlatform,
|
detectPlatform,
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
isNewerVersion,
|
isNewerVersion,
|
||||||
normalizeReleasePermissions,
|
normalizeReleasePermissions,
|
||||||
parseSemver,
|
parseSemver,
|
||||||
runtimeHashFromLockfile,
|
|
||||||
selectReleaseAsset,
|
selectReleaseAsset,
|
||||||
sanitizeAssetName,
|
sanitizeAssetName,
|
||||||
validateHttpsUrl,
|
validateHttpsUrl,
|
||||||
@@ -223,16 +221,9 @@ async function resolveRelease(options: UpdateRunOptions, platform: ReturnType<ty
|
|||||||
if (options.metadataUrl) {
|
if (options.metadataUrl) {
|
||||||
const metadataUrl = validateHttpsUrl(options.metadataUrl, options);
|
const metadataUrl = validateHttpsUrl(options.metadataUrl, options);
|
||||||
const release = await fetchReleaseMetadata(metadataUrl, options);
|
const release = await fetchReleaseMetadata(metadataUrl, options);
|
||||||
let runtimeHash: string | undefined;
|
|
||||||
try {
|
|
||||||
runtimeHash = runtimeHashFromLockfile(await readFile(path.join(options.currentDir, "pnpm-lock.yaml")));
|
|
||||||
} catch {
|
|
||||||
// Fall back to the full archive when the current installation predates
|
|
||||||
// runtime fingerprints or is missing deployment provenance.
|
|
||||||
}
|
|
||||||
let asset = options.assetUrl && !options.requireSignature
|
let asset = options.assetUrl && !options.requireSignature
|
||||||
? { name: sanitizeAssetName(options.assetName ?? path.basename(new URL(options.assetUrl).pathname)), url: validateHttpsUrl(options.assetUrl, { ...options, baseUrl: metadataUrl }).toString(), ...(options.expectedSha256 ? { sha256: options.expectedSha256 } : {}) }
|
? { name: sanitizeAssetName(options.assetName ?? path.basename(new URL(options.assetUrl).pathname)), url: validateHttpsUrl(options.assetUrl, { ...options, baseUrl: metadataUrl }).toString(), ...(options.expectedSha256 ? { sha256: options.expectedSha256 } : {}) }
|
||||||
: selectReleaseAsset(release, platform, runtimeHash);
|
: selectReleaseAsset(release, platform);
|
||||||
if (!asset) throw new Error("没有匹配当前平台的更新文件");
|
if (!asset) throw new Error("没有匹配当前平台的更新文件");
|
||||||
const integrity = await attachSidecarHash(release, asset, {
|
const integrity = await attachSidecarHash(release, asset, {
|
||||||
allowedHosts: options.allowedHosts ?? [],
|
allowedHosts: options.allowedHosts ?? [],
|
||||||
@@ -339,17 +330,8 @@ export async function runUpdate(options: UpdateRunOptions): Promise<UpdateRunRes
|
|||||||
if (!archivePath.endsWith(".tar.gz") && !archivePath.endsWith(".tgz") && !archivePath.endsWith(".tar") && !archivePath.endsWith(".zip")) throw new Error("更新文件格式仅支持 tar.gz、tar 或 zip");
|
if (!archivePath.endsWith(".tar.gz") && !archivePath.endsWith(".tgz") && !archivePath.endsWith(".tar") && !archivePath.endsWith(".zip")) throw new Error("更新文件格式仅支持 tar.gz、tar 或 zip");
|
||||||
const stagedDir = path.join(workspace, "payload");
|
const stagedDir = path.join(workspace, "payload");
|
||||||
await extractSafeArchive(archivePath, stagedDir, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
|
await extractSafeArchive(archivePath, stagedDir, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
|
||||||
if (applicationUpdateRuntimeHash(resolved.asset.name)) {
|
const embeddedRuntime = await lstat(path.join(stagedDir, "runtime")).catch(() => null);
|
||||||
const currentRelease = await realpath(options.currentDir).catch(() => { throw new Error("当前安装目录无效"); });
|
if (embeddedRuntime) throw new Error("发布包不应包含 Node.js runtime");
|
||||||
const currentInfo = await lstat(currentRelease).catch(() => null);
|
|
||||||
if (!currentInfo?.isDirectory() || currentInfo.isSymbolicLink()) throw new Error("当前安装目录无效");
|
|
||||||
for (const entry of ["node_modules", "runtime", "pnpm-lock.yaml"] as const) {
|
|
||||||
const source = path.join(currentRelease, entry);
|
|
||||||
const sourceInfo = await lstat(source).catch(() => null);
|
|
||||||
if (!sourceInfo || sourceInfo.isSymbolicLink()) throw new Error("当前运行时不完整,无法应用轻量更新");
|
|
||||||
await cp(source, path.join(stagedDir, entry), { recursive: sourceInfo.isDirectory(), errorOnExist: true, force: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await normalizeReleasePermissions(stagedDir);
|
await normalizeReleasePermissions(stagedDir);
|
||||||
const payloadInfo = await lstat(path.join(stagedDir, "dist")).catch(() => null);
|
const payloadInfo = await lstat(path.join(stagedDir, "dist")).catch(() => null);
|
||||||
if (!payloadInfo?.isDirectory() || payloadInfo.isSymbolicLink()) throw new Error("发布包缺少 dist 目录");
|
if (!payloadInfo?.isDirectory() || payloadInfo.isSymbolicLink()) throw new Error("发布包缺少 dist 目录");
|
||||||
|
|||||||
+11
-30
@@ -1,5 +1,5 @@
|
|||||||
import { lstatSync, realpathSync, readFileSync, unlinkSync } from "node:fs";
|
import { lstatSync, realpathSync, readFileSync, unlinkSync } from "node:fs";
|
||||||
import { chmod, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
import { chmod, lstat, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { createPublicKey, randomUUID, verify as verifySignature } from "node:crypto";
|
import { createPublicKey, randomUUID, verify as verifySignature } from "node:crypto";
|
||||||
import type Database from "better-sqlite3";
|
import type Database from "better-sqlite3";
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
isNewerVersion,
|
isNewerVersion,
|
||||||
normalizeReleasePermissions,
|
normalizeReleasePermissions,
|
||||||
parseSemver,
|
parseSemver,
|
||||||
runtimeHashFromLockfile,
|
|
||||||
sanitizeAssetName,
|
sanitizeAssetName,
|
||||||
selectReleaseAsset,
|
selectReleaseAsset,
|
||||||
validateHttpsUrl,
|
validateHttpsUrl,
|
||||||
@@ -211,14 +210,9 @@ export async function checkForUpdate(database: Database.Database, config: AppCon
|
|||||||
} catch {
|
} catch {
|
||||||
throw new AppError(502, "UPDATE_CHECK_FAILED", "暂时无法获取最新版本,请稍后重试");
|
throw new AppError(502, "UPDATE_CHECK_FAILED", "暂时无法获取最新版本,请稍后重试");
|
||||||
}
|
}
|
||||||
let runtimeHash: string | undefined;
|
// Always select the complete production archive. The host Node.js runtime
|
||||||
try {
|
// is reused, while the application package remains self-contained and
|
||||||
runtimeHash = runtimeHashFromLockfile(readFileSync(path.join(config.projectRoot, "pnpm-lock.yaml")));
|
// identical for first installs and in-place updates.
|
||||||
} catch {
|
|
||||||
// Legacy or source installations may not contain the lockfile. They stay
|
|
||||||
// on the full release asset instead of risking an incompatible runtime.
|
|
||||||
}
|
|
||||||
// Force choosing the full standalone archive so users always get a real, visible streaming download
|
|
||||||
let asset = selectReleaseAsset(metadata, platform, undefined);
|
let asset = selectReleaseAsset(metadata, platform, undefined);
|
||||||
let signatureVerified = false;
|
let signatureVerified = false;
|
||||||
if (asset) {
|
if (asset) {
|
||||||
@@ -688,8 +682,9 @@ export function cancelUpdateJob(
|
|||||||
* The root runner only needs to apply (stop/backup/switch/restart) afterwards.
|
* The root runner only needs to apply (stop/backup/switch/restart) afterwards.
|
||||||
*
|
*
|
||||||
* This function runs asynchronously outside the request lifecycle. It updates
|
* This function runs asynchronously outside the request lifecycle. It updates
|
||||||
* the job row in the database so the frontend can poll progress. On success it
|
* the job row in the database so the frontend can poll progress. A successful
|
||||||
* writes an apply request file so the systemd path unit triggers the runner.
|
* download only becomes staged; applying it is a separate, explicit action
|
||||||
|
* that the administrator submits after reviewing the verification result.
|
||||||
*/
|
*/
|
||||||
export async function downloadAndStageUpdate(
|
export async function downloadAndStageUpdate(
|
||||||
database: Database.Database,
|
database: Database.Database,
|
||||||
@@ -756,8 +751,10 @@ export async function downloadAndStageUpdate(
|
|||||||
await extractSafeArchive(archivePath, payloadDir);
|
await extractSafeArchive(archivePath, payloadDir);
|
||||||
await normalizeReleasePermissions(payloadDir);
|
await normalizeReleasePermissions(payloadDir);
|
||||||
|
|
||||||
|
const embeddedRuntime = await lstat(path.join(payloadDir, "runtime")).catch(() => null);
|
||||||
|
if (embeddedRuntime) throw new Error("发布包不应包含 Node.js runtime");
|
||||||
|
|
||||||
// Verify payload contains dist directory
|
// Verify payload contains dist directory
|
||||||
const { lstat } = await import("node:fs/promises");
|
|
||||||
const payloadInfo = await lstat(path.join(payloadDir, "dist")).catch(() => null);
|
const payloadInfo = await lstat(path.join(payloadDir, "dist")).catch(() => null);
|
||||||
if (!payloadInfo?.isDirectory() || payloadInfo.isSymbolicLink()) {
|
if (!payloadInfo?.isDirectory() || payloadInfo.isSymbolicLink()) {
|
||||||
throw new Error("发布包缺少 dist 目录");
|
throw new Error("发布包缺少 dist 目录");
|
||||||
@@ -765,26 +762,10 @@ export async function downloadAndStageUpdate(
|
|||||||
|
|
||||||
// Transition to staged
|
// Transition to staged
|
||||||
const staged = database.prepare(
|
const staged = database.prepare(
|
||||||
"UPDATE update_jobs SET status='staged', operation='apply', actual_sha256=?, size_bytes=?, download_path=?, updated_at=? WHERE id=? AND status IN ('verifying', 'downloading')",
|
"UPDATE update_jobs SET status='staged', operation='download', actual_sha256=?, size_bytes=?, download_path=?, updated_at=? WHERE id=? AND status IN ('verifying', 'downloading')",
|
||||||
).run(downloaded.sha256, downloaded.size, workspace, Date.now(), jobId);
|
).run(downloaded.sha256, downloaded.size, workspace, Date.now(), jobId);
|
||||||
if (staged.changes !== 1) return; // cancelled
|
if (staged.changes !== 1) return; // cancelled
|
||||||
|
|
||||||
// Write apply request file for the root runner
|
|
||||||
await writeUpdateRequest(config, {
|
|
||||||
jobId,
|
|
||||||
operation: "apply",
|
|
||||||
version,
|
|
||||||
metadataUrl,
|
|
||||||
assetUrl,
|
|
||||||
assetName,
|
|
||||||
expectedSha256,
|
|
||||||
requestedAt: Date.now(),
|
|
||||||
currentLink: config.currentLink,
|
|
||||||
releasesDir: config.releasesDir,
|
|
||||||
dataDir: config.dataDir,
|
|
||||||
stagedPath: workspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
writeAudit(database, {
|
writeAudit(database, {
|
||||||
requestId: `download:${jobId}`,
|
requestId: `download:${jobId}`,
|
||||||
actorAdminId: adminId,
|
actorAdminId: adminId,
|
||||||
|
|||||||
+1
-1
@@ -1000,7 +1000,7 @@ export async function normalizeReleasePermissions(rootPath: string): Promise<voi
|
|||||||
await walk(target);
|
await walk(target);
|
||||||
} else if (entry.isFile()) {
|
} else if (entry.isFile()) {
|
||||||
const relative = path.relative(root, target).split(path.sep).join("/");
|
const relative = path.relative(root, target).split(path.sep).join("/");
|
||||||
const executable = relative.startsWith("bin/") || relative.startsWith("scripts/") || relative.startsWith("runtime/bin/");
|
const executable = relative.startsWith("bin/") || relative.startsWith("scripts/");
|
||||||
await chmod(target, executable ? 0o755 : 0o644);
|
await chmod(target, executable ? 0o755 : 0o644);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("发布包包含不受支持的文件类型");
|
throw new Error("发布包包含不受支持的文件类型");
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ User=root
|
|||||||
Group=root
|
Group=root
|
||||||
WorkingDirectory=/opt/tallynote/current
|
WorkingDirectory=/opt/tallynote/current
|
||||||
EnvironmentFile=-/etc/tallynote/tallynote.env
|
EnvironmentFile=-/etc/tallynote/tallynote.env
|
||||||
|
Environment=TALLYNOTE_CONFIG_DIR=/etc/tallynote
|
||||||
ExecStart=/usr/local/libexec/tallynote-update-runner
|
ExecStart=/usr/local/libexec/tallynote-update-runner
|
||||||
Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
# The runner consumes queued requests immediately and applies its own bounded
|
# The runner consumes queued requests immediately and applies its own bounded
|
||||||
# phase timeouts while keeping full CLI diagnostics in the runner log.
|
# phase timeouts while keeping full CLI diagnostics in the runner log.
|
||||||
# Archive validation and data backups can exceed systemd's 90s
|
# Archive validation and data backups can exceed systemd's 90s
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ TALLYNOTE_HOST=127.0.0.1
|
|||||||
TALLYNOTE_PORT=3000
|
TALLYNOTE_PORT=3000
|
||||||
TALLYNOTE_DATA_DIR=/var/lib/tallynote
|
TALLYNOTE_DATA_DIR=/var/lib/tallynote
|
||||||
TALLYNOTE_INSTALL_PREFIX=/opt/tallynote
|
TALLYNOTE_INSTALL_PREFIX=/opt/tallynote
|
||||||
|
TALLYNOTE_CONFIG_DIR=/etc/tallynote
|
||||||
TALLYNOTE_PUBLIC_ORIGIN=http://127.0.0.1:3000
|
TALLYNOTE_PUBLIC_ORIGIN=http://127.0.0.1:3000
|
||||||
TALLYNOTE_COOKIE_SECURE=false
|
TALLYNOTE_COOKIE_SECURE=false
|
||||||
TALLYNOTE_ALLOW_INSECURE_HTTP=false
|
TALLYNOTE_ALLOW_INSECURE_HTTP=false
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ Group=tallynote
|
|||||||
WorkingDirectory=/opt/tallynote/current
|
WorkingDirectory=/opt/tallynote/current
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
EnvironmentFile=-/etc/tallynote/tallynote.env
|
EnvironmentFile=-/etc/tallynote/tallynote.env
|
||||||
Environment=PATH=/opt/tallynote/current/runtime/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=TALLYNOTE_CONFIG_DIR=/etc/tallynote
|
||||||
|
Environment=PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
||||||
ExecStart=/opt/tallynote/current/bin/tallynote
|
ExecStart=/opt/tallynote/current/bin/tallynote
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|||||||
@@ -455,11 +455,9 @@ describe("更新安全工具", () => {
|
|||||||
await mkdir(path.join(source, "dist", "server"), { recursive: true });
|
await mkdir(path.join(source, "dist", "server"), { recursive: true });
|
||||||
await mkdir(path.join(source, "bin"), { recursive: true });
|
await mkdir(path.join(source, "bin"), { recursive: true });
|
||||||
await mkdir(path.join(source, "scripts"), { recursive: true });
|
await mkdir(path.join(source, "scripts"), { recursive: true });
|
||||||
await mkdir(path.join(source, "runtime", "bin"), { recursive: true });
|
|
||||||
await writeFile(path.join(source, "dist", "server", "large.js"), Buffer.alloc(2 * 1024 * 1024, 0x41));
|
await writeFile(path.join(source, "dist", "server", "large.js"), Buffer.alloc(2 * 1024 * 1024, 0x41));
|
||||||
await writeFile(path.join(source, "bin", "tallynote"), "#!/bin/sh\n");
|
await writeFile(path.join(source, "bin", "tallynote"), "#!/bin/sh\n");
|
||||||
await writeFile(path.join(source, "scripts", "runner.sh"), "#!/bin/sh\n");
|
await writeFile(path.join(source, "scripts", "runner.sh"), "#!/bin/sh\n");
|
||||||
await writeFile(path.join(source, "runtime", "bin", "node"), "node");
|
|
||||||
const archive = path.join(root, "release.tar.gz");
|
const archive = path.join(root, "release.tar.gz");
|
||||||
await createSafeArchive(source, archive);
|
await createSafeArchive(source, archive);
|
||||||
expect((await stat(archive)).size).toBeLessThan(64 * 1024);
|
expect((await stat(archive)).size).toBeLessThan(64 * 1024);
|
||||||
@@ -472,7 +470,7 @@ describe("更新安全工具", () => {
|
|||||||
expect((await stat(path.join(destination, "dist", "server", "large.js"))).mode & 0o777).toBe(0o644);
|
expect((await stat(path.join(destination, "dist", "server", "large.js"))).mode & 0o777).toBe(0o644);
|
||||||
expect((await stat(path.join(destination, "bin", "tallynote"))).mode & 0o777).toBe(0o755);
|
expect((await stat(path.join(destination, "bin", "tallynote"))).mode & 0o777).toBe(0o755);
|
||||||
expect((await stat(path.join(destination, "scripts", "runner.sh"))).mode & 0o777).toBe(0o755);
|
expect((await stat(path.join(destination, "scripts", "runner.sh"))).mode & 0o777).toBe(0o755);
|
||||||
expect((await stat(path.join(destination, "runtime", "bin", "node"))).mode & 0o777).toBe(0o755);
|
expect(await stat(path.join(destination, "runtime")).catch(() => null)).toBeNull();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true });
|
await rm(root, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ remove_prefix() {
|
|||||||
log "warning: 保留非符号链接 current:$current"
|
log "warning: 保留非符号链接 current:$current"
|
||||||
fi
|
fi
|
||||||
remove_tree "$releases" 'releases'
|
remove_tree "$releases" 'releases'
|
||||||
|
remove_managed_node
|
||||||
remove_tree "$PREFIX/.update-work" 'update work'
|
remove_tree "$PREFIX/.update-work" 'update work'
|
||||||
remove_file_if_owned "$PREFIX/.update-state" 'update state'
|
remove_file_if_owned "$PREFIX/.update-state" 'update state'
|
||||||
if [[ -d "$PREFIX" && ! -L "$PREFIX" ]]; then
|
if [[ -d "$PREFIX" && ! -L "$PREFIX" ]]; then
|
||||||
@@ -429,6 +430,33 @@ remove_prefix() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove_managed_node() {
|
||||||
|
local node_root="$PREFIX/nodejs" marker
|
||||||
|
[[ -e "$node_root" || -L "$node_root" ]] || return 0
|
||||||
|
[[ -d "$node_root" && ! -L "$node_root" ]] || die "Node.js 管理目录不是安全目录:$node_root"
|
||||||
|
marker="$node_root/.tallynote-managed"
|
||||||
|
if [[ ! -f "$marker" || "$(sed -n '1p' "$marker" 2>/dev/null)" != tallynote-managed-node-v1 ]]; then
|
||||||
|
log "保留非 TallyNote 管理的 Node.js 目录:$node_root"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
allowed_owner "$node_root" || die "Node.js 管理目录的所有者不受信任:$node_root"
|
||||||
|
# Node distributions contain npm/corepack symlinks. They are safe to remove
|
||||||
|
# because rm never follows symlinks; validate ownership and permissions for
|
||||||
|
# every node while deliberately permitting those internal links.
|
||||||
|
local node mode_bits
|
||||||
|
while IFS= read -r node; do
|
||||||
|
allowed_owner "$node" || die "Node.js 管理目录节点的所有者不受信任:$node"
|
||||||
|
[[ -L "$node" ]] && continue
|
||||||
|
mode_bits=$(stat_mode_bits "$node")
|
||||||
|
(( (mode_bits & 18) == 0 )) || die "Node.js 管理目录权限过宽:$node"
|
||||||
|
done < <(find "$node_root" -print)
|
||||||
|
if (( DRY_RUN )); then
|
||||||
|
log "dry-run: remove managed Node.js $node_root"
|
||||||
|
else
|
||||||
|
rm -rf -- "$node_root"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
remove_config() {
|
remove_config() {
|
||||||
remove_file_if_owned "$CONFIG_DIR/update-signing-key.pub" 'update public key'
|
remove_file_if_owned "$CONFIG_DIR/update-signing-key.pub" 'update public key'
|
||||||
remove_file_if_owned "$CONFIG_DIR/tallynote.env" 'environment file'
|
remove_file_if_owned "$CONFIG_DIR/tallynote.env" 'environment file'
|
||||||
|
|||||||
Reference in New Issue
Block a user