"""Create a new SQLite snapshot; never replace a source or existing destination.

Usage: python3 sqlite_snapshot.py SOURCE.db NEW_SNAPSHOT.db
Stop all application writers before taking a recovery set across multiple stores.
Use an existing owner-only destination directory. Snapshot files contain private
application data and are not encrypted by this helper.
"""
import os
from contextlib import closing
from pathlib import Path
import sqlite3
import stat
import sys
import time


def snapshot(source, destination):
    source, destination = Path(source), Path(destination)
    if source.is_symlink() or not source.is_file():
        raise ValueError('Source must be an existing regular database file, not a symlink.')
    parent = destination.parent
    if not parent.is_dir():
        raise ValueError('Create a private destination directory first.')
    if os.name == 'posix':
        details = parent.stat()
        if details.st_uid != os.getuid() or stat.S_IMODE(details.st_mode) & 0o077:
            raise ValueError('Destination directory must be owned by this user with no group/other permissions.')
    # Exclusive creation also refuses a dangling symlink at the destination.
    descriptor = os.open(destination, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
    os.close(descriptor)
    started = time.monotonic()

    def progress(*_):
        if time.monotonic() - started > 30:
            raise TimeoutError('Snapshot exceeded 30 seconds; check for active writers or locks.')

    try:
        with closing(sqlite3.connect(source.resolve().as_uri() + '?mode=ro', uri=True, timeout=5)) as original:
            with closing(sqlite3.connect(destination, timeout=5)) as copy:
                original.backup(copy, pages=128, progress=progress)
                if copy.execute('PRAGMA integrity_check').fetchall() != [('ok',)]:
                    raise ValueError('Snapshot integrity check failed.')
                copy.execute('PRAGMA journal_mode=DELETE')
        with destination.open('rb') as handle:
            os.fsync(handle.fileno())
    except BaseException:
        destination.unlink(missing_ok=True)
        raise


if __name__ == '__main__':
    if len(sys.argv) != 3:
        raise SystemExit(__doc__)
    try:
        snapshot(sys.argv[1], sys.argv[2])
    except (ValueError, OSError, sqlite3.Error, TimeoutError) as error:
        raise SystemExit(str(error))
    print('Created a new integrity-checked SQLite snapshot. Protect it as private data.')
