@@ -98,7 +98,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
/** | |||
* This endpoint allows scheduling a task | |||
* | |||
* @param array<string, mixed> $input Input text | |||
* @param array<string, mixed> $input Task's input parameters | |||
* @param string $type Type of the task | |||
* @param string $appId ID of the app that will execute the task | |||
* @param string $identifier An arbitrary identifier for the task | |||
@@ -171,7 +171,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
* | |||
* @param int $id The id of the task | |||
* | |||
* @return DataResponse<Http::STATUS_OK, array{}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> | |||
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> | |||
* | |||
* 200: Task returned | |||
*/ | |||
@@ -260,10 +260,10 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
/** | |||
* @param Task $task | |||
* @return list<mixed> | |||
* @return list<int> | |||
* @throws \OCP\TaskProcessing\Exception\NotFoundException | |||
*/ | |||
private function extractFileIdsFromTask(Task $task) { | |||
private function extractFileIdsFromTask(Task $task): array { | |||
$ids = []; | |||
$taskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); | |||
if (!isset($taskTypes[$task->getTaskTypeId()])) { | |||
@@ -272,6 +272,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
$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<int> $inputSlot */ | |||
$inputSlot = $task->getInput()[$key]; | |||
if (is_array($inputSlot)) { | |||
$ids += $inputSlot; | |||
@@ -283,6 +284,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
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<int> $outputSlot */ | |||
$outputSlot = $task->getOutput()[$key]; | |||
if (is_array($outputSlot)) { | |||
$ids += $outputSlot; | |||
@@ -292,7 +294,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { | |||
} | |||
} | |||
} | |||
return $ids; | |||
return array_values($ids); | |||
} | |||
/** |
@@ -195,11 +195,11 @@ namespace OCA\Core; | |||
* @psalm-type CoreTaskProcessingTask = array{ | |||
* id: int, | |||
* type: string, | |||
* status: 0|1|2|3|4|5, | |||
* status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', | |||
* userId: ?string, | |||
* appId: string, | |||
* input: ?array<string, mixed>, | |||
* output: ?array<string, mixed>, | |||
* input: array<string, numeric|list<numeric>|string|list<string>>, | |||
* output: ?array<string, numeric|list<numeric>|string|list<string>>, | |||
* identifier: ?string, | |||
* completionExpectedAt: ?int, | |||
* progress: ?float |
@@ -509,15 +509,14 @@ | |||
"type": "string" | |||
}, | |||
"status": { | |||
"type": "integer", | |||
"format": "int64", | |||
"type": "string", | |||
"enum": [ | |||
0, | |||
1, | |||
2, | |||
3, | |||
4, | |||
5 | |||
"STATUS_CANCELLED", | |||
"STATUS_FAILED", | |||
"STATUS_SUCCESSFUL", | |||
"STATUS_RUNNING", | |||
"STATUS_SCHEDULED", | |||
"STATUS_UNKNOWN" | |||
] | |||
}, | |||
"userId": { | |||
@@ -529,16 +528,53 @@ | |||
}, | |||
"input": { | |||
"type": "object", | |||
"nullable": true, | |||
"additionalProperties": { | |||
"type": "object" | |||
"anyOf": [ | |||
{ | |||
"type": "number" | |||
}, | |||
{ | |||
"type": "array", | |||
"items": { | |||
"type": "number" | |||
} | |||
}, | |||
{ | |||
"type": "string" | |||
}, | |||
{ | |||
"type": "array", | |||
"items": { | |||
"type": "string" | |||
} | |||
} | |||
] | |||
} | |||
}, | |||
"output": { | |||
"type": "object", | |||
"nullable": true, | |||
"additionalProperties": { | |||
"type": "object" | |||
"anyOf": [ | |||
{ | |||
"type": "number" | |||
}, | |||
{ | |||
"type": "array", | |||
"items": { | |||
"type": "number" | |||
} | |||
}, | |||
{ | |||
"type": "string" | |||
}, | |||
{ | |||
"type": "array", | |||
"items": { | |||
"type": "string" | |||
} | |||
} | |||
] | |||
} | |||
}, | |||
"identifier": { | |||
@@ -3410,7 +3446,7 @@ | |||
{ | |||
"name": "input", | |||
"in": "query", | |||
"description": "Input text", | |||
"description": "Task's input parameters", | |||
"required": true, | |||
"schema": { | |||
"type": "string" | |||
@@ -3861,7 +3897,7 @@ | |||
"$ref": "#/components/schemas/OCSMeta" | |||
}, | |||
"data": { | |||
"type": "object" | |||
"nullable": true | |||
} | |||
} | |||
} |
@@ -164,10 +164,10 @@ class RegistrationContext { | |||
private array $teamResourceProviders = []; | |||
/** @var ServiceRegistration<\OCP\TaskProcessing\IProvider>[] */ | |||
private $taskProcessingProviders = []; | |||
private array $taskProcessingProviders = []; | |||
/** @var ServiceRegistration<\OCP\TaskProcessing\ITaskType>[] */ | |||
private $taskProcessingTaskTypes = []; | |||
private array $taskProcessingTaskTypes = []; | |||
public function __construct(LoggerInterface $logger) { | |||
$this->logger = $logger; |
@@ -505,9 +505,10 @@ class Manager implements IManager { | |||
} | |||
/** | |||
* @param array<string,mixed> $array The array to filter | |||
* @param array<string, mixed> ...$specs the specs that define which keys to keep | |||
* @return array<string, mixed> | |||
* @param array<array-key, T> $array The array to filter | |||
* @param ShapeDescriptor[] ...$specs the specs that define which keys to keep | |||
* @return array<array-key, T> | |||
* @psalm-template T | |||
*/ | |||
private function removeSuperfluousArrayKeys(array $array, ...$specs): array { | |||
$keys = array_unique(array_reduce($specs, fn ($carry, $spec) => $carry + array_keys($spec), [])); | |||
@@ -679,7 +680,7 @@ class Manager implements IManager { | |||
$this->validateOutput($outputShape, $result); | |||
$this->validateOutput($optionalOutputShape, $result, true); | |||
$output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape); | |||
// extract base64 data and put it in files, replace it with file ids | |||
// extract raw data and put it in files, replace it with file ids | |||
$output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape); | |||
$task->setOutput($output); | |||
$task->setProgress(1); | |||
@@ -726,36 +727,12 @@ class Manager implements IManager { | |||
} | |||
} | |||
public function getUserTask(int $id, ?string $userId): Task { | |||
try { | |||
$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId); | |||
return $taskEntity->toPublicTask(); | |||
} catch (DoesNotExistException $e) { | |||
throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e); | |||
} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e); | |||
} catch (\JsonException $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e); | |||
} | |||
} | |||
public function getUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { | |||
try { | |||
$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier); | |||
return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities); | |||
} catch (\OCP\DB\Exception $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e); | |||
} catch (\JsonException $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e); | |||
} | |||
} | |||
/** | |||
* Takes task input or output data and replaces fileIds with base64 data | |||
* | |||
* @param array<array-key, list<numeric|string>|numeric|string> $input | |||
* @param ShapeDescriptor[] ...$specs the specs | |||
* @param array $input | |||
* @return array | |||
* @return array<array-key, list<File|numeric|string>|numeric|string|File> | |||
* @throws GenericFileException | |||
* @throws LockedException | |||
* @throws NotPermittedException | |||
@@ -805,6 +782,30 @@ class Manager implements IManager { | |||
return $newInputOutput; | |||
} | |||
public function getUserTask(int $id, ?string $userId): Task { | |||
try { | |||
$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId); | |||
return $taskEntity->toPublicTask(); | |||
} catch (DoesNotExistException $e) { | |||
throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e); | |||
} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e); | |||
} catch (\JsonException $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e); | |||
} | |||
} | |||
public function getUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { | |||
try { | |||
$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier); | |||
return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities); | |||
} catch (\OCP\DB\Exception $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e); | |||
} catch (\JsonException $e) { | |||
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e); | |||
} | |||
} | |||
/** | |||
*Takes task input or output and replaces base64 data with file ids | |||
* | |||
@@ -846,6 +847,14 @@ class Manager implements IManager { | |||
return $newOutput; | |||
} | |||
/** | |||
* @param Task $task | |||
* @return array<array-key, list<numeric|string|File>|numeric|string|File> | |||
* @throws GenericFileException | |||
* @throws LockedException | |||
* @throws NotPermittedException | |||
* @throws ValidationException | |||
*/ | |||
public function prepareInputData(Task $task): array { | |||
$taskTypes = $this->getAvailableTaskTypes(); | |||
$inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape']; |
@@ -26,6 +26,7 @@ declare(strict_types=1); | |||
namespace OCP\TaskProcessing; | |||
use OCP\Files\File; | |||
use OCP\Files\GenericFileException; | |||
use OCP\Files\NotPermittedException; | |||
use OCP\Lock\LockedException; | |||
@@ -150,7 +151,7 @@ interface IManager { | |||
* ie. this replaces file ids with base64 data | |||
* | |||
* @param Task $task | |||
* @return array<string, mixed> | |||
* @return array<array-key, list<numeric|string|File>|numeric|string|File> | |||
* @throws NotPermittedException | |||
* @throws GenericFileException | |||
* @throws LockedException |
@@ -26,6 +26,7 @@ declare(strict_types=1); | |||
namespace OCP\TaskProcessing; | |||
use OCP\Files\File; | |||
use OCP\TaskProcessing\Exception\ProcessingException; | |||
/** | |||
@@ -38,11 +39,11 @@ interface ISynchronousProvider extends IProvider { | |||
/** | |||
* Returns the shape of optional output parameters | |||
* | |||
* @since 30.0.0 | |||
* @param null|string $userId The user that created the current task | |||
* @param array<string, mixed> $input The task input | |||
* @psalm-return array<string, mixed> | |||
* @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input | |||
* @psalm-return array<string, list<numeric|string>|numeric|string> | |||
* @throws ProcessingException | |||
*@since 30.0.0 | |||
*/ | |||
public function process(?string $userId, array $input): array; | |||
} |
@@ -75,7 +75,8 @@ final class Task implements \JsonSerializable { | |||
protected int $status = self::STATUS_UNKNOWN; | |||
/** | |||
* @param array<string,mixed> $input | |||
* @param string $taskTypeId | |||
* @param array<string,list<numeric|string>|numeric|string> $input | |||
* @param string $appId | |||
* @param string|null $userId | |||
* @param null|string $identifier An arbitrary identifier for this task. max length: 255 chars | |||
@@ -146,6 +147,7 @@ final class Task implements \JsonSerializable { | |||
} | |||
/** | |||
* @param null|array<array-key, list<numeric|string>|numeric|string> $output | |||
* @since 30.0.0 | |||
*/ | |||
final public function setOutput(?array $output): void { | |||
@@ -153,7 +155,7 @@ final class Task implements \JsonSerializable { | |||
} | |||
/** | |||
* @return array<array-key, mixed>|null | |||
* @return array<array-key, list<numeric|string>|numeric|string>|null | |||
* @since 30.0.0 | |||
*/ | |||
final public function getOutput(): ?array { | |||
@@ -161,7 +163,7 @@ final class Task implements \JsonSerializable { | |||
} | |||
/** | |||
* @return array<array-key, mixed> | |||
* @return array<array-key, list<numeric|string>|numeric|string> | |||
* @since 30.0.0 | |||
*/ | |||
final public function getInput(): array { | |||
@@ -193,20 +195,20 @@ final class Task implements \JsonSerializable { | |||
} | |||
/** | |||
* @psalm-return array{id: ?int, type: string, status: self::STATUS_*, userId: ?string, appId: string, input: ?array<array-key, mixed>, output: ?array<array-key, mixed>, identifier: ?string, completionExpectedAt: ?int, progress: ?float} | |||
* @psalm-return array{id: ?int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array<array-key, list<numeric|string>|numeric|string>, output: ?array<array-key, list<numeric|string>|numeric|string>, identifier: ?string, completionExpectedAt: ?int, progress: ?float} | |||
* @since 30.0.0 | |||
*/ | |||
public function jsonSerialize(): array { | |||
final public function jsonSerialize(): array { | |||
return [ | |||
'id' => $this->getId(), | |||
'type' => $this->getTaskTypeId(), | |||
'status' => $this->getStatus(), | |||
'status' => self::statusToString($this->getStatus()), | |||
'userId' => $this->getUserId(), | |||
'appId' => $this->getAppId(), | |||
'input' => $this->getInput(), | |||
'output' => $this->getOutput(), | |||
'identifier' => $this->getIdentifier(), | |||
'completionExpectedAt' => $this->getCompletionExpectedAt()->getTimestamp(), | |||
'completionExpectedAt' => $this->getCompletionExpectedAt()?->getTimestamp(), | |||
'progress' => $this->getProgress(), | |||
]; | |||
} | |||
@@ -216,7 +218,7 @@ final class Task implements \JsonSerializable { | |||
* @return void | |||
* @since 30.0.0 | |||
*/ | |||
public function setErrorMessage(?string $error) { | |||
final public function setErrorMessage(?string $error) { | |||
$this->errorMessage = $error; | |||
} | |||
@@ -224,7 +226,7 @@ final class Task implements \JsonSerializable { | |||
* @return string|null | |||
* @since 30.0.0 | |||
*/ | |||
public function getErrorMessage(): ?string { | |||
final public function getErrorMessage(): ?string { | |||
return $this->errorMessage; | |||
} | |||
@@ -233,7 +235,7 @@ final class Task implements \JsonSerializable { | |||
* @return void | |||
* @since 30.0.0 | |||
*/ | |||
public function setInput(array $input): void { | |||
final public function setInput(array $input): void { | |||
$this->input = $input; | |||
} | |||
@@ -243,7 +245,7 @@ final class Task implements \JsonSerializable { | |||
* @throws ValidationException | |||
* @since 30.0.0 | |||
*/ | |||
public function setProgress(?float $progress): void { | |||
final public function setProgress(?float $progress): void { | |||
if ($progress < 0 || $progress > 1.0) { | |||
throw new ValidationException('Progress must be between 0.0 and 1.0 inclusively; ' . $progress . ' given'); | |||
} | |||
@@ -254,7 +256,23 @@ final class Task implements \JsonSerializable { | |||
* @return float|null | |||
* @since 30.0.0 | |||
*/ | |||
public function getProgress(): ?float { | |||
final public function getProgress(): ?float { | |||
return $this->progress; | |||
} | |||
/** | |||
* @param int $status | |||
* @return 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN' | |||
* @since 30.0.0 | |||
*/ | |||
final public static function statusToString(int $status): string { | |||
return match ($status) { | |||
self::STATUS_CANCELLED => 'STATUS_CANCELLED', | |||
self::STATUS_FAILED => 'STATUS_FAILED', | |||
self::STATUS_SUCCESSFUL => 'STATUS_SUCCESSFUL', | |||
self::STATUS_RUNNING => 'STATUS_RUNNING', | |||
self::STATUS_SCHEDULED => 'STATUS_SCHEDULED', | |||
default => 'STATUS_UNKNOWN', | |||
}; | |||
} | |||
} |