37 lines
1.3 KiB
Python
37 lines
1.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Consistent encrypted SQLite backup, verified and atomically published on a separate filesystem."""
|
|
import datetime
|
|
import os
|
|
import pathlib
|
|
import sqlite3
|
|
import sys
|
|
from contextlib import closing
|
|
|
|
os.umask(0o077)
|
|
source, target = map(pathlib.Path, sys.argv[1:3])
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
temp = target / (stamp + ".tmp")
|
|
final = target / (stamp + ".sqlite3")
|
|
try:
|
|
with closing(sqlite3.connect(f"file:{source}?mode=ro", uri=True)) as src, closing(sqlite3.connect(temp)) as dst:
|
|
src.backup(dst)
|
|
dst.execute("PRAGMA journal_mode=DELETE")
|
|
if dst.execute("PRAGMA integrity_check").fetchone()[0] != "ok":
|
|
raise RuntimeError("Backup failed integrity check")
|
|
with temp.open("rb") as f:
|
|
os.fsync(f.fileno())
|
|
temp.replace(final)
|
|
descriptor = os.open(target, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
total = 0
|
|
for index, old in enumerate(sorted(target.glob("*.sqlite3"), reverse=True)):
|
|
total += old.stat().st_size
|
|
if index > 0 and (index >= 30 or total > 512 * 1024 * 1024):
|
|
old.unlink()
|
|
finally:
|
|
temp.unlink(missing_ok=True)
|