- 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>
110 lines
3.9 KiB
PHP
110 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Repository\DocumentRepository;
|
|
use App\Service\DocumentStorageService;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
use function strlen;
|
|
|
|
#[Route('/api/documents')]
|
|
class DocumentServeController extends AbstractController
|
|
{
|
|
public function __construct(
|
|
private readonly DocumentRepository $documentRepository,
|
|
private readonly DocumentStorageService $storageService,
|
|
) {}
|
|
|
|
#[Route('/{id}/file', name: 'document_serve_file', methods: ['GET'])]
|
|
public function serve(string $id): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
|
|
|
$document = $this->documentRepository->find($id);
|
|
if (!$document) {
|
|
return $this->json(['error' => 'Document not found.'], 404);
|
|
}
|
|
|
|
$path = $document->getPath();
|
|
|
|
// Backward compatibility: serve Base64 data URIs from DB
|
|
if ($this->storageService->isBase64DataUri($path)) {
|
|
$parts = explode(',', $path, 2);
|
|
$content = base64_decode($parts[1] ?? '', true);
|
|
if (false === $content) {
|
|
return $this->json(['error' => 'Invalid document data.'], 500);
|
|
}
|
|
|
|
return new Response($content, 200, [
|
|
'Content-Type' => $document->getMimeType(),
|
|
'Content-Disposition' => ResponseHeaderBag::DISPOSITION_INLINE.'; filename="'.$document->getFilename().'"',
|
|
'Content-Length' => (string) strlen($content),
|
|
'Cache-Control' => 'private, max-age=3600',
|
|
]);
|
|
}
|
|
|
|
// File-based path: serve from disk
|
|
$absolutePath = $this->storageService->getAbsolutePath($path);
|
|
if (!file_exists($absolutePath)) {
|
|
return $this->json(['error' => 'File not found on disk.'], 404);
|
|
}
|
|
|
|
$response = new BinaryFileResponse($absolutePath);
|
|
$response->headers->set('Content-Type', $document->getMimeType());
|
|
$response->setContentDisposition(
|
|
ResponseHeaderBag::DISPOSITION_INLINE,
|
|
$document->getFilename()
|
|
);
|
|
$response->headers->set('Cache-Control', 'private, max-age=3600');
|
|
|
|
return $response;
|
|
}
|
|
|
|
#[Route('/{id}/download', name: 'document_download_file', methods: ['GET'])]
|
|
public function download(string $id): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
|
|
|
$document = $this->documentRepository->find($id);
|
|
if (!$document) {
|
|
return $this->json(['error' => 'Document not found.'], 404);
|
|
}
|
|
|
|
$path = $document->getPath();
|
|
|
|
if ($this->storageService->isBase64DataUri($path)) {
|
|
$parts = explode(',', $path, 2);
|
|
$content = base64_decode($parts[1] ?? '', true);
|
|
if (false === $content) {
|
|
return $this->json(['error' => 'Invalid document data.'], 500);
|
|
}
|
|
|
|
return new Response($content, 200, [
|
|
'Content-Type' => 'application/octet-stream',
|
|
'Content-Disposition' => ResponseHeaderBag::DISPOSITION_ATTACHMENT.'; filename="'.$document->getFilename().'"',
|
|
'Content-Length' => (string) strlen($content),
|
|
]);
|
|
}
|
|
|
|
$absolutePath = $this->storageService->getAbsolutePath($path);
|
|
if (!file_exists($absolutePath)) {
|
|
return $this->json(['error' => 'File not found on disk.'], 404);
|
|
}
|
|
|
|
$response = new BinaryFileResponse($absolutePath);
|
|
$response->setContentDisposition(
|
|
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
|
$document->getFilename()
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
}
|