diff --git a/app.py b/app.py index 8178790..b8e196d 100755 --- a/app.py +++ b/app.py @@ -24,6 +24,7 @@ import auth import analytics as a import folder_secret_config_editor as fsce import helperfunctions as hf +import search_db_analyzer as sdb import fnmatch app_config = auth.return_app_config() @@ -56,6 +57,9 @@ app.add_url_rule('/searchcommand', view_func=search.searchcommand, methods=['POS app.add_url_rule('/admin/folder_secret_config_editor', view_func=auth.require_admin(fsce.folder_secret_config_editor), methods=['GET', 'POST']) app.add_url_rule('/admin/folder_secret_config_editor/data', view_func=auth.require_admin(auth.load_folder_config)) app.add_url_rule('/admin/folder_secret_config_editor/action', view_func=auth.require_admin(fsce.folder_secret_config_action), methods=['POST']) +app.add_url_rule('/admin/search_db_analyzer', view_func=auth.require_admin(sdb.search_db_analyzer)) +app.add_url_rule('/admin/search_db_analyzer/query', view_func=auth.require_admin(sdb.search_db_query), methods=['POST']) +app.add_url_rule('/admin/search_db_analyzer/folders', view_func=auth.require_admin(sdb.search_db_folders)) # Grab the HOST_RULE environment variable host_rule = os.getenv("HOST_RULE", "") diff --git a/search_db_analyzer.py b/search_db_analyzer.py new file mode 100644 index 0000000..6d87e3f --- /dev/null +++ b/search_db_analyzer.py @@ -0,0 +1,127 @@ +import os +import sqlite3 +from typing import List, Dict, Any + +from flask import render_template, request, jsonify + +import auth + +APP_CONFIG = auth.return_app_config() +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +SEARCH_DB_PATH = os.path.join(BASE_DIR, "search.db") + +# Open search.db in read-only mode to avoid accidental writes. +search_db = sqlite3.connect(f"file:{SEARCH_DB_PATH}?mode=ro", uri=True, check_same_thread=False) +search_db.row_factory = sqlite3.Row + + +def _normalize_folder_path(folder_path: str) -> str: + """Normalize folder paths to a consistent, slash-based format.""" + return folder_path.replace("\\", "/").strip().strip("/") + + +def _list_children(parent: str = "") -> List[str]: + """ + Return the next folder level for the given parent. + - parent == "" → first level (basefolder column) + - parent != "" → distinct next segment in relative_path below that parent + """ + cursor = search_db.cursor() + + normalized = _normalize_folder_path(parent) + if not normalized: + rows = cursor.execute("SELECT DISTINCT basefolder FROM files ORDER BY basefolder").fetchall() + return [row["basefolder"] for row in rows if row["basefolder"]] + + prefix = normalized + "/" + rows = cursor.execute( + "SELECT relative_path FROM files WHERE relative_path LIKE ?", + (prefix + "%",) + ).fetchall() + + children = set() + plen = len(prefix) + for row in rows: + rel = row["relative_path"] + # Strip the prefix and keep only the next segment + remainder = rel[plen:] + if "/" in remainder: + next_seg = remainder.split("/", 1)[0] + else: + # File directly under parent; no deeper folder + continue + if next_seg: + children.add(next_seg) + + return sorted(children) + + +def _query_counts(folder_path: str) -> Dict[str, Any]: + """Run a grouped count query on search.db filtered by folder_path.""" + normalized = _normalize_folder_path(folder_path) + params: List[str] = [] + conditions: List[str] = [] + + if not normalized: + raise ValueError("Bitte einen Ordnerpfad angeben.") + + # Match both basefolder and deeper paths inside that folder. + conditions.append("(relative_path LIKE ? OR basefolder = ?)") + params.extend([f"{normalized}/%", normalized]) + + where_sql = " AND ".join(conditions) if conditions else "1=1" + sql = f""" + SELECT COALESCE(category, 'Keine Kategorie') AS category_label, + COUNT(*) AS file_count + FROM files + WHERE {where_sql} + GROUP BY category_label + ORDER BY file_count DESC + """ + + cursor = search_db.cursor() + rows = cursor.execute(sql, params).fetchall() + + total = sum(row["file_count"] for row in rows) + categories = [ + {"category": row["category_label"], "count": row["file_count"]} + for row in rows + ] + + return {"total": total, "categories": categories} + + +def search_db_analyzer(): + """Render the UI for analyzing search.db by folder.""" + return render_template( + "search_db_analyzer.html", + admin_enabled=auth.is_admin(), + title_short=APP_CONFIG.get("TITLE_SHORT", "Default Title"), + title_long=APP_CONFIG.get("TITLE_LONG", "Default Title"), + ) + + +def search_db_query(): + """Return grouped counts by category for a given folder path.""" + payload = request.get_json(silent=True) or {} + folder_path = (payload.get("folder_path") or "").strip() + + try: + result = _query_counts(folder_path) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + except Exception as exc: # pragma: no cover - defensive logging + return jsonify({"error": f"Abfrage fehlgeschlagen: {exc}"}), 500 + + return jsonify(result) + + +def search_db_folders(): + """Return next-level folder names for the given parent path (or basefolders).""" + parent = request.args.get("parent", "").strip() + try: + children = _list_children(parent) + except Exception as exc: # pragma: no cover - defensive logging + return jsonify({"error": f"Ordner konnten nicht geladen werden: {exc}"}), 500 + + return jsonify({"children": children}) diff --git a/templates/base.html b/templates/base.html index f6fcbb2..d2b431a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,6 +42,8 @@ Wiederholungen | Ordnerkonfiguration + | + Dateiindex Analyse {% endif %} diff --git a/templates/search_db_analyzer.html b/templates/search_db_analyzer.html new file mode 100644 index 0000000..f64b4dd --- /dev/null +++ b/templates/search_db_analyzer.html @@ -0,0 +1,262 @@ +{% extends 'base.html' %} + +{% block title %}Search-DB Analyse{% endblock %} + +{% block content %} +
+

Index auswerten

+

+ Ordner auswählen und die Anzahl der Dateien pro Kategorie aus der Dateiindex abrufen. +

+ +
+
+ +
+
Wähle zuerst den Hauptordner, füge dann bei Bedarf Unterordner hinzu.
+
+
+ +
+
+ + + + +
+{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %}