Project-Image-Uploader/backend/src/database/DatabaseManager.js
matthias.lotz 170e1c20e6 feat: automatic preview generation on database init
Task 7: Batch-Migration Automation
- Add generateMissingPreviews() method to DatabaseManager
- Automatically runs after schema creation
- Finds all images without preview_path
- Generates previews for existing images on startup
- Graceful error handling (won't break server start)
- Progress logging: 'Found X images without preview, generating...'
- No manual script needed - fully automated

Benefits:
- Works on every backend restart
- Incremental (only missing previews)
- Non-blocking database initialization
- Perfect for deployments and updates
2025-10-30 20:51:35 +01:00

279 lines
10 KiB
JavaScript

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;