const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const fs = require('fs'); class DatabaseManager { constructor() { this.db = null; // Place database file under data/db this.dbPath = path.join(__dirname, '../data/db/image_uploader.db'); this.schemaPath = path.join(__dirname, 'schema.sql'); } async initialize() { try { // Stelle sicher, dass das data-Verzeichnis existiert const dataDir = path.dirname(this.dbPath); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } // Öffne Datenbankverbindung this.db = new sqlite3.Database(this.dbPath, (err) => { if (err) { console.error('Fehler beim Öffnen der Datenbank:', err.message); throw err; } else { console.log('✓ SQLite Datenbank verbunden:', this.dbPath); } }); // Aktiviere Foreign Keys await this.run('PRAGMA foreign_keys = ON'); // Erstelle Schema await this.createSchema(); // Generate missing previews for existing images await this.generateMissingPreviews(); console.log('✓ Datenbank erfolgreich initialisiert'); } catch (error) { console.error('Fehler bei Datenbank-Initialisierung:', error); throw error; } } async createSchema() { try { console.log('🔨 Erstelle Datenbank-Schema...'); // Erstelle Groups Tabelle await this.run(` CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id TEXT UNIQUE NOT NULL, year INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, name TEXT, upload_date DATETIME NOT NULL, approved BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Migration: Füge approved Feld zu bestehenden Tabellen hinzu (falls nicht vorhanden) try { await this.run('ALTER TABLE groups ADD COLUMN approved BOOLEAN DEFAULT FALSE'); console.log('✓ Approved Feld zur bestehenden Tabelle hinzugefügt'); } catch (error) { // Feld existiert bereits - das ist okay if (!error.message.includes('duplicate column')) { console.warn('Migration Warnung:', error.message); } } console.log('✓ Groups Tabelle erstellt'); // Erstelle Images Tabelle await this.run(` CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id TEXT NOT NULL, file_name TEXT NOT NULL, original_name TEXT NOT NULL, file_path TEXT NOT NULL, upload_order INTEGER NOT NULL, file_size INTEGER, mime_type TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (group_id) REFERENCES groups(group_id) ON DELETE CASCADE ) `); console.log('✓ Images Tabelle erstellt'); // Migration: Füge preview_path Feld zur images Tabelle hinzu (falls nicht vorhanden) try { await this.run('ALTER TABLE images ADD COLUMN preview_path TEXT'); console.log('✓ preview_path Feld zur images Tabelle hinzugefügt'); } catch (error) { // Feld existiert bereits - das ist okay if (!error.message.includes('duplicate column')) { console.warn('Migration Warnung:', error.message); } } // Erstelle Indizes await this.run('CREATE INDEX IF NOT EXISTS idx_groups_group_id ON groups(group_id)'); await this.run('CREATE INDEX IF NOT EXISTS idx_groups_year ON groups(year)'); await this.run('CREATE INDEX IF NOT EXISTS idx_groups_upload_date ON groups(upload_date)'); await this.run('CREATE INDEX IF NOT EXISTS idx_images_group_id ON images(group_id)'); await this.run('CREATE INDEX IF NOT EXISTS idx_images_upload_order ON images(upload_order)'); console.log('✓ Indizes erstellt'); // Erstelle Trigger await this.run(` CREATE TRIGGER IF NOT EXISTS update_groups_timestamp AFTER UPDATE ON groups FOR EACH ROW BEGIN UPDATE groups SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END `); console.log('✓ Trigger erstellt'); console.log('✅ Datenbank-Schema vollständig erstellt'); } catch (error) { console.error('💥 Fehler beim Erstellen des Schemas:', error); throw error; } } // Promise-wrapper für sqlite3.run run(sql, params = []) { return new Promise((resolve, reject) => { this.db.run(sql, params, function(err) { if (err) { reject(err); } else { resolve({ id: this.lastID, changes: this.changes }); } }); }); } // Promise-wrapper für sqlite3.get get(sql, params = []) { return new Promise((resolve, reject) => { this.db.get(sql, params, (err, row) => { if (err) { reject(err); } else { resolve(row); } }); }); } // Promise-wrapper für sqlite3.all all(sql, params = []) { return new Promise((resolve, reject) => { this.db.all(sql, params, (err, rows) => { if (err) { reject(err); } else { resolve(rows); } }); }); } // Transaction support async transaction(callback) { await this.run('BEGIN TRANSACTION'); try { const result = await callback(this); await this.run('COMMIT'); return result; } catch (error) { await this.run('ROLLBACK'); throw error; } } close() { return new Promise((resolve, reject) => { if (this.db) { this.db.close((err) => { if (err) { reject(err); } else { console.log('✓ Datenbankverbindung geschlossen'); resolve(); } }); } else { resolve(); } }); } // Gesundheitscheck async healthCheck() { try { const result = await this.get('SELECT 1 as test'); return result && result.test === 1; } catch (error) { console.error('Database health check failed:', error); return false; } } // Generate missing previews for existing images async generateMissingPreviews() { try { console.log('🔍 Checking for images without previews...'); // Get all images that don't have a preview_path yet const imagesWithoutPreview = await this.all(` SELECT id, group_id, file_name, file_path FROM images WHERE preview_path IS NULL OR preview_path = '' `); if (imagesWithoutPreview.length === 0) { console.log('✓ All images have previews'); return; } console.log(`📸 Found ${imagesWithoutPreview.length} image(s) without preview, generating...`); const ImagePreviewService = require('../services/ImagePreviewService'); const fsp = require('fs').promises; let successCount = 0; let failCount = 0; for (const image of imagesWithoutPreview) { try { // Check if original file exists const originalPath = ImagePreviewService.getOriginalPath(image.file_name); await fsp.access(originalPath); // Generate preview const previewFileName = ImagePreviewService._getPreviewFileName(image.file_name); const previewPath = ImagePreviewService.getPreviewPath(previewFileName); const result = await ImagePreviewService.generatePreview(originalPath, previewPath); if (result.success) { // Update database with preview_path await this.run(` UPDATE images SET preview_path = ? WHERE id = ? `, [previewFileName, image.id]); successCount++; } else { console.warn(` ⚠️ Preview generation failed for ${image.file_name}: ${result.error}`); failCount++; } } catch (error) { console.warn(` ⚠️ Could not process ${image.file_name}: ${error.message}`); failCount++; } } console.log(`✓ Preview generation complete: ${successCount} success, ${failCount} failed`); } catch (error) { console.warn('⚠️ Preview generation check failed:', error.message); // Don't throw - this shouldn't prevent DB initialization } } } // Singleton Instance const dbManager = new DatabaseManager(); module.exports = dbManager;