changes 0328092025
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
// Note: we intentionally use exec() with careful quoting and polling because on Windows soffice may spawn a child process.
|
||||
|
||||
class GenerateDocumentPreview implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
@@ -21,19 +23,49 @@ class GenerateDocumentPreview implements ShouldQueue
|
||||
*/
|
||||
public $timeout = 180; // 3 minutes
|
||||
|
||||
public function __construct(public int $documentId)
|
||||
{
|
||||
}
|
||||
public function __construct(public int $documentId) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$doc = Document::find($this->documentId);
|
||||
if (!$doc)
|
||||
if (! $doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
$disk = $doc->disk ?: 'public';
|
||||
if (!Storage::disk($disk)->exists($doc->path))
|
||||
return;
|
||||
// Normalize path to support legacy entries with a leading 'public/'
|
||||
$relPath = ltrim($doc->path ?? '', '/\\');
|
||||
if (str_starts_with($relPath, 'public/')) {
|
||||
$relPath = substr($relPath, 7);
|
||||
}
|
||||
$sourceBytes = null;
|
||||
if (Storage::disk($disk)->exists($relPath)) {
|
||||
$sourceBytes = Storage::disk($disk)->get($relPath);
|
||||
} else {
|
||||
// Fallback to public/ filesystem in case of legacy placement
|
||||
$publicFull = public_path($relPath);
|
||||
$real = @realpath($publicFull);
|
||||
$publicRoot = @realpath(public_path());
|
||||
$realN = $real ? str_replace('\\', '/', $real) : null;
|
||||
$rootN = $publicRoot ? str_replace('\\', '/', $publicRoot) : null;
|
||||
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
|
||||
\Log::info('Preview job: using public path fallback for source file', [
|
||||
'document_id' => $doc->id,
|
||||
'path' => $realN,
|
||||
]);
|
||||
$sourceBytes = @file_get_contents($real);
|
||||
} else {
|
||||
\Log::warning('Preview job: source file missing on disk and public fallback failed', [
|
||||
'document_id' => $doc->id,
|
||||
'disk' => $disk,
|
||||
'path' => $doc->path,
|
||||
'normalized' => $relPath,
|
||||
'public_candidate' => $publicFull,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($doc->original_name ?: $doc->file_name, PATHINFO_EXTENSION));
|
||||
|
||||
@@ -48,69 +80,52 @@ public function handle(): void
|
||||
'updated_at' => (string) $doc->updated_at,
|
||||
'preview_generated_at' => (string) $doc->preview_generated_at,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!in_array($ext, ['doc', 'docx']))
|
||||
return; // only convert office docs here
|
||||
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));
|
||||
$tmpBase = sys_get_temp_dir().DIRECTORY_SEPARATOR.'doc_in_'.uniqid();
|
||||
$tmpIn = $tmpBase.'.'.$ext; // e.g., .doc or .docx
|
||||
file_put_contents($tmpIn, $sourceBytes);
|
||||
|
||||
$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';
|
||||
// If an absolute path is configured, ensure it exists to avoid long PATH resolution delays
|
||||
if ($binCfg && preg_match('/^[a-zA-Z]:\\\\|^\//', $bin) && !file_exists($bin)) {
|
||||
if ($binCfg && preg_match('/^[a-zA-Z]:\\\\|^\//', $bin) && ! file_exists($bin)) {
|
||||
Log::warning('Configured LibreOffice binary not found; falling back to PATH', [
|
||||
'document_id' => $doc->id,
|
||||
'bin' => $bin,
|
||||
]);
|
||||
$bin = '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)) {
|
||||
$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), '/');
|
||||
$loProfileUri = 'file:///'.ltrim(str_replace('\\', '/', $loProfileDir), '/');
|
||||
|
||||
// Build command string for exec()
|
||||
$isWin = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||
$binPart = $isWin ? '"'.$bin.'"' : escapeshellcmd($bin);
|
||||
$outDirPart = $isWin ? '"'.$outDir.'"' : escapeshellarg($outDir);
|
||||
$inPart = $isWin ? '"'.$tmpIn.'"' : escapeshellarg($tmpIn);
|
||||
$profilePart = $isWin ? '"'.$loProfileUri.'"' : escapeshellarg($loProfileUri);
|
||||
$cmd = sprintf(
|
||||
'%s --headless --norestore --nolockcheck -env:UserInstallation=%s --convert-to pdf --outdir %s %s',
|
||||
'%s --headless --norestore --nolockcheck --nologo --nodefault --nofirststartwizard -env:UserInstallation=%s --convert-to pdf --outdir %s %s',
|
||||
$binPart,
|
||||
$isWin ? '"' . $loProfileUri . '"' : escapeshellarg($loProfileUri),
|
||||
$profilePart,
|
||||
$outDirPart,
|
||||
$inPart
|
||||
);
|
||||
|
||||
// Capture stderr as well for diagnostics
|
||||
$cmdWithStderr = $cmd . ' 2>&1';
|
||||
$t0 = microtime(true);
|
||||
Log::info('Starting LibreOffice preview conversion', [
|
||||
'document_id' => $doc->id,
|
||||
@@ -119,49 +134,64 @@ public function handle(): void
|
||||
]);
|
||||
$out = [];
|
||||
$ret = 0;
|
||||
exec($cmdWithStderr, $out, $ret);
|
||||
@exec($cmd.' 2>&1', $out, $ret);
|
||||
// Some Windows installs may return before file is fully written; we'll poll for the output file below anyway.
|
||||
if ($ret !== 0) {
|
||||
Log::warning('Preview generation failed', [
|
||||
'document_id' => $doc->id,
|
||||
'ret' => $ret,
|
||||
'cmd' => $cmd,
|
||||
'exit_code' => $ret,
|
||||
'output' => implode("\n", $out),
|
||||
]);
|
||||
@unlink($tmpIn);
|
||||
|
||||
return;
|
||||
}
|
||||
$elapsed = (int) round((microtime(true) - $t0) * 1000);
|
||||
|
||||
$pdfPathLocal = $tmpIn . '.pdf';
|
||||
$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)) {
|
||||
$pdfPathLocal = $outDir.DIRECTORY_SEPARATOR.$base.'.pdf';
|
||||
// Poll for up to 10s for the PDF to appear (handles async write on Windows)
|
||||
$waitUntil = microtime(true) + 10.0;
|
||||
while (! file_exists($pdfPathLocal) && microtime(true) < $waitUntil) {
|
||||
usleep(200 * 1000); // 200ms
|
||||
}
|
||||
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))
|
||||
$try = $outDir.DIRECTORY_SEPARATOR.$origBase.'.pdf';
|
||||
// brief poll for fallback name as well
|
||||
$waitUntil2 = microtime(true) + 5.0;
|
||||
while (! file_exists($try) && microtime(true) < $waitUntil2) {
|
||||
usleep(200 * 1000);
|
||||
}
|
||||
if (file_exists($try)) {
|
||||
$pdfPathLocal = $try;
|
||||
}
|
||||
}
|
||||
if (!file_exists($pdfPathLocal)) {
|
||||
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),
|
||||
'stdout' => implode("\n", $out),
|
||||
]);
|
||||
@unlink($tmpIn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute elapsed time once output exists
|
||||
$elapsed = (int) round((microtime(true) - $t0) * 1000);
|
||||
|
||||
// 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));
|
||||
$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_path = $previewDir.'/'.$doc->uuid.'.pdf';
|
||||
$doc->preview_mime = 'application/pdf';
|
||||
$doc->preview_generated_at = now();
|
||||
$doc->save();
|
||||
@@ -170,6 +200,12 @@ public function handle(): void
|
||||
'preview_path' => $doc->preview_path,
|
||||
'elapsed_ms' => $elapsed,
|
||||
]);
|
||||
} else {
|
||||
Log::warning('Preview generated but storing to disk failed', [
|
||||
'document_id' => $doc->id,
|
||||
'preview_disk' => $previewDisk,
|
||||
'target' => $previewDir.'/'.$doc->uuid.'.pdf',
|
||||
]);
|
||||
}
|
||||
|
||||
@unlink($tmpIn);
|
||||
|
||||
Reference in New Issue
Block a user