Skip to content

Commit 8868040

Browse files
committed
fix: Delete files on submission/question/form deletion
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 91f55e3 commit 8868040

5 files changed

Lines changed: 210 additions & 16 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Forms\BackgroundJob;
9+
10+
use OCA\Forms\Db\FormMapper;
11+
use OCA\Forms\Service\FormsService;
12+
use OCP\AppFramework\Utility\ITimeFactory;
13+
use OCP\BackgroundJob\QueuedJob;
14+
use OCP\Files\Folder;
15+
use OCP\Files\IRootFolder;
16+
use OCP\Files\NotFoundException;
17+
use Psr\Log\LoggerInterface;
18+
19+
class DeleteQuestionFoldersJob extends QueuedJob {
20+
public function __construct(
21+
ITimeFactory $time,
22+
private FormMapper $formMapper,
23+
private FormsService $formsService,
24+
private IRootFolder $rootFolder,
25+
private LoggerInterface $logger,
26+
) {
27+
parent::__construct($time);
28+
}
29+
30+
/**
31+
* @param array{formId: int, questionId: int, ownerId: string} $argument
32+
*/
33+
public function run($argument): void {
34+
$formId = $argument['formId'];
35+
$questionId = $argument['questionId'];
36+
$ownerId = $argument['ownerId'];
37+
38+
try {
39+
$form = $this->formMapper->findById($formId);
40+
$this->logger->debug('Deleting question folders for question {questionId} in form {formId}', [
41+
'questionId' => $questionId,
42+
'formId' => $formId,
43+
]);
44+
45+
$userFolder = $this->rootFolder->getUserFolder($ownerId);
46+
$formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form);
47+
48+
$formFolder = $userFolder->get($formFolderPath);
49+
if (!$formFolder instanceof Folder) {
50+
$this->logger->notice('Form folder not found, nothing to delete', [
51+
'formId' => $formId,
52+
]);
53+
return;
54+
}
55+
56+
$questionFolderPrefix = $questionId . ' - ';
57+
$deletedCount = 0;
58+
59+
// Iterate through submission folders and delete matching question folders
60+
foreach ($formFolder->getDirectoryListing() as $submissionFolder) {
61+
if (!$submissionFolder instanceof Folder) {
62+
continue;
63+
}
64+
foreach ($submissionFolder->getDirectoryListing() as $node) {
65+
if (str_starts_with($node->getName(), $questionFolderPrefix)) {
66+
$node->delete();
67+
$deletedCount++;
68+
}
69+
}
70+
}
71+
72+
$this->logger->info('Deleted {count} question folders for question {questionId}', [
73+
'count' => $deletedCount,
74+
'questionId' => $questionId,
75+
'formId' => $formId,
76+
]);
77+
} catch (NotFoundException) {
78+
// Folder doesn't exist, do nothing
79+
$this->logger->notice('Question folder not found, nothing to delete', [
80+
'questionId' => $questionId,
81+
'formId' => $formId,
82+
]);
83+
} catch (\Throwable $e) {
84+
$this->logger->warning('Failed to delete question folders: {error}', [
85+
'error' => $e->getMessage(),
86+
'questionId' => $questionId,
87+
'formId' => $formId,
88+
]);
89+
}
90+
}
91+
}

lib/Controller/ApiController.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace OCA\Forms\Controller;
99

10+
use OCA\Forms\BackgroundJob\DeleteQuestionFoldersJob;
1011
use OCA\Forms\BackgroundJob\SyncSubmissionsWithLinkedFileJob;
1112
use OCA\Forms\Constants;
1213
use OCA\Forms\Db\Answer;
@@ -46,6 +47,7 @@
4647
use OCP\AppFramework\OCS\OCSNotFoundException;
4748
use OCP\AppFramework\OCSController;
4849
use OCP\BackgroundJob\IJobList;
50+
use OCP\Files\Folder;
4951
use OCP\Files\IMimeTypeDetector;
5052
use OCP\Files\IRootFolder;
5153
use OCP\IL10N;
@@ -742,6 +744,14 @@ public function deleteQuestion(int $formId, int $questionId): DataResponse {
742744
$question->setOrder(0);
743745
$this->questionMapper->update($question);
744746

747+
if ($question->getType() === Constants::ANSWER_TYPE_FILE) {
748+
$this->jobList->add(DeleteQuestionFoldersJob::class, [
749+
'formId' => $form->getId(),
750+
'questionId' => $question->getId(),
751+
'ownerId' => $form->getOwnerId(),
752+
]);
753+
}
754+
745755
// Update all question-order > deleted order.
746756
$formQuestions = $this->questionMapper->findByForm($formId);
747757
foreach ($formQuestions as $question) {
@@ -1589,7 +1599,7 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse {
15891599
}
15901600

15911601
// Delete submission (incl. Answers)
1592-
$this->submissionMapper->deleteById($submissionId);
1602+
$this->submissionMapper->deleteById($form, $submissionId);
15931603
$this->formMapper->update($form);
15941604

15951605
return new DataResponse($submissionId);
@@ -1743,8 +1753,7 @@ public function uploadFiles(int $formId, int $questionId, string $shareHash = ''
17431753
} else {
17441754
$folder = $userFolder->newFolder($path);
17451755
}
1746-
/** @var \OCP\Files\Folder $folder */
1747-
1756+
/** @var Folder $folder */
17481757
$fileName = $folder->getNonExistingName($uploadedFile['name']);
17491758
$file = $folder->newFile($fileName, file_get_contents($uploadedFile['tmp_name']));
17501759

@@ -1819,8 +1828,7 @@ private function storeAnswersForQuestion(Form $form, $submissionId, array $quest
18191828
} else {
18201829
$folder = $userFolder->newFolder($path);
18211830
}
1822-
/** @var \OCP\Files\Folder $folder */
1823-
1831+
/** @var Folder $folder */
18241832
$file = $userFolder->getById($uploadedFile->getFileId())[0];
18251833
$name = $folder->getNonExistingName($file->getName());
18261834
$file->move($folder->getPath() . '/' . $name);

lib/Db/AnswerMapper.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,26 @@ public function deleteBySubmission(int $submissionId): void {
5454

5555
$qb->executeStatement();
5656
}
57+
58+
/**
59+
* Collect all fileIds for answers of a specific submission
60+
* @param int $submissionId
61+
* @return int[] Array of fileIds
62+
*/
63+
public function findFileIdsBySubmission(int $submissionId): array {
64+
$qb = $this->db->getQueryBuilder();
65+
66+
$qb->select('file_id')
67+
->from($this->getTableName())
68+
->where(
69+
$qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT))
70+
)
71+
->andWhere($qb->expr()->isNotNull('file_id'));
72+
73+
$result = $qb->executeQuery();
74+
$rows = $result->fetchFirstColumn();
75+
$result->closeCursor();
76+
77+
return array_map('intval', $rows);
78+
}
5779
}

lib/Db/FormMapper.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
use OCP\AppFramework\Db\QBMapper;
1414
use OCP\Comments\ICommentsManager;
1515
use OCP\DB\QueryBuilder\IQueryBuilder;
16+
use OCP\Files\Folder;
17+
use OCP\Files\IRootFolder;
18+
use OCP\Files\NotFoundException;
1619
use OCP\IDBConnection;
1720
use OCP\Share\IShare;
21+
use Psr\Log\LoggerInterface;
1822

1923
/**
2024
* @extends QBMapper<Form>
@@ -32,6 +36,8 @@ public function __construct(
3236
private SubmissionMapper $submissionMapper,
3337
private ConfigService $configService,
3438
private ICommentsManager $commentsManager,
39+
private IRootFolder $rootFolder,
40+
private LoggerInterface $logger,
3541
) {
3642
parent::__construct($db, 'forms_v2_forms', Form::class);
3743
}
@@ -225,6 +231,37 @@ public function deleteForm(Form $form): void {
225231
$this->shareMapper->deleteByForm($formId);
226232
$this->questionMapper->deleteByForm($formId);
227233
$this->commentsManager->deleteCommentsAtObject('forms', (string)$formId);
234+
$this->deleteFormFolder($form);
228235
$this->delete($form);
229236
}
237+
238+
/**
239+
* Delete the form folder from the file system
240+
* @param Form $form The form instance
241+
*/
242+
private function deleteFormFolder(Form $form): void {
243+
try {
244+
$userFolder = $this->rootFolder->getUserFolder($form->getOwnerId());
245+
$formsFolder = $userFolder->get(Constants::FILES_FOLDER);
246+
247+
if (!$formsFolder instanceof Folder) {
248+
return;
249+
}
250+
$formFolderPrefix = $form->getId() . ' - ';
251+
252+
// Iterate through form folders and delete matching folders
253+
foreach ($formsFolder->getDirectoryListing() as $node) {
254+
if (str_starts_with($node->getName(), $formFolderPrefix)) {
255+
$node->delete();
256+
}
257+
}
258+
} catch (NotFoundException) {
259+
// do nothing
260+
} catch (\Throwable $e) {
261+
$this->logger->warning('Failed to delete form folder: {error}', [
262+
'error' => $e->getMessage(),
263+
'formId' => $form->getId(),
264+
]);
265+
}
266+
}
230267
}

lib/Db/SubmissionMapper.php

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77

88
namespace OCA\Forms\Db;
99

10+
use OCA\Forms\Service\FormsService;
1011
use OCP\AppFramework\Db\DoesNotExistException;
1112
use OCP\AppFramework\Db\QBMapper;
1213
use OCP\DB\QueryBuilder\IQueryBuilder;
14+
use OCP\Files\Folder;
15+
use OCP\Files\IRootFolder;
16+
use OCP\Files\NotFoundException;
1317
use OCP\IDBConnection;
18+
use Psr\Log\LoggerInterface;
1419

1520
/**
1621
* @extends QBMapper<Submission>
@@ -20,10 +25,16 @@ class SubmissionMapper extends QBMapper {
2025
* SubmissionMapper constructor.
2126
* @param IDBConnection $db
2227
* @param AnswerMapper $answerMapper
28+
* @param IRootFolder $rootFolder
29+
* @param LoggerInterface $logger
30+
* @param FormsService $formsService
2331
*/
2432
public function __construct(
2533
IDBConnection $db,
2634
private AnswerMapper $answerMapper,
35+
private IRootFolder $rootFolder,
36+
private LoggerInterface $logger,
37+
private FormsService $formsService,
2738
) {
2839
parent::__construct($db, 'forms_v2_submissions', Submission::class);
2940
}
@@ -179,22 +190,17 @@ protected function countSubmissionsWithFilters(int $formId, ?string $userId = nu
179190

180191
/**
181192
* Delete the Submission, including answers.
193+
* @param Form $form Form the submission belongs to.
182194
* @param int $id of the submission to delete
183195
*/
184-
public function deleteById(int $id): void {
185-
$qb = $this->db->getQueryBuilder();
186-
187-
// First delete corresponding answers.
196+
public function deleteById(Form $form, int $id): void {
188197
$submissionEntity = $this->findById($id);
189-
$this->answerMapper->deleteBySubmission($submissionEntity->getId());
190198

191-
//Delete Submission
192-
$qb->delete($this->getTableName())
193-
->where(
194-
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
195-
);
199+
$this->deleteSubmissionFolder($form, $submissionEntity->getId());
196200

197-
$qb->executeStatement();
201+
$this->answerMapper->deleteBySubmission($submissionEntity->getId());
202+
203+
$this->delete($submissionEntity);
198204
}
199205

200206
/**
@@ -218,4 +224,34 @@ public function deleteByForm(int $formId): void {
218224

219225
$qb->executeStatement();
220226
}
227+
228+
/**
229+
* Delete the submission folder from the file system
230+
* @param Form $form The form instance
231+
* @param int $submissionId The submission ID
232+
*/
233+
private function deleteSubmissionFolder(Form $form, int $submissionId): void {
234+
try {
235+
$userFolder = $this->rootFolder->getUserFolder($form->getOwnerId());
236+
$formFolderPath = $this->formsService->getFormUploadedFilesFolderPath($form);
237+
238+
$formFolder = $userFolder->get($formFolderPath);
239+
if (!$formFolder instanceof Folder) {
240+
return;
241+
}
242+
243+
$submissionFolder = $formFolder->get((string)$submissionId);
244+
if ($submissionFolder instanceof Folder) {
245+
$submissionFolder->delete();
246+
}
247+
} catch (NotFoundException) {
248+
// Folder doesn't exist, do nothing
249+
} catch (\Throwable $e) {
250+
$this->logger->warning('Failed to delete submission folder: {error}', [
251+
'error' => $e->getMessage(),
252+
'submissionId' => $submissionId,
253+
'formId' => $form->getId(),
254+
]);
255+
}
256+
}
221257
}

0 commit comments

Comments
 (0)