fix(sync): rsync 推送后设置远程属主 www 与权限 755
推送默认 --chown=www:www --chmod=D755,F755,避免文件落盘为 root。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Changelog — 2026-06-07 推送文件默认 www:755
|
||||
|
||||
## 摘要
|
||||
|
||||
文件推送(rsync)后远程文件保留 SSH 用户(多为 root)属主与源权限,导致站点目录下文件为 root,Web 进程(www)无法读写。
|
||||
|
||||
## 动机
|
||||
|
||||
用户反馈:「推送文件权限问题,推送过去的文件应该为 www 755 不是 root」。
|
||||
|
||||
## 变更
|
||||
|
||||
- `server/config.py`:新增 `NEXUS_RSYNC_PUSH_CHOWN`(默认 `www:www`)、`NEXUS_RSYNC_PUSH_CHMOD`(默认 `D755,F755`)
|
||||
- `sync_engine_v2._rsync_push`:非 dry-run 时附加 `--chown` / `--chmod`(需对端 rsync 以 root 接收,与宝塔推送习惯一致)
|
||||
- `tests/test_rsync_push_permissions.py`:默认值与非法 chown 拒绝
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/config.py`
|
||||
- `server/application/services/sync_engine_v2.py`
|
||||
- `tests/test_rsync_push_permissions.py`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
- 需重启 API / 重建 Docker 镜像
|
||||
- 无 DB 迁移;可通过 `.env` 置空 `NEXUS_RSYNC_PUSH_CHOWN=` 关闭
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
. venv/bin/activate && pytest tests/test_rsync_push_permissions.py -q
|
||||
# 推送后子机: ls -la <target_path> 应为 www www,目录/文件 755
|
||||
```
|
||||
@@ -38,6 +38,33 @@ logger = logging.getLogger("nexus.sync_v2")
|
||||
MAX_CONCURRENT = 10
|
||||
RSYNC_TIMEOUT = 300
|
||||
|
||||
# rsync --chown / --chmod value validation (prevent shell injection via .env)
|
||||
_RSYNC_CHOWN_RE = re.compile(r"^[a-zA-Z0-9_.-]+(:[a-zA-Z0-9_.-]+)?$")
|
||||
_RSYNC_CHMOD_RE = re.compile(r"^[DF0-9a-zA-Z=+,:-]+$")
|
||||
|
||||
|
||||
def rsync_push_permission_args() -> list[str]:
|
||||
"""Extra rsync flags: set remote owner/mode after push (default www:www, 755).
|
||||
|
||||
Requires SSH/rsync receiver to run as root (typical Baota push). Empty env disables.
|
||||
"""
|
||||
from server.config import settings
|
||||
|
||||
args: list[str] = []
|
||||
chown = (settings.RSYNC_PUSH_CHOWN or "").strip()
|
||||
chmod = (settings.RSYNC_PUSH_CHMOD or "").strip()
|
||||
if chown:
|
||||
if _RSYNC_CHOWN_RE.fullmatch(chown):
|
||||
args.extend(["--chown", chown])
|
||||
else:
|
||||
logger.warning("Ignoring invalid NEXUS_RSYNC_PUSH_CHOWN: %r", chown)
|
||||
if chmod:
|
||||
if _RSYNC_CHMOD_RE.fullmatch(chmod):
|
||||
args.extend(["--chmod", chmod])
|
||||
else:
|
||||
logger.warning("Ignoring invalid NEXUS_RSYNC_PUSH_CHMOD: %r", chmod)
|
||||
return args
|
||||
|
||||
|
||||
class SyncEngineV2:
|
||||
"""Unified sync engine: rsync push from Nexus + preview + ZIP upload"""
|
||||
@@ -436,6 +463,9 @@ async def _rsync_push(
|
||||
if verbose:
|
||||
args.append("-v")
|
||||
|
||||
if not dry_run:
|
||||
args.extend(rsync_push_permission_args())
|
||||
|
||||
# SSH options: port + no strict host key checking (matches asyncssh pool behavior)
|
||||
ssh_opts = f"ssh -o StrictHostKeyChecking=no -p {ssh_port}"
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@ class Settings(BaseSettings):
|
||||
SCRIPT_EXEC_BATCH_SIZE: int = 50
|
||||
SCRIPT_EXEC_CONCURRENCY: int = 10 # max parallel SSH exec calls per batch
|
||||
|
||||
# Rsync push — remote owner/mode after file sync (Baota/1Panel www site convention)
|
||||
RSYNC_PUSH_CHOWN: str = "www:www"
|
||||
RSYNC_PUSH_CHMOD: str = "D755,F755"
|
||||
|
||||
# Alert thresholds (mutable — configurable from settings UI)
|
||||
CPU_ALERT_THRESHOLD: int = 80
|
||||
MEM_ALERT_THRESHOLD: int = 80
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Tests for rsync push permission flags."""
|
||||
|
||||
from server.application.services.sync_engine_v2 import rsync_push_permission_args
|
||||
from server.config import settings
|
||||
|
||||
|
||||
def test_rsync_push_permission_defaults():
|
||||
args = rsync_push_permission_args()
|
||||
assert "--chown" in args
|
||||
assert "www:www" in args
|
||||
assert "--chmod" in args
|
||||
assert "D755,F755" in args
|
||||
|
||||
|
||||
def test_rsync_push_permission_disabled(monkeypatch):
|
||||
monkeypatch.setattr(settings, "RSYNC_PUSH_CHOWN", "")
|
||||
monkeypatch.setattr(settings, "RSYNC_PUSH_CHMOD", "")
|
||||
assert rsync_push_permission_args() == []
|
||||
|
||||
|
||||
def test_rsync_push_permission_rejects_invalid_chown(monkeypatch):
|
||||
monkeypatch.setattr(settings, "RSYNC_PUSH_CHOWN", "www;rm -rf /")
|
||||
monkeypatch.setattr(settings, "RSYNC_PUSH_CHMOD", "")
|
||||
assert rsync_push_permission_args() == []
|
||||
Reference in New Issue
Block a user