196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
"""Post-upload permission fixup for file manager (reuse rsync push defaults)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import shlex
|
|
from typing import TYPE_CHECKING
|
|
|
|
from server.application.services.sync_engine_v2 import _RSYNC_CHMOD_RE, _RSYNC_CHOWN_RE
|
|
|
|
if TYPE_CHECKING:
|
|
from server.domain.models import Server
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def parse_upload_dir_mode(chmod_setting: str) -> str | None:
|
|
"""Extract octal directory mode from rsync-style ``D755,F755``."""
|
|
raw = (chmod_setting or "").strip()
|
|
if not raw or not _RSYNC_CHMOD_RE.fullmatch(raw):
|
|
return None
|
|
m = re.search(r"D([0-7]{3,4})", raw, re.IGNORECASE)
|
|
if m:
|
|
return m.group(1)
|
|
return parse_upload_file_mode(raw)
|
|
|
|
|
|
def parse_upload_file_mode(chmod_setting: str) -> str | None:
|
|
"""Extract octal file mode from rsync-style ``D755,F755`` or plain ``644``."""
|
|
raw = (chmod_setting or "").strip()
|
|
if not raw or not _RSYNC_CHMOD_RE.fullmatch(raw):
|
|
return None
|
|
m = re.search(r"F([0-7]{3,4})", raw, re.IGNORECASE)
|
|
if m:
|
|
return m.group(1)
|
|
if re.fullmatch(r"[0-7]{3,4}", raw):
|
|
return raw
|
|
for part in reversed(raw.split(",")):
|
|
piece = part.strip()
|
|
if len(piece) > 1 and piece[0] in "Ff" and re.fullmatch(r"[0-7]{3,4}", piece[1:]):
|
|
return piece[1:]
|
|
return None
|
|
|
|
|
|
def parse_upload_chown(chown_setting: str) -> tuple[str, str] | None:
|
|
"""Return ``(owner, group)`` when setting is valid; else None."""
|
|
raw = (chown_setting or "").strip()
|
|
if not raw or not _RSYNC_CHOWN_RE.fullmatch(raw):
|
|
return None
|
|
if ":" in raw:
|
|
owner, group = raw.split(":", 1)
|
|
return owner.strip(), group.strip()
|
|
return raw, raw
|
|
|
|
|
|
def upload_permission_plan() -> tuple[tuple[str, str] | None, str | None]:
|
|
"""Read ``NEXUS_RSYNC_PUSH_CHOWN`` / ``NEXUS_RSYNC_PUSH_CHMOD`` for uploads."""
|
|
from server.config import settings
|
|
|
|
return (
|
|
parse_upload_chown(settings.RSYNC_PUSH_CHOWN or ""),
|
|
parse_upload_file_mode(settings.RSYNC_PUSH_CHMOD or ""),
|
|
)
|
|
|
|
|
|
async def apply_upload_file_permissions(server: Server, remote_path: str) -> dict:
|
|
"""chown/chmod uploaded file; mirrors rsync push defaults (www:www, F755)."""
|
|
from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
|
|
|
|
chown_pair, file_mode = upload_permission_plan()
|
|
if not chown_pair and not file_mode:
|
|
return {"applied": False, "skipped": True}
|
|
|
|
path_q = shlex.quote(remote_path)
|
|
elevated = False
|
|
errors: list[str] = []
|
|
|
|
if chown_pair:
|
|
owner_spec = shlex.quote(f"{chown_pair[0]}:{chown_pair[1]}")
|
|
result = await exec_ssh_command_with_fallback(
|
|
server, f"chown {owner_spec} {path_q}", timeout=30,
|
|
)
|
|
elevated = bool(result.get("elevated"))
|
|
if result.get("exit_code") != 0:
|
|
err = ((result.get("stderr") or "") + (result.get("stdout") or "")).strip()[:200]
|
|
errors.append(err or "chown 失败")
|
|
|
|
if file_mode and not errors:
|
|
result = await exec_ssh_command_with_fallback(
|
|
server, f"chmod {file_mode} {path_q}", timeout=30,
|
|
)
|
|
elevated = elevated or bool(result.get("elevated"))
|
|
if result.get("exit_code") != 0:
|
|
err = ((result.get("stderr") or "") + (result.get("stdout") or "")).strip()[:200]
|
|
errors.append(err or "chmod 失败")
|
|
|
|
if errors:
|
|
logger.warning(
|
|
"Upload permission fixup failed for %s on server %s: %s",
|
|
remote_path,
|
|
getattr(server, "id", "?"),
|
|
"; ".join(errors),
|
|
)
|
|
|
|
owner, group = chown_pair if chown_pair else (None, None)
|
|
return {
|
|
"applied": not errors and bool(chown_pair or file_mode),
|
|
"skipped": False,
|
|
"owner": owner,
|
|
"group": group,
|
|
"mode": file_mode,
|
|
"elevated": elevated,
|
|
"error": "; ".join(errors) if errors else None,
|
|
}
|
|
|
|
|
|
async def apply_transfer_permissions(
|
|
server: Server,
|
|
remote_path: str,
|
|
*,
|
|
recursive: bool = True,
|
|
) -> dict:
|
|
"""chown/chmod after cross-server relay or package deliver (default www:www, D755/F755)."""
|
|
from server.config import settings
|
|
from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
|
|
|
|
chown_pair, file_mode = upload_permission_plan()
|
|
dir_mode = parse_upload_dir_mode(settings.RSYNC_PUSH_CHMOD or "")
|
|
if not chown_pair and not file_mode:
|
|
return {"applied": False, "skipped": True, "path": remote_path}
|
|
|
|
path_q = shlex.quote(remote_path)
|
|
elevated = False
|
|
errors: list[str] = []
|
|
|
|
if chown_pair:
|
|
owner_spec = shlex.quote(f"{chown_pair[0]}:{chown_pair[1]}")
|
|
chown_flag = "-R " if recursive else ""
|
|
result = await exec_ssh_command_with_fallback(
|
|
server,
|
|
f"chown {chown_flag}{owner_spec} {path_q}",
|
|
timeout=120,
|
|
)
|
|
elevated = bool(result.get("elevated"))
|
|
if result.get("exit_code") != 0:
|
|
err = ((result.get("stderr") or "") + (result.get("stdout") or "")).strip()[:200]
|
|
errors.append(err or "chown 失败")
|
|
|
|
if file_mode and not errors:
|
|
if recursive and dir_mode and dir_mode != file_mode:
|
|
for kind, mode in (("d", dir_mode), ("f", file_mode)):
|
|
result = await exec_ssh_command_with_fallback(
|
|
server,
|
|
f"find {path_q} -type {kind} -exec chmod {shlex.quote(mode)} {{}} +",
|
|
timeout=120,
|
|
)
|
|
elevated = elevated or bool(result.get("elevated"))
|
|
if result.get("exit_code") != 0:
|
|
err = ((result.get("stderr") or "") + (result.get("stdout") or "")).strip()[:200]
|
|
errors.append(err or f"chmod {kind} 失败")
|
|
break
|
|
else:
|
|
mode = file_mode
|
|
chmod_flag = "-R " if recursive else ""
|
|
result = await exec_ssh_command_with_fallback(
|
|
server,
|
|
f"chmod {chmod_flag}{shlex.quote(mode)} {path_q}",
|
|
timeout=120,
|
|
)
|
|
elevated = elevated or bool(result.get("elevated"))
|
|
if result.get("exit_code") != 0:
|
|
err = ((result.get("stderr") or "") + (result.get("stdout") or "")).strip()[:200]
|
|
errors.append(err or "chmod 失败")
|
|
|
|
if errors:
|
|
logger.warning(
|
|
"Transfer permission fixup failed for %s on server %s: %s",
|
|
remote_path,
|
|
getattr(server, "id", "?"),
|
|
"; ".join(errors),
|
|
)
|
|
|
|
owner, group = chown_pair if chown_pair else (None, None)
|
|
return {
|
|
"applied": not errors and bool(chown_pair or file_mode),
|
|
"skipped": False,
|
|
"path": remote_path,
|
|
"owner": owner,
|
|
"group": group,
|
|
"dir_mode": dir_mode,
|
|
"file_mode": file_mode,
|
|
"elevated": elevated,
|
|
"error": "; ".join(errors) if errors else None,
|
|
}
|