release 1.0
This commit is contained in:
Executable
+543
@@ -0,0 +1,543 @@
|
||||
#!/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user