You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

614 lines
23 KiB

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