544 lines
18 KiB
PHP
Executable File
544 lines
18 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
|
|
<?php
|
|
|
|
/**
|
|
* Firefly Import Preprocessor - Kommandozeilen-Einstiegspunkt
|
|
*
|
|
* PHP 8.1+ Tool zur Transformation von UBS E-Banking CSV-Exporten
|
|
* in ein Firefly III kompatibles Format.
|
|
*
|
|
* Voraussetzung: composer install
|
|
*/
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use UbsCsvTransformer\TransformerEngine;
|
|
use UbsCsvTransformer\ConfigurationLoader;
|
|
use UbsCsvTransformer\FireflyImporter;
|
|
|
|
// ============================================================================
|
|
// CLI-Argument-Verarbeitung
|
|
// ============================================================================
|
|
|
|
$argc = $_SERVER['argc'] ?? 0;
|
|
$argv = $_SERVER['argv'] ?? [];
|
|
|
|
if ($argc < 2) {
|
|
showHelp();
|
|
exit(0);
|
|
}
|
|
|
|
// Debug-Modus aktivierbar
|
|
$debug = in_array('--debug', $argv) || in_array('-d', $argv);
|
|
|
|
// Extrahiere Kommando
|
|
$command = $argv[1];
|
|
|
|
try {
|
|
match ($command) {
|
|
'test' => handleTest($argc, $argv),
|
|
'transform' => handleTransform($argc, $argv),
|
|
'validate' => handleValidate($argc, $argv),
|
|
'auto-import' => handleAutoImport($argc, $argv),
|
|
'help', '-h', '--help' => showHelp(),
|
|
default => throw new Exception("Unbekanntes Kommando: $command"),
|
|
};
|
|
} catch (Exception $e) {
|
|
fwrite(STDERR, "\n❌ ERROR: " . $e->getMessage() . "\n\n");
|
|
exit(1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMMAND HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Zeige Hilfe und Verwendungsanleitung
|
|
*/
|
|
function showHelp(): void
|
|
{
|
|
echo <<<'HELP'
|
|
╔════════════════════════════════════════════════════════════════════════════╗
|
|
║ Firefly Import Preprocessor - Kommandozeilen-Tool ║
|
|
║ ║
|
|
║ Ein schlankes PHP 8 Tool zur Transformation von UBS E-Banking Exporten ║
|
|
║ in ein Firefly III kompatibles Format. ║
|
|
╚════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
VERWENDUNG:
|
|
transformer [command] [options]
|
|
|
|
KOMMANDOS:
|
|
|
|
test [input] [config] [options]
|
|
Testet die Transformation mit limitierter Zeilenzahl
|
|
Optionen:
|
|
--rows=N Nur N Zeilen verarbeiten (Standard: 10)
|
|
--output=FILE, -o Ergebnis auch in Datei schreiben
|
|
Beispiel:
|
|
transformer test ubs-export.csv config.json --rows=5
|
|
transformer test ubs-export.csv config.json -o test-output.csv
|
|
|
|
transform [input] [config] [options]
|
|
Transformiert eine komplette CSV-Datei
|
|
Optionen:
|
|
--output=FILE, -o Output-Pfad (Standard: input-transformed.csv)
|
|
--no-import Nicht automatisch in Firefly III importieren
|
|
Beispiel:
|
|
transformer transform ubs-export.csv config.json
|
|
transformer transform ubs-export.csv config.json -o import.csv
|
|
|
|
validate [config] [options]
|
|
Validiert die Konfigurationsdatei
|
|
Optionen:
|
|
--strict Strikte Validierung (empfohlen)
|
|
Beispiel:
|
|
transformer validate config.json
|
|
transformer validate config.json --strict
|
|
|
|
auto-import [config] [options]
|
|
Überwacht Quellverzeichnis und verarbeitet neue Dateien
|
|
Optionen:
|
|
--watch Kontinuierliche Überwachung (Daemon-Modus)
|
|
--interval=SEC Prüfintervall in Sekunden (Standard: 60)
|
|
--dry-run Zeigt was gemacht würde (keine echte Verarbeitung)
|
|
Beispiel:
|
|
transformer auto-import config.json
|
|
transformer auto-import config.json --watch --interval=30
|
|
|
|
help, -h, --help
|
|
Zeige diese Hilfe
|
|
|
|
GLOBALE OPTIONEN:
|
|
--debug, -d Aktiviere Debug-Modus (detaillierte Ausgaben)
|
|
|
|
INSTALLATION:
|
|
|
|
1. PHP 8.1+ muss installiert sein
|
|
php --version
|
|
|
|
2. Autoloader-Setup (wähle eins):
|
|
Option A: Mit Composer (empfohlen)
|
|
composer install
|
|
Option B: Manuell - Dateien in Verzeichnisstruktur:
|
|
ff-imp-preprocessor/
|
|
├── bin/transformer.php
|
|
├── src/*.php
|
|
└── config/config.json
|
|
|
|
3. Ausführbar machen:
|
|
chmod +x bin/transformer.php
|
|
|
|
4. Konfiguration anpassen:
|
|
cp config/config.example.json config/config.json
|
|
nano config/config.json
|
|
|
|
BEISPIELE:
|
|
|
|
# Transformation mit Test (erste 5 Zeilen)
|
|
./bin/transformer test data/ubs-export.csv config/config.json --rows=5
|
|
|
|
# Komplette Transformation
|
|
./bin/transformer transform data/ubs-export.csv config/config.json \
|
|
--output=output/firefly-import.csv
|
|
|
|
# Konfiguration validieren
|
|
./bin/transformer validate config/config.json --strict
|
|
|
|
# Auto-Import mit Überwachung starten
|
|
./bin/transformer auto-import config/config.json --watch
|
|
|
|
# Nur nächste Datei verarbeiten
|
|
./bin/transformer auto-import config/config.json
|
|
|
|
KONFIGURATION:
|
|
|
|
Die config.json muss folgende Struktur haben:
|
|
{
|
|
"metadata": { "extractionRules": {...} },
|
|
"csvStructure": { "delimiter": ";", ... },
|
|
"columnTransformations": { ... },
|
|
"fireflyImport": { "apiUrl": "...", "apiKey": "..." },
|
|
"directories": {
|
|
"source": "./import/source",
|
|
"output": "./import/output",
|
|
"archive": "./import/archive",
|
|
"error": "./import/error"
|
|
}
|
|
}
|
|
|
|
DOKUMENTATION:
|
|
|
|
Siehe README.md und UBS_Transformer_Guide.md für vollständige Dokumentation
|
|
|
|
LIZENZ:
|
|
|
|
MIT License
|
|
|
|
HELP;
|
|
}
|
|
|
|
/**
|
|
* Expandiert ~ zu absolutem Home-Verzeichnis und löst relative Pfade auf
|
|
*/
|
|
function expandPath(string $path): string
|
|
{
|
|
if (str_starts_with($path, '~/') || $path === '~') {
|
|
$home = getenv('HOME') ?: posix_getpwuid(posix_getuid())['dir'];
|
|
$path = $home . substr($path, 1);
|
|
}
|
|
|
|
// Relative Pfade gegen cwd auflösen (ohne realpath, damit nicht-existierende Dirs erlaubt sind)
|
|
if (!str_starts_with($path, '/')) {
|
|
$path = getcwd() . '/' . $path;
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Parse CLI-Optionen in assoziatives Array
|
|
*/
|
|
function parseOptions(array $argv, int $startIndex = 0): array
|
|
{
|
|
$options = [];
|
|
|
|
for ($i = $startIndex; $i < count($argv); $i++) {
|
|
if (strpos($argv[$i], '--') === 0) {
|
|
$parts = explode('=', substr($argv[$i], 2), 2);
|
|
$options[$parts[0]] = $parts[1] ?? true;
|
|
} elseif (strpos($argv[$i], '-') === 0 && strlen($argv[$i]) > 1) {
|
|
$options[substr($argv[$i], 1)] = true;
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Teste Transformation mit begrenzter Zeilenzahl
|
|
*/
|
|
function handleTest($argc, $argv): void
|
|
{
|
|
if ($argc < 4) {
|
|
throw new Exception("Usage: transformer test [input-file] [config-file] [options]");
|
|
}
|
|
|
|
$inputFile = $argv[2];
|
|
$configFile = $argv[3];
|
|
$options = parseOptions($argv, 4);
|
|
$debug = isset($options['debug']) || isset($options['d']);
|
|
|
|
$maxRows = isset($options['rows']) ? (int)$options['rows'] : 10;
|
|
$outputFile = $options['output'] ?? $options['o'] ?? null;
|
|
|
|
if (!file_exists($inputFile)) {
|
|
throw new Exception("Input-Datei nicht gefunden: $inputFile");
|
|
}
|
|
if (!file_exists($configFile)) {
|
|
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
|
|
}
|
|
|
|
echo "\n📊 TEST-MODUS: Verarbeite max. $maxRows Zeilen\n";
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
|
|
|
$configLoader = new ConfigurationLoader($configFile);
|
|
$config = $configLoader->load();
|
|
|
|
$engine = new TransformerEngine($configLoader, $debug);
|
|
$result = $engine->transform($inputFile, $maxRows);
|
|
|
|
// Ausgabe Statistiken
|
|
echo "\n✅ STATISTIKEN:\n";
|
|
echo " Verarbeitete Zeilen: " . $result['rowsProcessed'] . "\n";
|
|
echo " Metadaten extrahiert: " . count($result['metadata'] ?? []) . "\n";
|
|
echo " Output-Spalten: " . $result['outputColumns'] . "\n";
|
|
|
|
if (!empty($result['metadata'])) {
|
|
echo "\n📋 EXTRAHIERTE METADATEN:\n";
|
|
foreach ($result['metadata'] as $key => $value) {
|
|
$display = substr($value, 0, 50);
|
|
if (strlen($value) > 50) {
|
|
$display .= "...";
|
|
}
|
|
echo " $key: $display\n";
|
|
}
|
|
}
|
|
|
|
if (!empty($result['sampleData'])) {
|
|
$sampleCount = min(5, count($result['sampleData']));
|
|
echo "\n📝 BEISPIEL-DATEN ($sampleCount Zeilen):\n";
|
|
foreach (array_slice($result['sampleData'], 0, $sampleCount) as $index => $row) {
|
|
echo " Zeile " . ($index + 1) . ": ";
|
|
foreach ($row as $col => $value) {
|
|
$val = substr($value, 0, 30);
|
|
if (strlen($value) > 30) {
|
|
$val .= "...";
|
|
}
|
|
//if (!is_int($col)) {
|
|
echo "$col=$val | ";
|
|
//}
|
|
}
|
|
echo "\n";
|
|
}
|
|
}
|
|
|
|
if ($outputFile) {
|
|
echo "\n💾 Output-Datei: $outputFile\n";
|
|
}
|
|
|
|
echo "\n✅ Test erfolgreich!\n\n";
|
|
}
|
|
|
|
/**
|
|
* Transformiere komplette CSV-Datei
|
|
*/
|
|
function handleTransform($argc, $argv): void
|
|
{
|
|
if ($argc < 4) {
|
|
throw new Exception("Usage: transformer transform [input-file] [config-file] [options]");
|
|
}
|
|
|
|
$inputFile = $argv[2];
|
|
$configFile = $argv[3];
|
|
$options = parseOptions($argv, 4);
|
|
$debug = isset($options['debug']) || isset($options['d']);
|
|
|
|
$outputFile = $options['output'] ?? $options['o'] ?? null;
|
|
|
|
if (!file_exists($inputFile)) {
|
|
throw new Exception("Input-Datei nicht gefunden: $inputFile");
|
|
}
|
|
if (!file_exists($configFile)) {
|
|
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
|
|
}
|
|
|
|
echo "\n🚀 TRANSFORMATION STARTEN\n";
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
|
|
|
$configLoader = new ConfigurationLoader($configFile);
|
|
$configLoader->load();
|
|
|
|
// --output überschreibt Zielverzeichnis und Dateiname aus der Konfiguration
|
|
if ($outputFile !== null) {
|
|
$outputFile = expandPath($outputFile);
|
|
$configLoader->set('directories.output', dirname($outputFile));
|
|
$configLoader->set('csvStructure.outputFilename', basename($outputFile));
|
|
}
|
|
|
|
$engine = new TransformerEngine($configLoader, $debug);
|
|
$result = $engine->transform($inputFile);
|
|
|
|
echo "✅ Transformation erfolgreich!\n";
|
|
echo " Output-Datei: " . ($result['outputFile'] ?? 'N/A') . "\n";
|
|
echo " Zeilen transformiert: " . ($result['rowsProcessed'] ?? 0) . "\n";
|
|
|
|
echo "\n✅ Fertig!\n\n";
|
|
}
|
|
|
|
/**
|
|
* Validiere Konfigurationsdatei
|
|
*/
|
|
function handleValidate($argc, $argv): void
|
|
{
|
|
if ($argc < 3) {
|
|
throw new Exception("Usage: transformer validate [config-file] [options]");
|
|
}
|
|
|
|
$configFile = $argv[2];
|
|
$options = parseOptions($argv, 3);
|
|
$strict = isset($options['strict']);
|
|
|
|
if (!file_exists($configFile)) {
|
|
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
|
|
}
|
|
|
|
echo "\n✔️ KONFIGURATION VALIDIEREN\n";
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
|
|
|
$configLoader = new ConfigurationLoader($configFile);
|
|
|
|
try {
|
|
$config = $configLoader->load();
|
|
|
|
// Basis-Validierung
|
|
echo "✅ JSON-Format valide\n";
|
|
|
|
$required = ['metadata', 'csvStructure', 'columnTransformations'];
|
|
$missing = [];
|
|
|
|
foreach ($required as $key) {
|
|
if (isset($config[$key])) {
|
|
echo "✅ Abschnitt '$key' vorhanden\n";
|
|
} else {
|
|
echo "⚠️ Abschnitt '$key' fehlt\n";
|
|
$missing[] = $key;
|
|
}
|
|
}
|
|
|
|
// Firefly-Validierung
|
|
if (isset($config['fireflyImport'])) {
|
|
echo "✅ Firefly III Konfiguration vorhanden\n";
|
|
if (empty($config['fireflyImport']['apiUrl'])) {
|
|
echo "⚠️ Firefly III API-URL fehlt\n";
|
|
if ($strict) {
|
|
throw new Exception("Strikte Validierung: Firefly III API-URL erforderlich");
|
|
}
|
|
}
|
|
if (empty($config['fireflyImport']['apiKey'])) {
|
|
echo "⚠️ Firefly III API-Key fehlt\n";
|
|
if ($strict) {
|
|
throw new Exception("Strikte Validierung: Firefly III API-Key erforderlich");
|
|
}
|
|
}
|
|
} else {
|
|
echo "⚠️ Firefly III Konfiguration nicht vorhanden (optional)\n";
|
|
}
|
|
|
|
// Verzeichnisse-Validierung
|
|
if (isset($config['directories'])) {
|
|
echo "✅ Verzeichnisse konfiguriert\n";
|
|
$dirs = ['source', 'output', 'archive', 'error'];
|
|
foreach ($dirs as $dir) {
|
|
if (!empty($config['directories'][$dir])) {
|
|
echo " ✅ $dir: " . $config['directories'][$dir] . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($missing)) {
|
|
echo "\n✅ Konfiguration ist VALIDE!\n\n";
|
|
} else {
|
|
if ($strict) {
|
|
throw new Exception("Strikte Validierung: " . count($missing) . " erforderliche Abschnitte fehlen");
|
|
}
|
|
echo "\n⚠️ Konfiguration hat Warnungen aber ist funktional\n\n";
|
|
}
|
|
} catch (Exception $e) {
|
|
throw new Exception("Validierungsfehler: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auto-Import mit Verzeichnis-Überwachung
|
|
*/
|
|
function handleAutoImport($argc, $argv): void
|
|
{
|
|
if ($argc < 3) {
|
|
throw new Exception("Usage: transformer auto-import [config-file] [options]");
|
|
}
|
|
|
|
$configFile = $argv[2];
|
|
$options = parseOptions($argv, 3);
|
|
$debug = isset($options['debug']) || isset($options['d']);
|
|
|
|
if (!file_exists($configFile)) {
|
|
throw new Exception("Konfigurationsdatei nicht gefunden: $configFile");
|
|
}
|
|
|
|
$configLoader = new ConfigurationLoader($configFile);
|
|
$config = $configLoader->load();
|
|
|
|
$sourceDir = $config['directories']['source'] ?? './import/source';
|
|
$outputDir = $config['directories']['output'] ?? './import/output';
|
|
$archiveDir = $config['directories']['archive'] ?? './import/archive';
|
|
$errorDir = $config['directories']['error'] ?? './import/error';
|
|
$dryRun = isset($options['dry-run']);
|
|
$watch = isset($options['watch']);
|
|
$interval = isset($options['interval']) ? (int)$options['interval'] : 60;
|
|
|
|
// Verzeichnisse erstellen
|
|
foreach ([$sourceDir, $outputDir, $archiveDir, $errorDir] as $dir) {
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
}
|
|
|
|
echo "\n🔍 AUTO-IMPORT GESTARTET\n";
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
|
echo " Quellverzeichnis: $sourceDir\n";
|
|
echo " Output-Verzeichnis: $outputDir\n";
|
|
echo " Archiv-Verzeichnis: $archiveDir\n";
|
|
|
|
if ($watch) {
|
|
echo " Mode: WATCH (kontinuierlich)\n";
|
|
echo " Intervall: {$interval}s\n";
|
|
} else {
|
|
echo " Mode: EINMALIG\n";
|
|
}
|
|
|
|
if ($dryRun) {
|
|
echo " Dry-Run: JA (keine echten Operationen)\n";
|
|
}
|
|
|
|
echo "\n";
|
|
|
|
if ($watch) {
|
|
echo "⏳ Drücke Ctrl+C zum Beenden.\n\n";
|
|
while (true) {
|
|
processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug);
|
|
sleep($interval);
|
|
}
|
|
} else {
|
|
processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verarbeite Verzeichnis mit CSV-Dateien
|
|
*/
|
|
function processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun = false, $debug = false): void
|
|
{
|
|
if (!is_dir($sourceDir)) {
|
|
return;
|
|
}
|
|
|
|
$files = glob($sourceDir . '/*.csv');
|
|
|
|
if (empty($files)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
$basename = basename($file);
|
|
|
|
try {
|
|
echo "📄 Verarbeite: $basename ... ";
|
|
|
|
$configLoader = new ConfigurationLoader($configFile);
|
|
$config = $configLoader->load();
|
|
|
|
$engine = new TransformerEngine($configLoader, $debug);
|
|
$outputFile = $outputDir . '/' . str_replace('.csv', '-transformed.csv', $basename);
|
|
|
|
if (!$dryRun) {
|
|
$result = $engine->transform($file);
|
|
$outputFile = $result['outputFile'] ?? $outputFile;
|
|
|
|
// Archiviere Original-Datei
|
|
$archiveFile = $archiveDir . '/' . $basename;
|
|
if (!rename($file, $archiveFile)) {
|
|
throw new Exception("Konnte nicht archivieren");
|
|
}
|
|
|
|
// Firefly Import
|
|
if (!empty($config['fireflyImport'])) {
|
|
$importer = new FireflyImporter($config['fireflyImport']);
|
|
$importer->import($outputFile);
|
|
}
|
|
}
|
|
|
|
echo "✅\n";
|
|
} catch (Exception $e) {
|
|
echo "❌ " . $e->getMessage() . "\n";
|
|
|
|
if (!$dryRun) {
|
|
// Verschiebe zu Error-Verzeichnis
|
|
$errorFile = $errorDir . '/' . $basename;
|
|
@rename($file, $errorFile);
|
|
}
|
|
}
|
|
}
|
|
}
|