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:
1 parent
13e02faa8b
commit
c1421665c0
8 files changed
+319
-25
No files matched your search
@@ -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})
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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>")
|
||||
|
||||
Reference in new issue
Block a user