diff --git a/src/Blob/BlobSubscriber.php b/src/Blob/BlobSubscriber.php new file mode 100644 index 0000000..47ce09f --- /dev/null +++ b/src/Blob/BlobSubscriber.php @@ -0,0 +1,97 @@ +config = $config; + $this->translator = $translator; + $this->typesenseSync = $typesenseSync; + } + + private function isForCabinet(FileData $fileData): bool + { + $bucket = $fileData->getBucket(); + if ($bucket->getBucketID() !== $this->config->getBlobBucketId()) { + return false; + } + if ($fileData->getPrefix() !== $this->config->getBlobBucketPrefix()) { + return false; + } + + return true; + } + + public static function getSubscribedEvents(): array + { + return [ + AddFileDataByPostSuccessEvent::class => 'onFileAdded', + ChangeFileDataByPatchSuccessEvent::class => 'onFileChanged', + DeleteFileDataByDeleteSuccessEvent::class => 'onFileRemoved', + ]; + } + + private function translateMetadata(FileData $fileData): array + { + $metadata = json_decode($fileData->getMetadata(), associative: true, flags: JSON_THROW_ON_ERROR); + $objectType = $metadata['objectType']; + $input = [ + 'id' => $fileData->getIdentifier(), + 'fileSource' => $fileData->getBucket()->getBucketID(), + 'fileName' => $fileData->getFileName(), + 'metadata' => $metadata, + ]; + + return $this->translator->translateDocument($objectType, $input); + } + + public function onFileAdded(AddFileDataByPostSuccessEvent $event) + { + $fileData = $event->getFileData(); + if (!$this->isForCabinet($fileData)) { + return; + } + + $translated = $this->translateMetadata($fileData); + $this->typesenseSync->upsertPartialFile($translated); + } + + public function onFileChanged(ChangeFileDataByPatchSuccessEvent $event) + { + $fileData = $event->getFileData(); + if (!$this->isForCabinet($fileData)) { + return; + } + + $translated = $this->translateMetadata($fileData); + $this->typesenseSync->upsertPartialFile($translated); + } + + public function onFileRemoved(DeleteFileDataByDeleteSuccessEvent $event) + { + $fileData = $event->getFileData(); + if (!$this->isForCabinet($fileData)) { + return; + } + + $translated = $this->translateMetadata($fileData); + $this->typesenseSync->removePartialFile($translated); + } +} diff --git a/src/Command/DeleteFileCommand.php b/src/Command/DeleteFileCommand.php new file mode 100644 index 0000000..99d6581 --- /dev/null +++ b/src/Command/DeleteFileCommand.php @@ -0,0 +1,49 @@ +blobService = $blobService; + } + + protected function configure(): void + { + $this->setName('dbp:relay:cabinet:delete-file'); + $this->setDescription('Delete a file from the cabinet blob bucket'); + $this->addArgument('id', InputArgument::REQUIRED, 'The ID of the file'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $fileId = $input->getArgument('id'); + + try { + $this->blobService->deleteFile($fileId); + $output->writeln("File deleted successfully: $fileId"); + + return Command::SUCCESS; + } catch (BlobApiError $e) { + $output->writeln('Error deleting file: '.$e->getMessage().' '); + $output->writeln(print_r($e->getErrorId(), true)); + $output->writeln(print_r($e->getErrorDetails(), true)); + + return Command::FAILURE; + } + } +} diff --git a/src/Command/UploadFileCommand.php b/src/Command/UploadFileCommand.php index 9bc2dc4..4314478 100644 --- a/src/Command/UploadFileCommand.php +++ b/src/Command/UploadFileCommand.php @@ -32,6 +32,8 @@ protected function configure(): void $this->addArgument('filepath', InputArgument::REQUIRED, 'The path to the file to upload'); $this->addOption('type', 't', InputOption::VALUE_OPTIONAL, 'The type of the file'); $this->addOption('metadata', 'm', InputOption::VALUE_OPTIONAL, 'The metadata of the file'); + $this->addOption('filename', 'f', InputOption::VALUE_REQUIRED, + 'The filename, defaults to the filename of the given filepath'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -48,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $filename = basename($filepath); + $filename = $input->getOption('filename') ?? basename($filepath); $payload = file_get_contents($filepath); if ($payload === false) { diff --git a/src/EventSubscriber/BlobAddFileSuccessSubscriber.php b/src/EventSubscriber/BlobAddFileSuccessSubscriber.php deleted file mode 100644 index 320d8fa..0000000 --- a/src/EventSubscriber/BlobAddFileSuccessSubscriber.php +++ /dev/null @@ -1,27 +0,0 @@ - 'onPost', - ]; - } - - public function onPost(AddFileDataByPostSuccessEvent $event) - { - // dump($event); - } -} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 1137086..73b9f5c 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -48,6 +48,10 @@ services: autowire: true autoconfigure: true + Dbp\Relay\CabinetBundle\Command\DeleteFileCommand: + autowire: true + autoconfigure: true + Dbp\Relay\CabinetBundle\Authorization\AuthorizationService: autowire: true autoconfigure: true @@ -65,8 +69,8 @@ services: autowire: true autoconfigure: true - Dbp\Relay\CabinetBundle\EventSubscriber\: - resource: '../../EventSubscriber' + Dbp\Relay\CabinetBundle\Blob\: + resource: '../../Blob' autowire: true autoconfigure: true diff --git a/src/Service/BlobService.php b/src/Service/BlobService.php index 8f654c2..48ac25b 100644 --- a/src/Service/BlobService.php +++ b/src/Service/BlobService.php @@ -49,6 +49,13 @@ public function uploadFile(string $filename, string $payload, ?string $type = nu return $blobApi->uploadFile($this->config->getBlobBucketPrefix(), $filename, $payload, $metadata ?? '', $type ?? ''); } + public function deleteFile(string $id): void + { + $blobApi = $this->getInternalBlobApi(); + + $blobApi->deleteFileByIdentifier($id); + } + public function getSignatureForGivenRequest(Request $request): Response { if (!$this->auth->isAuthenticated()) { diff --git a/src/TypesenseClient/SearchIndex.php b/src/TypesenseClient/SearchIndex.php index ebc905d..394d208 100644 --- a/src/TypesenseClient/SearchIndex.php +++ b/src/TypesenseClient/SearchIndex.php @@ -91,6 +91,30 @@ public function addDocumentsToCollection(string $collectionName, array $document } } + /** + * Returns all documents of type $type where $key matches $value. + */ + public function findDocuments(string $collectionName, string $type, string $key, string $value): array + { + if (preg_match('/\s/', $value) || preg_match('/\s/', $type)) { + throw new \RuntimeException('no whitespace supported'); + } + $filterBy = $key.':='.$value.' && @type := '.$type; + $searchResult = $this->getClient()->collections[$collectionName]->documents->search(['q' => '*', 'filter_by' => $filterBy]); + + $documents = []; + foreach ($searchResult['hits'] as $hit) { + $documents[] = $hit['document']; + } + + return $documents; + } + + public function deleteDocument(string $collectionName, string $id): void + { + $this->getClient()->collections[$collectionName]->documents[$id]->delete(); + } + public function updateAlias(string $collectionName): void { $aliasName = $this->getAliasName(); diff --git a/src/TypesenseSync/TypesenseSync.php b/src/TypesenseSync/TypesenseSync.php index 0fe46ef..4b93478 100644 --- a/src/TypesenseSync/TypesenseSync.php +++ b/src/TypesenseSync/TypesenseSync.php @@ -149,6 +149,32 @@ private static function generateRandomPDFNames($count = 50): array return $fileNames; } + public function upsertPartialFile(array $partialFileDocument): void + { + $collectionName = $this->searchIndex->getAliasName(); + + $id = $partialFileDocument['base']['identNrObfuscated']; + + // Find the matching person, and copy over the base info + $results = $this->searchIndex->findDocuments($collectionName, 'Person', 'base.identNrObfuscated', $id); + if ($results) { + $partialFileDocument['base'] = $results[0]['base']; + } else { + // FIXME: what if the person is missing? Just ignore the document until the next full sync, or poll later? + // atm the schema needs those fields, so just skip for now + return; + } + + $this->searchIndex->addDocumentsToCollection($collectionName, [$partialFileDocument]); + } + + public function removePartialFile(array $partialFileDocument): void + { + $collectionName = $this->searchIndex->getAliasName(); + $id = $partialFileDocument['id']; + $this->searchIndex->deleteDocument($collectionName, $id); + } + public function addDummyDocuments(string $collectionName, array $personDocuments): void { $documents = [];