feat(documents) : filesystem storage, server-side pagination and PDF compression

- Add DocumentStorageService for file-based storage (replaces Base64 in DB)
- Add DocumentServeController with /file and /download endpoints
- Add DocumentUploadProcessor using FormData + filesystem storage
- Add DocumentNormalizer exposing fileUrl/downloadUrl on all responses
- Add DocumentFileCleanupListener for automatic file deletion
- Add MigrateDocumentsToFilesystemCommand (Base64 → files, memory-safe)
- Add ApiFilter (SearchFilter, ExistsFilter, OrderFilter) on Document entity
- Add PdfCompressorService + refactor CompressPdfCommand for batch processing
- Fix TypeMachine PUT: deserialize=false + validate=false to prevent
  UniqueEntity false positive and writableLink collection interference
- Update CHANGELOG for v1.8.0
- Update frontend submodule

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-03 15:18:55 +01:00
parent 7a5dd0b555
commit d89c97f0a0
14 changed files with 942 additions and 90 deletions

View File

@@ -6,6 +6,63 @@ namespace App\Service;
class PdfCompressorService
{
/**
* Compress an actual PDF file on disk. Returns metadata or null if no gain.
*
* @return null|array{size: int, originalSize: int, saved: int}
*/
public function compressFile(string $absolutePath): ?array
{
exec('which qpdf', $qpdfPath, $returnCode);
if (0 !== $returnCode) {
return null;
}
if (!file_exists($absolutePath)) {
return null;
}
$originalSize = filesize($absolutePath);
if (false === $originalSize) {
return null;
}
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
$command = sprintf(
'qpdf --linearize --object-streams=generate %s %s 2>&1',
escapeshellarg($absolutePath),
escapeshellarg($tempOutput)
);
exec($command, $cmdOutput, $returnCode);
if (0 !== $returnCode || !file_exists($tempOutput)) {
@unlink($tempOutput);
return null;
}
$compressedSize = filesize($tempOutput);
if (false === $compressedSize || $compressedSize >= $originalSize) {
@unlink($tempOutput);
return null;
}
if (!rename($tempOutput, $absolutePath)) {
@unlink($tempOutput);
return null;
}
return [
'size' => $compressedSize,
'originalSize' => $originalSize,
'saved' => $originalSize - $compressedSize,
];
}
public function compressBase64Pdf(string $base64Data): ?array
{
// Check if qpdf is available