Browse Source

feat(TaskProcessingApi): Add endpoint for getting the next task

Signed-off-by: provokateurin <kate@provokateurin.de>
pull/45391/head
provokateurin 2 years ago
parent
commit
f5ff8136ac
Failed to extract signature
  1. 183
      core/Controller/TaskProcessingApiController.php
  2. 844
      core/openapi-administration.json
  3. 2
      core/openapi-administration.json.license
  4. 8701
      core/openapi-full.json
  5. 2
      core/openapi-full.json.license
  6. 338
      core/openapi.json
  7. 33
      lib/private/TaskProcessing/Db/TaskMapper.php
  8. 40
      lib/private/TaskProcessing/Manager.php
  9. 4
      lib/private/TaskProcessing/SynchronousBackgroundJob.php
  10. 22
      lib/public/TaskProcessing/IManager.php

183
core/Controller/TaskProcessingApiController.php

@ -14,21 +14,29 @@ use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\ExAppRequired;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\Lock\LockedException;
use OCP\TaskProcessing\EShapeType;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\NotFoundException;
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\ShapeDescriptor;
use OCP\TaskProcessing\Task;
use RuntimeException;
/**
* @psalm-import-type CoreTaskProcessingTask from ResponseDefinitions
@ -36,11 +44,11 @@ use OCP\TaskProcessing\Task;
*/
class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private \OCP\TaskProcessing\IManager $taskProcessingManager,
private IL10N $l,
private ?string $userId,
string $appName,
IRequest $request,
private IManager $taskProcessingManager,
private IL10N $l,
private ?string $userId,
private IRootFolder $rootFolder,
) {
parent::__construct($appName, $request);
@ -109,13 +117,13 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'task' => $json,
]);
} catch (\OCP\TaskProcessing\Exception\PreConditionNotMetException) {
} catch (PreConditionNotMetException) {
return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED);
} catch (ValidationException $e) {
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
} catch (UnauthorizedException $e) {
} catch (UnauthorizedException) {
return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED);
} catch (\OCP\TaskProcessing\Exception\Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -144,9 +152,9 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'task' => $json,
]);
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
} catch (\RuntimeException $e) {
} catch (RuntimeException) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -169,9 +177,9 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
$this->taskProcessingManager->deleteTask($task);
return new DataResponse(null);
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
} catch (NotFoundException) {
return new DataResponse(null);
} catch (\OCP\TaskProcessing\Exception\Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -199,7 +207,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'tasks' => $json,
]);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -226,7 +234,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'tasks' => $json,
]);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -247,37 +255,72 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
public function getFileContents(int $taskId, int $fileId): Http\DataDownloadResponse|DataResponse {
try {
$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
$ids = $this->extractFileIdsFromTask($task);
if (!in_array($fileId, $ids)) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
}
$node = $this->rootFolder->getFirstNodeById($fileId);
if ($node === null) {
$node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
if (!$node instanceof File) {
throw new \OCP\TaskProcessing\Exception\NotFoundException('Node is not a file');
}
} elseif (!$node instanceof File) {
throw new \OCP\TaskProcessing\Exception\NotFoundException('Node is not a file');
}
return new Http\DataDownloadResponse($node->getContent(), $node->getName(), $node->getMimeType());
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
return $this->getFileContentsInternal($task, $fileId);
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* Returns the contents of a file referenced in a task(ExApp route version)
*
* @param int $taskId The id of the task
* @param int $fileId The file id of the file to retrieve
* @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: File content returned
* 404: Task or file not found
*/
#[ExAppRequired]
#[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')]
public function getFileContentsExApp(int $taskId, int $fileId): Http\DataDownloadResponse|DataResponse {
try {
$task = $this->taskProcessingManager->getTask($taskId);
return $this->getFileContentsInternal($task, $fileId);
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @throws NotPermittedException
* @throws NotFoundException
* @throws GenericFileException
* @throws LockedException
*
* @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*/
private function getFileContentsInternal(Task $task, int $fileId): Http\DataDownloadResponse|DataResponse {
$ids = $this->extractFileIdsFromTask($task);
if (!in_array($fileId, $ids)) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
}
$node = $this->rootFolder->getFirstNodeById($fileId);
if ($node === null) {
$node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
if (!$node instanceof File) {
throw new NotFoundException('Node is not a file');
}
} elseif (!$node instanceof File) {
throw new NotFoundException('Node is not a file');
}
return new Http\DataDownloadResponse($node->getContent(), $node->getName(), $node->getMimeType());
}
/**
* @param Task $task
* @return list<int>
* @throws \OCP\TaskProcessing\Exception\NotFoundException
* @throws NotFoundException
*/
private function extractFileIdsFromTask(Task $task): array {
$ids = [];
$taskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
if (!isset($taskTypes[$task->getTaskTypeId()])) {
throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find task type');
throw new NotFoundException('Could not find task type');
}
$taskType = $taskTypes[$task->getTaskTypeId()];
foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
@ -317,12 +360,12 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
* 200: Progress updated successfully
* 404: Task not found
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/progress', root: '/taskprocessing')]
#[ExAppRequired]
#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')]
public function setProgress(int $taskId, float $progress): DataResponse {
try {
$this->taskProcessingManager->setTaskProgress($taskId, $progress);
$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
$task = $this->taskProcessingManager->getTask($taskId);
/** @var CoreTaskProcessingTask $json */
$json = $task->jsonSerialize();
@ -330,9 +373,9 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'task' => $json,
]);
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -348,15 +391,13 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
* 200: Result updated successfully
* 404: Task not found
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/result', root: '/taskprocessing')]
#[ExAppRequired]
#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
try {
// Check if the current user can access the task
$this->taskProcessingManager->getUserTask($taskId, $this->userId);
// set result
$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output);
$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
$task = $this->taskProcessingManager->getTask($taskId);
/** @var CoreTaskProcessingTask $json */
$json = $task->jsonSerialize();
@ -364,9 +405,9 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'task' => $json,
]);
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
@ -396,9 +437,59 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse([
'task' => $json,
]);
} catch (\OCP\TaskProcessing\Exception\NotFoundException $e) {
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* Returns the next scheduled task for the taskTypeId
*
* @param list<string> $providerIds The ids of the providers
* @param list<string> $taskTypeIds The ids of the task types
* @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask, provider: array{name: string}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 204: No task found
*/
#[ExAppRequired]
#[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')]
public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse {
try {
// restrict $providerIds to providers that are configured as preferred for the passed task types
$providerIds = array_values(array_intersect(array_unique(array_map(fn ($taskTypeId) => $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId(), $taskTypeIds)), $providerIds));
// restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers
$taskTypeIds = array_values(array_filter($taskTypeIds, fn ($taskTypeId) => in_array($this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId(), $providerIds, true)));
if (count($providerIds) === 0 || count($taskTypeIds) === 0) {
throw new NotFoundException();
}
$taskIdsToIgnore = [];
while (true) {
$task = $this->taskProcessingManager->getNextScheduledTask($taskTypeIds, $taskIdsToIgnore);
$provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
if (in_array($provider->getId(), $providerIds, true)) {
if ($this->taskProcessingManager->lockTask($task)) {
break;
}
}
$taskIdsToIgnore[] = (int)$task->getId();
}
/** @var CoreTaskProcessingTask $json */
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
'provider' => [
'name' => $provider->getId(),
],
]);
} catch (NotFoundException) {
return new DataResponse(null, Http::STATUS_NO_CONTENT);
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

844
core/openapi-administration.json

@ -0,0 +1,844 @@
{
"openapi": "3.0.3",
"info": {
"title": "core-administration",
"version": "0.0.1",
"description": "Core functionality of Nextcloud",
"license": {
"name": "agpl"
}
},
"components": {
"securitySchemes": {
"basic_auth": {
"type": "http",
"scheme": "basic"
},
"bearer_auth": {
"type": "http",
"scheme": "bearer"
}
},
"schemas": {
"Capabilities": {
"type": "object",
"required": [
"core"
],
"properties": {
"core": {
"type": "object",
"required": [
"pollinterval",
"webdav-root",
"reference-api",
"reference-regex",
"mod-rewrite-working"
],
"properties": {
"pollinterval": {
"type": "integer",
"format": "int64"
},
"webdav-root": {
"type": "string"
},
"reference-api": {
"type": "boolean"
},
"reference-regex": {
"type": "string"
},
"mod-rewrite-working": {
"type": "boolean"
}
}
}
}
},
"OCSMeta": {
"type": "object",
"required": [
"status",
"statuscode"
],
"properties": {
"status": {
"type": "string"
},
"statuscode": {
"type": "integer"
},
"message": {
"type": "string"
},
"totalitems": {
"type": "string"
},
"itemsperpage": {
"type": "string"
}
}
},
"PublicCapabilities": {
"type": "object",
"required": [
"bruteforce"
],
"properties": {
"bruteforce": {
"type": "object",
"required": [
"delay",
"allow-listed"
],
"properties": {
"delay": {
"type": "integer",
"format": "int64"
},
"allow-listed": {
"type": "boolean"
}
}
}
}
},
"TaskProcessingIO": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "number"
},
{
"type": "array",
"items": {
"type": "number"
}
},
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"TaskProcessingTask": {
"type": "object",
"required": [
"id",
"lastUpdated",
"type",
"status",
"userId",
"appId",
"input",
"output",
"customId",
"completionExpectedAt",
"progress"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"lastUpdated": {
"type": "integer",
"format": "int64"
},
"type": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"STATUS_CANCELLED",
"STATUS_FAILED",
"STATUS_SUCCESSFUL",
"STATUS_RUNNING",
"STATUS_SCHEDULED",
"STATUS_UNKNOWN"
]
},
"userId": {
"type": "string",
"nullable": true
},
"appId": {
"type": "string"
},
"input": {
"$ref": "#/components/schemas/TaskProcessingIO"
},
"output": {
"$ref": "#/components/schemas/TaskProcessingIO",
"nullable": true
},
"customId": {
"type": "string",
"nullable": true
},
"completionExpectedAt": {
"type": "integer",
"format": "int64",
"nullable": true
},
"progress": {
"type": "number",
"format": "double",
"nullable": true
}
}
}
}
},
"paths": {
"/ocs/v2.php/taskprocessing/tasks_provider/{taskId}/file/{fileId}": {
"get": {
"operationId": "task_processing_api-get-file-contents-ex-app",
"summary": "Returns the contents of a file referenced in a task(ExApp route version)",
"description": "This endpoint requires admin access",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "taskId",
"in": "path",
"description": "The id of the task",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "fileId",
"in": "path",
"description": "The file id of the file to retrieve",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "File content returned",
"content": {
"*/*": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "Task or file not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/taskprocessing/tasks_provider/{taskId}/progress": {
"post": {
"operationId": "task_processing_api-set-progress",
"summary": "Sets the task progress",
"description": "This endpoint requires admin access",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "progress",
"in": "query",
"description": "The progress",
"required": true,
"schema": {
"type": "number",
"format": "double"
}
},
{
"name": "taskId",
"in": "path",
"description": "The id of the task",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Progress updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"task"
],
"properties": {
"task": {
"$ref": "#/components/schemas/TaskProcessingTask"
}
}
}
}
}
}
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/taskprocessing/tasks_provider/{taskId}/result": {
"post": {
"operationId": "task_processing_api-set-result",
"summary": "Sets the task result",
"description": "This endpoint requires admin access",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "output",
"in": "query",
"description": "The resulting task output",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "errorMessage",
"in": "query",
"description": "An error message if the task failed",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "taskId",
"in": "path",
"description": "The id of the task",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Result updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"task"
],
"properties": {
"task": {
"$ref": "#/components/schemas/TaskProcessingTask"
}
}
}
}
}
}
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/taskprocessing/tasks_provider/next": {
"get": {
"operationId": "task_processing_api-get-next-scheduled-task",
"summary": "Returns the next scheduled task for the taskTypeId",
"description": "This endpoint requires admin access",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "providerIds[]",
"in": "query",
"description": "The ids of the providers",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "taskTypeIds[]",
"in": "query",
"description": "The ids of the task types",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Task returned",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"task",
"provider"
],
"properties": {
"task": {
"$ref": "#/components/schemas/TaskProcessingTask"
},
"provider": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"204": {
"description": "No task found"
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"tags": [
{
"name": "avatar",
"description": "Class AvatarController"
},
{
"name": "guest_avatar",
"description": "This controller handles guest avatar requests."
},
{
"name": "ocm",
"description": "Controller about the endpoint /ocm-provider/"
}
]
}

2
core/openapi-administration.json.license

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later

8701
core/openapi-full.json
File diff suppressed because it is too large
View File

2
core/openapi-full.json.license

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later

338
core/openapi.json

@ -4331,344 +4331,6 @@
}
}
},
"/ocs/v2.php/taskprocessing/tasks/{taskId}/progress": {
"post": {
"operationId": "task_processing_api-set-progress",
"summary": "Sets the task progress",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "progress",
"in": "query",
"description": "The progress",
"required": true,
"schema": {
"type": "number",
"format": "double"
}
},
{
"name": "taskId",
"in": "path",
"description": "The id of the task",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Progress updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"task"
],
"properties": {
"task": {
"$ref": "#/components/schemas/TaskProcessingTask"
}
}
}
}
}
}
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/taskprocessing/tasks/{taskId}/result": {
"post": {
"operationId": "task_processing_api-set-result",
"summary": "Sets the task result",
"tags": [
"task_processing_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "output",
"in": "query",
"description": "The resulting task output",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "errorMessage",
"in": "query",
"description": "An error message if the task failed",
"schema": {
"type": "string",
"nullable": true
}
},
{
"name": "taskId",
"in": "path",
"description": "The id of the task",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Result updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"task"
],
"properties": {
"task": {
"$ref": "#/components/schemas/TaskProcessingTask"
}
}
}
}
}
}
}
}
}
},
"500": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/taskprocessing/tasks/{taskId}/cancel": {
"post": {
"operationId": "task_processing_api-cancel-task",

33
lib/private/TaskProcessing/Db/TaskMapper.php

@ -45,21 +45,33 @@ class TaskMapper extends QBMapper {
}
/**
* @param string|null $taskType
* @param list<string> $taskTypes
* @param list<int> $taskIdsToIgnore
* @return Task
* @throws DoesNotExistException
* @throws Exception
*/
public function findOldestScheduledByType(?string $taskType): Task {
public function findOldestScheduledByType(array $taskTypes, array $taskIdsToIgnore): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT)))
->setMaxResults(1)
->orderBy('last_updated', 'ASC');
if ($taskType !== null) {
$qb->andWhere($qb->expr()->eq('type', $qb->createPositionalParameter($taskType)));
if (count($taskTypes) > 0) {
$filter = $qb->expr()->orX();
foreach ($taskTypes as $taskType) {
$filter->add($qb->expr()->eq('type', $qb->createPositionalParameter($taskType)));
}
$qb->andWhere($filter);
}
if (count($taskIdsToIgnore) > 0) {
$qb->andWhere($qb->expr()->notIn('id', $qb->createNamedParameter($taskIdsToIgnore, IQueryBuilder::PARAM_INT_ARRAY)));
}
return $this->findEntity($qb);
}
@ -140,4 +152,17 @@ class TaskMapper extends QBMapper {
$entity->setLastUpdated($this->timeFactory->now()->getTimestamp());
return parent::update($entity);
}
public function lockTask(Entity $entity): int {
$qb = $this->db->getQueryBuilder();
$qb->update($this->tableName)
->set('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_RUNNING, IQueryBuilder::PARAM_INT))
->where($qb->expr()->eq('id', $qb->createPositionalParameter($entity->getId(), IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->neq('status', $qb->createPositionalParameter(2, IQueryBuilder::PARAM_INT)));
try {
return $qb->executeStatement();
} catch (Exception) {
return 0;
}
}
}

40
lib/private/TaskProcessing/Manager.php

@ -420,21 +420,6 @@ class Manager implements IManager {
return $taskTypes;
}
/**
* @param string $taskType
* @return IProvider
* @throws \OCP\TaskProcessing\Exception\Exception
*/
private function _getPreferredProvider(string $taskType) {
$providers = $this->getProviders();
foreach ($providers as $provider) {
if ($provider->getTaskTypeId() === $taskType) {
return $provider;
}
}
throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
}
/**
* @param ShapeDescriptor[] $spec
* @param array $io
@ -507,6 +492,16 @@ class Manager implements IManager {
return $this->providers;
}
public function getPreferredProvider(string $taskType) {
$providers = $this->getProviders();
foreach ($providers as $provider) {
if ($provider->getTaskTypeId() === $taskType) {
return $provider;
}
}
throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
}
public function getAvailableTaskTypes(): array {
if ($this->availableTaskTypes === null) {
$taskTypes = $this->_getTaskTypes();
@ -579,7 +574,7 @@ class Manager implements IManager {
// remove superfluous keys and set input
$task->setInput($this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape));
$task->setStatus(Task::STATUS_SCHEDULED);
$provider = $this->_getPreferredProvider($task->getTaskTypeId());
$provider = $this->getPreferredProvider($task->getTaskTypeId());
// calculate expected completion time
$completionExpectedAt = new \DateTime('now');
$completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S'));
@ -698,9 +693,9 @@ class Manager implements IManager {
$this->dispatcher->dispatchTyped($event);
}
public function getNextScheduledTask(?string $taskTypeId = null): Task {
public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
try {
$taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeId);
$taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
return $taskEntity->toPublicTask();
} catch (DoesNotExistException $e) {
throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
@ -866,4 +861,13 @@ class Manager implements IManager {
$input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
return $input;
}
public function lockTask(Task $task): bool {
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
if ($this->taskMapper->lockTask($taskEntity) === 0) {
return false;
}
$task->setStatus(Task::STATUS_RUNNING);
return true;
}
}

4
lib/private/TaskProcessing/SynchronousBackgroundJob.php

@ -43,7 +43,7 @@ class SynchronousBackgroundJob extends QueuedJob {
}
$taskType = $provider->getTaskTypeId();
try {
$task = $this->taskProcessingManager->getNextScheduledTask($taskType);
$task = $this->taskProcessingManager->getNextScheduledTask([$taskType]);
} catch (NotFoundException $e) {
continue;
} catch (Exception $e) {
@ -91,7 +91,7 @@ class SynchronousBackgroundJob extends QueuedJob {
));
$taskTypesWithTasks = array_filter($taskTypes, function ($taskType) {
try {
$this->taskProcessingManager->getNextScheduledTask($taskType);
$this->taskProcessingManager->getNextScheduledTask([$taskType]);
return true;
} catch (NotFoundException|Exception $e) {
return false;

22
lib/public/TaskProcessing/IManager.php

@ -37,6 +37,14 @@ interface IManager {
*/
public function getProviders(): array;
/**
* @param string $taskType
* @return IProvider
* @throws Exception
* @since 30.0.0
*/
public function getPreferredProvider(string $taskType);
/**
* @return array<string,array{name: string, description: string, inputShape: ShapeDescriptor[], optionalInputShape: ShapeDescriptor[], outputShape: ShapeDescriptor[], optionalOutputShape: ShapeDescriptor[]}>
* @since 30.0.0
@ -101,13 +109,14 @@ interface IManager {
public function setTaskProgress(int $id, float $progress): bool;
/**
* @param string|null $taskTypeId
* @param list<string> $taskTypeIds
* @param list<int> $taskIdsToIgnore
* @return Task
* @throws Exception If the query failed
* @throws NotFoundException If no task could not be found
* @since 30.0.0
*/
public function getNextScheduledTask(?string $taskTypeId = null): Task;
public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task;
/**
* @param int $id The id of the task
@ -154,4 +163,13 @@ interface IManager {
* @since 30.0.0
*/
public function prepareInputData(Task $task): array;
/**
* Changes the task status to STATUS_RUNNING and, if successful, returns True.
*
* @param Task $task
* @return bool
* @since 30.0.0
*/
public function lockTask(Task $task): bool;
}
Loading…
Cancel
Save