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
279 lines
10 KiB
JavaScript
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; |