Migrate MVLog worker to webserver-local deployment

This commit is contained in:
hbrain 2026-06-07 15:22:03 +00:00
parent 8fc2e3b4c9
commit 85c4333e77
6 changed files with 126 additions and 19 deletions

View file

@ -51,8 +51,13 @@ def remote_q(path: str) -> str:
return quote(path)
def list_jobs(host: str, remote_root: str) -> list[str]:
cmd = f"cd {remote_q(remote_root)} && find in-dir -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' | sort"
def list_jobs(host: str | None, remote_root: str) -> list[str]:
if host is None:
root = Path(remote_root) / "in-dir"
if not root.is_dir():
return []
return sorted(path.name for path in root.iterdir() if path.is_dir() and not path.name.startswith('.'))
cmd = f"cd {remote_q(remote_root)} && find in-dir -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\\n' | sort"
out = ssh(host, cmd).stdout
return [line.strip() for line in out.splitlines() if line.strip()]
@ -80,17 +85,41 @@ def read_article_id(host: str | None, job_dir: str) -> str | None:
return None
def acquire_lock(host: str, job_dir: str) -> bool:
def acquire_lock(host: str | None, job_dir: str) -> bool:
lock = f"{job_dir}/{LOCK_NAME}"
if host is None:
try:
Path(lock).mkdir()
Path(lock, "started_at").write_text(now() + "\n", encoding="utf-8")
return True
except FileExistsError:
return False
cmd = f"mkdir {remote_q(lock)} 2>/dev/null && date -Iseconds > {remote_q(lock + '/started_at')}"
return ssh(host, cmd, check=False).returncode == 0
def release_lock(host: str, job_dir: str) -> None:
def release_lock(host: str | None, job_dir: str) -> None:
if host is None:
shutil.rmtree(Path(job_dir) / LOCK_NAME, ignore_errors=True)
return
ssh(host, f"rm -rf {remote_q(job_dir + '/' + LOCK_NAME)}", check=False)
def remote_fingerprint(host: str, job_dir: str) -> str:
def remote_fingerprint(host: str | None, job_dir: str) -> str:
if host is None:
entries = []
root = Path(job_dir)
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d != LOCK_NAME]
for name in filenames:
if name in {STATE_NAME, ENABLED_NAME, PREVIEW_NAME, HIDDEN_NAME, PERMALINK_NAME, ARTICLE_ID_NAME, '.mvlog-hide-map'}:
continue
path = Path(dirpath) / name
rel = path.relative_to(root).as_posix()
st = path.stat()
entries.append([rel, st.st_size, st.st_mtime_ns])
entries.sort()
return hashlib.sha256(json.dumps(entries, ensure_ascii=False, separators=(",", ":")).encode("utf-8")).hexdigest()
script = r'''
import hashlib, json, os, sys
root = sys.argv[1]
@ -169,15 +198,28 @@ def write_state(host: str | None, job_dir: str, state: dict) -> None:
ssh(host, cmd, input_text=data)
def copy_job_from_remote(host: str, job_dir: str, local_input: Path) -> None:
def copy_job_from_remote(host: str | None, job_dir: str, local_input: Path) -> None:
local_input.mkdir(parents=True, exist_ok=True)
excludes = {LOCK_NAME, STATE_NAME, PERMALINK_NAME}
if host is None:
src = Path(job_dir)
for path in src.rglob("*"):
if any(part in excludes for part in path.relative_to(src).parts):
continue
dest = local_input / path.relative_to(src)
if path.is_dir():
dest.mkdir(parents=True, exist_ok=True)
elif path.is_file():
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dest)
return
remote_cmd = (
f"tar --exclude={quote(LOCK_NAME)} --exclude={quote(STATE_NAME)} --exclude={quote(PERMALINK_NAME)} "
f"-C {remote_q(job_dir)} -cf - ."
)
p1 = subprocess.Popen(["ssh", host, remote_cmd], stdout=subprocess.PIPE)
try:
p2 = subprocess.run(["tar", "-xf", "-", "-C", str(local_input)], stdin=p1.stdout, check=True)
subprocess.run(["tar", "-xf", "-", "-C", str(local_input)], stdin=p1.stdout, check=True)
assert p1.stdout is not None
p1.stdout.close()
rc = p1.wait()
@ -190,8 +232,19 @@ def copy_job_from_remote(host: str, job_dir: str, local_input: Path) -> None:
def publish_output(host: str, remote_root: str, local_mp4: Path, old_output: str | None) -> str:
def publish_output(host: str | None, remote_root: str, local_mp4: Path, old_output: str | None) -> str:
out_name = local_mp4.name
if host is None:
out_dir = Path(remote_root) / "out-dir"
out_dir.mkdir(parents=True, exist_ok=True)
final = out_dir / out_name
tmp = out_dir / f".{out_name}.tmp"
shutil.copy2(local_mp4, tmp)
tmp.replace(final)
(Path(remote_root) / "cache" / "videos.json").unlink(missing_ok=True)
if old_output and old_output != out_name:
(out_dir / old_output).unlink(missing_ok=True)
return out_name
remote_tmp = f"{remote_root}/out-dir/.{out_name}.tmp"
remote_final = f"{remote_root}/out-dir/{out_name}"
subprocess.run(["scp", str(local_mp4), f"{host}:{remote_tmp}"], check=True)
@ -271,7 +324,7 @@ def render_job(local_input: Path, local_out: Path, extra_args: list[str]) -> Pat
return outputs[0]
def process_job(host: str, remote_root: str, job: str, work_dir: Path, extra_args: list[str], force: bool) -> None:
def process_job(host: str | None, remote_root: str, job: str, work_dir: Path, extra_args: list[str], force: bool) -> None:
job_dir = f"{remote_root}/in-dir/{job}"
article_id = read_article_id(host, job_dir)
state = read_state(host, job_dir)