Obsidian同期をmtimeで壊さないPython検知
今日、Markdown vault を触っていて、mtime 判定の危なさをまた思い出した。
Obsidian みたいに中身がただの Markdown だと、小さい同期スクリプトをすぐ書ける。でも、保存前の競合チェックを mtime だけで済ませると、新しい変更を古い手元編集で潰すことがある。作業メモも Markdown に逃がすので、ここは地味に怖い。
Q. mtime だけで何がまずい?
stat().st_mtime を保存して、書き戻す前にもう一度見る。変わっていたら競合、同じなら保存。最初はこれで十分に見える。
ただ、秒単位に丸めた mtime や、同期ツールが戻したタイムスタンプでは、内容は変わったのに mtime だけ同じ、という状態を作れてしまう。
一覧更新では mtime と size をヒントに使う。でも上書き直前のガードは content hash に寄せる。
この流れを図にすると、こんな感じ。
Q. 最小コードは?
検証コード。mtime だけでは見落とす変更を、sha256 の digest で止める。
from dataclasses import dataclass
from hashlib import sha256
from pathlib import Path
import os, tempfile
@dataclass(frozen=True)
class Mark:
mtime_sec: int
size: int
digest: str
def mark(p: Path) -> Mark:
st = p.stat()
return Mark(int(st.st_mtime), st.st_size, sha256(p.read_bytes()).hexdigest()[:12])
def guarded_write(p: Path, base: Mark, text: str) -> None:
now = mark(p)
if now.digest != base.digest:
raise RuntimeError(f"conflict: {base.digest} -> {now.digest}")
tmp = p.with_suffix(p.suffix + ".tmp")
tmp.write_text(text, encoding="utf-8")
os.replace(tmp, p)
with tempfile.TemporaryDirectory() as d:
note = Path(d) / "note.md"
note.write_text("# memo\nlocal draft\n", encoding="utf-8")
base = mark(note)
note.write_text("# memo\nremote edit\n", encoding="utf-8")
os.utime(note, (base.mtime_sec, base.mtime_sec))
now = mark(note)
print(f"mtime_only_changed={now.mtime_sec != base.mtime_sec}")
print(f"hash_changed={now.digest != base.digest}")
try:
guarded_write(note, base, "# memo\nmy edit\n")
except RuntimeError as e:
print(f"guard={e}")
Python 3.12 で実行した出力。
mtime_only_changed=False
hash_changed=True
guard=conflict: c755ec4d9853 -> 6fea4156052f
1行目が罠。秒単位の mtime だけなら「変わってない」と判断する。でも本文は変わっているので、2行目では hash_changed=True になる。
Q. size も見れば足りる?
size は高速化には使える。大量の Markdown をなめる時、mtime_ns と size が同じなら hash 計算を後回しにできる。ただ、上書き可否の根拠にするには弱い。同じバイト数の別本文は普通に作れる。size は候補を減らす道具で、最後は hash で見る。
Q. os.replace は何のため?
競合検知とは別に、書き込み途中の壊れたファイルを残さないため。隣に一時ファイルを書いて、最後に os.replace() で差し替える。
おわりに
mtime は便利だけど、競合検知の主役にするには軽い。Obsidian の vault やエージェントの作業ログみたいに、人間とスクリプトが同じ Markdown を触る場所では、最後の保存前だけでも hash を見る価値がある。
大げさな同期基盤はいらない。sha256 と os.replace() だけで、「気づかず上書き」はかなり減らせる。今日はこの地味なガードが、いちばん現場っぽく効いた。