Completed
Push — master ( c1b3b3...c8a12a )
by Marcel
29:41
created
core/Controller/TaskProcessingApiController.php 1 patch
Indentation   +566 added lines, -566 removed lines patch added patch discarded remove patch
@@ -45,570 +45,570 @@
 block discarded – undo
45 45
  * @psalm-import-type CoreTaskProcessingTaskType from ResponseDefinitions
46 46
  */
47 47
 class TaskProcessingApiController extends OCSController {
48
-	public function __construct(
49
-		string $appName,
50
-		IRequest $request,
51
-		private IManager $taskProcessingManager,
52
-		private IL10N $l,
53
-		private ?string $userId,
54
-		private IRootFolder $rootFolder,
55
-		private IAppData $appData,
56
-		private IMimeTypeDetector $mimeTypeDetector,
57
-	) {
58
-		parent::__construct($appName, $request);
59
-	}
60
-
61
-	/**
62
-	 * Returns all available TaskProcessing task types
63
-	 *
64
-	 * @return DataResponse<Http::STATUS_OK, array{types: array<string, CoreTaskProcessingTaskType>}, array{}>
65
-	 *
66
-	 * 200: Task types returned
67
-	 */
68
-	#[NoAdminRequired]
69
-	#[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')]
70
-	public function taskTypes(): DataResponse {
71
-		/** @var array<string, CoreTaskProcessingTaskType> $taskTypes */
72
-		$taskTypes = array_map(function (array $tt) {
73
-			$tt['inputShape'] = array_map(function ($descriptor) {
74
-				return $descriptor->jsonSerialize();
75
-			}, $tt['inputShape']);
76
-			if (empty($tt['inputShape'])) {
77
-				$tt['inputShape'] = new stdClass;
78
-			}
79
-
80
-			$tt['outputShape'] = array_map(function ($descriptor) {
81
-				return $descriptor->jsonSerialize();
82
-			}, $tt['outputShape']);
83
-			if (empty($tt['outputShape'])) {
84
-				$tt['outputShape'] = new stdClass;
85
-			}
86
-
87
-			$tt['optionalInputShape'] = array_map(function ($descriptor) {
88
-				return $descriptor->jsonSerialize();
89
-			}, $tt['optionalInputShape']);
90
-			if (empty($tt['optionalInputShape'])) {
91
-				$tt['optionalInputShape'] = new stdClass;
92
-			}
93
-
94
-			$tt['optionalOutputShape'] = array_map(function ($descriptor) {
95
-				return $descriptor->jsonSerialize();
96
-			}, $tt['optionalOutputShape']);
97
-			if (empty($tt['optionalOutputShape'])) {
98
-				$tt['optionalOutputShape'] = new stdClass;
99
-			}
100
-
101
-			$tt['inputShapeEnumValues'] = array_map(function (array $enumValues) {
102
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
103
-			}, $tt['inputShapeEnumValues']);
104
-			if (empty($tt['inputShapeEnumValues'])) {
105
-				$tt['inputShapeEnumValues'] = new stdClass;
106
-			}
107
-
108
-			$tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) {
109
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
110
-			}, $tt['optionalInputShapeEnumValues']);
111
-			if (empty($tt['optionalInputShapeEnumValues'])) {
112
-				$tt['optionalInputShapeEnumValues'] = new stdClass;
113
-			}
114
-
115
-			$tt['outputShapeEnumValues'] = array_map(function (array $enumValues) {
116
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
117
-			}, $tt['outputShapeEnumValues']);
118
-			if (empty($tt['outputShapeEnumValues'])) {
119
-				$tt['outputShapeEnumValues'] = new stdClass;
120
-			}
121
-
122
-			$tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) {
123
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
124
-			}, $tt['optionalOutputShapeEnumValues']);
125
-			if (empty($tt['optionalOutputShapeEnumValues'])) {
126
-				$tt['optionalOutputShapeEnumValues'] = new stdClass;
127
-			}
128
-
129
-			if (empty($tt['inputShapeDefaults'])) {
130
-				$tt['inputShapeDefaults'] = new stdClass;
131
-			}
132
-			if (empty($tt['optionalInputShapeDefaults'])) {
133
-				$tt['optionalInputShapeDefaults'] = new stdClass;
134
-			}
135
-			return $tt;
136
-		}, $this->taskProcessingManager->getAvailableTaskTypes());
137
-		return new DataResponse([
138
-			'types' => $taskTypes,
139
-		]);
140
-	}
141
-
142
-	/**
143
-	 * Schedules a task
144
-	 *
145
-	 * @param array<string, mixed> $input Task's input parameters
146
-	 * @param string $type Type of the task
147
-	 * @param string $appId ID of the app that will execute the task
148
-	 * @param string $customId An arbitrary identifier for the task
149
-	 * @param string|null $webhookUri URI to be requested when the task finishes
150
-	 * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)
151
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
152
-	 *
153
-	 * 200: Task scheduled successfully
154
-	 * 400: Scheduling task is not possible
155
-	 * 412: Scheduling task is not possible
156
-	 * 401: Cannot schedule task because it references files in its input that the user doesn't have access to
157
-	 */
158
-	#[UserRateLimit(limit: 20, period: 120)]
159
-	#[NoAdminRequired]
160
-	#[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')]
161
-	public function schedule(
162
-		array $input, string $type, string $appId, string $customId = '',
163
-		?string $webhookUri = null, ?string $webhookMethod = null,
164
-	): DataResponse {
165
-		$task = new Task($type, $input, $appId, $this->userId, $customId);
166
-		$task->setWebhookUri($webhookUri);
167
-		$task->setWebhookMethod($webhookMethod);
168
-		try {
169
-			$this->taskProcessingManager->scheduleTask($task);
170
-
171
-			/** @var CoreTaskProcessingTask $json */
172
-			$json = $task->jsonSerialize();
173
-
174
-			return new DataResponse([
175
-				'task' => $json,
176
-			]);
177
-		} catch (PreConditionNotMetException) {
178
-			return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED);
179
-		} catch (ValidationException $e) {
180
-			return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
181
-		} catch (UnauthorizedException) {
182
-			return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED);
183
-		} catch (Exception) {
184
-			return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
185
-		}
186
-	}
187
-
188
-	/**
189
-	 * Gets a task including status and result
190
-	 *
191
-	 * Tasks are removed 1 week after receiving their last update
192
-	 *
193
-	 * @param int $id The id of the task
194
-	 *
195
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
196
-	 *
197
-	 * 200: Task returned
198
-	 * 404: Task not found
199
-	 */
200
-	#[NoAdminRequired]
201
-	#[ApiRoute(verb: 'GET', url: '/task/{id}', root: '/taskprocessing')]
202
-	public function getTask(int $id): DataResponse {
203
-		try {
204
-			$task = $this->taskProcessingManager->getUserTask($id, $this->userId);
205
-
206
-			/** @var CoreTaskProcessingTask $json */
207
-			$json = $task->jsonSerialize();
208
-
209
-			return new DataResponse([
210
-				'task' => $json,
211
-			]);
212
-		} catch (NotFoundException) {
213
-			return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
214
-		} catch (RuntimeException) {
215
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
216
-		}
217
-	}
218
-
219
-	/**
220
-	 * Deletes a task
221
-	 *
222
-	 * @param int $id The id of the task
223
-	 *
224
-	 * @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
225
-	 *
226
-	 * 200: Task deleted
227
-	 */
228
-	#[NoAdminRequired]
229
-	#[ApiRoute(verb: 'DELETE', url: '/task/{id}', root: '/taskprocessing')]
230
-	public function deleteTask(int $id): DataResponse {
231
-		try {
232
-			$task = $this->taskProcessingManager->getUserTask($id, $this->userId);
233
-
234
-			$this->taskProcessingManager->deleteTask($task);
235
-
236
-			return new DataResponse(null);
237
-		} catch (NotFoundException) {
238
-			return new DataResponse(null);
239
-		} catch (Exception) {
240
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
241
-		}
242
-	}
243
-
244
-
245
-	/**
246
-	 * Returns tasks for the current user filtered by the appId and optional customId
247
-	 *
248
-	 * @param string $appId ID of the app
249
-	 * @param string|null $customId An arbitrary identifier for the task
250
-	 * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
251
-	 *
252
-	 * 200: Tasks returned
253
-	 */
254
-	#[NoAdminRequired]
255
-	#[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/taskprocessing')]
256
-	public function listTasksByApp(string $appId, ?string $customId = null): DataResponse {
257
-		try {
258
-			$tasks = $this->taskProcessingManager->getUserTasksByApp($this->userId, $appId, $customId);
259
-			$json = array_map(static function (Task $task) {
260
-				return $task->jsonSerialize();
261
-			}, $tasks);
262
-
263
-			return new DataResponse([
264
-				'tasks' => $json,
265
-			]);
266
-		} catch (Exception) {
267
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
268
-		}
269
-	}
270
-
271
-	/**
272
-	 * Returns tasks for the current user filtered by the optional taskType and optional customId
273
-	 *
274
-	 * @param string|null $taskType The task type to filter by
275
-	 * @param string|null $customId An arbitrary identifier for the task
276
-	 * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
277
-	 *
278
-	 * 200: Tasks returned
279
-	 */
280
-	#[NoAdminRequired]
281
-	#[ApiRoute(verb: 'GET', url: '/tasks', root: '/taskprocessing')]
282
-	public function listTasks(?string $taskType, ?string $customId = null): DataResponse {
283
-		try {
284
-			$tasks = $this->taskProcessingManager->getUserTasks($this->userId, $taskType, $customId);
285
-			$json = array_map(static function (Task $task) {
286
-				return $task->jsonSerialize();
287
-			}, $tasks);
288
-
289
-			return new DataResponse([
290
-				'tasks' => $json,
291
-			]);
292
-		} catch (Exception) {
293
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
294
-		}
295
-	}
296
-
297
-	/**
298
-	 * Returns the contents of a file referenced in a task
299
-	 *
300
-	 * @param int $taskId The id of the task
301
-	 * @param int $fileId The file id of the file to retrieve
302
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
303
-	 *
304
-	 * 200: File content returned
305
-	 * 404: Task or file not found
306
-	 */
307
-	#[NoAdminRequired]
308
-	#[NoCSRFRequired]
309
-	#[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')]
310
-	public function getFileContents(int $taskId, int $fileId): StreamResponse|DataResponse {
311
-		try {
312
-			$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
313
-			return $this->getFileContentsInternal($task, $fileId);
314
-		} catch (NotFoundException) {
315
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
316
-		} catch (LockedException) {
317
-			return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
318
-		} catch (Exception) {
319
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
320
-		}
321
-	}
322
-
323
-	/**
324
-	 * Returns the contents of a file referenced in a task(ExApp route version)
325
-	 *
326
-	 * @param int $taskId The id of the task
327
-	 * @param int $fileId The file id of the file to retrieve
328
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
329
-	 *
330
-	 * 200: File content returned
331
-	 * 404: Task or file not found
332
-	 */
333
-	#[ExAppRequired]
334
-	#[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')]
335
-	public function getFileContentsExApp(int $taskId, int $fileId): StreamResponse|DataResponse {
336
-		try {
337
-			$task = $this->taskProcessingManager->getTask($taskId);
338
-			return $this->getFileContentsInternal($task, $fileId);
339
-		} catch (NotFoundException) {
340
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
341
-		} catch (LockedException) {
342
-			return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
343
-		} catch (Exception) {
344
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
345
-		}
346
-	}
347
-
348
-	/**
349
-	 * Upload a file so it can be referenced in a task result (ExApp route version)
350
-	 *
351
-	 * Use field 'file' for the file upload
352
-	 *
353
-	 * @param int $taskId The id of the task
354
-	 * @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{}>
355
-	 *
356
-	 * 201: File created
357
-	 * 400: File upload failed or no file was uploaded
358
-	 * 404: Task not found
359
-	 */
360
-	#[ExAppRequired]
361
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')]
362
-	public function setFileContentsExApp(int $taskId): DataResponse {
363
-		try {
364
-			$task = $this->taskProcessingManager->getTask($taskId);
365
-			$file = $this->request->getUploadedFile('file');
366
-			if (!isset($file['tmp_name'])) {
367
-				return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST);
368
-			}
369
-			$handle = fopen($file['tmp_name'], 'r');
370
-			if (!$handle) {
371
-				return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
372
-			}
373
-			$fileId = $this->setFileContentsInternal($handle);
374
-			return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
375
-		} catch (NotFoundException) {
376
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
377
-		} catch (Exception) {
378
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
379
-		}
380
-	}
381
-
382
-	/**
383
-	 * @throws NotPermittedException
384
-	 * @throws NotFoundException
385
-	 * @throws LockedException
386
-	 *
387
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
388
-	 */
389
-	private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse {
390
-		$ids = $this->taskProcessingManager->extractFileIdsFromTask($task);
391
-		if (!in_array($fileId, $ids)) {
392
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
393
-		}
394
-		if ($task->getUserId() !== null) {
395
-			\OC_Util::setupFS($task->getUserId());
396
-		}
397
-		$node = $this->rootFolder->getFirstNodeById($fileId);
398
-		if ($node === null) {
399
-			$node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
400
-			if (!$node instanceof File) {
401
-				throw new NotFoundException('Node is not a file');
402
-			}
403
-		} elseif (!$node instanceof File) {
404
-			throw new NotFoundException('Node is not a file');
405
-		}
406
-
407
-		$contentType = $node->getMimeType();
408
-		if (function_exists('mime_content_type')) {
409
-			$mimeType = mime_content_type($node->fopen('rb'));
410
-			if ($mimeType !== false) {
411
-				$mimeType = $this->mimeTypeDetector->getSecureMimeType($mimeType);
412
-				if ($mimeType !== 'application/octet-stream') {
413
-					$contentType = $mimeType;
414
-				}
415
-			}
416
-		}
417
-
418
-		$response = new StreamResponse($node->fopen('rb'));
419
-		$response->addHeader(
420
-			'Content-Disposition',
421
-			'attachment; filename="' . rawurldecode($node->getName()) . '"'
422
-		);
423
-		$response->addHeader('Content-Type', $contentType);
424
-		return $response;
425
-	}
426
-
427
-	/**
428
-	 * Sets the task progress
429
-	 *
430
-	 * @param int $taskId The id of the task
431
-	 * @param float $progress The progress
432
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
433
-	 *
434
-	 * 200: Progress updated successfully
435
-	 * 404: Task not found
436
-	 */
437
-	#[ExAppRequired]
438
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')]
439
-	public function setProgress(int $taskId, float $progress): DataResponse {
440
-		try {
441
-			$this->taskProcessingManager->setTaskProgress($taskId, $progress);
442
-			$task = $this->taskProcessingManager->getTask($taskId);
443
-
444
-			/** @var CoreTaskProcessingTask $json */
445
-			$json = $task->jsonSerialize();
446
-
447
-			return new DataResponse([
448
-				'task' => $json,
449
-			]);
450
-		} catch (NotFoundException) {
451
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
452
-		} catch (Exception) {
453
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
454
-		}
455
-	}
456
-
457
-	/**
458
-	 * Sets the task result
459
-	 *
460
-	 * @param int $taskId The id of the task
461
-	 * @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
462
-	 * @param string|null $errorMessage An error message if the task failed
463
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
464
-	 *
465
-	 * 200: Result updated successfully
466
-	 * 404: Task not found
467
-	 */
468
-	#[ExAppRequired]
469
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
470
-	public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
471
-		try {
472
-			// set result
473
-			$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true);
474
-			$task = $this->taskProcessingManager->getTask($taskId);
475
-
476
-			/** @var CoreTaskProcessingTask $json */
477
-			$json = $task->jsonSerialize();
478
-
479
-			return new DataResponse([
480
-				'task' => $json,
481
-			]);
482
-		} catch (NotFoundException) {
483
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
484
-		} catch (Exception) {
485
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
486
-		}
487
-	}
488
-
489
-	/**
490
-	 * Cancels a task
491
-	 *
492
-	 * @param int $taskId The id of the task
493
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
494
-	 *
495
-	 * 200: Task canceled successfully
496
-	 * 404: Task not found
497
-	 */
498
-	#[NoAdminRequired]
499
-	#[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/cancel', root: '/taskprocessing')]
500
-	public function cancelTask(int $taskId): DataResponse {
501
-		try {
502
-			// Check if the current user can access the task
503
-			$this->taskProcessingManager->getUserTask($taskId, $this->userId);
504
-			// set result
505
-			$this->taskProcessingManager->cancelTask($taskId);
506
-			$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
507
-
508
-			/** @var CoreTaskProcessingTask $json */
509
-			$json = $task->jsonSerialize();
510
-
511
-			return new DataResponse([
512
-				'task' => $json,
513
-			]);
514
-		} catch (NotFoundException) {
515
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
516
-		} catch (Exception) {
517
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
518
-		}
519
-	}
520
-
521
-	/**
522
-	 * Returns the next scheduled task for the taskTypeId
523
-	 *
524
-	 * @param list<string> $providerIds The ids of the providers
525
-	 * @param list<string> $taskTypeIds The ids of the task types
526
-	 * @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{}>
527
-	 *
528
-	 * 200: Task returned
529
-	 * 204: No task found
530
-	 */
531
-	#[ExAppRequired]
532
-	#[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')]
533
-	public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse {
534
-		try {
535
-			$providerIdsBasedOnTaskTypesWithNull = array_unique(array_map(function ($taskTypeId) {
536
-				try {
537
-					return $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
538
-				} catch (Exception) {
539
-					return null;
540
-				}
541
-			}, $taskTypeIds));
542
-
543
-			$providerIdsBasedOnTaskTypes = array_filter($providerIdsBasedOnTaskTypesWithNull, fn ($providerId) => $providerId !== null);
544
-
545
-			// restrict $providerIds to providers that are configured as preferred for the passed task types
546
-			$possibleProviderIds = array_values(array_intersect($providerIdsBasedOnTaskTypes, $providerIds));
547
-
548
-			// restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers
549
-			$possibleTaskTypeIds = array_values(array_filter($taskTypeIds, function ($taskTypeId) use ($possibleProviderIds) {
550
-				try {
551
-					$providerForTaskType = $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
552
-				} catch (Exception) {
553
-					// no provider found for task type
554
-					return false;
555
-				}
556
-				return in_array($providerForTaskType, $possibleProviderIds, true);
557
-			}));
558
-
559
-			if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
560
-				throw new NotFoundException();
561
-			}
562
-
563
-			$taskIdsToIgnore = [];
564
-			while (true) {
565
-				// Until we find a task whose task type is set to be provided by the providers requested with this request
566
-				// Or no scheduled task is found anymore (given the taskIds to ignore)
567
-				$task = $this->taskProcessingManager->getNextScheduledTask($possibleTaskTypeIds, $taskIdsToIgnore);
568
-				try {
569
-					$provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
570
-					if (in_array($provider->getId(), $possibleProviderIds, true)) {
571
-						if ($this->taskProcessingManager->lockTask($task)) {
572
-							break;
573
-						}
574
-					}
575
-				} catch (Exception) {
576
-					// There is no provider set for the task type of this task
577
-					// proceed to ignore this task
578
-				}
579
-
580
-				$taskIdsToIgnore[] = (int)$task->getId();
581
-			}
582
-
583
-			/** @var CoreTaskProcessingTask $json */
584
-			$json = $task->jsonSerialize();
585
-
586
-			return new DataResponse([
587
-				'task' => $json,
588
-				'provider' => [
589
-					'name' => $provider->getId(),
590
-				],
591
-			]);
592
-		} catch (NotFoundException) {
593
-			return new DataResponse(null, Http::STATUS_NO_CONTENT);
594
-		} catch (Exception) {
595
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
596
-		}
597
-	}
598
-
599
-	/**
600
-	 * @param resource $data
601
-	 * @return int
602
-	 * @throws NotPermittedException
603
-	 */
604
-	private function setFileContentsInternal($data): int {
605
-		try {
606
-			$folder = $this->appData->getFolder('TaskProcessing');
607
-		} catch (\OCP\Files\NotFoundException) {
608
-			$folder = $this->appData->newFolder('TaskProcessing');
609
-		}
610
-		/** @var SimpleFile $file */
611
-		$file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
612
-		return $file->getId();
613
-	}
48
+    public function __construct(
49
+        string $appName,
50
+        IRequest $request,
51
+        private IManager $taskProcessingManager,
52
+        private IL10N $l,
53
+        private ?string $userId,
54
+        private IRootFolder $rootFolder,
55
+        private IAppData $appData,
56
+        private IMimeTypeDetector $mimeTypeDetector,
57
+    ) {
58
+        parent::__construct($appName, $request);
59
+    }
60
+
61
+    /**
62
+     * Returns all available TaskProcessing task types
63
+     *
64
+     * @return DataResponse<Http::STATUS_OK, array{types: array<string, CoreTaskProcessingTaskType>}, array{}>
65
+     *
66
+     * 200: Task types returned
67
+     */
68
+    #[NoAdminRequired]
69
+    #[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')]
70
+    public function taskTypes(): DataResponse {
71
+        /** @var array<string, CoreTaskProcessingTaskType> $taskTypes */
72
+        $taskTypes = array_map(function (array $tt) {
73
+            $tt['inputShape'] = array_map(function ($descriptor) {
74
+                return $descriptor->jsonSerialize();
75
+            }, $tt['inputShape']);
76
+            if (empty($tt['inputShape'])) {
77
+                $tt['inputShape'] = new stdClass;
78
+            }
79
+
80
+            $tt['outputShape'] = array_map(function ($descriptor) {
81
+                return $descriptor->jsonSerialize();
82
+            }, $tt['outputShape']);
83
+            if (empty($tt['outputShape'])) {
84
+                $tt['outputShape'] = new stdClass;
85
+            }
86
+
87
+            $tt['optionalInputShape'] = array_map(function ($descriptor) {
88
+                return $descriptor->jsonSerialize();
89
+            }, $tt['optionalInputShape']);
90
+            if (empty($tt['optionalInputShape'])) {
91
+                $tt['optionalInputShape'] = new stdClass;
92
+            }
93
+
94
+            $tt['optionalOutputShape'] = array_map(function ($descriptor) {
95
+                return $descriptor->jsonSerialize();
96
+            }, $tt['optionalOutputShape']);
97
+            if (empty($tt['optionalOutputShape'])) {
98
+                $tt['optionalOutputShape'] = new stdClass;
99
+            }
100
+
101
+            $tt['inputShapeEnumValues'] = array_map(function (array $enumValues) {
102
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
103
+            }, $tt['inputShapeEnumValues']);
104
+            if (empty($tt['inputShapeEnumValues'])) {
105
+                $tt['inputShapeEnumValues'] = new stdClass;
106
+            }
107
+
108
+            $tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) {
109
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
110
+            }, $tt['optionalInputShapeEnumValues']);
111
+            if (empty($tt['optionalInputShapeEnumValues'])) {
112
+                $tt['optionalInputShapeEnumValues'] = new stdClass;
113
+            }
114
+
115
+            $tt['outputShapeEnumValues'] = array_map(function (array $enumValues) {
116
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
117
+            }, $tt['outputShapeEnumValues']);
118
+            if (empty($tt['outputShapeEnumValues'])) {
119
+                $tt['outputShapeEnumValues'] = new stdClass;
120
+            }
121
+
122
+            $tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) {
123
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
124
+            }, $tt['optionalOutputShapeEnumValues']);
125
+            if (empty($tt['optionalOutputShapeEnumValues'])) {
126
+                $tt['optionalOutputShapeEnumValues'] = new stdClass;
127
+            }
128
+
129
+            if (empty($tt['inputShapeDefaults'])) {
130
+                $tt['inputShapeDefaults'] = new stdClass;
131
+            }
132
+            if (empty($tt['optionalInputShapeDefaults'])) {
133
+                $tt['optionalInputShapeDefaults'] = new stdClass;
134
+            }
135
+            return $tt;
136
+        }, $this->taskProcessingManager->getAvailableTaskTypes());
137
+        return new DataResponse([
138
+            'types' => $taskTypes,
139
+        ]);
140
+    }
141
+
142
+    /**
143
+     * Schedules a task
144
+     *
145
+     * @param array<string, mixed> $input Task's input parameters
146
+     * @param string $type Type of the task
147
+     * @param string $appId ID of the app that will execute the task
148
+     * @param string $customId An arbitrary identifier for the task
149
+     * @param string|null $webhookUri URI to be requested when the task finishes
150
+     * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)
151
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
152
+     *
153
+     * 200: Task scheduled successfully
154
+     * 400: Scheduling task is not possible
155
+     * 412: Scheduling task is not possible
156
+     * 401: Cannot schedule task because it references files in its input that the user doesn't have access to
157
+     */
158
+    #[UserRateLimit(limit: 20, period: 120)]
159
+    #[NoAdminRequired]
160
+    #[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')]
161
+    public function schedule(
162
+        array $input, string $type, string $appId, string $customId = '',
163
+        ?string $webhookUri = null, ?string $webhookMethod = null,
164
+    ): DataResponse {
165
+        $task = new Task($type, $input, $appId, $this->userId, $customId);
166
+        $task->setWebhookUri($webhookUri);
167
+        $task->setWebhookMethod($webhookMethod);
168
+        try {
169
+            $this->taskProcessingManager->scheduleTask($task);
170
+
171
+            /** @var CoreTaskProcessingTask $json */
172
+            $json = $task->jsonSerialize();
173
+
174
+            return new DataResponse([
175
+                'task' => $json,
176
+            ]);
177
+        } catch (PreConditionNotMetException) {
178
+            return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED);
179
+        } catch (ValidationException $e) {
180
+            return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
181
+        } catch (UnauthorizedException) {
182
+            return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED);
183
+        } catch (Exception) {
184
+            return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
185
+        }
186
+    }
187
+
188
+    /**
189
+     * Gets a task including status and result
190
+     *
191
+     * Tasks are removed 1 week after receiving their last update
192
+     *
193
+     * @param int $id The id of the task
194
+     *
195
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
196
+     *
197
+     * 200: Task returned
198
+     * 404: Task not found
199
+     */
200
+    #[NoAdminRequired]
201
+    #[ApiRoute(verb: 'GET', url: '/task/{id}', root: '/taskprocessing')]
202
+    public function getTask(int $id): DataResponse {
203
+        try {
204
+            $task = $this->taskProcessingManager->getUserTask($id, $this->userId);
205
+
206
+            /** @var CoreTaskProcessingTask $json */
207
+            $json = $task->jsonSerialize();
208
+
209
+            return new DataResponse([
210
+                'task' => $json,
211
+            ]);
212
+        } catch (NotFoundException) {
213
+            return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
214
+        } catch (RuntimeException) {
215
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
216
+        }
217
+    }
218
+
219
+    /**
220
+     * Deletes a task
221
+     *
222
+     * @param int $id The id of the task
223
+     *
224
+     * @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
225
+     *
226
+     * 200: Task deleted
227
+     */
228
+    #[NoAdminRequired]
229
+    #[ApiRoute(verb: 'DELETE', url: '/task/{id}', root: '/taskprocessing')]
230
+    public function deleteTask(int $id): DataResponse {
231
+        try {
232
+            $task = $this->taskProcessingManager->getUserTask($id, $this->userId);
233
+
234
+            $this->taskProcessingManager->deleteTask($task);
235
+
236
+            return new DataResponse(null);
237
+        } catch (NotFoundException) {
238
+            return new DataResponse(null);
239
+        } catch (Exception) {
240
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
241
+        }
242
+    }
243
+
244
+
245
+    /**
246
+     * Returns tasks for the current user filtered by the appId and optional customId
247
+     *
248
+     * @param string $appId ID of the app
249
+     * @param string|null $customId An arbitrary identifier for the task
250
+     * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
251
+     *
252
+     * 200: Tasks returned
253
+     */
254
+    #[NoAdminRequired]
255
+    #[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/taskprocessing')]
256
+    public function listTasksByApp(string $appId, ?string $customId = null): DataResponse {
257
+        try {
258
+            $tasks = $this->taskProcessingManager->getUserTasksByApp($this->userId, $appId, $customId);
259
+            $json = array_map(static function (Task $task) {
260
+                return $task->jsonSerialize();
261
+            }, $tasks);
262
+
263
+            return new DataResponse([
264
+                'tasks' => $json,
265
+            ]);
266
+        } catch (Exception) {
267
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
268
+        }
269
+    }
270
+
271
+    /**
272
+     * Returns tasks for the current user filtered by the optional taskType and optional customId
273
+     *
274
+     * @param string|null $taskType The task type to filter by
275
+     * @param string|null $customId An arbitrary identifier for the task
276
+     * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
277
+     *
278
+     * 200: Tasks returned
279
+     */
280
+    #[NoAdminRequired]
281
+    #[ApiRoute(verb: 'GET', url: '/tasks', root: '/taskprocessing')]
282
+    public function listTasks(?string $taskType, ?string $customId = null): DataResponse {
283
+        try {
284
+            $tasks = $this->taskProcessingManager->getUserTasks($this->userId, $taskType, $customId);
285
+            $json = array_map(static function (Task $task) {
286
+                return $task->jsonSerialize();
287
+            }, $tasks);
288
+
289
+            return new DataResponse([
290
+                'tasks' => $json,
291
+            ]);
292
+        } catch (Exception) {
293
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
294
+        }
295
+    }
296
+
297
+    /**
298
+     * Returns the contents of a file referenced in a task
299
+     *
300
+     * @param int $taskId The id of the task
301
+     * @param int $fileId The file id of the file to retrieve
302
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
303
+     *
304
+     * 200: File content returned
305
+     * 404: Task or file not found
306
+     */
307
+    #[NoAdminRequired]
308
+    #[NoCSRFRequired]
309
+    #[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')]
310
+    public function getFileContents(int $taskId, int $fileId): StreamResponse|DataResponse {
311
+        try {
312
+            $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
313
+            return $this->getFileContentsInternal($task, $fileId);
314
+        } catch (NotFoundException) {
315
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
316
+        } catch (LockedException) {
317
+            return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
318
+        } catch (Exception) {
319
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
320
+        }
321
+    }
322
+
323
+    /**
324
+     * Returns the contents of a file referenced in a task(ExApp route version)
325
+     *
326
+     * @param int $taskId The id of the task
327
+     * @param int $fileId The file id of the file to retrieve
328
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
329
+     *
330
+     * 200: File content returned
331
+     * 404: Task or file not found
332
+     */
333
+    #[ExAppRequired]
334
+    #[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')]
335
+    public function getFileContentsExApp(int $taskId, int $fileId): StreamResponse|DataResponse {
336
+        try {
337
+            $task = $this->taskProcessingManager->getTask($taskId);
338
+            return $this->getFileContentsInternal($task, $fileId);
339
+        } catch (NotFoundException) {
340
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
341
+        } catch (LockedException) {
342
+            return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
343
+        } catch (Exception) {
344
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Upload a file so it can be referenced in a task result (ExApp route version)
350
+     *
351
+     * Use field 'file' for the file upload
352
+     *
353
+     * @param int $taskId The id of the task
354
+     * @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{}>
355
+     *
356
+     * 201: File created
357
+     * 400: File upload failed or no file was uploaded
358
+     * 404: Task not found
359
+     */
360
+    #[ExAppRequired]
361
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')]
362
+    public function setFileContentsExApp(int $taskId): DataResponse {
363
+        try {
364
+            $task = $this->taskProcessingManager->getTask($taskId);
365
+            $file = $this->request->getUploadedFile('file');
366
+            if (!isset($file['tmp_name'])) {
367
+                return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST);
368
+            }
369
+            $handle = fopen($file['tmp_name'], 'r');
370
+            if (!$handle) {
371
+                return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
372
+            }
373
+            $fileId = $this->setFileContentsInternal($handle);
374
+            return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
375
+        } catch (NotFoundException) {
376
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
377
+        } catch (Exception) {
378
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
379
+        }
380
+    }
381
+
382
+    /**
383
+     * @throws NotPermittedException
384
+     * @throws NotFoundException
385
+     * @throws LockedException
386
+     *
387
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
388
+     */
389
+    private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse {
390
+        $ids = $this->taskProcessingManager->extractFileIdsFromTask($task);
391
+        if (!in_array($fileId, $ids)) {
392
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
393
+        }
394
+        if ($task->getUserId() !== null) {
395
+            \OC_Util::setupFS($task->getUserId());
396
+        }
397
+        $node = $this->rootFolder->getFirstNodeById($fileId);
398
+        if ($node === null) {
399
+            $node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
400
+            if (!$node instanceof File) {
401
+                throw new NotFoundException('Node is not a file');
402
+            }
403
+        } elseif (!$node instanceof File) {
404
+            throw new NotFoundException('Node is not a file');
405
+        }
406
+
407
+        $contentType = $node->getMimeType();
408
+        if (function_exists('mime_content_type')) {
409
+            $mimeType = mime_content_type($node->fopen('rb'));
410
+            if ($mimeType !== false) {
411
+                $mimeType = $this->mimeTypeDetector->getSecureMimeType($mimeType);
412
+                if ($mimeType !== 'application/octet-stream') {
413
+                    $contentType = $mimeType;
414
+                }
415
+            }
416
+        }
417
+
418
+        $response = new StreamResponse($node->fopen('rb'));
419
+        $response->addHeader(
420
+            'Content-Disposition',
421
+            'attachment; filename="' . rawurldecode($node->getName()) . '"'
422
+        );
423
+        $response->addHeader('Content-Type', $contentType);
424
+        return $response;
425
+    }
426
+
427
+    /**
428
+     * Sets the task progress
429
+     *
430
+     * @param int $taskId The id of the task
431
+     * @param float $progress The progress
432
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
433
+     *
434
+     * 200: Progress updated successfully
435
+     * 404: Task not found
436
+     */
437
+    #[ExAppRequired]
438
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')]
439
+    public function setProgress(int $taskId, float $progress): DataResponse {
440
+        try {
441
+            $this->taskProcessingManager->setTaskProgress($taskId, $progress);
442
+            $task = $this->taskProcessingManager->getTask($taskId);
443
+
444
+            /** @var CoreTaskProcessingTask $json */
445
+            $json = $task->jsonSerialize();
446
+
447
+            return new DataResponse([
448
+                'task' => $json,
449
+            ]);
450
+        } catch (NotFoundException) {
451
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
452
+        } catch (Exception) {
453
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
454
+        }
455
+    }
456
+
457
+    /**
458
+     * Sets the task result
459
+     *
460
+     * @param int $taskId The id of the task
461
+     * @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
462
+     * @param string|null $errorMessage An error message if the task failed
463
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
464
+     *
465
+     * 200: Result updated successfully
466
+     * 404: Task not found
467
+     */
468
+    #[ExAppRequired]
469
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
470
+    public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null): DataResponse {
471
+        try {
472
+            // set result
473
+            $this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, true);
474
+            $task = $this->taskProcessingManager->getTask($taskId);
475
+
476
+            /** @var CoreTaskProcessingTask $json */
477
+            $json = $task->jsonSerialize();
478
+
479
+            return new DataResponse([
480
+                'task' => $json,
481
+            ]);
482
+        } catch (NotFoundException) {
483
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
484
+        } catch (Exception) {
485
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
486
+        }
487
+    }
488
+
489
+    /**
490
+     * Cancels a task
491
+     *
492
+     * @param int $taskId The id of the task
493
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
494
+     *
495
+     * 200: Task canceled successfully
496
+     * 404: Task not found
497
+     */
498
+    #[NoAdminRequired]
499
+    #[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/cancel', root: '/taskprocessing')]
500
+    public function cancelTask(int $taskId): DataResponse {
501
+        try {
502
+            // Check if the current user can access the task
503
+            $this->taskProcessingManager->getUserTask($taskId, $this->userId);
504
+            // set result
505
+            $this->taskProcessingManager->cancelTask($taskId);
506
+            $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
507
+
508
+            /** @var CoreTaskProcessingTask $json */
509
+            $json = $task->jsonSerialize();
510
+
511
+            return new DataResponse([
512
+                'task' => $json,
513
+            ]);
514
+        } catch (NotFoundException) {
515
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
516
+        } catch (Exception) {
517
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
518
+        }
519
+    }
520
+
521
+    /**
522
+     * Returns the next scheduled task for the taskTypeId
523
+     *
524
+     * @param list<string> $providerIds The ids of the providers
525
+     * @param list<string> $taskTypeIds The ids of the task types
526
+     * @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{}>
527
+     *
528
+     * 200: Task returned
529
+     * 204: No task found
530
+     */
531
+    #[ExAppRequired]
532
+    #[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')]
533
+    public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse {
534
+        try {
535
+            $providerIdsBasedOnTaskTypesWithNull = array_unique(array_map(function ($taskTypeId) {
536
+                try {
537
+                    return $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
538
+                } catch (Exception) {
539
+                    return null;
540
+                }
541
+            }, $taskTypeIds));
542
+
543
+            $providerIdsBasedOnTaskTypes = array_filter($providerIdsBasedOnTaskTypesWithNull, fn ($providerId) => $providerId !== null);
544
+
545
+            // restrict $providerIds to providers that are configured as preferred for the passed task types
546
+            $possibleProviderIds = array_values(array_intersect($providerIdsBasedOnTaskTypes, $providerIds));
547
+
548
+            // restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers
549
+            $possibleTaskTypeIds = array_values(array_filter($taskTypeIds, function ($taskTypeId) use ($possibleProviderIds) {
550
+                try {
551
+                    $providerForTaskType = $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
552
+                } catch (Exception) {
553
+                    // no provider found for task type
554
+                    return false;
555
+                }
556
+                return in_array($providerForTaskType, $possibleProviderIds, true);
557
+            }));
558
+
559
+            if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
560
+                throw new NotFoundException();
561
+            }
562
+
563
+            $taskIdsToIgnore = [];
564
+            while (true) {
565
+                // Until we find a task whose task type is set to be provided by the providers requested with this request
566
+                // Or no scheduled task is found anymore (given the taskIds to ignore)
567
+                $task = $this->taskProcessingManager->getNextScheduledTask($possibleTaskTypeIds, $taskIdsToIgnore);
568
+                try {
569
+                    $provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
570
+                    if (in_array($provider->getId(), $possibleProviderIds, true)) {
571
+                        if ($this->taskProcessingManager->lockTask($task)) {
572
+                            break;
573
+                        }
574
+                    }
575
+                } catch (Exception) {
576
+                    // There is no provider set for the task type of this task
577
+                    // proceed to ignore this task
578
+                }
579
+
580
+                $taskIdsToIgnore[] = (int)$task->getId();
581
+            }
582
+
583
+            /** @var CoreTaskProcessingTask $json */
584
+            $json = $task->jsonSerialize();
585
+
586
+            return new DataResponse([
587
+                'task' => $json,
588
+                'provider' => [
589
+                    'name' => $provider->getId(),
590
+                ],
591
+            ]);
592
+        } catch (NotFoundException) {
593
+            return new DataResponse(null, Http::STATUS_NO_CONTENT);
594
+        } catch (Exception) {
595
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
596
+        }
597
+    }
598
+
599
+    /**
600
+     * @param resource $data
601
+     * @return int
602
+     * @throws NotPermittedException
603
+     */
604
+    private function setFileContentsInternal($data): int {
605
+        try {
606
+            $folder = $this->appData->getFolder('TaskProcessing');
607
+        } catch (\OCP\Files\NotFoundException) {
608
+            $folder = $this->appData->newFolder('TaskProcessing');
609
+        }
610
+        /** @var SimpleFile $file */
611
+        $file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
612
+        return $file->getId();
613
+    }
614 614
 }
Please login to merge, or discard this patch.