Oprava Import SDF.

master
Kowalski 2026-11-01 09:27:41 +01:00
parent 236abde4ec
commit 38e169bfcf
5 changed files with 96 additions and 35 deletions

View File

@ -358,6 +358,12 @@ class DatabazeFacade
public function getSdfFilePath(int $loginId): ?string public function getSdfFilePath(int $loginId): ?string
{ {
// JSON export funguje i na Linuxu; .sdf vyžaduje Windows + SQL Compact.
$export = $this->sdfExportPath($loginId);
if (is_file($export)) {
return $export;
}
$path = $this->sdfPath($loginId); $path = $this->sdfPath($loginId);
return is_file($path) ? $path : null; return is_file($path) ? $path : null;
} }
@ -386,12 +392,25 @@ class DatabazeFacade
} }
$extension = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)); $extension = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION));
if ($extension !== 'sdf') { if (!in_array($extension, ['sdf', 'json'], true)) {
throw new \InvalidArgumentException('Soubor musí mít příponu .sdf.'); throw new \InvalidArgumentException('Soubor musí mít příponu .sdf nebo .json (export).');
} }
FileSystem::createDir($this->sdfStorageDir); FileSystem::createDir($this->sdfStorageDir);
$file->move($this->sdfPath($loginId));
$target = $extension === 'json'
? $this->sdfExportPath($loginId)
: $this->sdfPath($loginId);
// Nový upload přepíše druhý formát, ať import nebere starý soubor.
$other = $extension === 'json'
? $this->sdfPath($loginId)
: $this->sdfExportPath($loginId);
if (is_file($other)) {
FileSystem::delete($other);
}
$file->move($target);
$this->clearSdfImportMeta($loginId); $this->clearSdfImportMeta($loginId);
$meta = [ $meta = [
@ -399,6 +418,7 @@ class DatabazeFacade
'uploadedAt' => (new \DateTimeImmutable())->format('c'), 'uploadedAt' => (new \DateTimeImmutable())->format('c'),
'size' => $file->getSize(), 'size' => $file->getSize(),
'loginId' => $loginId, 'loginId' => $loginId,
'format' => $extension,
]; ];
FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true)); FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true));
} }
@ -490,12 +510,12 @@ class DatabazeFacade
} }
/** /**
* @return array{originalName?: string, uploadedAt?: string, size?: int}|array{} * @return array{originalName?: string, uploadedAt?: string, size?: int, format?: string}|array{}
*/ */
private function readSdfMeta(int $loginId): array private function readSdfMeta(int $loginId): array
{ {
$sdf = $this->sdfPath($loginId); $source = $this->getSdfFilePath($loginId);
if (!is_file($sdf)) { if ($source === null) {
return []; return [];
} }
@ -510,9 +530,10 @@ class DatabazeFacade
} }
} }
$meta['size'] = filesize($sdf) ?: ($meta['size'] ?? null); $meta['size'] = filesize($source) ?: ($meta['size'] ?? null);
$meta['uploadedAt'] ??= date('c', filemtime($sdf) ?: time()); $meta['uploadedAt'] ??= date('c', filemtime($source) ?: time());
$meta['originalName'] ??= $loginId . '.sdf'; $meta['originalName'] ??= basename($source);
$meta['format'] ??= strtolower(pathinfo($source, PATHINFO_EXTENSION));
return $meta; return $meta;
} }
@ -550,6 +571,11 @@ class DatabazeFacade
return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.sdf'; return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.sdf';
} }
private function sdfExportPath(int $loginId): string
{
return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.export.json';
}
private function sdfMetaPath(int $loginId): string private function sdfMetaPath(int $loginId): string
{ {
return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.json'; return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.json';

View File

@ -14,7 +14,7 @@ class SdfCompactExporter
} }
/** /**
* Přečte SQL Compact .sdf a vrátí tabulky jako pole. * Přečte SQL Compact .sdf (Windows) nebo hotový JSON export a vrátí tabulky jako pole.
* *
* @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array} * @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array}
*/ */
@ -23,38 +23,67 @@ class SdfCompactExporter
if (!is_file($sdfPath)) { if (!is_file($sdfPath)) {
throw new \RuntimeException('SDF soubor nebyl nalezen.'); throw new \RuntimeException('SDF soubor nebyl nalezen.');
} }
$extension = strtolower(pathinfo($sdfPath, PATHINFO_EXTENSION));
if ($extension === 'json') {
return $this->normalizeExport(Json::decode(FileSystem::read($sdfPath), forceArrays: true));
}
if (!is_file($this->scriptPath)) { if (!is_file($this->scriptPath)) {
throw new \RuntimeException('Exportní skript bin/sdf-export.ps1 chybí.'); throw new \RuntimeException('Exportní skript bin/sdf-export.ps1 chybí.');
} }
$powershell = $this->findPowershell();
if ($powershell === null) {
throw new \RuntimeException(
'Čtení .sdf vyžaduje Windows a PowerShell se SQL Server Compact 4.0. '
. 'Na Linux serveru nejdřív na Windows spusťte bin/sdf-export.ps1 '
. 'a nahrajte výsledný .json.'
);
}
$workDir = dirname($sdfPath) . DIRECTORY_SEPARATOR . 'export-' . bin2hex(random_bytes(4)); $workDir = dirname($sdfPath) . DIRECTORY_SEPARATOR . 'export-' . bin2hex(random_bytes(4));
FileSystem::createDir($workDir); FileSystem::createDir($workDir);
$outFile = $workDir . DIRECTORY_SEPARATOR . 'export.json'; $outFile = $workDir . DIRECTORY_SEPARATOR . 'export.json';
try { try {
$this->runPowershell($sdfPath, $outFile); $this->runPowershell($powershell, $sdfPath, $outFile);
$raw = FileSystem::read($outFile); $raw = FileSystem::read($outFile);
$data = Json::decode($raw, forceArrays: true); return $this->normalizeExport(Json::decode($raw, forceArrays: true));
if (!is_array($data)) {
throw new \RuntimeException('Export SDF nevrátil platná data.');
}
return [
'uzivatel' => $data['uzivatel'] ?? [],
'zakaznik' => $data['zakaznik'] ?? [],
'faktura' => $data['faktura'] ?? [],
'polozka' => $data['polozka'] ?? [],
'platba' => $data['platba'] ?? [],
'konfigurace' => $data['konfigurace'] ?? [],
];
} finally { } finally {
FileSystem::delete($workDir); FileSystem::delete($workDir);
} }
} }
private function runPowershell(string $sdfPath, string $outFile): void /**
* @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array}
*/
private function normalizeExport(mixed $data): array
{
if (!is_array($data)) {
throw new \RuntimeException('Export SDF nevrátil platná data.');
}
$hasTables = isset($data['uzivatel']) || isset($data['zakaznik']) || isset($data['faktura']);
if (!$hasTables) {
throw new \RuntimeException(
'JSON nemá očekávanou strukturu exportu (uzivatel/zakaznik/faktura). '
. 'Vygenerujte ho přes bin/sdf-export.ps1.'
);
}
return [
'uzivatel' => $data['uzivatel'] ?? [],
'zakaznik' => $data['zakaznik'] ?? [],
'faktura' => $data['faktura'] ?? [],
'polozka' => $data['polozka'] ?? [],
'platba' => $data['platba'] ?? [],
'konfigurace' => $data['konfigurace'] ?? [],
];
}
private function runPowershell(string $powershell, string $sdfPath, string $outFile): void
{ {
$powershell = $this->findPowershell();
$cmd = sprintf( $cmd = sprintf(
'%s -NoProfile -NonInteractive -ExecutionPolicy Bypass -File %s -SdfPath %s -OutFile %s -Password %s', '%s -NoProfile -NonInteractive -ExecutionPolicy Bypass -File %s -SdfPath %s -OutFile %s -Password %s',
$this->winArg($powershell), $this->winArg($powershell),
@ -85,8 +114,12 @@ class SdfCompactExporter
} }
} }
private function findPowershell(): string private function findPowershell(): ?string
{ {
if (PHP_OS_FAMILY !== 'Windows') {
return null;
}
$candidates = [ $candidates = [
getenv('SystemRoot') . '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', getenv('SystemRoot') . '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',

View File

@ -39,7 +39,7 @@ class SdfKufrikImporter
$sdfPath = $this->databazeFacade->getSdfFilePath($loginId); $sdfPath = $this->databazeFacade->getSdfFilePath($loginId);
if ($sdfPath === null) { if ($sdfPath === null) {
throw new \RuntimeException('K této databázi ještě není nahraný soubor SDF.'); throw new \RuntimeException('K této databázi ještě není nahraný soubor SDF ani JSON export.');
} }
$exported = $this->sdfExporter->export($sdfPath); $exported = $this->sdfExporter->export($sdfPath);

View File

@ -248,8 +248,8 @@ class AdminPresenter extends BasePresenter
$maxBytes = min(self::SDF_MAX_BYTES, ...($iniLimits ?: [self::SDF_MAX_BYTES])); $maxBytes = min(self::SDF_MAX_BYTES, ...($iniLimits ?: [self::SDF_MAX_BYTES]));
$maxMb = max(1, (int) floor($maxBytes / (1024 * 1024))); $maxMb = max(1, (int) floor($maxBytes / (1024 * 1024)));
$form->addUpload('sdf', 'Soubor SDF:') $form->addUpload('sdf', 'Soubor SDF / JSON:')
->setRequired('Vyberte soubor s příponou .sdf.') ->setRequired('Vyberte soubor .sdf nebo JSON export.')
->addRule($form::MaxFileSize, "Soubor může mít nejvýše {$maxMb} MB.", $maxBytes) ->addRule($form::MaxFileSize, "Soubor může mít nejvýše {$maxMb} MB.", $maxBytes)
->addRule( ->addRule(
function (UploadControl $control): bool { function (UploadControl $control): bool {
@ -258,9 +258,11 @@ class AdminPresenter extends BasePresenter
return false; return false;
} }
return strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)) === 'sdf'; $ext = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION));
return in_array($ext, ['sdf', 'json'], true);
}, },
'Soubor musí mít příponu .sdf.' 'Soubor musí mít příponu .sdf nebo .json.'
); );
$form->addSubmit('send', 'Import'); $form->addSubmit('send', 'Import');

View File

@ -46,7 +46,7 @@
<form n:name="sdfImportForm" class="d-none"> <form n:name="sdfImportForm" class="d-none">
<input n:name="loginId"> <input n:name="loginId">
<input n:name="sdf" accept=".sdf"> <input n:name="sdf" accept=".sdf,.json,application/json">
<button n:name="send">Import</button> <button n:name="send">Import</button>
</form> </form>
@ -114,7 +114,7 @@
class="btn btn-sm btn-success js-sdf-import" class="btn btn-sm btn-success js-sdf-import"
data-login-id="{$firma->LOGIN_ID}" data-login-id="{$firma->LOGIN_ID}"
data-firma="{$firma->NAZEV}" data-firma="{$firma->NAZEV}"
title="Import ze SDF (smaže data firmy)"> title="Import ze SDF / JSON exportu (smaže data firmy)">
<i class="bi bi-box-arrow-in-down me-1"></i>Import <i class="bi bi-box-arrow-in-down me-1"></i>Import
</button> </button>
{else} {else}
@ -148,7 +148,7 @@ $(document).ready(function() {
$(document).on('click', '.js-sdf-import', function() { $(document).on('click', '.js-sdf-import', function() {
const firma = $(this).data('firma'); const firma = $(this).data('firma');
if (!confirm('VAROVÁNÍ: Import ze SDF pro firmu ' + firma + ' SMAŽE všechna stávající data této firmy.\n\nOpravdu pokračovat?')) { if (!confirm('VAROVÁNÍ: Import ze SDF/JSON pro firmu ' + firma + ' SMAŽE všechna stávající data této firmy.\n\nNa Linux serveru nahrajte JSON z bin/sdf-export.ps1 (Windows).\n\nOpravdu pokračovat?')) {
return; return;
} }
$importLoginId.val($(this).data('login-id')); $importLoginId.val($(this).data('login-id'));