Files
Nexus/server/utils/files_upload_permissions.py
2026-07-08 22:31:31 +08:00

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,
}