aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/Controller/TaskProcessingApiController.php57
-rw-r--r--core/openapi-ex_app.json197
-rw-r--r--core/openapi-full.json197
-rw-r--r--lib/private/TaskProcessing/Manager.php193
-rw-r--r--lib/private/TaskProcessing/SynchronousBackgroundJob.php3
-rw-r--r--lib/public/TaskProcessing/EShapeType.php36
-rw-r--r--lib/public/TaskProcessing/IManager.php6
-rw-r--r--tests/lib/TaskProcessing/TaskProcessingTest.php83
8 files changed, 703 insertions, 69 deletions
diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php
index 90ee650f1ed..d9bcbd5da45 100644
--- a/core/Controller/TaskProcessingApiController.php
+++ b/core/Controller/TaskProcessingApiController.php
@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace OC\Core\Controller;
use OC\Core\ResponseDefinitions;
+use OC\Files\SimpleFS\SimpleFile;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\ApiRoute;
@@ -22,6 +23,7 @@ use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\File;
use OCP\Files\GenericFileException;
+use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
@@ -50,6 +52,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
private IL10N $l,
private ?string $userId,
private IRootFolder $rootFolder,
+ private IAppData $appData,
) {
parent::__construct($appName, $request);
}
@@ -287,6 +290,40 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
}
/**
+ * Upload a file so it can be referenced in a task result (ExApp route version)
+ *
+ * Use field 'file' for the file upload
+ *
+ * @param int $taskId The id of the task
+ * @return DataResponse<Http::STATUS_CREATED, array{fileId: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
+ *
+ * 201: File created
+ * 400: File upload failed or no file was uploaded
+ * 404: Task not found
+ */
+ #[ExAppRequired]
+ #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')]
+ public function setFileContentsExApp(int $taskId): DataResponse {
+ try {
+ $task = $this->taskProcessingManager->getTask($taskId);
+ $file = $this->request->getUploadedFile('file');
+ if (!isset($file['tmp_name'])) {
+ return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST);
+ }
+ $handle = fopen($file['tmp_name'], 'r');
+ if (!$handle) {
+ return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ $fileId = $this->setFileContentsInternal($handle);
+ return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
+ } 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);
+ }
+ }
+
+ /**
* @throws NotPermittedException
* @throws NotFoundException
* @throws GenericFileException
@@ -384,7 +421,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
* Sets the task result
*
* @param int $taskId The id of the task
- * @param array<string,mixed>|null $output The resulting task output
+ * @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
* @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
@@ -396,7 +433,7 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
try {
// set result
- $this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output);
+ $this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true);
$task = $this->taskProcessingManager->getTask($taskId);
/** @var CoreTaskProcessingTask $json */
@@ -493,4 +530,20 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
+
+ /**
+ * @param resource $data
+ * @return int
+ * @throws NotPermittedException
+ */
+ private function setFileContentsInternal($data): int {
+ try {
+ $folder = $this->appData->getFolder('TaskProcessing');
+ } catch (\OCP\Files\NotFoundException) {
+ $folder = $this->appData->newFolder('TaskProcessing');
+ }
+ /** @var SimpleFile $file */
+ $file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
+ return $file->getId();
+ }
}
diff --git a/core/openapi-ex_app.json b/core/openapi-ex_app.json
index e0cf06753de..3f5de516172 100644
--- a/core/openapi-ex_app.json
+++ b/core/openapi-ex_app.json
@@ -339,6 +339,201 @@
}
}
},
+ "/ocs/v2.php/taskprocessing/tasks_provider/{taskId}/file": {
+ "post": {
+ "operationId": "task_processing_api-set-file-contents-ex-app",
+ "summary": "Upload a file so it can be referenced in a task result (ExApp route version)",
+ "description": "Use field 'file' for the file upload\nThis 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": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "File created",
+ "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": [
+ "fileId"
+ ],
+ "properties": {
+ "fileId": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "File upload failed or no file was uploaded",
+ "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"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "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}/progress": {
"post": {
"operationId": "task_processing_api-set-progress",
@@ -541,7 +736,7 @@
"output": {
"type": "object",
"nullable": true,
- "description": "The resulting task output",
+ "description": "The resulting task output, files are represented by their IDs",
"additionalProperties": {
"type": "object"
}
diff --git a/core/openapi-full.json b/core/openapi-full.json
index a62e587bf06..290dc462cdc 100644
--- a/core/openapi-full.json
+++ b/core/openapi-full.json
@@ -8597,6 +8597,201 @@
}
}
},
+ "/ocs/v2.php/taskprocessing/tasks_provider/{taskId}/file": {
+ "post": {
+ "operationId": "task_processing_api-set-file-contents-ex-app",
+ "summary": "Upload a file so it can be referenced in a task result (ExApp route version)",
+ "description": "Use field 'file' for the file upload\nThis 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": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "File created",
+ "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": [
+ "fileId"
+ ],
+ "properties": {
+ "fileId": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "File upload failed or no file was uploaded",
+ "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"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "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}/progress": {
"post": {
"operationId": "task_processing_api-set-progress",
@@ -8799,7 +8994,7 @@
"output": {
"type": "object",
"nullable": true,
- "description": "The resulting task output",
+ "description": "The resulting task output, files are represented by their IDs",
"additionalProperties": {
"type": "object"
}
diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php
index f720776a239..d5a09ff2472 100644
--- a/lib/private/TaskProcessing/Manager.php
+++ b/lib/private/TaskProcessing/Manager.php
@@ -18,10 +18,13 @@ use OCP\BackgroundJob\IJobList;
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\Config\IUserMountCache;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\IAppData;
+use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
+use OCP\Files\Node;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
@@ -79,7 +82,7 @@ class Manager implements IManager {
private \OCP\TextProcessing\IManager $textProcessingManager,
private \OCP\TextToImage\IManager $textToImageManager,
private \OCP\SpeechToText\ISpeechToTextManager $speechToTextManager,
- private \OCP\Share\IManager $shareManager,
+ private IUserMountCache $userMountCache,
) {
$this->appData = $appDataFactory->get('core');
}
@@ -456,7 +459,7 @@ class Manager implements IManager {
* @return void
* @throws ValidationException
*/
- private function validateOutput(array $spec, array $io, bool $optional = false): void {
+ private function validateOutputWithFileIds(array $spec, array $io, bool $optional = false): void {
foreach ($spec as $key => $descriptor) {
$type = $descriptor->getShapeType();
if (!isset($io[$key])) {
@@ -466,7 +469,31 @@ class Manager implements IManager {
throw new ValidationException('Missing key: "' . $key . '"');
}
try {
- $type->validateOutput($io[$key]);
+ $type->validateOutputWithFileIds($io[$key]);
+ } catch (ValidationException $e) {
+ throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * @param ShapeDescriptor[] $spec
+ * @param array $io
+ * @param bool $optional
+ * @return void
+ * @throws ValidationException
+ */
+ private function validateOutputWithFileData(array $spec, array $io, bool $optional = false): void {
+ foreach ($spec as $key => $descriptor) {
+ $type = $descriptor->getShapeType();
+ if (!isset($io[$key])) {
+ if ($optional) {
+ continue;
+ }
+ throw new ValidationException('Missing key: "' . $key . '"');
+ }
+ try {
+ $type->validateOutputWithFileData($io[$key]);
} catch (ValidationException $e) {
throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
}
@@ -575,19 +602,8 @@ class Manager implements IManager {
}
}
foreach ($ids as $fileId) {
- $node = $this->rootFolder->getFirstNodeById($fileId);
- if ($node === null) {
- $node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
- if ($node === null) {
- throw new ValidationException('Could not find file ' . $fileId);
- }
- }
- /** @var array{users:array<string,array{node_id:int, node_path: string}>, remote: array<string,array{node_id:int, node_path: string}>, mail: array<string,array{node_id:int, node_path: string}>} $accessList */
- $accessList = $this->shareManager->getAccessList($node, true, true);
- $userIds = array_map(fn ($id) => strval($id), array_keys($accessList['users']));
- if (!in_array($task->getUserId(), $userIds)) {
- throw new UnauthorizedException('User ' . $task->getUserId() . ' does not have access to file ' . $fileId);
- }
+ $this->validateFileId($fileId);
+ $this->validateUserAccessToFile($fileId, $task->getUserId());
}
// remove superfluous keys and set input
$task->setInput($this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape));
@@ -657,7 +673,7 @@ class Manager implements IManager {
return true;
}
- public function setTaskResult(int $id, ?string $error, ?array $result): void {
+ public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): 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) {
@@ -674,11 +690,29 @@ class Manager implements IManager {
$optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
try {
// validate output
- $this->validateOutput($outputShape, $result);
- $this->validateOutput($optionalOutputShape, $result, true);
+ if (!$isUsingFileIds) {
+ $this->validateOutputWithFileData($outputShape, $result);
+ $this->validateOutputWithFileData($optionalOutputShape, $result, true);
+ } else {
+ $this->validateOutputWithFileIds($outputShape, $result);
+ $this->validateOutputWithFileIds($optionalOutputShape, $result, true);
+ }
$output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
// extract raw data and put it in files, replace it with file ids
- $output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
+ if (!$isUsingFileIds) {
+ $output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
+ } else {
+ $this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
+ }
+ // Turn file objects into IDs
+ foreach ($output as $key => $value) {
+ if ($value instanceof Node) {
+ $output[$key] = $value->getId();
+ }
+ if (is_array($value) && $value[0] instanceof Node) {
+ $output[$key] = array_map(fn ($node) => $node->getId(), $value);
+ }
+ }
$task->setOutput($output);
$task->setProgress(1);
$task->setStatus(Task::STATUS_SUCCESSFUL);
@@ -694,7 +728,12 @@ class Manager implements IManager {
$error = 'The task was processed successfully but storing the output in a file failed';
$task->setErrorMessage($error);
$this->logger->error($error, ['exception' => $e]);
-
+ } catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
+ $task->setProgress(1);
+ $task->setStatus(Task::STATUS_FAILED);
+ $error = 'The task was processed successfully but the result file could not be found';
+ $task->setErrorMessage($error);
+ $this->logger->error($error, ['exception' => $e]);
}
}
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
@@ -725,16 +764,13 @@ class Manager implements IManager {
}
/**
- * Takes task input or output data and replaces fileIds with base64 data
+ * Takes task input data and replaces fileIds with File objects
*
* @param string|null $userId
* @param array<array-key, list<numeric|string>|numeric|string> $input
* @param ShapeDescriptor[] ...$specs the specs
* @return array<array-key, list<File|numeric|string>|numeric|string|File>
- * @throws GenericFileException
- * @throws LockedException
- * @throws NotPermittedException
- * @throws ValidationException
+ * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
*/
public function fillInputFileData(?string $userId, array $input, ...$specs): array {
if ($userId !== null) {
@@ -751,31 +787,17 @@ class Manager implements IManager {
$newInputOutput[$key] = $input[$key];
continue;
}
- if ($type->value < 10) {
- $node = $this->rootFolder->getFirstNodeById((int)$input[$key]);
- if ($node === null) {
- $node = $this->rootFolder->getFirstNodeByIdInPath((int)$input[$key], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
- if (!$node instanceof File) {
- throw new ValidationException('File id given for key "' . $key . '" is not a file');
- }
- } elseif (!$node instanceof File) {
- throw new ValidationException('File id given for key "' . $key . '" is not a file');
- }
- // TODO: Validate if userId has access to this file
+ if (EShapeType::getScalarType($type) === $type) {
+ // is scalar
+ $node = $this->validateFileId((int)$input[$key]);
+ $this->validateUserAccessToFile($input[$key], $userId);
$newInputOutput[$key] = $node;
} else {
+ // is list
$newInputOutput[$key] = [];
foreach ($input[$key] as $item) {
- $node = $this->rootFolder->getFirstNodeById((int)$item);
- if ($node === null) {
- $node = $this->rootFolder->getFirstNodeByIdInPath((int)$item, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
- if (!$node instanceof File) {
- throw new ValidationException('File id given for key "' . $key . '" is not a file');
- }
- } elseif (!$node instanceof File) {
- throw new ValidationException('File id given for key "' . $key . '" is not a file');
- }
- // TODO: Validate if userId has access to this file
+ $node = $this->validateFileId((int)$item);
+ $this->validateUserAccessToFile($item, $userId);
$newInputOutput[$key][] = $node;
}
}
@@ -843,15 +865,15 @@ class Manager implements IManager {
$newOutput[$key] = $output[$key];
continue;
}
- if ($type->value < 10) {
+ if (EShapeType::getScalarType($type) === $type) {
/** @var SimpleFile $file */
- $file = $folder->newFile((string) rand(0, 10000000), $output[$key]);
+ $file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
} else {
$newOutput = [];
foreach ($output[$key] as $item) {
/** @var SimpleFile $file */
- $file = $folder->newFile((string) rand(0, 10000000), $item);
+ $file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
$newOutput[$key][] = $file->getId();
}
}
@@ -865,7 +887,7 @@ class Manager implements IManager {
* @throws GenericFileException
* @throws LockedException
* @throws NotPermittedException
- * @throws ValidationException
+ * @throws ValidationException|UnauthorizedException
*/
public function prepareInputData(Task $task): array {
$taskTypes = $this->getAvailableTaskTypes();
@@ -898,4 +920,73 @@ class Manager implements IManager {
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
$this->taskMapper->update($taskEntity);
}
+
+ /**
+ * @param array $output
+ * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
+ * @return array
+ * @throws NotPermittedException
+ */
+ private function validateOutputFileIds(array $output, ...$specs): array {
+ $newOutput = [];
+ $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
+ foreach($spec as $key => $descriptor) {
+ $type = $descriptor->getShapeType();
+ if (!isset($output[$key])) {
+ continue;
+ }
+ if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
+ $newOutput[$key] = $output[$key];
+ continue;
+ }
+ if (EShapeType::getScalarType($type) === $type) {
+ // Is scalar file ID
+ $newOutput[$key] = $this->validateFileId($output[$key]);
+ } else {
+ // Is list of file IDs
+ $newOutput = [];
+ foreach ($output[$key] as $item) {
+ $newOutput[$key][] = $this->validateFileId($item);
+ }
+ }
+ }
+ return $newOutput;
+ }
+
+ /**
+ * @param mixed $id
+ * @return File
+ * @throws ValidationException
+ */
+ private function validateFileId(mixed $id): File {
+ $node = $this->rootFolder->getFirstNodeById($id);
+ if ($node === null) {
+ $node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
+ if ($node === null) {
+ throw new ValidationException('Could not find file ' . $id);
+ } elseif (!$node instanceof File) {
+ throw new ValidationException('File with id "' . $id . '" is not a file');
+ }
+ } elseif (!$node instanceof File) {
+ throw new ValidationException('File with id "' . $id . '" is not a file');
+ }
+ return $node;
+ }
+
+ /**
+ * @param mixed $fileId
+ * @param string $userId
+ * @return void
+ * @throws UnauthorizedException
+ */
+ private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
+ if ($userId === null) {
+ throw new UnauthorizedException('User does not have access to file ' . $fileId);
+ }
+ $mounts = $this->userMountCache->getMountsForFileId($fileId);
+ $userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
+ if (!in_array($userId, $userIds)) {
+ throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
+ }
+ }
}
diff --git a/lib/private/TaskProcessing/SynchronousBackgroundJob.php b/lib/private/TaskProcessing/SynchronousBackgroundJob.php
index 7f1ab623190..093882d4c1e 100644
--- a/lib/private/TaskProcessing/SynchronousBackgroundJob.php
+++ b/lib/private/TaskProcessing/SynchronousBackgroundJob.php
@@ -15,6 +15,7 @@ use OCP\Lock\LockedException;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\NotFoundException;
use OCP\TaskProcessing\Exception\ProcessingException;
+use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\ISynchronousProvider;
@@ -54,7 +55,7 @@ class SynchronousBackgroundJob extends QueuedJob {
try {
try {
$input = $this->taskProcessingManager->prepareInputData($task);
- } catch (GenericFileException|NotPermittedException|LockedException|ValidationException $e) {
+ } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
$this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
$this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null);
// Schedule again
diff --git a/lib/public/TaskProcessing/EShapeType.php b/lib/public/TaskProcessing/EShapeType.php
index d66de6e01a8..059f9d0c3c7 100644
--- a/lib/public/TaskProcessing/EShapeType.php
+++ b/lib/public/TaskProcessing/EShapeType.php
@@ -89,7 +89,7 @@ enum EShapeType: int {
* @throws ValidationException
* @since 30.0.0
*/
- public function validateOutput(mixed $value) {
+ public function validateOutputWithFileData(mixed $value): void {
$this->validateNonFileType($value);
if ($this === EShapeType::Image && !is_string($value)) {
throw new ValidationException('Non-image item provided for Image slot');
@@ -118,6 +118,40 @@ enum EShapeType: int {
}
/**
+ * @param mixed $value
+ * @return void
+ * @throws ValidationException
+ * @since 30.0.0
+ */
+ public function validateOutputWithFileIds(mixed $value): void {
+ $this->validateNonFileType($value);
+ if ($this === EShapeType::Image && !is_numeric($value)) {
+ throw new ValidationException('Non-image item provided for Image slot');
+ }
+ if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_numeric($item))) > 0)) {
+ throw new ValidationException('Non-image list item provided for ListOfImages slot');
+ }
+ if ($this === EShapeType::Audio && !is_string($value)) {
+ throw new ValidationException('Non-audio item provided for Audio slot');
+ }
+ if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_numeric($item))) > 0)) {
+ throw new ValidationException('Non-audio list item provided for ListOfAudio slot');
+ }
+ if ($this === EShapeType::Video && !is_string($value)) {
+ throw new ValidationException('Non-video item provided for Video slot');
+ }
+ if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_numeric($item))) > 0)) {
+ throw new ValidationException('Non-video list item provided for ListOfTexts slot');
+ }
+ if ($this === EShapeType::File && !is_string($value)) {
+ throw new ValidationException('Non-file item provided for File slot');
+ }
+ if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_numeric($item))) > 0)) {
+ throw new ValidationException('Non-audio list item provided for ListOfFiles slot');
+ }
+ }
+
+ /**
* @param EShapeType $type
* @return EShapeType
* @since 30.0.0
diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php
index 599bd244d8a..c68ad1afbac 100644
--- a/lib/public/TaskProcessing/IManager.php
+++ b/lib/public/TaskProcessing/IManager.php
@@ -91,11 +91,12 @@ interface IManager {
* @param int $id The id of the task
* @param string|null $error
* @param array|null $result
+ * @param bool $isUsingFileIds
* @throws Exception If the query failed
* @throws NotFoundException If the task could not be found
* @since 30.0.0
*/
- public function setTaskResult(int $id, ?string $error, ?array $result): void;
+ public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void;
/**
* @param int $id
@@ -152,7 +153,7 @@ interface IManager {
/**
* Prepare the task's input data, so it can be processed by the provider
- * ie. this replaces file ids with base64 data
+ * ie. this replaces file ids with File objects
*
* @param Task $task
* @return array<array-key, list<numeric|string|File>|numeric|string|File>
@@ -160,6 +161,7 @@ interface IManager {
* @throws GenericFileException
* @throws LockedException
* @throws ValidationException
+ * @throws UnauthorizedException
* @since 30.0.0
*/
public function prepareInputData(Task $task): array;
diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php
index a1857fad1b3..27f46bed17c 100644
--- a/tests/lib/TaskProcessing/TaskProcessingTest.php
+++ b/tests/lib/TaskProcessing/TaskProcessingTest.php
@@ -16,11 +16,13 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\AppData\IAppDataFactory;
-use OCP\Files\IAppData;
+use OCP\Files\Config\ICachedMountInfo;
+use OCP\Files\Config\IUserMountCache;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IServerContainer;
+use OCP\IUser;
use OCP\IUserManager;
use OCP\SpeechToText\ISpeechToTextManager;
use OCP\TaskProcessing\EShapeType;
@@ -295,8 +297,7 @@ class TaskProcessingTest extends \Test\TestCase {
private RegistrationContext $registrationContext;
private TaskMapper $taskMapper;
private IJobList $jobList;
- private IAppData $appData;
- private \OCP\Share\IManager $shareManager;
+ private IUserMountCache $userMountCache;
private IRootFolder $rootFolder;
public const TEST_USER = 'testuser';
@@ -370,7 +371,7 @@ class TaskProcessingTest extends \Test\TestCase {
\OC::$server->get(IAppDataFactory::class),
);
- $this->shareManager = $this->createMock(\OCP\Share\IManager::class);
+ $this->userMountCache = $this->createMock(IUserMountCache::class);
$this->manager = new Manager(
\OC::$server->get(IConfig::class),
@@ -385,7 +386,7 @@ class TaskProcessingTest extends \Test\TestCase {
$textProcessingManager,
$text2imageManager,
\OC::$server->get(ISpeechToTextManager::class),
- $this->shareManager,
+ $this->userMountCache,
);
}
@@ -416,17 +417,21 @@ class TaskProcessingTest extends \Test\TestCase {
}
public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation() {
- $this->shareManager->expects($this->any())->method('getAccessList')->willReturn(['users' => []]);
$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
new ServiceRegistration('test', AudioToImage::class)
]);
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
new ServiceRegistration('test', AsyncProvider::class)
]);
- $this->shareManager->expects($this->any())->method('getAccessList')->willReturn(['users' => [null]]);
- self::assertCount(1, $this->manager->getAvailableTaskTypes());
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())->method('getUID')->willReturn(null);
+ $mount = $this->createMock(ICachedMountInfo::class);
+ $mount->expects($this->any())->method('getUser')->willReturn($user);
+ $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
+ self::assertCount(1, $this->manager->getAvailableTaskTypes());
self::assertTrue($this->manager->hasProviders());
+
$audioId = $this->getFile('audioInput', 'Hello')->getId();
$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
self::assertNull($task->getId());
@@ -537,14 +542,20 @@ class TaskProcessingTest extends \Test\TestCase {
self::assertEquals(1, $task->getProgress());
}
- public function testAsyncProviderWithFilesShouldBeRegisteredAndRun() {
+ public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData() {
$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
new ServiceRegistration('test', AudioToImage::class)
]);
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
new ServiceRegistration('test', AsyncProvider::class)
]);
- $this->shareManager->expects($this->any())->method('getAccessList')->willReturn(['users' => ['testuser' => 1]]);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())->method('getUID')->willReturn('testuser');
+ $mount = $this->createMock(ICachedMountInfo::class);
+ $mount->expects($this->any())->method('getUser')->willReturn($user);
+ $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
+
self::assertCount(1, $this->manager->getAvailableTaskTypes());
self::assertTrue($this->manager->hasProviders());
@@ -583,6 +594,58 @@ class TaskProcessingTest extends \Test\TestCase {
self::assertEquals('World', $node->getContent());
}
+ public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds() {
+ $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
+ new ServiceRegistration('test', AudioToImage::class)
+ ]);
+ $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
+ new ServiceRegistration('test', AsyncProvider::class)
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())->method('getUID')->willReturn('testuser');
+ $mount = $this->createMock(ICachedMountInfo::class);
+ $mount->expects($this->any())->method('getUser')->willReturn($user);
+ $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
+ self::assertCount(1, $this->manager->getAvailableTaskTypes());
+
+ self::assertTrue($this->manager->hasProviders());
+ $audioId = $this->getFile('audioInput', 'Hello')->getId();
+ $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
+ self::assertNull($task->getId());
+ self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
+ $this->manager->scheduleTask($task);
+ self::assertNotNull($task->getId());
+ self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
+
+ // Task object retrieved from db is up-to-date
+ $task2 = $this->manager->getTask($task->getId());
+ self::assertEquals($task->getId(), $task2->getId());
+ self::assertEquals(['audio' => $audioId], $task2->getInput());
+ self::assertNull($task2->getOutput());
+ self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
+
+ $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
+
+ $this->manager->setTaskProgress($task2->getId(), 0.1);
+ $input = $this->manager->prepareInputData($task2);
+ self::assertTrue(isset($input['audio']));
+ self::assertInstanceOf(\OCP\Files\File::class, $input['audio']);
+ self::assertEquals($audioId, $input['audio']->getId());
+
+ $outputFileId = $this->getFile('audioOutput', 'World')->getId();
+
+ $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
+
+ $task = $this->manager->getTask($task->getId());
+ self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
+ self::assertEquals(1, $task->getProgress());
+ self::assertTrue(isset($task->getOutput()['spectrogram']));
+ $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
+ self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
+ self::assertInstanceOf(\OCP\Files\File::class, $node);
+ self::assertEquals('World', $node->getContent());
+ }
+
public function testNonexistentTask() {
$this->expectException(\OCP\TaskProcessing\Exception\NotFoundException::class);
$this->manager->getTask(2147483646);