This commit is contained in:
root
2026-05-18 06:48:30 +00:00
parent f1145e3a36
commit e638a4656c
14 changed files with 172 additions and 11 deletions

View File

@@ -29,6 +29,13 @@ def _read_text(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace")
def _raise_write_permission_error(path: Path, exc: OSError) -> None:
raise PermissionError(
f"Permission denied: {path} is not writable by the current web service user. "
"Fix deployment ownership for runtime dirs such as config/logs/backup_config/.pm2/.process_runner/downloads/backup."
) from exc
def _file_kind(path: Path) -> str:
name = path.name.lower()
if name == "url_config.ini" or (name.startswith("url_config.") and name.endswith(".ini")):
@@ -158,13 +165,20 @@ def backup_managed_file(root_id: str, relative_path: str, *, reason: str) -> Opt
if not target.is_file():
return None
hist_dir = _history_dir(root_id, rel_path)
hist_dir.mkdir(parents=True, exist_ok=True)
try:
hist_dir.mkdir(parents=True, exist_ok=True)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(hist_dir, exc)
raise
suffix = target.suffix or ".bak"
backup_name = f"{_now_stamp()}__{reason}{suffix}"
backup_path = hist_dir / backup_name
try:
shutil.copy2(target, backup_path)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(backup_path, exc)
if exc.errno != errno.ENOSPC:
raise
_reclaim_history_space(root_id, rel_path, keep=0)
@@ -172,6 +186,8 @@ def backup_managed_file(root_id: str, relative_path: str, *, reason: str) -> Opt
hist_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(target, backup_path)
except OSError as retry_exc:
if retry_exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(backup_path, retry_exc)
if retry_exc.errno == errno.ENOSPC:
return None
raise
@@ -674,10 +690,17 @@ def save_managed_file(root_id: str, relative_path: str, content: str) -> dict[st
"backup": None,
}
backup = backup_managed_file(root_id, rel_path.as_posix(), reason="save")
target.parent.mkdir(parents=True, exist_ok=True)
try:
target.parent.mkdir(parents=True, exist_ok=True)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(target.parent, exc)
raise
try:
target.write_text(content, encoding="utf-8")
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(target, exc)
if exc.errno != errno.ENOSPC:
raise
_reclaim_history_space(root_id, rel_path, keep=0)
@@ -697,7 +720,12 @@ def delete_managed_file(root_id: str, relative_path: str) -> dict[str, Any]:
if not target.exists():
raise FileNotFoundError(relative_path)
backup = backup_managed_file(root_id, rel_path.as_posix(), reason="delete")
target.unlink()
try:
target.unlink()
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(target, exc)
raise
return {
"root_id": root.root_id,
"path": rel_path.as_posix(),
@@ -725,8 +753,13 @@ def restore_backup(root_id: str, relative_path: str, backup_id: str) -> dict[str
backup = backup_managed_file(root_id, rel_path.as_posix(), reason="restore")
content = _read_text(source)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
try:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM}:
_raise_write_permission_error(target, exc)
raise
return {
"root_id": root.root_id,
"path": rel_path.as_posix(),

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Any, Callable
@@ -10,6 +11,16 @@ except ModuleNotFoundError: # pragma: no cover - Windows fallback for local tes
resource = None
MsgPushFn = Callable[..., Any]
RUNTIME_DIRS = (
"config",
"logs",
"downloads",
"backup",
"backup_config",
".pm2",
".process_runner",
"services/neko/data",
)
def ensure_repo_root(entry_file: str | Path) -> Path:
@@ -31,6 +42,21 @@ def _noop_msg_push(reason: str) -> MsgPushFn:
return _push
def ensure_runtime_layout(base_dir: str | Path) -> list[str]:
root = Path(base_dir).resolve()
issues: list[str] = []
for rel in RUNTIME_DIRS:
path = root / rel
try:
path.mkdir(parents=True, exist_ok=True)
except OSError as exc:
issues.append(f"{path}: mkdir failed: {exc}")
continue
if not os.access(path, os.W_OK | os.X_OK):
issues.append(f"{path}: directory is not writable by uid={os.geteuid()}")
return issues
def compute_nofile_target(soft: int, hard: int, minimum: int = 65536) -> int:
target = max(int(soft), int(minimum))
if resource is not None and hard == resource.RLIM_INFINITY:

View File

@@ -61,6 +61,10 @@ def resolve_live_video_encoder(
return arm_candidates[0], ""
return "", ""
if requested == "h264_qsv":
if has_encoder("h264_vaapi") and has_render_node:
return "h264_vaapi", "LIVE_VIDEO_ENCODER=h264_qsv is not enabled in this Linux stack yet, fallback to h264_vaapi"
return "", "LIVE_VIDEO_ENCODER=h264_qsv is not enabled in this Linux stack yet, fallback to libx264"
if requested == "h264_vaapi" and not has_render_node:
return "", "LIVE_VIDEO_ENCODER=h264_vaapi requires /dev/dri/renderD*; fallback to libx264"
if requested and requested.startswith("h264_") and has_encoder(requested):