fix(install): 安装锁定后将 install.html 归档为 .bak

防止已安装环境继续提供安装向导静态页;entrypoint 与升级脚本避免容器重建后恢复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Deploy
2026-06-06 21:30:15 +08:00
parent dd66d99f2d
commit 58bdf820dc
6 changed files with 131 additions and 6 deletions
+34 -4
View File
@@ -333,6 +333,7 @@ set_install_complete() {
[[ -n "$cname" ]] || return 0 [[ -n "$cname" ]] || return 0
restore_install_lock_in_container "$cname" restore_install_lock_in_container "$cname"
if nexus_container_has_install_lock "$cname"; then if nexus_container_has_install_lock "$cname"; then
archive_install_wizard_in_container "$cname"
upsert_env_var "$env_file" "NEXUS_INSTALL_WIZARD_PENDING" "0" upsert_env_var "$env_file" "NEXUS_INSTALL_WIZARD_PENDING" "0"
info "安装已完成:NEXUS_INSTALL_WIZARD_PENDING=0" info "安装已完成:NEXUS_INSTALL_WIZARD_PENDING=0"
fi fi
@@ -897,10 +898,33 @@ compose_up() {
verify_nexus_1panel_network "$root" || return 1 verify_nexus_1panel_network "$root" || return 1
} }
archive_install_wizard_in_container() {
local cname="$1"
[[ -n "$cname" ]] || return 0
docker exec "$cname" sh -c '
if [ -f /app/web/app/install.html.bak ]; then
rm -f /app/web/app/install.html
elif [ -f /app/web/app/install.html ]; then
mv /app/web/app/install.html /app/web/app/install.html.bak
fi
' 2>/dev/null || true
}
verify_install_wizard() { verify_install_wizard() {
local root="$1" local root="$1"
local port="${2:-$(nexus_publish_port "$root")}" local port="${2:-$(nexus_publish_port "$root")}"
local code local code cname
cname="$(nexus_container_name)"
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
archive_install_wizard_in_container "$cname"
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
if [[ "$code" == "404" ]]; then
info " /app/install.html 已归档 → HTTP 404"
return 0
fi
warn " 已锁定但 /app/install.html → HTTP $code"
return 1
fi
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000") code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
if [[ "$code" == "200" ]]; then if [[ "$code" == "200" ]]; then
info " /app/install.html → HTTP 200" info " /app/install.html → HTTP 200"
@@ -1167,9 +1191,15 @@ cmd_upgrade() {
[[ "$health" == "ok" ]] && break [[ "$health" == "ok" ]] && break
sleep 2 sleep 2
done done
wiz_code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")" cname="$(nexus_container_name)"
if [[ "$wiz_code" != "200" && -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" archive_install_wizard_in_container "$cname"
info "安装已锁定,跳过 install.html 热同步"
else
wiz_code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")"
if [[ "$wiz_code" != "200" && -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
fi
fi fi
set_install_complete "$NEXUS_ROOT" set_install_complete "$NEXUS_ROOT"
if [[ "$PRUNE_IMAGES" == true ]]; then if [[ "$PRUNE_IMAGES" == true ]]; then
+8 -2
View File
@@ -41,8 +41,14 @@ main() {
exit 1 exit 1
fi fi
resolve_container resolve_container
info "同步安装向导静态文件..." if docker exec "$CONTAINER" test -f /app/.install_locked 2>/dev/null \
sync_file "$app/install.html" /app/web/app/install.html || docker exec "$CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|| docker exec "$CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
info "安装已锁定或向导已归档,跳过 install.html 同步"
else
info "同步安装向导静态文件..."
sync_file "$app/install.html" /app/web/app/install.html
fi
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
+18
View File
@@ -54,6 +54,23 @@ restore_persisted_install_lock() {
fi fi
} }
archive_install_wizard_if_locked() {
if [ ! -f /app/.install_locked ] && [ ! -f "${PERSIST_DIR}/.install_locked" ]; then
return 0
fi
if [ -f /app/web/app/install.html.bak ]; then
if [ -f /app/web/app/install.html ]; then
rm -f /app/web/app/install.html
echo "entrypoint: removed install.html (wizard archived as install.html.bak)"
fi
return 0
fi
if [ -f /app/web/app/install.html ]; then
mv /app/web/app/install.html /app/web/app/install.html.bak
echo "entrypoint: archived install.html → install.html.bak"
fi
}
restore_persisted_env() { restore_persisted_env() {
if [ ! -f "${PERSIST_ENV}" ]; then if [ ! -f "${PERSIST_ENV}" ]; then
return 0 return 0
@@ -136,6 +153,7 @@ if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT
fi fi
restore_persisted_install_lock restore_persisted_install_lock
archive_install_wizard_if_locked
restore_persisted_env restore_persisted_env
write_env_from_environment write_env_from_environment
export_app_env export_app_env
@@ -0,0 +1,34 @@
# 安装完成后归档 install.html
| 项 | 内容 |
|----|------|
| 日期 | 2026-06-06 |
| 动机 | 已安装环境仍可访问 `/app/install.html` 并展示「系统已安装」,应物理移除向导页 |
## 变更摘要
- `install.py``_finalize_install_lock()``web/app/install.html` 重命名为 `install.html.bak`
- `entrypoint.sh`:检测到安装锁时启动阶段归档(防止镜像重建恢复向导页)。
- `sync-install-wizard-to-container.sh`:已锁定/已归档时不再覆盖 `install.html`
- `nexus-1panel.sh`:升级与健康检查在已安装时期望 `install.html` → HTTP 404。
## 涉及文件
- `server/api/install.py`
- `docker/entrypoint.sh`
- `deploy/sync-install-wizard-to-container.sh`
- `deploy/nexus-1panel.sh`
- `tests/test_security_unit.py`
## 迁移 / 重启
- 需重建 Nexus 镜像(entrypoint)并重启容器。
- 已安装实例:`nx update``set_install_complete` / entrypoint 自动归档;或手动 `mv install.html install.html.bak`
## 验证
```bash
pytest tests/test_security_unit.py -k archive_install -q
curl -s -o /dev/null -w "%{http_code}\n" https://api.synaglobal.vip/app/install.html # 404
docker exec nexus-prod-nexus-1 test -f /app/web/app/install.html.bak
```
+21
View File
@@ -31,6 +31,8 @@ router = APIRouter(prefix="/api/install", tags=["install"])
ROOT_DIR = Path(__file__).resolve().parent.parent.parent ROOT_DIR = Path(__file__).resolve().parent.parent.parent
ENV_FILE = ROOT_DIR / ".env" ENV_FILE = ROOT_DIR / ".env"
INSTALL_LOCK = ROOT_DIR / ".install_locked" INSTALL_LOCK = ROOT_DIR / ".install_locked"
INSTALL_HTML = ROOT_DIR / "web" / "app" / "install.html"
INSTALL_HTML_BAK = ROOT_DIR / "web" / "app" / "install.html.bak"
CONFIG_DIR = ROOT_DIR / "web" / "data" CONFIG_DIR = ROOT_DIR / "web" / "data"
CONFIG_JSON = CONFIG_DIR / "config.json" CONFIG_JSON = CONFIG_DIR / "config.json"
ONEPANEL_HOSTS_JSON = CONFIG_DIR / "1panel-hosts.json" ONEPANEL_HOSTS_JSON = CONFIG_DIR / "1panel-hosts.json"
@@ -854,6 +856,24 @@ def _clear_state_redis_pass() -> None:
write_utf8_lf(STATE_FILE, json.dumps(state, ensure_ascii=False) + "\n") write_utf8_lf(STATE_FILE, json.dumps(state, ensure_ascii=False) + "\n")
def _archive_install_wizard_html() -> None:
"""Rename install.html → install.html.bak so the wizard is not served after lock."""
if INSTALL_HTML_BAK.is_file():
if INSTALL_HTML.is_file():
try:
INSTALL_HTML.unlink()
except OSError as e:
logger.warning("Failed to remove install.html after archive: %s", e)
return
if not INSTALL_HTML.is_file():
return
try:
INSTALL_HTML.rename(INSTALL_HTML_BAK)
logger.info("Archived install wizard: %s%s", INSTALL_HTML, INSTALL_HTML_BAK)
except OSError as e:
logger.warning("Failed to archive install.html: %s", e)
def _finalize_install_lock() -> None: def _finalize_install_lock() -> None:
"""Write install lock marker and remove wizard state file.""" """Write install lock marker and remove wizard state file."""
_sync_env_to_persist_volume() _sync_env_to_persist_volume()
@@ -862,6 +882,7 @@ def _finalize_install_lock() -> None:
f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n", f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n",
) )
_sync_lock_to_persist_volume() _sync_lock_to_persist_volume()
_archive_install_wizard_html()
if STATE_FILE.exists(): if STATE_FILE.exists():
try: try:
STATE_FILE.unlink() STATE_FILE.unlink()
+16
View File
@@ -403,6 +403,22 @@ def test_script_aggregate_empty_server_ids_no_pending():
assert _aggregate_execution_status([], {}) == "failed" assert _aggregate_execution_status([], {}) == "failed"
def test_archive_install_wizard_html(monkeypatch, tmp_path):
from server.api import install as install_api
app_dir = tmp_path / "web" / "app"
app_dir.mkdir(parents=True)
html = app_dir / "install.html"
html.write_text("<html>wizard</html>", encoding="utf-8")
monkeypatch.setattr(install_api, "INSTALL_HTML", html)
monkeypatch.setattr(install_api, "INSTALL_HTML_BAK", app_dir / "install.html.bak")
install_api._archive_install_wizard_html()
assert not html.is_file()
assert (app_dir / "install.html.bak").read_text(encoding="utf-8") == "<html>wizard</html>"
def test_sync_lock_to_persist_volume(monkeypatch, tmp_path): def test_sync_lock_to_persist_volume(monkeypatch, tmp_path):
from server.api import install as install_api from server.api import install as install_api