diff --git a/README.md b/README.md
index 2875a5d..33c194b 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,9 @@ napevno domovský adresář. Ze zdrojáků zůstává relativní, ať se dá mí
sad vedle sebe.
Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho
-najde kdekoli.
+najde kdekoli. Kdo ho hledat nechce — a z binárky na Windows je zalezlý —
+si ho stáhne ze síně slávy odkazem **Stáhnout databázi** (`/export`). Kopie
+se dělá přes `VACUUM INTO`, takže je použitelná i uprostřed rozehrané hry.
Schéma má verzi uloženou v `PRAGMA user_version` a `storage.migrate()` si
databázi při každém připojení dorovná. Starší soubory z verzí, kdy se verze
diff --git a/templates/stats.html b/templates/stats.html
index 0f748e2..3e12ada 100644
--- a/templates/stats.html
+++ b/templates/stats.html
@@ -8,7 +8,8 @@
Počitadlo
Síň slávy
- Nová hra
+ Nová hra
+ · Stáhnout databázi
{% if records.best_turn %}
diff --git a/tests/test_web.py b/tests/test_web.py
index e6d28a7..6abb950 100644
--- a/tests/test_web.py
+++ b/tests/test_web.py
@@ -326,3 +326,36 @@ def test_qr_obdelniky_kryji_stejne_moduly():
assert painted == expected
assert svg.count("`), notebook ukazuje zápisník (`/board`).
Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step.
"""
+import io
import os
+import shutil
import socket
import sys
+import tempfile
+from datetime import date
from pathlib import Path
import qrcode
from flask import (Flask, abort, g, redirect, render_template, request,
- url_for)
+ send_file, url_for)
from markupsafe import Markup, escape
from waitress import serve
@@ -319,6 +323,36 @@ def board_panel(game_id: int):
return render_template("_panel.html", **view(game, game_id, row))
+@app.route("/export")
+def export():
+ """Databáze ke stažení.
+
+ Dokud se hrálo ze zdrojáků, ležel `dice.db` v pracovním adresáři a záloha
+ byla otázka zkopírování. Z binárky se schovává do systémového datového
+ adresáře, který na Windows běžný člověk nenajde — tak ať se dá stáhnout
+ přes prohlížeč.
+
+ `VACUUM INTO` udělá konzistentní kopii i uprostřed zápisu, na rozdíl od
+ prostého přečtení souboru. Výsledek si přečteme do paměti a kopii hned
+ zahodíme: databáze počitadla se počítá v kilobajtech a takhle nezůstane
+ ležet v dočasném adresáři, když si ji někdo nestáhne celou.
+ """
+ conn = db()
+ conn.commit() # VACUUM nesmí běžet v transakci
+
+ docasny = tempfile.mkdtemp()
+ try:
+ kopie = Path(docasny) / "dice.db"
+ conn.execute("VACUUM INTO ?", (str(kopie),))
+ data = kopie.read_bytes()
+ finally:
+ shutil.rmtree(docasny, ignore_errors=True)
+
+ return send_file(io.BytesIO(data), mimetype="application/vnd.sqlite3",
+ as_attachment=True,
+ download_name=f"kostky-{date.today().isoformat()}.db")
+
+
@app.route("/stats")
def hall():
conn = db()