phase a audit
This commit is contained in:
@@ -21,12 +21,10 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use function function_exists;
|
||||
|
||||
#[Route('/admin/documents')]
|
||||
class DocumentController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route('', name: 'admin_documents')]
|
||||
public function index(EntityManagerInterface $em): Response
|
||||
{
|
||||
@@ -41,7 +39,7 @@ class DocumentController extends AbstractController
|
||||
->getResult();
|
||||
|
||||
return $this->render('admin/document/index.html.twig', [
|
||||
'documents' => $documents
|
||||
'documents' => $documents,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -54,7 +52,7 @@ class DocumentController extends AbstractController
|
||||
{
|
||||
try {
|
||||
$uuid = Uuid::fromString($id);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Exception) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ class DocumentController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('admin/document/show.html.twig', [
|
||||
'document' => $document
|
||||
'document' => $document,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -76,92 +74,72 @@ class DocumentController extends AbstractController
|
||||
FormatText $formatText,
|
||||
IngestJobService $jobService,
|
||||
ParameterBagInterface $params
|
||||
): Response
|
||||
{
|
||||
if ($request->isMethod('POST')) {
|
||||
|
||||
/** @var UploadedFile|null $file */
|
||||
$file = $request->files->get('file');
|
||||
|
||||
if (!$file instanceof UploadedFile) {
|
||||
throw new \InvalidArgumentException('No valid file uploaded.');
|
||||
}
|
||||
|
||||
$rawTitle = $request->request->get('title');
|
||||
|
||||
$title = is_string($rawTitle) && $rawTitle !== ''
|
||||
? $rawTitle
|
||||
: $formatText->slugify($file->getClientOriginalName());
|
||||
|
||||
|
||||
if (!$title) {
|
||||
$this->addFlash('error', 'Titel ist erforderlich.');
|
||||
return $this->redirectToRoute('admin_document_new');
|
||||
}
|
||||
|
||||
$uploadDir = $params->get('mto.vector.data.upload.path');
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$newFilename = uniqid() . '_' . $file->getClientOriginalName();
|
||||
|
||||
try {
|
||||
$file->move($uploadDir, $newFilename);
|
||||
} catch (FileException $e) {
|
||||
throw new \RuntimeException('File upload failed.');
|
||||
}
|
||||
|
||||
$filePath = $uploadDir . '/' . $newFilename;
|
||||
|
||||
// Dokument erstellen
|
||||
$document = $documentService->createDocument(
|
||||
$title,
|
||||
$filePath,
|
||||
$this->getUser()
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// AUTO-INTEGRATION: gleicher Flow wie "Version aktivieren"
|
||||
// ---------------------------------------------------------
|
||||
|
||||
$version = $document->getCurrentVersion();
|
||||
|
||||
$job = $jobService->startJob(
|
||||
IngestJob::TYPE_DOCUMENT_VERSION_ACTIVATE,
|
||||
$this->getUser(),
|
||||
$version->getDocument()->getId(),
|
||||
$version->getId(),
|
||||
null,
|
||||
IngestJob::STATUS_QUEUED
|
||||
);
|
||||
|
||||
$projectDir = (string)$this->getParameter('kernel.project_dir');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg($console),
|
||||
escapeshellarg('mto:agent:ingest:run'),
|
||||
escapeshellarg((string)$job->getId()),
|
||||
escapeshellarg('--no-interaction'),
|
||||
);
|
||||
|
||||
if (!function_exists('exec')) {
|
||||
$jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
|
||||
$this->addFlash('danger', 'Dokument erstellt, aber Ingest konnte nicht asynchron gestartet werden.');
|
||||
return $this->redirectToRoute('admin_documents');
|
||||
}
|
||||
|
||||
exec($cmd);
|
||||
|
||||
return $this->redirectToRoute('admin_job_show', [
|
||||
'id' => (string)$job->getId(),
|
||||
]);
|
||||
): Response {
|
||||
if (!$request->isMethod('POST')) {
|
||||
return $this->render('admin/document/new.html.twig');
|
||||
}
|
||||
|
||||
return $this->render('admin/document/new.html.twig');
|
||||
/** @var UploadedFile|null $file */
|
||||
$file = $request->files->get('file');
|
||||
if (!$file instanceof UploadedFile) {
|
||||
throw new \InvalidArgumentException('No valid file uploaded.');
|
||||
}
|
||||
|
||||
$rawTitle = $request->request->get('title');
|
||||
$title = is_string($rawTitle) && $rawTitle !== ''
|
||||
? $rawTitle
|
||||
: $formatText->slugify($file->getClientOriginalName());
|
||||
|
||||
if (!$title) {
|
||||
$this->addFlash('error', 'Titel ist erforderlich.');
|
||||
return $this->redirectToRoute('admin_document_new');
|
||||
}
|
||||
|
||||
$uploadDir = (string)$params->get('mto.vector.data.upload.path');
|
||||
$this->ensureDir($uploadDir);
|
||||
|
||||
$newFilename = uniqid('', true) . '_' . $file->getClientOriginalName();
|
||||
|
||||
try {
|
||||
$file->move($uploadDir, $newFilename);
|
||||
} catch (FileException) {
|
||||
throw new \RuntimeException('File upload failed.');
|
||||
}
|
||||
|
||||
$filePath = $uploadDir . '/' . $newFilename;
|
||||
|
||||
$document = $documentService->createDocument(
|
||||
$title,
|
||||
$filePath,
|
||||
$this->getUser()
|
||||
);
|
||||
|
||||
$version = $document->getCurrentVersion();
|
||||
if (!$version instanceof DocumentVersion) {
|
||||
$this->addFlash('danger', 'Dokument erstellt, aber es wurde keine aktuelle Version erzeugt.');
|
||||
return $this->redirectToRoute('admin_documents');
|
||||
}
|
||||
|
||||
$job = $jobService->startJob(
|
||||
IngestJob::TYPE_DOCUMENT_VERSION_ACTIVATE,
|
||||
$this->getUser(),
|
||||
$version->getDocument()->getId(),
|
||||
$version->getId(),
|
||||
null,
|
||||
IngestJob::STATUS_QUEUED
|
||||
);
|
||||
|
||||
if (!$this->canExec()) {
|
||||
$jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
|
||||
$this->addFlash('danger', 'Dokument erstellt, aber Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');
|
||||
return $this->redirectToRoute('admin_documents');
|
||||
}
|
||||
|
||||
$this->startIngestJob((string)$job->getId());
|
||||
|
||||
return $this->redirectToRoute('admin_job_show', [
|
||||
'id' => (string)$job->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}/version/new', name: 'admin_document_version_new', requirements: ['id' => '[0-9a-fA-F\-]{36}'])]
|
||||
@@ -171,52 +149,46 @@ class DocumentController extends AbstractController
|
||||
EntityManagerInterface $em,
|
||||
DocumentService $documentService,
|
||||
ParameterBagInterface $params
|
||||
): Response
|
||||
{
|
||||
|
||||
): Response {
|
||||
$document = $em->getRepository(Document::class)->find($id);
|
||||
|
||||
if (!$document) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
|
||||
$file = $request->files->get('file');
|
||||
|
||||
if (!$file) {
|
||||
$this->addFlash('error', 'Datei ist erforderlich.');
|
||||
return $this->redirectToRoute('admin_document_version_new', ['id' => $id]);
|
||||
}
|
||||
|
||||
$uploadDir = $params->get('mto.vector.data.upload.path');
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$newFilename = uniqid() . '_' . $file->getClientOriginalName();
|
||||
|
||||
try {
|
||||
$file->move($uploadDir, $newFilename);
|
||||
} catch (FileException $e) {
|
||||
throw new \RuntimeException('File upload failed.');
|
||||
}
|
||||
|
||||
$filePath = $uploadDir . '/' . $newFilename;
|
||||
|
||||
$documentService->addVersion(
|
||||
$document,
|
||||
$filePath,
|
||||
$this->getUser()
|
||||
);
|
||||
|
||||
return $this->redirectToRoute('admin_document_show', ['id' => $id]);
|
||||
if (!$request->isMethod('POST')) {
|
||||
return $this->render('admin/document/new_version.html.twig', [
|
||||
'document' => $document,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('admin/document/new_version.html.twig', [
|
||||
'document' => $document
|
||||
]);
|
||||
/** @var UploadedFile|null $file */
|
||||
$file = $request->files->get('file');
|
||||
if (!$file instanceof UploadedFile) {
|
||||
$this->addFlash('error', 'Datei ist erforderlich.');
|
||||
return $this->redirectToRoute('admin_document_version_new', ['id' => $id]);
|
||||
}
|
||||
|
||||
$uploadDir = (string)$params->get('mto.vector.data.upload.path');
|
||||
$this->ensureDir($uploadDir);
|
||||
|
||||
$newFilename = uniqid('', true) . '_' . $file->getClientOriginalName();
|
||||
|
||||
try {
|
||||
$file->move($uploadDir, $newFilename);
|
||||
} catch (FileException) {
|
||||
throw new \RuntimeException('File upload failed.');
|
||||
}
|
||||
|
||||
$filePath = $uploadDir . '/' . $newFilename;
|
||||
|
||||
$documentService->addVersion(
|
||||
$document,
|
||||
$filePath,
|
||||
$this->getUser()
|
||||
);
|
||||
|
||||
return $this->redirectToRoute('admin_document_show', ['id' => $id]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
@@ -231,27 +203,18 @@ class DocumentController extends AbstractController
|
||||
EntityManagerInterface $em,
|
||||
DocumentService $documentService,
|
||||
IngestJobService $jobService,
|
||||
): RedirectResponse
|
||||
{
|
||||
|
||||
if (!$this->isCsrfTokenValid('activate_version_' . $versionId, $request->request->get('_token'))) {
|
||||
): RedirectResponse {
|
||||
if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string)$request->request->get('_token'))) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$version = $em->getRepository(DocumentVersion::class)->find($versionId);
|
||||
|
||||
if (!$version) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
try {
|
||||
$documentService->activateVersion($version);
|
||||
// ---------------------------------------------------------
|
||||
// Saubere IngestJob-Integration:
|
||||
// 1) Job als QUEUED anlegen (spezieller Typ für Aktivierung)
|
||||
// 2) Symfony-Command im Hintergrund starten
|
||||
// 3) Direkt auf Job-Detailseite redirecten (Loader + Polling)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
$job = $jobService->startJob(
|
||||
IngestJob::TYPE_DOCUMENT_VERSION_ACTIVATE,
|
||||
@@ -262,28 +225,15 @@ class DocumentController extends AbstractController
|
||||
IngestJob::STATUS_QUEUED
|
||||
);
|
||||
|
||||
// Hintergrundprozess starten (Provider-kompatibel, kein Worker/Daemon)
|
||||
$projectDir = (string)$this->getParameter('kernel.project_dir');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg($console),
|
||||
escapeshellarg('mto:agent:ingest:run'),
|
||||
escapeshellarg((string)$job->getId()),
|
||||
escapeshellarg('--no-interaction'),
|
||||
);
|
||||
|
||||
// Best effort: wenn exec deaktiviert ist, sauber abbrechen.
|
||||
if (!function_exists('exec')) {
|
||||
if (!$this->canExec()) {
|
||||
$jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
|
||||
$this->addFlash('danger', 'Aktivierung ok, aber Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');
|
||||
return $this->redirectToRoute('admin_document_show', [
|
||||
'id' => $version->getDocument()->getId()
|
||||
'id' => $version->getDocument()->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
exec($cmd);
|
||||
$this->startIngestJob((string)$job->getId());
|
||||
|
||||
$this->addFlash('success', 'Version aktiviert. Ingest-Job wurde erstellt und gestartet.');
|
||||
|
||||
@@ -295,7 +245,7 @@ class DocumentController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_document_show', [
|
||||
'id' => $version->getDocument()->getId()
|
||||
'id' => $version->getDocument()->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -310,19 +260,17 @@ class DocumentController extends AbstractController
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
IngestJobService $jobService,
|
||||
): ?RedirectResponse
|
||||
{
|
||||
$dryRun = false;
|
||||
if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, $request->request->get('_token'))) {
|
||||
): ?RedirectResponse {
|
||||
if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string)$request->request->get('_token'))) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$version = $em->getRepository(DocumentVersion::class)->find($versionId);
|
||||
|
||||
if (!$version) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
/** @var IngestJob|null $existing */
|
||||
$existing = $em->getRepository(IngestJob::class)
|
||||
->findOneBy(
|
||||
['documentVersionId' => $version->getId()],
|
||||
@@ -333,13 +281,6 @@ class DocumentController extends AbstractController
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Asynchroner Ingest (ohne Messenger):
|
||||
// 1) Job als QUEUED anlegen
|
||||
// 2) Symfony-Command im Hintergrund starten
|
||||
// 3) Direkt auf Job-Detailseite redirecten (Loader + Polling)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
$job = $jobService->startJob(
|
||||
IngestJob::TYPE_DOCUMENT,
|
||||
$this->getUser(),
|
||||
@@ -349,28 +290,15 @@ class DocumentController extends AbstractController
|
||||
IngestJob::STATUS_QUEUED
|
||||
);
|
||||
|
||||
// Hintergrundprozess starten (Provider-kompatibel, kein Worker/Daemon)
|
||||
$projectDir = (string)$this->getParameter('kernel.project_dir');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg($console),
|
||||
escapeshellarg('mto:agent:ingest:run'),
|
||||
escapeshellarg((string)$job->getId()),
|
||||
escapeshellarg('--no-interaction'),
|
||||
);
|
||||
|
||||
// Best effort: wenn exec deaktiviert ist, sauber abbrechen.
|
||||
if (!function_exists('exec')) {
|
||||
if (!$this->canExec()) {
|
||||
$jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
|
||||
$this->addFlash('error', 'Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');
|
||||
return $this->redirectToRoute('admin_document_show', [
|
||||
'id' => $version->getDocument()->getId()
|
||||
'id' => $version->getDocument()->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
exec($cmd);
|
||||
$this->startIngestJob((string)$job->getId());
|
||||
|
||||
return $this->redirectToRoute('admin_job_show', [
|
||||
'id' => (string)$job->getId(),
|
||||
@@ -384,17 +312,21 @@ class DocumentController extends AbstractController
|
||||
)]
|
||||
public function resetCompleteSystem(ParameterBagInterface $params, Connection $connection): ?RedirectResponse
|
||||
{
|
||||
if (!function_exists('exec')) {
|
||||
if (!$this->canExec()) {
|
||||
$this->addFlash('danger', 'Der Reset konnte nicht gestartet werden (exec deaktiviert).');
|
||||
return $this->redirectToRoute('admin_dashboard');
|
||||
}
|
||||
|
||||
@unlink($params->get('mto.knowledge.ndjson'));
|
||||
@unlink($params->get('mto.knowledge.vector_index'));
|
||||
@unlink($params->get('mto.knowledge.vector_index_meta'));
|
||||
@unlink($params->get('mto.knowledge.index_meta'));
|
||||
@unlink($params->get('mto.runtime.meta'));
|
||||
exec('rm -rf ' . $params->get('mto.knowledge.upload'));
|
||||
@unlink((string)$params->get('mto.knowledge.ndjson'));
|
||||
@unlink((string)$params->get('mto.knowledge.vector_index'));
|
||||
@unlink((string)$params->get('mto.knowledge.vector_index_meta'));
|
||||
@unlink((string)$params->get('mto.knowledge.index_meta'));
|
||||
@unlink((string)$params->get('mto.runtime.meta'));
|
||||
|
||||
$uploadDir = (string)$params->get('mto.knowledge.upload');
|
||||
if ($uploadDir !== '' && is_dir($uploadDir)) {
|
||||
exec('rm -rf ' . escapeshellarg($uploadDir));
|
||||
}
|
||||
|
||||
$sql = '
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
@@ -425,39 +357,29 @@ class DocumentController extends AbstractController
|
||||
EntityManagerInterface $em,
|
||||
IngestJobService $jobService,
|
||||
LockService $lockService,
|
||||
DocumentService $documentService
|
||||
): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('delete_document_' . $id, $request->request->get('_token'))) {
|
||||
): RedirectResponse {
|
||||
if (!$this->isCsrfTokenValid('delete_document_' . $id, (string)$request->request->get('_token'))) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
try {
|
||||
$uuid = Uuid::fromString($id);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Exception) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
/** @var Document|null $document */
|
||||
$document = $em->getRepository(Document::class)->find($uuid);
|
||||
|
||||
if (!$document) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 🔒 Delete nur erlauben wenn kein anderer Job läuft
|
||||
// ---------------------------------------------------------
|
||||
if (!$lockService->acquire()) {
|
||||
$this->addFlash('danger', 'Ein Ingest-Job läuft bereits. Löschen derzeit nicht möglich.');
|
||||
return $this->redirectToRoute('admin_documents');
|
||||
}
|
||||
|
||||
// Nur Test-Lock – echter Lock im Orchestrator
|
||||
$lockService->release();
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 1) Delete-Job anlegen (QUEUED)
|
||||
// ---------------------------------------------------------
|
||||
$job = $jobService->startJob(
|
||||
IngestJob::TYPE_DOCUMENT_DELETE,
|
||||
$this->getUser(),
|
||||
@@ -467,27 +389,13 @@ class DocumentController extends AbstractController
|
||||
IngestJob::STATUS_QUEUED
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2) Hintergrundprozess starten
|
||||
// ---------------------------------------------------------
|
||||
$projectDir = (string)$this->getParameter('kernel.project_dir');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg($console),
|
||||
escapeshellarg('mto:agent:ingest:run'),
|
||||
escapeshellarg((string)$job->getId()),
|
||||
escapeshellarg('--no-interaction'),
|
||||
);
|
||||
|
||||
if (!function_exists('exec')) {
|
||||
if (!$this->canExec()) {
|
||||
$jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
|
||||
$this->addFlash('danger', 'Löschen konnte nicht gestartet werden (exec deaktiviert).');
|
||||
return $this->redirectToRoute('admin_documents');
|
||||
}
|
||||
|
||||
exec($cmd);
|
||||
$this->startIngestJob((string)$job->getId());
|
||||
|
||||
$this->addFlash('success', 'Löschvorgang gestartet. Dokument wird nach Index-Rebuild entfernt.');
|
||||
|
||||
@@ -495,4 +403,42 @@ class DocumentController extends AbstractController
|
||||
'id' => (string)$job->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Helpers
|
||||
// =========================================================
|
||||
|
||||
private function canExec(): bool
|
||||
{
|
||||
return function_exists('exec');
|
||||
}
|
||||
|
||||
private function ensureDir(string $dir): void
|
||||
{
|
||||
if ($dir === '') {
|
||||
throw new \RuntimeException('Upload directory not configured.');
|
||||
}
|
||||
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
throw new \RuntimeException('Unable to create upload directory.');
|
||||
}
|
||||
}
|
||||
|
||||
private function startIngestJob(string $jobId): void
|
||||
{
|
||||
$projectDir = (string)$this->getParameter('kernel.project_dir');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
// WICHTIG: --no-interaction ist ein GLOBAL-Flag und muss VOR dem Command stehen!
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg(PHP_BINARY),
|
||||
escapeshellarg($console),
|
||||
'--no-interaction',
|
||||
escapeshellarg('mto:agent:ingest:run'),
|
||||
escapeshellarg($jobId),
|
||||
);
|
||||
|
||||
exec($cmd);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user