Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions core/Controller/TaskProcessingApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -460,17 +460,18 @@ public function setProgress(int $taskId, float $progress): DataResponse {
* @param int $taskId The id of the task
* @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
* @param string|null $errorMessage An error message if the task failed
* @param string|null $userFacingErrorMessage An error message that will be shown to the user
* @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Result updated successfully
* 404: Task not found
*/
#[ExAppRequired]
#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null, ?string $userFacingErrorMessage = null): DataResponse {
try {
// set result
$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true);
$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, isUsingFileIds: true, userFacingError: $userFacingErrorMessage);
$task = $this->taskProcessingManager->getTask($taskId);

/** @var CoreTaskProcessingTask $json */
Expand Down
48 changes: 48 additions & 0 deletions core/Migrations/Version33000Date20251013110519.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
*
*/
#[AddColumn(table: 'taskprocessing_tasks', name: 'user_facing_error_message', type: ColumnType::STRING)]
class Version33000Date20251013110519 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if ($schema->hasTable('taskprocessing_tasks')) {
$table = $schema->getTable('taskprocessing_tasks');
if (!$table->hasColumn('user_facing_error_message')) {
$table->addColumn('user_facing_error_message', Types::STRING, [
'notnull' => false,
'length' => 4000,
]);
return $schema;
}
}

return null;
}
}
6 changes: 6 additions & 0 deletions core/openapi-ex_app.json
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,12 @@
"nullable": true,
"default": null,
"description": "An error message if the task failed"
},
"userFacingErrorMessage": {
"type": "string",
"nullable": true,
"default": null,
"description": "An error message that will be shown to the user"
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions core/openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -11068,6 +11068,12 @@
"nullable": true,
"default": null,
"description": "An error message if the task failed"
},
"userFacingErrorMessage": {
"type": "string",
"nullable": true,
"default": null,
"description": "An error message that will be shown to the user"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@
'OCP\\TaskProcessing\\Exception\\PreConditionNotMetException' => $baseDir . '/lib/public/TaskProcessing/Exception/PreConditionNotMetException.php',
'OCP\\TaskProcessing\\Exception\\ProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/ProcessingException.php',
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => $baseDir . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => $baseDir . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\IInternalTaskType' => $baseDir . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => $baseDir . '/lib/public/TaskProcessing/IManager.php',
Expand Down Expand Up @@ -1530,6 +1531,7 @@
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Migrations\\Version33000Date20250819110529' => $baseDir . '/core/Migrations/Version33000Date20250819110529.php',
'OC\\Core\\Migrations\\Version33000Date20251013110519' => $baseDir . '/core/Migrations/Version33000Date20251013110519.php',
'OC\\Core\\Migrations\\Version33000Date20251023110529' => $baseDir . '/core/Migrations/Version33000Date20251023110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023120529' => $baseDir . '/core/Migrations/Version33000Date20251023120529.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
Expand Down
10 changes: 10 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
'NCU\\' => 4,
),
'B' =>
array (
'Bamarni\\Composer\\Bin\\' => 21,
),
);

public static $prefixDirsPsr4 = array (
Expand All @@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
'Bamarni\\Composer\\Bin\\' =>
array (
0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
),
);

public static $fallbackDirsPsr4 = array (
Expand Down Expand Up @@ -909,6 +917,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\TaskProcessing\\Exception\\PreConditionNotMetException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/PreConditionNotMetException.php',
'OCP\\TaskProcessing\\Exception\\ProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ProcessingException.php',
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\IInternalTaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IManager.php',
Expand Down Expand Up @@ -1571,6 +1580,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Migrations\\Version33000Date20250819110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20250819110529.php',
'OC\\Core\\Migrations\\Version33000Date20251013110519' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251013110519.php',
'OC\\Core\\Migrations\\Version33000Date20251023110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023120529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023120529.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
Expand Down
10 changes: 8 additions & 2 deletions lib/private/TaskProcessing/Db/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
* @method int getEndedAt()
* @method setAllowCleanup(int $allowCleanup)
* @method int getAllowCleanup()
* @method setUserFacingErrorMessage(null|string $message)
* @method null|string getUserFacingErrorMessage()
*/
class Task extends Entity {
protected $lastUpdated;
Expand All @@ -66,16 +68,17 @@ class Task extends Entity {
protected $startedAt;
protected $endedAt;
protected $allowCleanup;
protected $userFacingErrorMessage;

/**
* @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', 'allow_cleanup'];
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', 'user_facing_error_message'];

/**
* @var string[]
*/
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup'];
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage'];


public function __construct() {
Expand All @@ -98,6 +101,7 @@ public function __construct() {
$this->addType('startedAt', 'integer');
$this->addType('endedAt', 'integer');
$this->addType('allowCleanup', 'integer');
$this->addType('userFacingErrorMessage', 'string');
}

public function toRow(): array {
Expand Down Expand Up @@ -127,6 +131,7 @@ public static function fromPublicTask(OCPTask $task): self {
'startedAt' => $task->getStartedAt(),
'endedAt' => $task->getEndedAt(),
'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
'userFacingErrorMessage' => $task->getUserFacingErrorMessage(),
]);
return $taskEntity;
}
Expand All @@ -150,6 +155,7 @@ public function toPublicTask(): OCPTask {
$task->setStartedAt($this->getStartedAt());
$task->setEndedAt($this->getEndedAt());
$task->setAllowCleanup($this->getAllowCleanup() !== 0);
$task->setUserFacingErrorMessage($this->getUserFacingErrorMessage());
return $task;
}
}
20 changes: 13 additions & 7 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use OCP\TaskProcessing\Exception\NotFoundException;
use OCP\TaskProcessing\Exception\ProcessingException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\UserFacingProcessingException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\IInternalTaskType;
use OCP\TaskProcessing\IManager;
Expand Down Expand Up @@ -211,7 +212,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
try {
return ['output' => $this->provider->process($input['input'])];
} catch (\RuntimeException $e) {
throw new ProcessingException($e->getMessage(), 0, $e);
throw new ProcessingException($e->getMessage(), previous: $e);
}
}

Expand Down Expand Up @@ -362,7 +363,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
try {
$this->provider->generate($input['input'], $resources);
} catch (\RuntimeException $e) {
throw new ProcessingException($e->getMessage(), 0, $e);
throw new ProcessingException($e->getMessage(), previous: $e);
}
for ($i = 0; $i < $input['numberOfImages']; $i++) {
if (is_resource($resources[$i])) {
Expand Down Expand Up @@ -480,7 +481,7 @@ public function process(?string $userId, array $input, callable $reportProgress)
try {
$result = $this->provider->transcribeFile($input['input']);
} catch (\RuntimeException $e) {
throw new ProcessingException($e->getMessage(), 0, $e);
throw new ProcessingException($e->getMessage(), previous: $e);
}
return ['output' => $result];
}
Expand Down Expand Up @@ -1041,7 +1042,8 @@ public function processTask(Task $task, ISynchronousProvider $provider): bool {
$output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
} catch (ProcessingException $e) {
$this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
$this->setTaskResult($task->getId(), $e->getMessage(), null);
$userFacingErrorMessage = $e instanceof UserFacingProcessingException ? $e->getUserFacingMessage() : null;
$this->setTaskResult($task->getId(), $e->getMessage(), null, userFacingError: $userFacingErrorMessage);
return false;
} catch (\Throwable $e) {
$this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
Expand Down Expand Up @@ -1112,7 +1114,7 @@ public function setTaskProgress(int $id, float $progress): bool {
return true;
}

public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void {
// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
$task = $this->getTask($id);
if ($task->getStatus() === Task::STATUS_CANCELLED) {
Expand All @@ -1122,8 +1124,12 @@ public function setTaskResult(int $id, ?string $error, ?array $result, bool $isU
if ($error !== null) {
$task->setStatus(Task::STATUS_FAILED);
$task->setEndedAt(time());
// truncate error message to 1000 characters
$task->setErrorMessage(mb_substr($error, 0, 1000));
// truncate error message to 4000 characters
$task->setErrorMessage(substr($error, 0, 4000));
// truncate error message to 4000 characters
if ($userFacingError !== null) {
$task->setUserFacingErrorMessage(substr($userFacingError, 0, 4000));
}
$this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
} elseif ($result !== null) {
$taskTypes = $this->getAvailableTaskTypes();
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/Exception/ProcessingException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@

namespace OCP\TaskProcessing\Exception;

use OCP\AppFramework\Attribute\Consumable;

/**
* Exception thrown during processing of a task
* by a synchronous provider
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
class ProcessingException extends \RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/


namespace OCP\TaskProcessing\Exception;

use OCP\AppFramework\Attribute\Consumable;

/**
* Exception thrown during processing of a task
* by a synchronous provider with the possibility to set a user-facing
* error message
*
* @since 33.0.0
*/
#[Consumable(since: '33.0.0')]
class UserFacingProcessingException extends ProcessingException {

/**
* @param string $message
* @param int $code
* @param \Throwable|null $previous
* @param string|null $userFacingMessage
* @since 33.0.0
*/
public function __construct(
string $message = '',
int $code = 0,
?\Throwable $previous = null,
private ?string $userFacingMessage = null,
) {
parent::__construct($message, $code, $previous);
}

/**
* @since 33.0.0
*/
public function getUserFacingMessage(): ?string {
return $this->userFacingMessage;
}

/**
* @param null|string $userFacingMessage Must be already translated into the language of the user
* @since 33.0.0
*/
public function setUserFacingMessage(?string $userFacingMessage): void {
$this->userFacingMessage = $userFacingMessage;
}
}
6 changes: 5 additions & 1 deletion lib/public/TaskProcessing/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\NotPermittedException;
Expand All @@ -25,6 +26,7 @@
* without known which providers are installed
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
interface IManager {

/**
Expand Down Expand Up @@ -133,11 +135,13 @@ public function cancelTask(int $id): void;
* @param string|null $error
* @param array|null $result
* @param bool $isUsingFileIds
* @param string|null $userFacingError
* @throws Exception If the query failed
* @throws NotFoundException If the task could not be found
* @since 30.0.0
* @since 33.0.0 Added `userFacingError` parameter
*/
public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void;
public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void;

/**
* @param int $id
Expand Down
Loading
Loading