1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Obsidian同期をmtimeで壊さないPython検知

1
Posted at

Obsidian同期をmtimeで壊さないPython検知

今日、Markdown vault を触っていて、mtime 判定の危なさをまた思い出した。

Obsidian みたいに中身がただの Markdown だと、小さい同期スクリプトをすぐ書ける。でも、保存前の競合チェックを mtime だけで済ませると、新しい変更を古い手元編集で潰すことがある。作業メモも Markdown に逃がすので、ここは地味に怖い。

Q. mtime だけで何がまずい?

stat().st_mtime を保存して、書き戻す前にもう一度見る。変わっていたら競合、同じなら保存。最初はこれで十分に見える。

ただ、秒単位に丸めた mtime や、同期ツールが戻したタイムスタンプでは、内容は変わったのに mtime だけ同じ、という状態を作れてしまう。

一覧更新では mtimesize をヒントに使う。でも上書き直前のガードは 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_nssize が同じなら hash 計算を後回しにできる。ただ、上書き可否の根拠にするには弱い。同じバイト数の別本文は普通に作れる。size は候補を減らす道具で、最後は hash で見る。

Q. os.replace は何のため?

競合検知とは別に、書き込み途中の壊れたファイルを残さないため。隣に一時ファイルを書いて、最後に os.replace() で差し替える。

おわりに

mtime は便利だけど、競合検知の主役にするには軽い。Obsidian の vault やエージェントの作業ログみたいに、人間とスクリプトが同じ Markdown を触る場所では、最後の保存前だけでも hash を見る価値がある。

大げさな同期基盤はいらない。sha256os.replace() だけで、「気づかず上書き」はかなり減らせる。今日はこの地味なガードが、いちばん現場っぽく効いた。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?