feat: persistent SQLite storage for bookmarks + positions keyed by content_hash

- new bookish/services/database.py: SQLite with WAL mode, tables for
  bookmarks and reading_positions, keyed by content_hash (SHA256)
- new bookish/routes/api.py: REST endpoints for CRUD bookmarks
  and save/load reading position
- upload endpoint returns content_hash (SHA256 of file)
- reader route computes SHA256 and passes BOOKISH_HASH to template
- reader.js: bookmarks + positions use API server as primary storage
  with localStorage fallback for offline/resilience
- position saves debounced (1.5s) to avoid hammering server
- .gitignore: added bookish.db
This commit is contained in:
truetype committed 2026-07-23 22:35:04 +02:00
1 parent 13e02faa8b
commit c1421665c0
8 files changed
+319 -25

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
import time
from flask import Blueprint, jsonify, request, current_app
from bookish.services.database import (
save_bookmark,
get_bookmarks,
delete_bookmark,
save_position,
get_position,
)
api_bp = Blueprint("api", __name__, url_prefix="/api")
# --- Bookmarks ---
@api_bp.route("/bookmarks/<content_hash>", methods=["GET"])
def api_get_bookmarks(content_hash):
bookmarks = get_bookmarks(content_hash)
return jsonify({"bookmarks": bookmarks})
@api_bp.route("/bookmarks", methods=["POST"])
def api_add_bookmark():
data = request.get_json(silent=True)
if not data or not data.get("content_hash") or not data.get("id"):
return jsonify({"error": "Richiesta non valida"}), 400
data["created"] = data.get("created", time.time())
save_bookmark(data)
return jsonify({"ok": True})
@api_bp.route("/bookmarks/<bookmark_id>", methods=["DELETE"])
def api_delete_bookmark(bookmark_id):
delete_bookmark(bookmark_id)
return jsonify({"ok": True})
# --- Reading positions ---
@api_bp.route("/position/<content_hash>", methods=["GET"])
def api_get_position(content_hash):
pos = get_position(content_hash)
if pos:
return jsonify({"position": pos})
return jsonify({"position": None})
@api_bp.route("/position/<content_hash>", methods=["PUT"])
def api_save_position(content_hash):
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Richiesta non valida"}), 400
data["type"] = data.get("type", "unknown")
save_position(content_hash, data)
return jsonify({"ok": True})
+8
View File
@@ -1,5 +1,6 @@
import os
import uuid
import hashlib
import logging
import threading
from flask import Blueprint, request, jsonify, Response, send_file, render_template, current_app
@@ -58,6 +59,12 @@ def upload():
input_path = os.path.join(current_app.config["UPLOAD_FOLDER"], unique_name)
file.save(input_path)
sha256_hash = hashlib.sha256()
with open(input_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256_hash.update(chunk)
content_hash = sha256_hash.hexdigest()
output_format = request.form.get("output_format", "").lower()
if output_format not in SUPPORTED_FORMATS:
return jsonify({"error": f"Formato output '{output_format}' non supportato"}), 400
@@ -89,6 +96,7 @@ def upload():
"output_format": output_format,
"filename": filename,
"stored_filename": unique_name,
"content_hash": content_hash,
})
+11 -1
View File
@@ -1,4 +1,5 @@
import os
import hashlib
from flask import Blueprint, render_template, jsonify, send_file, current_app
from config import SUPPORTED_FORMATS
@@ -12,6 +13,14 @@ def reader():
return render_template("reader.html")
def _file_hash(filepath):
h = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
@reader_bp.route("/reader/<filename>")
def reader_file(filename):
safe = os.path.basename(filename)
@@ -25,7 +34,8 @@ def reader_file(filename):
if ext not in READABLE_FORMATS:
return jsonify({"error": f"Formato '{ext}' non supportato dal reader"}), 400
return render_template("reader.html", filename=safe, file_ext=ext)
content_hash = _file_hash(filepath)
return render_template("reader.html", filename=safe, file_ext=ext, content_hash=content_hash)
@reader_bp.route("/reader/serve/<filename>")