s
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user