feat: self-hosted frontend JS (offline) + docker hardening
- Vendored epub.js/pdf.js+worker/JSZip/DOMPurify in static/vendor/ - reader.html/reader.js servono i JS da /static (niente CDN, offline-ready) - CSP: rimosse allowlist CDN, aggiunto worker-src 'self' blob: - Test CSP aggiornati; tutti 16 verdi; verificato smoke test offline in container
This commit is contained in:
1 parent
e62c50c616
commit
0f15b7958f
31 files changed
+1363
-215
No files matched your search
+18
-15
@@ -5,7 +5,7 @@ import logging
|
||||
import threading
|
||||
from flask import Blueprint, request, jsonify, Response, send_file, render_template, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from config import SUPPORTED_FORMATS, OUTPUT_FORMATS_BY_INPUT
|
||||
from config import SUPPORTED_FORMATS, OUTPUT_FORMATS_BY_INPUT, CLEANUP_HOURS
|
||||
from bookish.services.sse_manager import create_task, generate_sse_events, get_status
|
||||
from bookish.services.converter import run_conversion, get_format_from_ext
|
||||
|
||||
@@ -24,7 +24,7 @@ def _get_output_ext(output_format):
|
||||
return f".{output_format}"
|
||||
|
||||
|
||||
def _clean_old_files(folder, hours=1):
|
||||
def _clean_old_files(folder, hours=CLEANUP_HOURS):
|
||||
import time
|
||||
now = time.time()
|
||||
for f in os.listdir(folder):
|
||||
@@ -50,21 +50,11 @@ def upload():
|
||||
supported = ", ".join(sorted(SUPPORTED_FORMATS.keys()))
|
||||
return jsonify({"error": f"Formato '{ext}' non supportato. Supportati: {supported}"}), 400
|
||||
|
||||
_clean_old_files(current_app.config["UPLOAD_FOLDER"])
|
||||
_clean_old_files(current_app.config["CONVERTED_FOLDER"])
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
if not filename:
|
||||
return jsonify({"error": "Nome file non valido"}), 400
|
||||
|
||||
input_ext = get_format_from_ext(filename)
|
||||
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
||||
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
|
||||
@@ -75,6 +65,19 @@ def upload():
|
||||
"error": f"Conversione da {input_ext} a {output_format} non supportata"
|
||||
}), 400
|
||||
|
||||
_clean_old_files(current_app.config["UPLOAD_FOLDER"])
|
||||
_clean_old_files(current_app.config["CONVERTED_FOLDER"])
|
||||
|
||||
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
||||
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_ext = _get_output_ext(output_format)
|
||||
output_name = f"{uuid.uuid4().hex}{output_ext}"
|
||||
output_path = os.path.join(current_app.config["CONVERTED_FOLDER"], output_name)
|
||||
|
||||
+34
-14
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import hashlib
|
||||
from flask import Blueprint, render_template, jsonify, send_file, current_app
|
||||
from flask import Blueprint, render_template, jsonify, send_file, current_app, url_for
|
||||
from config import SUPPORTED_FORMATS
|
||||
|
||||
reader_bp = Blueprint("reader", __name__)
|
||||
@@ -8,9 +8,13 @@ reader_bp = Blueprint("reader", __name__)
|
||||
READABLE_FORMATS = {"epub", "pdf", "html", "md", "txt", "fb2", "docx"}
|
||||
|
||||
|
||||
def _pdf_worker_url():
|
||||
return url_for("static", filename="vendor/pdf.worker.min.js")
|
||||
|
||||
|
||||
@reader_bp.route("/reader")
|
||||
def reader():
|
||||
return render_template("reader.html")
|
||||
return render_template("reader.html", pdf_worker_url=_pdf_worker_url())
|
||||
|
||||
|
||||
def _file_hash(filepath):
|
||||
@@ -21,30 +25,46 @@ def _file_hash(filepath):
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _resolve_file(filename):
|
||||
"""Trova il file su disco gestendo sia il nome di upload (<uuid>_<name>)
|
||||
sia il nome convertito (<uuid>.ext). Ritorna (path, ext) o (None, None)."""
|
||||
safe = os.path.basename(filename)
|
||||
candidates = []
|
||||
for folder in (current_app.config["UPLOAD_FOLDER"], current_app.config["CONVERTED_FOLDER"]):
|
||||
candidates.append(os.path.join(folder, safe))
|
||||
# Fallback: un file 'libro_test.epub' passato a mano potrebbe essere
|
||||
# stato salvato come <uuid>_libro_test.epub (upload) → cerchiamo per
|
||||
# suffisso dentro i folder.
|
||||
if os.path.isdir(folder):
|
||||
for f in os.listdir(folder):
|
||||
if f == safe:
|
||||
continue
|
||||
if f.endswith("_" + safe):
|
||||
candidates.append(os.path.join(folder, f))
|
||||
for p in candidates:
|
||||
if os.path.isfile(p):
|
||||
ext = os.path.splitext(p)[1].lower().lstrip(".")
|
||||
return p, ext
|
||||
return None, None
|
||||
|
||||
|
||||
@reader_bp.route("/reader/<filename>")
|
||||
def reader_file(filename):
|
||||
safe = os.path.basename(filename)
|
||||
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
|
||||
if not os.path.exists(filepath):
|
||||
filepath, ext = _resolve_file(filename)
|
||||
if not filepath:
|
||||
return jsonify({"error": "File non trovato"}), 404
|
||||
|
||||
ext = os.path.splitext(safe)[1].lower().lstrip(".")
|
||||
if ext not in READABLE_FORMATS:
|
||||
return jsonify({"error": f"Formato '{ext}' non supportato dal reader"}), 400
|
||||
|
||||
content_hash = _file_hash(filepath)
|
||||
return render_template("reader.html", filename=safe, file_ext=ext, content_hash=content_hash)
|
||||
return render_template("reader.html", filename=os.path.basename(filepath), file_ext=ext, content_hash=content_hash, pdf_worker_url=_pdf_worker_url())
|
||||
|
||||
|
||||
@reader_bp.route("/reader/serve/<filename>")
|
||||
def reader_serve(filename):
|
||||
safe = os.path.basename(filename)
|
||||
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
|
||||
if not os.path.exists(filepath):
|
||||
filepath, ext = _resolve_file(filename)
|
||||
if not filepath:
|
||||
return jsonify({"error": "File non trovato"}), 404
|
||||
|
||||
return send_file(filepath, mimetype="application/octet-stream")
|
||||
|
||||
Reference in new issue
Block a user