documentId); if (! $doc) { return; } $disk = $doc->disk ?: 'public'; // 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)); // If a preview was already generated after the document was last updated, skip re-generation if ($doc->preview_generated_at && $doc->updated_at && $doc->preview_path) { $previewDisk = config('files.preview_disk', 'public'); if (Storage::disk($previewDisk)->exists($doc->preview_path)) { if ($doc->updated_at->lte($doc->preview_generated_at)) { Log::info('Skipping preview generation (already up to date)', [ 'document_id' => $doc->id, 'preview_path' => $doc->preview_path, '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 // 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, $sourceBytes); $outDir = sys_get_temp_dir(); // 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)) { Log::warning('Configured LibreOffice binary not found; falling back to PATH', [ 'document_id' => $doc->id, 'bin' => $bin, ]); $bin = 'soffice'; } // 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), '/'); // 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 --nologo --nodefault --nofirststartwizard -env:UserInstallation=%s --convert-to pdf --outdir %s %s', $binPart, $profilePart, $outDirPart, $inPart ); $t0 = microtime(true); Log::info('Starting LibreOffice preview conversion', [ 'document_id' => $doc->id, 'cmd' => $cmd, 'is_windows' => $isWin, ]); $out = []; $ret = 0; @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, 'exit_code' => $ret, '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'; // 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'; // 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)) { Log::warning('Preview generation did not produce expected PDF output', [ 'document_id' => $doc->id, 'out_dir' => $outDir, 'tmp_base' => $base, 'command' => $cmd, '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)); if ($stored) { $doc->preview_path = $previewDir.'/'.$doc->uuid.'.pdf'; $doc->preview_mime = 'application/pdf'; $doc->preview_generated_at = now(); $doc->save(); Log::info('Preview generated and stored', [ 'document_id' => $doc->id, '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); @unlink($pdfPathLocal); // Clean up temporary LO profile directory try { if (is_dir($loProfileDir)) { @rmdir($loProfileDir); } } catch (\Throwable $e) { // ignore } } }