Add optimistic locking: return base_hash on load, reject 409 on conflict
This commit is contained in:
+39
-6
@@ -49,14 +49,34 @@ _GIT_DIR = os.environ.get("GIT_DIR")
|
|||||||
_git_lock = asyncio.Lock()
|
_git_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
async def _git_commit(file_path: Path, handle: str, verb: str) -> None:
|
def _git_env() -> dict:
|
||||||
rel = str(file_path.relative_to(_ROOT))
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if _GIT_DIR:
|
if _GIT_DIR:
|
||||||
env["GIT_DIR"] = _GIT_DIR
|
env["GIT_DIR"] = _GIT_DIR
|
||||||
env["GIT_WORK_TREE"] = str(_ROOT)
|
env["GIT_WORK_TREE"] = str(_ROOT)
|
||||||
env.setdefault("GIT_COMMITTER_NAME", "bincio-wiki")
|
env.setdefault("GIT_COMMITTER_NAME", "bincio-wiki")
|
||||||
env.setdefault("GIT_COMMITTER_EMAIL", "wiki@bincio.wiki")
|
env.setdefault("GIT_COMMITTER_EMAIL", "wiki@bincio.wiki")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
async def _git_file_hash(file_path: Path) -> str:
|
||||||
|
rel = str(file_path.relative_to(_ROOT))
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"git", "log", "-1", "--format=%H", "--", rel,
|
||||||
|
cwd=str(_ROOT), env=_git_env(),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
stdout, _ = await proc.communicate()
|
||||||
|
return stdout.decode().strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
async def _git_commit(file_path: Path, handle: str, verb: str) -> None:
|
||||||
|
rel = str(file_path.relative_to(_ROOT))
|
||||||
|
env = _git_env()
|
||||||
try:
|
try:
|
||||||
async with _git_lock:
|
async with _git_lock:
|
||||||
add = await asyncio.create_subprocess_exec(
|
add = await asyncio.create_subprocess_exec(
|
||||||
@@ -256,6 +276,7 @@ def _slug_to_path(slug: str, base: Path) -> Path:
|
|||||||
|
|
||||||
class PageBody(BaseModel):
|
class PageBody(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
|
base_hash: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def _list(base: Path) -> list[str]:
|
def _list(base: Path) -> list[str]:
|
||||||
@@ -263,11 +284,13 @@ def _list(base: Path) -> list[str]:
|
|||||||
return []
|
return []
|
||||||
return [str(p.relative_to(base).with_suffix("")) for p in sorted(base.rglob("*.md"))]
|
return [str(p.relative_to(base).with_suffix("")) for p in sorted(base.rglob("*.md"))]
|
||||||
|
|
||||||
def _get(slug: str, base: Path) -> JSONResponse:
|
async def _get(slug: str, base: Path) -> JSONResponse:
|
||||||
path = _slug_to_path(slug, base)
|
path = _slug_to_path(slug, base)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise HTTPException(404, "Not found")
|
raise HTTPException(404, "Not found")
|
||||||
return JSONResponse({"slug": slug, "content": path.read_text(encoding="utf-8")})
|
content = path.read_text(encoding="utf-8")
|
||||||
|
base_hash = await _git_file_hash(path)
|
||||||
|
return JSONResponse({"slug": slug, "content": content, "base_hash": base_hash})
|
||||||
|
|
||||||
def _save(slug: str, body: PageBody, base: Path) -> JSONResponse:
|
def _save(slug: str, body: PageBody, base: Path) -> JSONResponse:
|
||||||
path = _slug_to_path(slug, base)
|
path = _slug_to_path(slug, base)
|
||||||
@@ -295,8 +318,13 @@ async def get_page(slug: str, user: User = Depends(require_auth)) -> JSONRespons
|
|||||||
|
|
||||||
@app.post("/pages/{slug:path}")
|
@app.post("/pages/{slug:path}")
|
||||||
async def save_page(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse:
|
async def save_page(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse:
|
||||||
|
path = _slug_to_path(slug, pages_dir)
|
||||||
|
if body.base_hash is not None:
|
||||||
|
current = await _git_file_hash(path)
|
||||||
|
if current and current != body.base_hash:
|
||||||
|
raise HTTPException(409, "Pagina modificata da qualcun altro — ricarica prima di salvare")
|
||||||
result = _save(slug, body, pages_dir)
|
result = _save(slug, body, pages_dir)
|
||||||
await _git_commit(_slug_to_path(slug, pages_dir), user.handle, "edited")
|
await _git_commit(path, user.handle, "edited")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@app.delete("/pages/{slug:path}")
|
@app.delete("/pages/{slug:path}")
|
||||||
@@ -319,8 +347,13 @@ async def get_story(slug: str, user: User = Depends(require_auth)) -> JSONRespon
|
|||||||
|
|
||||||
@app.post("/stories/{slug:path}")
|
@app.post("/stories/{slug:path}")
|
||||||
async def save_story(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse:
|
async def save_story(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse:
|
||||||
|
path = _slug_to_path(slug, stories_dir)
|
||||||
|
if body.base_hash is not None:
|
||||||
|
current = await _git_file_hash(path)
|
||||||
|
if current and current != body.base_hash:
|
||||||
|
raise HTTPException(409, "Pagina modificata da qualcun altro — ricarica prima di salvare")
|
||||||
result = _save(slug, body, stories_dir)
|
result = _save(slug, body, stories_dir)
|
||||||
await _git_commit(_slug_to_path(slug, stories_dir), user.handle, "edited")
|
await _git_commit(path, user.handle, "edited")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@app.delete("/stories/{slug:path}")
|
@app.delete("/stories/{slug:path}")
|
||||||
|
|||||||
+1
-1
Submodule site updated: d6eda096d9...1539bdb3a7
Reference in New Issue
Block a user