diff --git a/core/Command/TaskProcessing/Cleanup.php b/core/Command/TaskProcessing/Cleanup.php
new file mode 100644
index 0000000000000..2ed2cbdec94a2
--- /dev/null
+++ b/core/Command/TaskProcessing/Cleanup.php
@@ -0,0 +1,93 @@
+appData = $appDataFactory->get('core');
+ }
+
+ protected function configure() {
+ $this
+ ->setName('taskprocessing:task:cleanup')
+ ->setDescription('cleanup old tasks')
+ ->addArgument(
+ 'maxAgeSeconds',
+ InputArgument::OPTIONAL,
+ // default is not defined as an argument default value because we want to show a nice "4 months" value
+ 'delete tasks that are older than this number of seconds, defaults to ' . Manager::MAX_TASK_AGE_SECONDS . ' (4 months)',
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $maxAgeSeconds = $input->getArgument('maxAgeSeconds') ?? Manager::MAX_TASK_AGE_SECONDS;
+ $output->writeln('Cleanup up tasks older than ' . $maxAgeSeconds . ' seconds and the related output files');
+
+ $taskIdsToCleanup = [];
+ try {
+ $fileCleanupGenerator = $this->taskProcessingManager->cleanupTaskProcessingTaskFiles($maxAgeSeconds);
+ foreach ($fileCleanupGenerator as $cleanedUpEntry) {
+ $output->writeln(
+ "\t - " . 'Deleted appData/core/TaskProcessing/' . $cleanedUpEntry['file_name']
+ . ' (fileId: ' . $cleanedUpEntry['file_id'] . ', taskId: ' . $cleanedUpEntry['task_id'] . ')'
+ );
+ }
+ $taskIdsToCleanup = $fileCleanupGenerator->getReturn();
+ } catch (\Exception $e) {
+ $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]);
+ $output->writeln('Failed to delete stale task processing tasks files');
+ }
+ try {
+ $deletedTaskCount = $this->taskMapper->deleteOlderThan($maxAgeSeconds);
+ foreach ($taskIdsToCleanup as $taskId) {
+ $output->writeln("\t - " . 'Deleted task ' . $taskId . ' from the database');
+ }
+ $output->writeln("\t - " . 'Deleted ' . $deletedTaskCount . ' tasks from the database');
+ } catch (\OCP\DB\Exception $e) {
+ $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]);
+ $output->writeln('Failed to delete stale task processing tasks');
+ }
+ try {
+ $textToImageDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'), $maxAgeSeconds);
+ foreach ($textToImageDeletedFileNames as $entry) {
+ $output->writeln("\t - " . 'Deleted appData/core/text2image/' . $entry . '');
+ }
+ } catch (\OCP\Files\NotFoundException $e) {
+ // noop
+ }
+ try {
+ $audioToTextDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'), $maxAgeSeconds);
+ foreach ($audioToTextDeletedFileNames as $entry) {
+ $output->writeln("\t - " . 'Deleted appData/core/audio2text/' . $entry . '');
+ }
+ } catch (\OCP\Files\NotFoundException $e) {
+ // noop
+ }
+
+ return 0;
+ }
+}
diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php
index 0d4b831812ce7..a99e3e001b9e6 100644
--- a/core/Command/TaskProcessing/EnabledCommand.php
+++ b/core/Command/TaskProcessing/EnabledCommand.php
@@ -1,5 +1,7 @@
|DataResponse
*/
private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse {
- $ids = $this->extractFileIdsFromTask($task);
+ $ids = $this->taskProcessingManager->extractFileIdsFromTask($task);
if (!in_array($fileId, $ids)) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
}
@@ -428,45 +427,6 @@ private function getFileContentsInternal(Task $task, int $fileId): StreamRespons
return $response;
}
- /**
- * @param Task $task
- * @return list
- * @throws NotFoundException
- */
- private function extractFileIdsFromTask(Task $task): array {
- $ids = [];
- $taskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
- if (!isset($taskTypes[$task->getTaskTypeId()])) {
- throw new NotFoundException('Could not find task type');
- }
- $taskType = $taskTypes[$task->getTaskTypeId()];
- foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
- if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
- /** @var int|list $inputSlot */
- $inputSlot = $task->getInput()[$key];
- if (is_array($inputSlot)) {
- $ids = array_merge($inputSlot, $ids);
- } else {
- $ids[] = $inputSlot;
- }
- }
- }
- if ($task->getOutput() !== null) {
- foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
- if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
- /** @var int|list $outputSlot */
- $outputSlot = $task->getOutput()[$key];
- if (is_array($outputSlot)) {
- $ids = array_merge($outputSlot, $ids);
- } else {
- $ids[] = $outputSlot;
- }
- }
- }
- }
- return $ids;
- }
-
/**
* Sets the task progress
*
diff --git a/core/Migrations/Version32000Date20250806110519.php b/core/Migrations/Version32000Date20250806110519.php
new file mode 100644
index 0000000000000..c498a1cc82062
--- /dev/null
+++ b/core/Migrations/Version32000Date20250806110519.php
@@ -0,0 +1,49 @@
+hasTable('taskprocessing_tasks')) {
+ $table = $schema->getTable('taskprocessing_tasks');
+ if (!$table->hasColumn('allow_cleanup')) {
+ $table->addColumn('allow_cleanup', Types::SMALLINT, [
+ 'notnull' => true,
+ 'default' => 1,
+ 'unsigned' => true,
+ ]);
+ return $schema;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php
index 5fb2502c3885c..1d3992369c630 100644
--- a/core/ResponseDefinitions.php
+++ b/core/ResponseDefinitions.php
@@ -200,6 +200,7 @@
* scheduledAt: ?int,
* startedAt: ?int,
* endedAt: ?int,
+ * allowCleanup: bool,
* }
*
* @psalm-type CoreProfileAction = array{
diff --git a/core/openapi-ex_app.json b/core/openapi-ex_app.json
index 4dad268c1b3f5..21e349ffbd08b 100644
--- a/core/openapi-ex_app.json
+++ b/core/openapi-ex_app.json
@@ -145,7 +145,8 @@
"progress",
"scheduledAt",
"startedAt",
- "endedAt"
+ "endedAt",
+ "allowCleanup"
],
"properties": {
"id": {
@@ -216,6 +217,9 @@
"type": "integer",
"format": "int64",
"nullable": true
+ },
+ "allowCleanup": {
+ "type": "boolean"
}
}
}
diff --git a/core/openapi-full.json b/core/openapi-full.json
index d4f69abf5354f..fd606e01966a3 100644
--- a/core/openapi-full.json
+++ b/core/openapi-full.json
@@ -639,7 +639,8 @@
"progress",
"scheduledAt",
"startedAt",
- "endedAt"
+ "endedAt",
+ "allowCleanup"
],
"properties": {
"id": {
@@ -710,6 +711,9 @@
"type": "integer",
"format": "int64",
"nullable": true
+ },
+ "allowCleanup": {
+ "type": "boolean"
}
}
},
diff --git a/core/openapi.json b/core/openapi.json
index 1a7ddc55c92a5..c5b9263e3939f 100644
--- a/core/openapi.json
+++ b/core/openapi.json
@@ -639,7 +639,8 @@
"progress",
"scheduledAt",
"startedAt",
- "endedAt"
+ "endedAt",
+ "allowCleanup"
],
"properties": {
"id": {
@@ -710,6 +711,9 @@
"type": "integer",
"format": "int64",
"nullable": true
+ },
+ "allowCleanup": {
+ "type": "boolean"
}
}
},
diff --git a/core/register_command.php b/core/register_command.php
index ed64983e76225..9fd5b9b611e0e 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -253,6 +253,7 @@
$application->add(Server::get(EnabledCommand::class));
$application->add(Server::get(Command\TaskProcessing\ListCommand::class));
$application->add(Server::get(Statistics::class));
+ $application->add(Server::get(Command\TaskProcessing\Cleanup::class));
$application->add(Server::get(RedisCommand::class));
$application->add(Server::get(DistributedClear::class));
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index fda8798fe43c2..f406f24dfe628 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1346,6 +1346,7 @@
'OC\\Core\\Command\\SystemTag\\Delete' => $baseDir . '/core/Command/SystemTag/Delete.php',
'OC\\Core\\Command\\SystemTag\\Edit' => $baseDir . '/core/Command/SystemTag/Edit.php',
'OC\\Core\\Command\\SystemTag\\ListCommand' => $baseDir . '/core/Command/SystemTag/ListCommand.php',
+ 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => $baseDir . '/core/Command/TaskProcessing/Cleanup.php',
'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => $baseDir . '/core/Command/TaskProcessing/EnabledCommand.php',
'OC\\Core\\Command\\TaskProcessing\\GetCommand' => $baseDir . '/core/Command/TaskProcessing/GetCommand.php',
'OC\\Core\\Command\\TaskProcessing\\ListCommand' => $baseDir . '/core/Command/TaskProcessing/ListCommand.php',
@@ -1512,6 +1513,7 @@
'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
+ 'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 69e3c41284b51..70e61075aa78e 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1387,6 +1387,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\SystemTag\\Delete' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Delete.php',
'OC\\Core\\Command\\SystemTag\\Edit' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Edit.php',
'OC\\Core\\Command\\SystemTag\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/SystemTag/ListCommand.php',
+ 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/Cleanup.php',
'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/EnabledCommand.php',
'OC\\Core\\Command\\TaskProcessing\\GetCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/GetCommand.php',
'OC\\Core\\Command\\TaskProcessing\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/ListCommand.php',
@@ -1553,6 +1554,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
+ 'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php
index 4d919deaf94a3..05c0ae9ac742d 100644
--- a/lib/private/TaskProcessing/Db/Task.php
+++ b/lib/private/TaskProcessing/Db/Task.php
@@ -45,6 +45,8 @@
* @method int getStartedAt()
* @method setEndedAt(int $endedAt)
* @method int getEndedAt()
+ * @method setAllowCleanup(int $allowCleanup)
+ * @method int getAllowCleanup()
*/
class Task extends Entity {
protected $lastUpdated;
@@ -63,16 +65,17 @@ class Task extends Entity {
protected $scheduledAt;
protected $startedAt;
protected $endedAt;
+ protected $allowCleanup;
/**
* @var string[]
*/
- public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at'];
+ public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup'];
/**
* @var string[]
*/
- public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt'];
+ public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup'];
public function __construct() {
@@ -94,6 +97,7 @@ public function __construct() {
$this->addType('scheduledAt', 'integer');
$this->addType('startedAt', 'integer');
$this->addType('endedAt', 'integer');
+ $this->addType('allowCleanup', 'integer');
}
public function toRow(): array {
@@ -122,6 +126,7 @@ public static function fromPublicTask(OCPTask $task): self {
'scheduledAt' => $task->getScheduledAt(),
'startedAt' => $task->getStartedAt(),
'endedAt' => $task->getEndedAt(),
+ 'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
]);
return $taskEntity;
}
@@ -144,6 +149,7 @@ public function toPublicTask(): OCPTask {
$task->setScheduledAt($this->getScheduledAt());
$task->setStartedAt($this->getStartedAt());
$task->setEndedAt($this->getEndedAt());
+ $task->setAllowCleanup($this->getAllowCleanup() !== 0);
return $task;
}
}
diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php
index 91fd68820ae56..fee9653463319 100644
--- a/lib/private/TaskProcessing/Db/TaskMapper.php
+++ b/lib/private/TaskProcessing/Db/TaskMapper.php
@@ -183,16 +183,39 @@ public function findTasks(
/**
* @param int $timeout
+ * @param bool $force If true, ignore the allow_cleanup flag
* @return int the number of deleted tasks
* @throws Exception
*/
- public function deleteOlderThan(int $timeout): int {
+ public function deleteOlderThan(int $timeout, bool $force = false): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout)));
+ if (!$force) {
+ $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT)));
+ }
return $qb->executeStatement();
}
+ /**
+ * @param int $timeout
+ * @param bool $force If true, ignore the allow_cleanup flag
+ * @return \Generator
+ * @throws Exception
+ */
+ public function getTasksToCleanup(int $timeout, bool $force = false): \Generator {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select(Task::$columns)
+ ->from($this->tableName)
+ ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout)));
+ if (!$force) {
+ $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT)));
+ }
+ foreach ($this->yieldEntities($qb) as $entity) {
+ yield $entity;
+ };
+ }
+
public function update(Entity $entity): Entity {
$entity->setLastUpdated($this->timeFactory->now()->getTimestamp());
return parent::update($entity);
diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php
index 11fb2bed559e2..e288f2981a817 100644
--- a/lib/private/TaskProcessing/Manager.php
+++ b/lib/private/TaskProcessing/Manager.php
@@ -30,6 +30,7 @@
use OCP\Files\Node;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\ICache;
@@ -78,6 +79,8 @@ class Manager implements IManager {
'ai.taskprocessing_provider_preferences',
];
+ public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months
+
/** @var list|null */
private ?array $providers = null;
@@ -1448,6 +1451,97 @@ private function validateUserAccessToFile(mixed $fileId, ?string $userId): void
}
}
+ /**
+ * @param Task $task
+ * @return list
+ * @throws NotFoundException
+ */
+ public function extractFileIdsFromTask(Task $task): array {
+ $ids = [];
+ $taskTypes = $this->getAvailableTaskTypes();
+ if (!isset($taskTypes[$task->getTaskTypeId()])) {
+ throw new NotFoundException('Could not find task type');
+ }
+ $taskType = $taskTypes[$task->getTaskTypeId()];
+ foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
+ if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
+ /** @var int|list $inputSlot */
+ $inputSlot = $task->getInput()[$key];
+ if (is_array($inputSlot)) {
+ $ids = array_merge($inputSlot, $ids);
+ } else {
+ $ids[] = $inputSlot;
+ }
+ }
+ }
+ if ($task->getOutput() !== null) {
+ foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
+ if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
+ /** @var int|list $outputSlot */
+ $outputSlot = $task->getOutput()[$key];
+ if (is_array($outputSlot)) {
+ $ids = array_merge($outputSlot, $ids);
+ } else {
+ $ids[] = $outputSlot;
+ }
+ }
+ }
+ }
+ return $ids;
+ }
+
+ /**
+ * @param ISimpleFolder $folder
+ * @param int $ageInSeconds
+ * @return \Generator
+ */
+ public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
+ foreach ($folder->getDirectoryListing() as $file) {
+ if ($file->getMTime() < time() - $ageInSeconds) {
+ try {
+ $fileName = $file->getName();
+ $file->delete();
+ yield $fileName;
+ } catch (NotPermittedException $e) {
+ $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param int $ageInSeconds
+ * @return \Generator
+ * @throws Exception
+ * @throws InvalidPathException
+ * @throws NotFoundException
+ * @throws \JsonException
+ * @throws \OCP\Files\NotFoundException
+ */
+ public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
+ $taskIdsToCleanup = [];
+ foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) {
+ $taskIdsToCleanup[] = $task->getId();
+ $ocpTask = $task->toPublicTask();
+ $fileIds = $this->extractFileIdsFromTask($ocpTask);
+ foreach ($fileIds as $fileId) {
+ // only look for output files stored in appData/TaskProcessing/
+ $file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/');
+ if ($file instanceof File) {
+ try {
+ $fileId = $file->getId();
+ $fileName = $file->getName();
+ $file->delete();
+ yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName];
+ } catch (NotPermittedException $e) {
+ $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
+ }
+ }
+ }
+ }
+ return $taskIdsToCleanup;
+ }
+
/**
* Make a request to the task's webhookUri if necessary
*
diff --git a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php
index 42d073a024d31..52fc204b7525d 100644
--- a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php
+++ b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php
@@ -10,17 +10,14 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\AppData\IAppDataFactory;
-use OCP\Files\NotFoundException;
-use OCP\Files\NotPermittedException;
-use OCP\Files\SimpleFS\ISimpleFolder;
use Psr\Log\LoggerInterface;
class RemoveOldTasksBackgroundJob extends TimedJob {
- public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months
private \OCP\Files\IAppData $appData;
public function __construct(
ITimeFactory $timeFactory,
+ private Manager $taskProcessingManager,
private TaskMapper $taskMapper,
private LoggerInterface $logger,
IAppDataFactory $appDataFactory,
@@ -32,48 +29,29 @@ public function __construct(
$this->appData = $appDataFactory->get('core');
}
-
/**
* @inheritDoc
*/
protected function run($argument): void {
try {
- $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS);
- } catch (\OCP\DB\Exception $e) {
- $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]);
+ iterator_to_array($this->taskProcessingManager->cleanupTaskProcessingTaskFiles());
+ } catch (\Exception $e) {
+ $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]);
}
try {
- $this->clearFilesOlderThan($this->appData->getFolder('text2image'), self::MAX_TASK_AGE_SECONDS);
- } catch (NotFoundException $e) {
- // noop
+ $this->taskMapper->deleteOlderThan(Manager::MAX_TASK_AGE_SECONDS);
+ } catch (\OCP\DB\Exception $e) {
+ $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]);
}
try {
- $this->clearFilesOlderThan($this->appData->getFolder('audio2text'), self::MAX_TASK_AGE_SECONDS);
- } catch (NotFoundException $e) {
+ iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image')));
+ } catch (\OCP\Files\NotFoundException $e) {
// noop
}
try {
- $this->clearFilesOlderThan($this->appData->getFolder('TaskProcessing'), self::MAX_TASK_AGE_SECONDS);
- } catch (NotFoundException $e) {
+ iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text')));
+ } catch (\OCP\Files\NotFoundException $e) {
// noop
}
}
-
- /**
- * @param ISimpleFolder $folder
- * @param int $ageInSeconds
- * @return void
- */
- private function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds): void {
- foreach ($folder->getDirectoryListing() as $file) {
- if ($file->getMTime() < time() - $ageInSeconds) {
- try {
- $file->delete();
- } catch (NotPermittedException $e) {
- $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
- }
- }
- }
- }
-
}
diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php
index 723eca8f61561..731250d7aa1c2 100644
--- a/lib/public/TaskProcessing/IManager.php
+++ b/lib/public/TaskProcessing/IManager.php
@@ -234,4 +234,14 @@ public function lockTask(Task $task): bool;
* @since 30.0.0
*/
public function setTaskStatus(Task $task, int $status): void;
+
+ /**
+ * Extract all input and output file IDs from a task
+ *
+ * @param Task $task
+ * @return list
+ * @throws NotFoundException
+ * @since 32.0.0
+ */
+ public function extractFileIdsFromTask(Task $task): array;
}
diff --git a/lib/public/TaskProcessing/Task.php b/lib/public/TaskProcessing/Task.php
index 71c271cd43d52..06dc84d59ff4a 100644
--- a/lib/public/TaskProcessing/Task.php
+++ b/lib/public/TaskProcessing/Task.php
@@ -66,6 +66,7 @@ final class Task implements \JsonSerializable {
protected ?int $scheduledAt = null;
protected ?int $startedAt = null;
protected ?int $endedAt = null;
+ protected bool $allowCleanup = true;
/**
* @param string $taskTypeId
@@ -253,7 +254,23 @@ final public function setEndedAt(?int $endedAt): void {
}
/**
- * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array|numeric|string>, output: ?array|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int}
+ * @return bool
+ * @since 32.0.0
+ */
+ final public function getAllowCleanup(): bool {
+ return $this->allowCleanup;
+ }
+
+ /**
+ * @param bool $allowCleanup
+ * @since 32.0.0
+ */
+ final public function setAllowCleanup(bool $allowCleanup): void {
+ $this->allowCleanup = $allowCleanup;
+ }
+
+ /**
+ * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array|numeric|string>, output: ?array|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int, allowCleanup: bool}
* @since 30.0.0
*/
final public function jsonSerialize(): array {
@@ -272,6 +289,7 @@ final public function jsonSerialize(): array {
'scheduledAt' => $this->getScheduledAt(),
'startedAt' => $this->getStartedAt(),
'endedAt' => $this->getEndedAt(),
+ 'allowCleanup' => $this->getAllowCleanup(),
];
}
diff --git a/openapi.json b/openapi.json
index cf391e80eea31..af0c627d39f93 100644
--- a/openapi.json
+++ b/openapi.json
@@ -677,7 +677,8 @@
"progress",
"scheduledAt",
"startedAt",
- "endedAt"
+ "endedAt",
+ "allowCleanup"
],
"properties": {
"id": {
@@ -748,6 +749,9 @@
"type": "integer",
"format": "int64",
"nullable": true
+ },
+ "allowCleanup": {
+ "type": "boolean"
}
}
},
diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php
index fee4e9ba3bad8..d2f619da3495d 100644
--- a/tests/lib/TaskProcessing/TaskProcessingTest.php
+++ b/tests/lib/TaskProcessing/TaskProcessingTest.php
@@ -972,6 +972,7 @@ public function testOldTasksShouldBeCleanedUp(): void {
// run background job
$bgJob = new RemoveOldTasksBackgroundJob(
$timeFactory,
+ $this->manager,
$this->taskMapper,
Server::get(LoggerInterface::class),
Server::get(IAppDataFactory::class),
diff --git a/version.php b/version.php
index c13cc6eebc28b..97d570fc3dbce 100644
--- a/version.php
+++ b/version.php
@@ -9,7 +9,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
-$OC_Version = [32, 0, 0, 2];
+$OC_Version = [32, 0, 0, 3];
// The human-readable string
$OC_VersionString = '32.0.0 dev';