From 8868040524897fb90605a64a7d125af3cae33141 Mon Sep 17 00:00:00 2001 From: Kostiantyn Miakshyn Date: Sat, 16 May 2026 01:14:01 +0200 Subject: [PATCH] fix: Delete files on submission/question/form deletion Signed-off-by: Kostiantyn Miakshyn --- .../DeleteQuestionFoldersJob.php | 91 +++++++++++++++++++ lib/Controller/ApiController.php | 18 +++- lib/Db/AnswerMapper.php | 22 +++++ lib/Db/FormMapper.php | 37 ++++++++ lib/Db/SubmissionMapper.php | 58 +++++++++--- 5 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 lib/BackgroundJob/DeleteQuestionFoldersJob.php diff --git a/lib/BackgroundJob/DeleteQuestionFoldersJob.php b/lib/BackgroundJob/DeleteQuestionFoldersJob.php new file mode 100644 index 000000000..adf70a287 --- /dev/null +++ b/lib/BackgroundJob/DeleteQuestionFoldersJob.php @@ -0,0 +1,91 @@ +formMapper->findById($formId); + $this->logger->debug('Deleting question folders for question {questionId} in form {formId}', [ + 'questionId' => $questionId, + 'formId' => $formId, + ]); + + $userFolder = $this->rootFolder->getUserFolder($ownerId); + $formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form); + + $formFolder = $userFolder->get($formFolderPath); + if (!$formFolder instanceof Folder) { + $this->logger->notice('Form folder not found, nothing to delete', [ + 'formId' => $formId, + ]); + return; + } + + $questionFolderPrefix = $questionId . ' - '; + $deletedCount = 0; + + // Iterate through submission folders and delete matching question folders + foreach ($formFolder->getDirectoryListing() as $submissionFolder) { + if (!$submissionFolder instanceof Folder) { + continue; + } + foreach ($submissionFolder->getDirectoryListing() as $node) { + if (str_starts_with($node->getName(), $questionFolderPrefix)) { + $node->delete(); + $deletedCount++; + } + } + } + + $this->logger->info('Deleted {count} question folders for question {questionId}', [ + 'count' => $deletedCount, + 'questionId' => $questionId, + 'formId' => $formId, + ]); + } catch (NotFoundException) { + // Folder doesn't exist, do nothing + $this->logger->notice('Question folder not found, nothing to delete', [ + 'questionId' => $questionId, + 'formId' => $formId, + ]); + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete question folders: {error}', [ + 'error' => $e->getMessage(), + 'questionId' => $questionId, + 'formId' => $formId, + ]); + } + } +} diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index e98d5c188..d3ac8a3cd 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -7,6 +7,7 @@ namespace OCA\Forms\Controller; +use OCA\Forms\BackgroundJob\DeleteQuestionFoldersJob; use OCA\Forms\BackgroundJob\SyncSubmissionsWithLinkedFileJob; use OCA\Forms\Constants; use OCA\Forms\Db\Answer; @@ -46,6 +47,7 @@ use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\BackgroundJob\IJobList; +use OCP\Files\Folder; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; use OCP\IL10N; @@ -742,6 +744,14 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse { $question->setOrder(0); $this->questionMapper->update($question); + if ($question->getType() === Constants::ANSWER_TYPE_FILE) { + $this->jobList->add(DeleteQuestionFoldersJob::class, [ + 'formId' => $form->getId(), + 'questionId' => $question->getId(), + 'ownerId' => $form->getOwnerId(), + ]); + } + // Update all question-order > deleted order. $formQuestions = $this->questionMapper->findByForm($formId); foreach ($formQuestions as $question) { @@ -1589,7 +1599,7 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse { } // Delete submission (incl. Answers) - $this->submissionMapper->deleteById($submissionId); + $this->submissionMapper->deleteById($form, $submissionId); $this->formMapper->update($form); return new DataResponse($submissionId); @@ -1743,8 +1753,7 @@ public function uploadFiles(int $formId, int $questionId, string $shareHash = '' } else { $folder = $userFolder->newFolder($path); } - /** @var \OCP\Files\Folder $folder */ - + /** @var Folder $folder */ $fileName = $folder->getNonExistingName($uploadedFile['name']); $file = $folder->newFile($fileName, file_get_contents($uploadedFile['tmp_name'])); @@ -1819,8 +1828,7 @@ private function storeAnswersForQuestion(Form $form, $submissionId, array $quest } else { $folder = $userFolder->newFolder($path); } - /** @var \OCP\Files\Folder $folder */ - + /** @var Folder $folder */ $file = $userFolder->getById($uploadedFile->getFileId())[0]; $name = $folder->getNonExistingName($file->getName()); $file->move($folder->getPath() . '/' . $name); diff --git a/lib/Db/AnswerMapper.php b/lib/Db/AnswerMapper.php index 2420f6b34..830de9b5e 100644 --- a/lib/Db/AnswerMapper.php +++ b/lib/Db/AnswerMapper.php @@ -54,4 +54,26 @@ public function deleteBySubmission(int $submissionId): void { $qb->executeStatement(); } + + /** + * Collect all fileIds for answers of a specific submission + * @param int $submissionId + * @return int[] Array of fileIds + */ + public function findFileIdsBySubmission(int $submissionId): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('file_id') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere($qb->expr()->isNotNull('file_id')); + + $result = $qb->executeQuery(); + $rows = $result->fetchFirstColumn(); + $result->closeCursor(); + + return array_map('intval', $rows); + } } diff --git a/lib/Db/FormMapper.php b/lib/Db/FormMapper.php index b0f17d8e5..6eb5a866d 100644 --- a/lib/Db/FormMapper.php +++ b/lib/Db/FormMapper.php @@ -13,8 +13,12 @@ use OCP\AppFramework\Db\QBMapper; use OCP\Comments\ICommentsManager; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IDBConnection; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; /** * @extends QBMapper
@@ -32,6 +36,8 @@ public function __construct( private SubmissionMapper $submissionMapper, private ConfigService $configService, private ICommentsManager $commentsManager, + private IRootFolder $rootFolder, + private LoggerInterface $logger, ) { parent::__construct($db, 'forms_v2_forms', Form::class); } @@ -225,6 +231,37 @@ public function deleteForm(Form $form): void { $this->shareMapper->deleteByForm($formId); $this->questionMapper->deleteByForm($formId); $this->commentsManager->deleteCommentsAtObject('forms', (string)$formId); + $this->deleteFormFolder($form); $this->delete($form); } + + /** + * Delete the form folder from the file system + * @param Form $form The form instance + */ + private function deleteFormFolder(Form $form): void { + try { + $userFolder = $this->rootFolder->getUserFolder($form->getOwnerId()); + $formsFolder = $userFolder->get(Constants::FILES_FOLDER); + + if (!$formsFolder instanceof Folder) { + return; + } + $formFolderPrefix = $form->getId() . ' - '; + + // Iterate through form folders and delete matching folders + foreach ($formsFolder->getDirectoryListing() as $node) { + if (str_starts_with($node->getName(), $formFolderPrefix)) { + $node->delete(); + } + } + } catch (NotFoundException) { + // do nothing + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete form folder: {error}', [ + 'error' => $e->getMessage(), + 'formId' => $form->getId(), + ]); + } + } } diff --git a/lib/Db/SubmissionMapper.php b/lib/Db/SubmissionMapper.php index 367d94504..0db703140 100644 --- a/lib/Db/SubmissionMapper.php +++ b/lib/Db/SubmissionMapper.php @@ -7,10 +7,15 @@ namespace OCA\Forms\Db; +use OCA\Forms\Service\FormsService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; /** * @extends QBMapper @@ -20,10 +25,16 @@ class SubmissionMapper extends QBMapper { * SubmissionMapper constructor. * @param IDBConnection $db * @param AnswerMapper $answerMapper + * @param IRootFolder $rootFolder + * @param LoggerInterface $logger + * @param FormsService $formsService */ public function __construct( IDBConnection $db, private AnswerMapper $answerMapper, + private IRootFolder $rootFolder, + private LoggerInterface $logger, + private FormsService $formsService, ) { parent::__construct($db, 'forms_v2_submissions', Submission::class); } @@ -179,22 +190,17 @@ protected function countSubmissionsWithFilters(int $formId, ?string $userId = nu /** * Delete the Submission, including answers. + * @param Form $form Form the submission belongs to. * @param int $id of the submission to delete */ - public function deleteById(int $id): void { - $qb = $this->db->getQueryBuilder(); - - // First delete corresponding answers. + public function deleteById(Form $form, int $id): void { $submissionEntity = $this->findById($id); - $this->answerMapper->deleteBySubmission($submissionEntity->getId()); - //Delete Submission - $qb->delete($this->getTableName()) - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) - ); + $this->deleteSubmissionFolder($form, $submissionEntity->getId()); - $qb->executeStatement(); + $this->answerMapper->deleteBySubmission($submissionEntity->getId()); + + $this->delete($submissionEntity); } /** @@ -218,4 +224,34 @@ public function deleteByForm(int $formId): void { $qb->executeStatement(); } + + /** + * Delete the submission folder from the file system + * @param Form $form The form instance + * @param int $submissionId The submission ID + */ + private function deleteSubmissionFolder(Form $form, int $submissionId): void { + try { + $userFolder = $this->rootFolder->getUserFolder($form->getOwnerId()); + $formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form); + + $formFolder = $userFolder->get($formFolderPath); + if (!$formFolder instanceof Folder) { + return; + } + + $submissionFolder = $formFolder->get((string)$submissionId); + if ($submissionFolder instanceof Folder) { + $submissionFolder->delete(); + } + } catch (NotFoundException) { + // Folder doesn't exist, do nothing + } catch (\Throwable $e) { + $this->logger->warning('Failed to delete submission folder: {error}', [ + 'error' => $e->getMessage(), + 'submissionId' => $submissionId, + 'formId' => $form->getId(), + ]); + } + } }