aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/screenshots/apply.py
blob: 32303c66d3c8140911d3e8e0a3701899dfc556d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Local writes: the doc definition URL and the lock record.

Both are idempotent and refuse to overwrite anything they did not expect,
raising Conflict instead.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Optional

import docrefs
from common import PipelineError, atomic_write_json, atomic_write_text, load_json, read_text_exact, sha256_bytes

LOCK_FIELDS = ("url", "webp_sha256", "pixels_sha256", "gpgfrontend_commit",
               "inputs_hash", "env_hash", "env", "generated_at")
WRITTEN = "written"
ALREADY = "already-applied"


class Conflict(PipelineError):
    pass


def _contains_del(obj: Any) -> bool:
    if isinstance(obj, dict):
        return "del" in obj or any(_contains_del(v) for v in obj.values())
    if isinstance(obj, list):
        return any(_contains_del(v) for v in obj)
    return False


def read_lock(path: Path) -> dict[str, Any]:
    data = load_json(path, {})
    if not isinstance(data, dict):
        raise PipelineError(f"{path}: the lock must be a JSON object keyed by entry id")
    return data


def write_lock(path: Path, lock: dict[str, Any]) -> None:
    if _contains_del(lock):
        raise PipelineError("refusing to write a `del` capability URL into the committed lock")
    atomic_write_json(path, lock)


def doc_sha256(path: Path) -> str:
    return sha256_bytes(Path(path).read_bytes())


def apply_doc(path: Path, label: str, old_url: str, new_url: str, sha_before: str) -> str:
    """Points the single definition of `label` at new_url.

    Unchanged doc: rewrite it. Concurrently edited doc: rewrite only if the
    definition still carries old_url. Already new_url: nothing to do.
    """
    path = Path(path)
    if not path.is_file():
        raise Conflict(f"{path} no longer exists")
    text = read_text_exact(path)
    if sha256_bytes(text.encode("utf-8")) == sha_before:
        atomic_write_text(path, docrefs.replace_definition_url(text, label, new_url, old_url))
        return WRITTEN
    defs = docrefs.find_definitions(text, label)
    if len(defs) != 1:
        raise Conflict(f"{path}: [{label}] now has {len(defs)} definitions")
    if defs[0].url == new_url:
        return ALREADY
    if defs[0].url != old_url:
        raise Conflict(f"{path}: [{label}] was changed to {defs[0].url!r}")
    atomic_write_text(path, docrefs.replace_url(text, defs[0], new_url))
    return WRITTEN


def lock_state(path: Path, entry_id: str, before: Optional[dict[str, Any]],
               target: dict[str, Any]) -> str:
    current = read_lock(path).get(entry_id)
    if current == target:
        return "target"
    if current == before:
        return "before"
    return "other"


def apply_lock(path: Path, entry_id: str, before: Optional[dict[str, Any]], target: dict[str, Any]) -> str:
    lock = read_lock(path)
    current = lock.get(entry_id)
    if current == target:
        return ALREADY
    if current != before:
        raise Conflict(f"lock record for {entry_id} was changed by something else")
    lock[entry_id] = target
    write_lock(path, lock)
    return WRITTEN