Teren-app/app/Jobs/GenerateDocumentPreview.php
Simon Pocrnjič 7227c888d4 Mager updated
2025-09-27 17:45:55 +02:00

150 lines
5.6 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\Document;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class GenerateDocumentPreview implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public int $documentId)
{
}
public function handle(): void
{
$doc = Document::find($this->documentId);
if (!$doc)
return;
$disk = $doc->disk ?: 'public';
if (!Storage::disk($disk)->exists($doc->path))
return;
$ext = strtolower(pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_EXTENSION));
if (!in_array($ext, ['doc', 'docx']))
return; // only convert office docs here
// Prepare temp files - keep original extension so LibreOffice can detect filter
$tmpBase = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'doc_in_' . uniqid();
$tmpIn = $tmpBase . '.' . $ext; // e.g., .doc or .docx
file_put_contents($tmpIn, Storage::disk($disk)->get($doc->path));
$outDir = sys_get_temp_dir();
// Ensure exec is available
if (!function_exists('exec')) {
Log::error('Preview generation failed: exec() not available in this PHP environment', ['document_id' => $doc->id]);
return;
}
$disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
if (in_array('exec', $disabled, true)) {
Log::error('Preview generation failed: exec() is disabled in php.ini (disable_functions)', ['document_id' => $doc->id]);
return;
}
// Run soffice headless to convert to PDF
$binCfg = config('files.libreoffice_bin');
$bin = $binCfg ? (string) $binCfg : 'soffice';
// Windows quoting differs from POSIX. Build command parts safely.
$isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
if ($isWin) {
$binPart = '"' . $bin . '"';
$outDirPart = '"' . $outDir . '"';
$inPart = '"' . $tmpIn . '"';
} else {
$binPart = escapeshellcmd($bin);
$outDirPart = escapeshellarg($outDir);
$inPart = escapeshellarg($tmpIn);
}
// Use a temporary user profile to avoid permissions/profile lock issues
$loProfileDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'lo_profile_' . $doc->id;
if (!is_dir($loProfileDir)) {
@mkdir($loProfileDir, 0700, true);
}
$loProfileUri = 'file:///' . ltrim(str_replace('\\', '/', $loProfileDir), '/');
$cmd = sprintf(
'%s --headless --norestore --nolockcheck -env:UserInstallation=%s --convert-to pdf --outdir %s %s',
$binPart,
$isWin ? '"' . $loProfileUri . '"' : escapeshellarg($loProfileUri),
$outDirPart,
$inPart
);
// Capture stderr as well for diagnostics
$cmdWithStderr = $cmd . ' 2>&1';
Log::info('Starting LibreOffice preview conversion', [
'document_id' => $doc->id,
'cmd' => $cmd,
'is_windows' => $isWin,
]);
$out = [];
$ret = 0;
exec($cmdWithStderr, $out, $ret);
if ($ret !== 0) {
Log::warning('Preview generation failed', [
'document_id' => $doc->id,
'ret' => $ret,
'cmd' => $cmd,
'output' => implode("\n", $out),
]);
@unlink($tmpIn);
return;
}
$pdfPathLocal = $tmpIn . '.pdf';
// LibreOffice writes output with source filename base; derive path
$base = pathinfo($tmpIn, PATHINFO_FILENAME);
$pdfPathLocal = $outDir . DIRECTORY_SEPARATOR . $base . '.pdf';
if (!file_exists($pdfPathLocal)) {
// fallback: try with original name base
$origBase = pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_FILENAME);
$try = $outDir . DIRECTORY_SEPARATOR . $origBase . '.pdf';
if (file_exists($try))
$pdfPathLocal = $try;
}
if (!file_exists($pdfPathLocal)) {
Log::warning('Preview generation did not produce expected PDF output', [
'document_id' => $doc->id,
'out_dir' => $outDir,
'tmp_base' => $base,
'command' => $cmd,
'output' => implode("\n", $out),
]);
@unlink($tmpIn);
return;
}
// Store preview PDF to configured disk inside configured previews base path
$previewDisk = config('files.preview_disk', 'public');
$base = trim(config('files.preview_base', 'previews/cases'), '/');
$previewDir = $base . '/' . ($doc->documentable?->uuid ?? 'unknown');
$stored = Storage::disk($previewDisk)->put($previewDir . '/' . ($doc->uuid) . '.pdf', file_get_contents($pdfPathLocal));
if ($stored) {
$doc->preview_path = $previewDir . '/' . $doc->uuid . '.pdf';
$doc->preview_mime = 'application/pdf';
$doc->preview_generated_at = now();
$doc->save();
}
@unlink($tmpIn);
@unlink($pdfPathLocal);
// Clean up temporary LO profile directory
try {
if (is_dir($loProfileDir)) {
@rmdir($loProfileDir);
}
} catch (\Throwable $e) {
// ignore
}
}
}