# deck_endpoints.py from flask import Blueprint, request, jsonify import sqlite3 import os import shutil import logging deck_bp = Blueprint('deck_bp', __name__) DATABASE = 'mydatabase.db' # Logger konfigurieren (angenommen, ocr_server3.py konfiguriert das Logging) logger = logging.getLogger(__name__) def get_db_connection(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn # Erstellen der Tabelle Deck, falls sie nicht existiert def init_db(): conn = get_db_connection() cursor = conn.cursor() # Tabelle Deck erstellen cursor.execute(''' CREATE TABLE IF NOT EXISTS Deck ( id INTEGER PRIMARY KEY AUTOINCREMENT, deckname TEXT NOT NULL, bildname TEXT, bildid TEXT, iconindex INTEGER, x1 REAL, x2 REAL, y1 REAL, y2 REAL, due INTEGER DEFAULT (CAST((julianday('now') - julianday('1970-01-01')) AS INTEGER)), ivl REAL DEFAULT 0.0, factor REAL DEFAULT 2.5, reps INTEGER DEFAULT 0, lapses INTEGER DEFAULT 0, isGraduated INTEGER DEFAULT 0 -- Neue Spalte hinzugefügt ) ''') conn.commit() conn.close() def clean_debug_directories(): """ Löscht alle Verzeichnisse unter 'debug_images', die keinen Eintrag in der Deck-Tabelle mit bildname oder bildid haben. """ debug_base_dir = 'debug_images' if not os.path.exists(debug_base_dir): logger.info(f"Debug-Verzeichnis '{debug_base_dir}' existiert nicht. Nichts zu bereinigen.") return try: conn = get_db_connection() cursor = conn.cursor() # Sammle alle bildname und bildid, die in der Deck-Tabelle vorhanden sind cursor.execute('SELECT DISTINCT bildname FROM Deck WHERE bildname IS NOT NULL') bildnames = {row['bildname'] for row in cursor.fetchall()} cursor.execute('SELECT DISTINCT bildid FROM Deck WHERE bildid IS NOT NULL') bildids = {row['bildid'] for row in cursor.fetchall()} conn.close() except sqlite3.Error as e: logger.error(f"Fehler beim Abrufen der bildname und bildid aus der Datenbank: {e}") return # Durchlaufe alle Verzeichnisse unter 'debug_images' for dir_name in os.listdir(debug_base_dir): dir_path = os.path.join(debug_base_dir, dir_name) if os.path.isdir(dir_path): if dir_name not in bildnames and dir_name not in bildids: try: shutil.rmtree(dir_path) logger.info(f"Nicht verwendetes Debug-Verzeichnis gelöscht: {dir_path}") except Exception as e: logger.error(f"Fehler beim Löschen des Verzeichnisses '{dir_path}': {e}") else: logger.debug(f"Debug-Verzeichnis behalten: {dir_path}") logger.info("Bereinigung der Debug-Verzeichnisse abgeschlossen.") def clean_db_entries(): """ Löscht alle Einträge aus der Deck-Tabelle, für die es kein entsprechendes Verzeichnis unter 'debug_images' gibt. """ debug_base_dir = 'debug_images' if not os.path.exists(debug_base_dir): logger.info(f"Debug-Verzeichnis '{debug_base_dir}' existiert nicht. Keine DB-Einträge zu bereinigen.") return try: # Liste der vorhandenen Verzeichnisse unter 'debug_images' existing_dirs = {name for name in os.listdir(debug_base_dir) if os.path.isdir(os.path.join(debug_base_dir, name))} except Exception as e: logger.error(f"Fehler beim Zugriff auf Verzeichnisse in '{debug_base_dir}': {e}") return try: conn = get_db_connection() conn.row_factory = sqlite3.Row cursor = conn.cursor() # Alle bildid-Werte aus der Deck-Tabelle abrufen, die nicht NULL sind cursor.execute('SELECT DISTINCT bildid FROM Deck WHERE bildid IS NOT NULL') bildids = {row['bildid'] for row in cursor.fetchall()} # bildid-Werte identifizieren, für die es kein Verzeichnis gibt missing_dirs = bildids - existing_dirs if missing_dirs: # Einträge aus der Deck-Tabelle löschen, deren bildid keinen entsprechenden Ordner hat placeholders = ','.join('?' for _ in missing_dirs) sql = f'DELETE FROM Deck WHERE bildid IN ({placeholders})' cursor.execute(sql, tuple(missing_dirs)) deleted_count = cursor.rowcount conn.commit() logger.info(f"{deleted_count} Einträge aus der Deck-Tabelle gelöscht, deren 'bildid'-Verzeichnisse fehlen.") else: logger.info("Keine DB-Einträge zu löschen; alle 'bildid'-Verzeichnisse existieren.") conn.close() except sqlite3.Error as e: logger.error(f"Datenbankfehler beim Löschen von Einträgen aus der Deck-Tabelle: {e}") return # ------ # Endpoints # ------ # ------ # Deck - POST, GET, DELETE # ------ @deck_bp.route('/api/decks', methods=['POST']) def create_deck(): data = request.get_json() if not data or 'deckname' not in data: return jsonify({'error': 'No deckname provided'}), 400 deckname = data['deckname'] conn = get_db_connection() cursor = conn.cursor() try: # Ein neuer Eintrag für das Deck ohne Bilddaten cursor.execute('INSERT INTO Deck (deckname) VALUES (?)', (deckname,)) conn.commit() deck_id = cursor.lastrowid conn.close() return jsonify({'status': 'success', 'deck_id': deck_id}), 201 except sqlite3.Error as e: conn.close() logger.error(f"Datenbankfehler beim Erstellen des Decks: {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 @deck_bp.route('/api/decks', methods=['GET']) def get_decks(): conn = get_db_connection() cursor = conn.cursor() # Alle Einträge abrufen entries = cursor.execute('SELECT * FROM Deck').fetchall() conn.close() decks = {} for entry in entries: deckname = entry['deckname'] if deckname not in decks: decks[deckname] = { 'name': deckname, 'images': [] } if entry['bildname'] and entry['bildid']: image = { 'name': entry['bildname'], 'id': entry['bildid'], 'iconindex': entry['iconindex'], 'boxid':entry['id'], 'x1': entry['x1'], 'x2': entry['x2'], 'y1': entry['y1'], 'y2': entry['y2'], 'due': entry['due'], 'ivl': entry['ivl'], 'factor': entry['factor'], 'reps': entry['reps'], 'lapses': entry['lapses'], 'isGraduated': bool(entry['isGraduated']) # isGraduated hinzufügen } decks[deckname]['images'].append(image) deck_list = list(decks.values()) return jsonify(deck_list) @deck_bp.route('/api/decks/', methods=['DELETE']) def delete_deck(deckname): conn = get_db_connection() cursor = conn.cursor() try: # Überprüfen, ob das Deck existiert cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (deckname,)) result = cursor.fetchone() if result['count'] == 0: conn.close() return jsonify({'error': 'Deck not found'}), 404 # Löschen aller Einträge mit dem gegebenen deckname cursor.execute('DELETE FROM Deck WHERE deckname = ?', (deckname,)) conn.commit() conn.close() return jsonify({'status': 'success'}), 200 except sqlite3.Error as e: conn.close() logger.error(f"Datenbankfehler beim Löschen des Decks '{deckname}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 @deck_bp.route('/api/decks//rename', methods=['PUT']) def rename_deck(old_deckname): conn = get_db_connection() cursor = conn.cursor() try: # Überprüfen, ob das Deck existiert cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (old_deckname,)) result = cursor.fetchone() if result['count'] == 0: conn.close() return jsonify({'error': 'Deck not found'}), 404 # Neuen Decknamen aus dem Request-Body holen new_deckname = request.json.get('newDeckName') if not new_deckname: conn.close() return jsonify({'error': 'New deck name is required'}), 400 # Überprüfen, ob der neue Deckname bereits existiert cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (new_deckname,)) result = cursor.fetchone() if result['count'] > 0: conn.close() return jsonify({'error': 'Deck with the new name already exists'}), 409 # Deck umbenennen cursor.execute('UPDATE Deck SET deckname = ? WHERE deckname = ?', (new_deckname, old_deckname)) conn.commit() conn.close() return jsonify({'status': 'success', 'message': 'Deck renamed successfully'}), 200 except sqlite3.Error as e: conn.close() logger.error(f"Datenbankfehler beim Umbenennen des Decks '{old_deckname}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 # ------ # Image - POST, GET, DELETE # ------ @deck_bp.route('/api/decks/image', methods=['POST']) def update_image(): data = request.get_json() if not data: return jsonify({'error': 'No data provided'}), 400 # Überprüfen, ob die erforderlichen Felder vorhanden sind required_fields = ['deckname', 'bildname', 'bildid', 'boxes'] if not all(field in data for field in required_fields): return jsonify({'error': 'Missing fields in data'}), 400 deckname = data['deckname'] bildname = data['bildname'] bildid = data['bildid'] boxes = data['boxes'] # Überprüfen, ob 'boxes' eine Liste ist und mindestens ein Box-Element enthält if not isinstance(boxes, list) or len(boxes) == 0: return jsonify({'error': "'boxes' must be a non-empty list"}), 400 # Verbindung zur Datenbank herstellen conn = get_db_connection() cursor = conn.cursor() try: # Überprüfen, ob das Deck existiert cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ?', (deckname,)) result = cursor.fetchone() if result['count'] == 0: conn.close() return jsonify({'error': 'Deck not found'}), 404 # 1. Lösche alle Einträge für das Deck ohne bildname cursor.execute('DELETE FROM Deck WHERE deckname = ? AND bildid = ?', (deckname,bildid)) inserted_image_ids = [] # 2. Füge neue Image-Einträge hinzu for index, box in enumerate(boxes): # Überprüfen, ob alle erforderlichen Koordinaten vorhanden sind box_fields = ['x1', 'x2', 'y1', 'y2'] if not all(field in box for field in box_fields): conn.close() return jsonify({'error': 'Missing fields in one of the boxes'}), 400 x1 = box['x1'] x2 = box['x2'] y1 = box['y1'] y2 = box['y2'] # Setzen des iconindex auf den aktuellen Index der Box iconindex = index cursor.execute(''' INSERT INTO Deck (deckname, bildname, bildid, iconindex, x1, x2, y1, y2) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', (deckname, bildname, bildid, iconindex, x1, x2, y1, y2)) inserted_image_ids.append(cursor.lastrowid) conn.commit() return jsonify({'status': 'success', 'inserted_image_ids': inserted_image_ids}), 201 except sqlite3.Error as e: conn.rollback() logger.error(f"Datenbankfehler beim Aktualisieren der Images für Deck '{deckname}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 finally: conn.close() # @deck_bp.route('/api/decks/image/', methods=['GET']) # def get_images_by_bildid(bildid): # conn = get_db_connection() # cursor = conn.cursor() # images = cursor.execute('SELECT * FROM Deck WHERE bildid = ?', (bildid,)).fetchall() # conn.close() # image_list = [dict(image) for image in images] # return jsonify(image_list) @deck_bp.route('/api/decks/image/', methods=['DELETE']) def delete_images_by_bildid(bildid): """ Löscht alle Einträge in der Deck-Tabelle für die gegebene bildid. Falls es das letzte Image für dieses Deck ist, wird ein neuer Deck-Eintrag erstellt. Optional: Löscht das zugehörige Debug-Verzeichnis, wenn keine weiteren Einträge bestehen. """ try: # Sicherheitsmaßnahme: Nur erlaubte Zeichen in bildid if not all(c.isalnum() or c in ('_', '-') for c in bildid): logger.warning(f"Ungültige bildid angefordert: {bildid}") return jsonify({'error': 'Invalid image ID'}), 400 conn = get_db_connection() cursor = conn.cursor() # Start einer Transaktion conn.execute('BEGIN') # Schritt 1: Identifizieren der betroffenen Decks cursor.execute('SELECT DISTINCT deckname FROM Deck WHERE bildid = ?', (bildid,)) affected_decks = {row['deckname'] for row in cursor.fetchall()} if not affected_decks: conn.close() return jsonify({'error': 'No entries found for the given image ID'}), 404 # Schritt 2: Löschen der Image-Einträge cursor.execute('DELETE FROM Deck WHERE bildid = ?', (bildid,)) logger.info(f"Alle Einträge für bildid '{bildid}' wurden gelöscht.") # Schritt 3: Überprüfen und Wiederherstellen der Deck-Einträge for deckname in affected_decks: # Überprüfen, ob noch andere Image-Einträge für dieses Deck existieren cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ? AND bildid IS NOT NULL', (deckname,)) result = cursor.fetchone() image_count = result['count'] if result else 0 if image_count == 0: # Überprüfen, ob bereits ein Deck-Eintrag existiert (ohne bildid) cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE deckname = ? AND bildid IS NULL', (deckname,)) deck_entry = cursor.fetchone() if deck_entry['count'] == 0: # Ein neuer Deck-Eintrag wird erstellt cursor.execute('INSERT INTO Deck (deckname) VALUES (?)', (deckname,)) logger.info(f"Neuer Deck-Eintrag für '{deckname}' erstellt, da keine weiteren Images vorhanden sind.") # Commit der Transaktion conn.commit() # Schritt 4: Optionales Löschen des Debug-Verzeichnisses debug_dir = os.path.join('debug_images', bildid) if os.path.exists(debug_dir): try: shutil.rmtree(debug_dir) logger.info(f"Debug-Verzeichnis gelöscht: {debug_dir}") except Exception as e: logger.error(f"Fehler beim Löschen des Debug-Verzeichnisses '{debug_dir}': {e}") # Eine Warnung wird zurückgegeben, aber die Anfrage wird als erfolgreich betrachtet return jsonify({ 'status': 'success', 'message': 'Database entries deleted, but failed to delete debug directory.', 'details': str(e) }), 200 return jsonify({'status': 'success', 'message': f'All entries for image ID "{bildid}" have been deleted.'}), 200 except sqlite3.Error as e: # Rollback der Transaktion bei einem Datenbankfehler conn.rollback() logger.error(f"Datenbankfehler beim Löschen der Image-Einträge für '{bildid}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 except Exception as e: # Rollback der Transaktion bei einem unerwarteten Fehler conn.rollback() logger.error(f"Unerwarteter Fehler beim Löschen der Image-Einträge für '{bildid}': {e}") return jsonify({'error': 'Failed to delete image entries', 'details': str(e)}), 500 finally: conn.close() @deck_bp.route('/api/decks/images//move', methods=['POST']) def move_image(bildid): """ Verschiebt alle Einträge einer bildid von ihrem aktuellen Deck in ein neues Deck. Das übergebene JSON sollte { "targetDeckId": "neuerDeckname" } sein. """ try: data = request.get_json() if not data or 'targetDeckId' not in data: return jsonify({'error': 'No targetDeckId provided'}), 400 target_deck_id = data['targetDeckId'] # Sicherheitsmaßnahme: Nur erlaubte Zeichen im targetDeckId if not all(c.isalnum() or c in ('_', '-') for c in target_deck_id): logger.warning(f"Ungültiger targetDeckId angefordert: {target_deck_id}") return jsonify({'error': 'Invalid targetDeckId'}), 400 conn = get_db_connection() cursor = conn.cursor() # Überprüfen, ob es Einträge mit der gegebenen bildid gibt cursor.execute('SELECT COUNT(*) as count FROM Deck WHERE bildid = ?', (bildid,)) result = cursor.fetchone() if result['count'] == 0: conn.close() return jsonify({'error': 'No entries found for the given image ID'}), 404 # Update deckname für alle Einträge mit der bildid cursor.execute('UPDATE Deck SET deckname = ? WHERE bildid = ?', (target_deck_id, bildid)) updated_rows = cursor.rowcount conn.commit() conn.close() logger.info(f"Bild mit bildid '{bildid}' wurde in das Deck '{target_deck_id}' verschoben. Aktualisierte Einträge: {updated_rows}") return jsonify({'status': 'success', 'moved_entries': updated_rows}), 200 except sqlite3.Error as e: if conn: conn.rollback() logger.error(f"Datenbankfehler beim Verschieben der Image-Einträge für '{bildid}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 except Exception as e: if conn: conn.rollback() logger.error(f"Unerwarteter Fehler beim Verschieben der Image-Einträge für '{bildid}': {e}") return jsonify({'error': 'Failed to move image entries', 'details': str(e)}), 500 finally: if conn: conn.close() @deck_bp.route('/api/decks/boxes/', methods=['PUT']) def update_box(box_id): data = request.get_json() due = data.get('due', 0) ivl = data.get('ivl', 0.0) factor = data.get('factor', 2.5) reps = data.get('reps', 0) lapses = data.get('lapses', 0) is_graduated = data.get('isGraduated') # Neues Feld aus den Daten extrahieren # Validierung des isGraduated-Feldes if is_graduated is not None: if isinstance(is_graduated, bool): is_graduated_int = int(is_graduated) else: return jsonify({'error': 'isGraduated muss ein Boolean-Wert sein'}), 400 else: is_graduated_int = None # Kein Update für isGraduated conn = get_db_connection() cursor = conn.cursor() try: if is_graduated_int is not None: cursor.execute(""" UPDATE Deck SET due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?, isGraduated = ? WHERE id = ? """, (due, ivl, factor, reps, lapses, is_graduated_int, box_id)) else: cursor.execute(""" UPDATE Deck SET due = ?, ivl = ?, factor = ?, reps = ?, lapses = ? WHERE id = ? """, (due, ivl, factor, reps, lapses, box_id)) if cursor.rowcount == 0: conn.close() return jsonify({'error': 'Box nicht gefunden'}), 404 conn.commit() conn.close() return jsonify({'status': 'success'}), 200 except sqlite3.Error as e: conn.rollback() conn.close() logger.error(f"Datenbankfehler beim Aktualisieren der Box '{box_id}': {e}") return jsonify({'error': 'Database error', 'details': str(e)}), 500 return jsonify({'status': 'success'}), 200 # Sicherstellen, dass die Datenbank existiert if not os.path.exists(DATABASE): init_db() clean_debug_directories() clean_db_entries()