Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions core/Controller/TaskProcessingApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\FileShaped;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\ShapeEnumValue;
use OCP\TaskProcessing\Task;
Expand Down Expand Up @@ -534,7 +535,8 @@ public function setFileContentsExApp(int $taskId): DataResponse {
if (!$handle) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$fileId = $this->setFileContentsInternal($handle);
$ext = pathinfo(($file['name'] ?? ''), PATHINFO_EXTENSION);
$fileId = $this->setFileContentsInternal($handle, $ext);
return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
Expand Down Expand Up @@ -854,14 +856,15 @@ public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds
* @return int
* @throws NotPermittedException
*/
private function setFileContentsInternal($data): int {
private function setFileContentsInternal($data, string $ext = ''): int {
try {
$folder = $this->appData->getFolder('TaskProcessing');
} catch (\OCP\Files\NotFoundException) {
$folder = $this->appData->newFolder('TaskProcessing');
}
$ext = FileShaped::sanitizeExtension($ext);
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
return $file->getId();
Comment thread
marcelklehr marked this conversation as resolved.
}

Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => $baseDir . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => $baseDir . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\FileShaped' => $baseDir . '/lib/public/TaskProcessing/FileShaped.php',
'OCP\\TaskProcessing\\IInternalTaskType' => $baseDir . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => $baseDir . '/lib/public/TaskProcessing/IManager.php',
'OCP\\TaskProcessing\\IProvider' => $baseDir . '/lib/public/TaskProcessing/IProvider.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\FileShaped' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/FileShaped.php',
'OCP\\TaskProcessing\\IInternalTaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IManager.php',
'OCP\\TaskProcessing\\IProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IProvider.php',
Expand Down
23 changes: 19 additions & 4 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\UserFacingProcessingException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\FileShaped;
use OCP\TaskProcessing\IInternalTaskType;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\IProvider;
Expand Down Expand Up @@ -1603,14 +1604,28 @@ public function encapsulateOutputFileData(array $output, ...$specs): array {
continue;
}
if (EShapeType::getScalarType($type) === $type) {
if ($output[$key] instanceof FileShaped) {
$data = $output[$key]->getData();
$ext = FileShaped::sanitizeExtension($output[$key]->getExtension());
} else {
$data = $output[$key];
$ext = '';
}
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
Comment thread
marcelklehr marked this conversation as resolved.
$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
Comment thread
marcelklehr marked this conversation as resolved.
} else {
$newOutput = [];
$newOutput[$key] = [];
foreach ($output[$key] as $item) {
if ($item instanceof FileShaped) {
$data = $item->getData();
$ext = FileShaped::sanitizeExtension($item->getExtension());
} else {
$data = $item;
$ext = '';
}
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
$newOutput[$key][] = $file->getId();
}
}
Expand Down Expand Up @@ -1757,7 +1772,7 @@ private function validateOutputFileIds(array $output, ...$specs): array {
$newOutput[$key] = $this->validateFileId($output[$key]);
} else {
// Is list of file IDs
$newOutput = [];
$newOutput[$key] = [];
foreach ($output[$key] as $item) {
$newOutput[$key][] = $this->validateFileId($item);
}
Expand Down
24 changes: 13 additions & 11 deletions lib/public/TaskProcessing/EShapeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;
use OCP\TaskProcessing\Exception\ValidationException;

/**
* The input and output Shape types
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
enum EShapeType: int {
/**
* @since 30.0.0
Expand Down Expand Up @@ -146,7 +148,7 @@ public function validateInput(mixed $value): void {
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');
throw new ValidationException('Non-file list item provided for ListOfFiles slot');
}
}

Expand All @@ -156,29 +158,29 @@ public function validateInput(mixed $value): void {
*/
public function validateOutputWithFileData(mixed $value): void {
$this->validateNonFileType($value);
if ($this === EShapeType::Image && !is_string($value)) {
if ($this === EShapeType::Image && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Image)) {
throw new ValidationException('Non-image item provided for Image slot');
}
if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Image))) > 0)) {
Comment thread
marcelklehr marked this conversation as resolved.
throw new ValidationException('Non-image list item provided for ListOfImages slot');
}
if ($this === EShapeType::Audio && !is_string($value)) {
if ($this === EShapeType::Audio && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Audio)) {
throw new ValidationException('Non-audio item provided for Audio slot');
}
if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Audio))) > 0)) {
throw new ValidationException('Non-audio list item provided for ListOfAudio slot');
}
if ($this === EShapeType::Video && !is_string($value)) {
if ($this === EShapeType::Video && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Video)) {
throw new ValidationException('Non-video item provided for Video slot');
}
if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
throw new ValidationException('Non-video list item provided for ListOfTexts slot');
if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Video))) > 0)) {
throw new ValidationException('Non-video list item provided for ListOfVideos slot');
}
if ($this === EShapeType::File && !is_string($value)) {
if ($this === EShapeType::File && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::File)) {
throw new ValidationException('Non-file item provided for File slot');
}
if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
throw new ValidationException('Non-audio list item provided for ListOfFiles slot');
if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::File))) > 0)) {
throw new ValidationException('Non-file list item provided for ListOfFiles slot');
}
}

Expand Down
69 changes: 69 additions & 0 deletions lib/public/TaskProcessing/FileShaped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for file-shaped output entries
*
* @since 35.0.0
*/
#[Consumable(since: '35.0.0')]
Comment thread
marcelklehr marked this conversation as resolved.
final class FileShaped {
/**
* @param EShapeType $shapeType
* @param string $data
* @param string $extension (optional)
*
* @since 35.0.0
*/
public function __construct(
private EShapeType $shapeType,
private string $data,
private string $extension = '',
) {
$this->extension = self::sanitizeExtension($this->extension);
}

/**
* @return string
* @since 35.0.0
*/
public function getData(): string {
return $this->data;
}

/**
* @since 35.0.0
*/
public function getShapeType(): EShapeType {
return $this->shapeType;
}

/**
* @since 35.0.0
*/
public function getExtension(): string {
return $this->extension;
}

/**
* @since 35.0.0
*/
public static function sanitizeExtension(string $ext): string {
Comment thread
marcelklehr marked this conversation as resolved.
if ($ext === '' || strtolower($ext) === 'php' || strtolower($ext) === 'htaccess' || strtolower($ext) === 'phar') {
return '';
}
$ext = str_replace(['.', '/'], '', $ext);
Comment thread
marcelklehr marked this conversation as resolved.
Comment thread
marcelklehr marked this conversation as resolved.
$ext = preg_replace('/[^A-Za-z0-9]/', '', $ext) ?? $ext;
$ext = strtolower($ext);
$ext = substr($ext, 0, 16);
return $ext;
}
}
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/IInternalTaskType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is a task type interface that is implemented by task processing
* task types that should not show up in the assistant UI
*
* @since 33.0.0
*/
#[Implementable(since: '33.0.0')]
interface IInternalTaskType extends ITaskType {

}
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/IProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is the interface that is implemented by apps that
* implement a task processing provider
*
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface IProvider {
/**
* The unique id of this provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;
use OCP\Files\File;
use OCP\TaskProcessing\Exception\ProcessingException;

Expand All @@ -17,6 +18,7 @@
* implement a task processing provider
* @since 35.0.0
*/
#[Implementable(since: '35.0.0')]
interface ISynchronousOptionsAwareProvider extends ISynchronousProvider {

/**
Expand All @@ -26,7 +28,7 @@ interface ISynchronousOptionsAwareProvider extends ISynchronousProvider {
* @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input
* @param callable(float):bool $reportProgress Report the task progress. If this returns false, that means the task was cancelled and processing should be stopped.
* @param SynchronousProviderOptions $options The task options
* @psalm-return array<string, list<numeric|string>|numeric|string>
* @psalm-return array<string, list<numeric|string|FileShaped>|numeric|string|FileShaped>
* @throws ProcessingException
* @since 35.0.0
*/
Expand Down
5 changes: 4 additions & 1 deletion lib/public/TaskProcessing/ISynchronousProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;
use OCP\Files\File;
use OCP\TaskProcessing\Exception\ProcessingException;

Expand All @@ -17,6 +18,7 @@
* implement a task processing provider
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface ISynchronousProvider extends IProvider {

/**
Expand All @@ -25,9 +27,10 @@ interface ISynchronousProvider extends IProvider {
* @param null|string $userId The user that created the current task
* @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input
* @param callable(float):bool $reportProgress Report the task progress. If this returns false, that means the task was cancelled and processing should be stopped.
* @psalm-return array<string, list<numeric|string>|numeric|string>
* @psalm-return array<string, list<numeric|string|FileShaped>|numeric|string|FileShaped>
Comment thread
marcelklehr marked this conversation as resolved.
* @throws ProcessingException
* @since 30.0.0
* @since 35.0.0 You can now also return a FileShaped object or a list of FileShaped objects
*/
public function process(?string $userId, array $input, callable $reportProgress): array;
}
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ITaskType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is a task type interface that is implemented by task processing
* task types
*
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface ITaskType {
/**
* Returns the unique id of this task type
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ITriggerableProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is the interface that is implemented by apps that
* implement a task processing provider with support for being triggered
*
* @since 33.0.0
*/
#[Implementable(since: '33.0.0')]
interface ITriggerableProvider extends IProvider {

/**
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ShapeDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for input output shape entries
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
class ShapeDescriptor implements \JsonSerializable {
/**
* @param string $name
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ShapeEnumValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for input output shape enum slot value
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
class ShapeEnumValue implements \JsonSerializable {
/**
* @param string $name
Expand Down
3 changes: 3 additions & 0 deletions lib/public/TaskProcessing/SynchronousProviderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* @since 35.0.0
*/
#[Consumable(since: '35.0.0')]
class SynchronousProviderOptions {
private \Closure $reportIntermediateOutput;

Expand Down
Loading
Loading