#!/usr/bin/env python3 """Create today's note from the most recent one, pruning finished work. Runs on the server, once, at 00:01. A single writer is the whole point: two clients each creating today's file independently is what produced the Syncthing conflicts this system is replacing. Never modifies an existing file. If today's note is already there it exits without touching anything, so re-running is always safe. """ import argparse import os import re import stat import sys import tempfile from datetime import datetime from zoneinfo import ZoneInfo sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from prune import prune NOTE = re.compile(r"^\d{4}-\d{2}-\d{2}\.md$") DATE_HEADING = re.compile(r"^#\s+\d{4}-\d{2}-\d{2}\s*$") def latest_note(d, exclude=None): """Newest note by filename. Deliberately not by mtime: this tree is on NFS, where mtimes come from the server's clock rather than the writer's.""" notes = sorted(f for f in os.listdir(d) if NOTE.match(f) and f != exclude) return notes[-1] if notes else None def retitle(text, today): """Replace the date heading on line 1, or prepend one if it is missing.""" lines = text.split("\n") if lines and DATE_HEADING.match(lines[0]): lines[0] = f"# {today}" return "\n".join(lines) return f"# {today}\n\n" + text def write_atomic(path, text, mode): d = os.path.dirname(path) fd, tmp = tempfile.mkstemp(dir=d, prefix=".rollover-", suffix=".tmp") try: with os.fdopen(fd, "w") as fh: fh.write(text) # mkstemp makes the file private; the notes are 0644 and syncthing and # sftpgo both read this tree, so carry the previous note's mode over. os.chmod(tmp, mode) os.replace(tmp, path) except BaseException: os.path.exists(tmp) and os.unlink(tmp) raise def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--dir", default=os.path.expanduser("~/docs/notes/todo")) ap.add_argument("--tz", default="America/Louisville") ap.add_argument("--date", help="override today, for testing") ap.add_argument("--dry-run", action="store_true", help="print, do not write") a = ap.parse_args() today = a.date or datetime.now(ZoneInfo(a.tz)).strftime("%Y-%m-%d") target = os.path.join(a.dir, f"{today}.md") if os.path.exists(target) and not a.dry_run: print(f"{today}.md already exists, nothing to do") return 0 prev = latest_note(a.dir, exclude=f"{today}.md") if prev is None: sys.exit(f"no notes in {a.dir} to roll forward from, refusing to guess") src = os.path.join(a.dir, prev) mode = stat.S_IMODE(os.stat(src).st_mode) body = retitle(prune(open(src).read()), today) if a.dry_run: sys.stdout.write(body) print(f"\n--- would write {target} from {prev} ---", file=sys.stderr) return 0 write_atomic(target, body, mode) print(f"created {today}.md from {prev}") return 0 if __name__ == "__main__": sys.exit(main())