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()