2024-04-29 10:21:07 -04:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2024-04-29 10:21:07 -04:00
*/
namespace OC\TaskProcessing ;
2024-07-17 06:35:13 -04:00
use GuzzleHttp\Exception\ClientException ;
use GuzzleHttp\Exception\ServerException ;
2024-04-29 10:21:07 -04:00
use OC\AppFramework\Bootstrap\Coordinator ;
2024-04-30 09:48:00 -04:00
use OC\Files\SimpleFS\SimpleFile ;
2024-04-29 10:21:07 -04:00
use OC\TaskProcessing\Db\TaskMapper ;
2024-07-17 06:35:13 -04:00
use OCP\App\IAppManager ;
2024-04-29 10:21:07 -04:00
use OCP\AppFramework\Db\DoesNotExistException ;
use OCP\AppFramework\Db\MultipleObjectsReturnedException ;
use OCP\BackgroundJob\IJobList ;
2024-07-03 10:08:13 -04:00
use OCP\DB\Exception ;
2024-04-29 10:21:07 -04:00
use OCP\EventDispatcher\IEventDispatcher ;
use OCP\Files\AppData\IAppDataFactory ;
2024-07-09 05:43:11 -04:00
use OCP\Files\Config\IUserMountCache ;
2024-04-29 10:21:07 -04:00
use OCP\Files\File ;
use OCP\Files\GenericFileException ;
use OCP\Files\IAppData ;
2024-07-13 05:42:06 -04:00
use OCP\Files\InvalidPathException ;
2024-04-29 10:21:07 -04:00
use OCP\Files\IRootFolder ;
2024-07-09 05:43:11 -04:00
use OCP\Files\Node ;
2024-04-29 10:21:07 -04:00
use OCP\Files\NotPermittedException ;
2024-05-03 08:15:03 -04:00
use OCP\Files\SimpleFS\ISimpleFile ;
2025-08-06 09:34:15 -04:00
use OCP\Files\SimpleFS\ISimpleFolder ;
2024-07-17 06:35:13 -04:00
use OCP\Http\Client\IClientService ;
2025-08-01 05:56:10 -04:00
use OCP\IAppConfig ;
2025-01-23 03:36:29 -05:00
use OCP\ICache ;
use OCP\ICacheFactory ;
2024-05-03 05:46:36 -04:00
use OCP\IL10N ;
2024-04-29 10:21:07 -04:00
use OCP\IServerContainer ;
2025-07-02 12:10:51 -04:00
use OCP\IUserManager ;
use OCP\IUserSession ;
2024-05-03 05:46:36 -04:00
use OCP\L10N\IFactory ;
2024-04-29 10:21:07 -04:00
use OCP\Lock\LockedException ;
use OCP\SpeechToText\ISpeechToTextProvider ;
use OCP\SpeechToText\ISpeechToTextProviderWithId ;
use OCP\TaskProcessing\EShapeType ;
2025-04-08 13:45:37 -04:00
use OCP\TaskProcessing\Events\GetTaskProcessingProvidersEvent ;
2024-04-29 10:21:07 -04:00
use OCP\TaskProcessing\Events\TaskFailedEvent ;
use OCP\TaskProcessing\Events\TaskSuccessfulEvent ;
use OCP\TaskProcessing\Exception\NotFoundException ;
use OCP\TaskProcessing\Exception\ProcessingException ;
2024-05-06 04:03:24 -04:00
use OCP\TaskProcessing\Exception\UnauthorizedException ;
2024-04-29 10:21:07 -04:00
use OCP\TaskProcessing\Exception\ValidationException ;
2025-10-09 05:54:35 -04:00
use OCP\TaskProcessing\IInternalTaskType ;
2024-04-29 10:21:07 -04:00
use OCP\TaskProcessing\IManager ;
use OCP\TaskProcessing\IProvider ;
use OCP\TaskProcessing\ISynchronousProvider ;
use OCP\TaskProcessing\ITaskType ;
2025-10-14 05:01:39 -04:00
use OCP\TaskProcessing\ITriggerableProvider ;
2024-04-29 10:21:07 -04:00
use OCP\TaskProcessing\ShapeDescriptor ;
2024-07-24 09:34:51 -04:00
use OCP\TaskProcessing\ShapeEnumValue ;
2024-04-29 10:21:07 -04:00
use OCP\TaskProcessing\Task ;
use OCP\TaskProcessing\TaskTypes\AudioToText ;
use OCP\TaskProcessing\TaskTypes\TextToImage ;
use OCP\TaskProcessing\TaskTypes\TextToText ;
use OCP\TaskProcessing\TaskTypes\TextToTextHeadline ;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary ;
use OCP\TaskProcessing\TaskTypes\TextToTextTopics ;
2024-07-17 06:35:13 -04:00
use Psr\Container\ContainerExceptionInterface ;
use Psr\Container\NotFoundExceptionInterface ;
2024-04-29 10:21:07 -04:00
use Psr\Log\LoggerInterface ;
class Manager implements IManager {
public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:' ;
public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:' ;
public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:' ;
2025-08-01 09:35:56 -04:00
public const LAZY_CONFIG_KEYS = [
'ai.taskprocessing_type_preferences' ,
'ai.taskprocessing_provider_preferences' ,
];
2025-10-06 10:44:53 -04:00
public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 31 * 6 ; // 6 months
2025-08-06 09:34:15 -04:00
2025-09-17 05:05:28 -04:00
private const TASK_TYPES_CACHE_KEY = 'available_task_types_v3' ;
2025-09-03 11:10:12 -04:00
private const TASK_TYPE_IDS_CACHE_KEY = 'available_task_type_ids' ;
2024-08-23 09:10:27 -04:00
/** @var list<IProvider>|null */
2024-04-29 10:21:07 -04:00
private ? array $providers = null ;
2024-07-24 09:34:51 -04:00
/**
2025-10-09 05:54:35 -04:00
* @ var array < array - key , array { name : string , description : string , inputShape : ShapeDescriptor [], inputShapeEnumValues : ShapeEnumValue [][], inputShapeDefaults : array < array - key , numeric | string > , isInternal : bool , optionalInputShape : ShapeDescriptor [], optionalInputShapeEnumValues : ShapeEnumValue [][], optionalInputShapeDefaults : array < array - key , numeric | string > , outputShape : ShapeDescriptor [], outputShapeEnumValues : ShapeEnumValue [][], optionalOutputShape : ShapeDescriptor [], optionalOutputShapeEnumValues : ShapeEnumValue [][]} >
2024-07-24 09:34:51 -04:00
*/
2024-04-29 10:21:07 -04:00
private ? array $availableTaskTypes = null ;
2025-09-03 11:10:12 -04:00
/** @var list<string>|null */
private ? array $availableTaskTypeIds = null ;
2024-04-29 10:21:07 -04:00
private IAppData $appData ;
2025-01-21 14:05:51 -05:00
private ? array $preferences = null ;
2025-01-24 04:12:56 -05:00
private ? array $providersById = null ;
2025-04-08 13:45:37 -04:00
/** @var ITaskType[]|null */
private ? array $taskTypes = null ;
2025-01-23 04:40:42 -05:00
private ICache $distributedCache ;
2025-01-23 03:36:29 -05:00
2025-04-08 13:45:37 -04:00
private ? GetTaskProcessingProvidersEvent $eventResult = null ;
2024-04-29 10:21:07 -04:00
public function __construct (
2025-08-01 05:56:10 -04:00
private IAppConfig $appConfig ,
2024-07-24 09:43:01 -04:00
private Coordinator $coordinator ,
private IServerContainer $serverContainer ,
private LoggerInterface $logger ,
private TaskMapper $taskMapper ,
private IJobList $jobList ,
private IEventDispatcher $dispatcher ,
IAppDataFactory $appDataFactory ,
private IRootFolder $rootFolder ,
private \OCP\TextToImage\IManager $textToImageManager ,
private IUserMountCache $userMountCache ,
private IClientService $clientService ,
private IAppManager $appManager ,
2025-07-02 12:10:51 -04:00
private IUserManager $userManager ,
private IUserSession $userSession ,
2025-01-23 03:36:29 -05:00
ICacheFactory $cacheFactory ,
2025-09-17 05:05:28 -04:00
private IFactory $l10nFactory ,
2024-07-24 09:43:01 -04:00
) {
$this -> appData = $appDataFactory -> get ( 'core' );
2025-01-23 05:41:39 -05:00
$this -> distributedCache = $cacheFactory -> createDistributed ( 'task_processing::' );
2024-04-29 10:21:07 -04:00
}
2024-08-30 04:01:21 -04:00
/**
* This is almost a copy of textProcessingManager -> getProviders
* to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
*/
2024-08-28 05:50:23 -04:00
private function _getRawTextProcessingProviders () : array {
2024-08-30 04:01:21 -04:00
$context = $this -> coordinator -> getRegistrationContext ();
if ( $context === null ) {
return [];
}
$providers = [];
foreach ( $context -> getTextProcessingProviders () as $providerServiceRegistration ) {
$class = $providerServiceRegistration -> getService ();
try {
$providers [ $class ] = $this -> serverContainer -> get ( $class );
} catch ( \Throwable $e ) {
$this -> logger -> error ( 'Failed to load Text processing provider ' . $class , [
'exception' => $e ,
]);
}
}
return $providers ;
2024-08-28 05:50:23 -04:00
}
2024-04-29 10:21:07 -04:00
private function _getTextProcessingProviders () : array {
2024-08-28 05:50:23 -04:00
$oldProviders = $this -> _getRawTextProcessingProviders ();
2024-04-29 10:21:07 -04:00
$newProviders = [];
foreach ( $oldProviders as $oldProvider ) {
$provider = new class ( $oldProvider ) implements IProvider , ISynchronousProvider {
private \OCP\TextProcessing\IProvider $provider ;
public function __construct ( \OCP\TextProcessing\IProvider $provider ) {
$this -> provider = $provider ;
}
public function getId () : string {
if ( $this -> provider instanceof \OCP\TextProcessing\IProviderWithId ) {
return $this -> provider -> getId ();
}
return Manager :: LEGACY_PREFIX_TEXTPROCESSING . $this -> provider :: class ;
}
public function getName () : string {
return $this -> provider -> getName ();
}
2024-05-03 06:23:59 -04:00
public function getTaskTypeId () : string {
2024-04-29 10:21:07 -04:00
return match ( $this -> provider -> getTaskType ()) {
\OCP\TextProcessing\FreePromptTaskType :: class => TextToText :: ID ,
\OCP\TextProcessing\HeadlineTaskType :: class => TextToTextHeadline :: ID ,
\OCP\TextProcessing\TopicsTaskType :: class => TextToTextTopics :: ID ,
\OCP\TextProcessing\SummaryTaskType :: class => TextToTextSummary :: ID ,
default => Manager :: LEGACY_PREFIX_TEXTPROCESSING . $this -> provider -> getTaskType (),
};
}
public function getExpectedRuntime () : int {
if ( $this -> provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime ) {
return $this -> provider -> getExpectedRuntime ();
}
return 60 ;
}
public function getOptionalInputShape () : array {
return [];
}
public function getOptionalOutputShape () : array {
return [];
}
2024-05-07 07:04:44 -04:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-04-29 10:21:07 -04:00
if ( $this -> provider instanceof \OCP\TextProcessing\IProviderWithUserId ) {
$this -> provider -> setUserId ( $userId );
}
try {
return [ 'output' => $this -> provider -> process ( $input [ 'input' ])];
2024-09-05 15:23:38 -04:00
} catch ( \RuntimeException $e ) {
2024-04-29 10:21:07 -04:00
throw new ProcessingException ( $e -> getMessage (), 0 , $e );
}
}
2024-07-24 09:34:51 -04:00
public function getInputShapeEnumValues () : array {
return [];
}
public function getInputShapeDefaults () : array {
return [];
}
public function getOptionalInputShapeEnumValues () : array {
return [];
}
public function getOptionalInputShapeDefaults () : array {
return [];
}
public function getOutputShapeEnumValues () : array {
return [];
}
public function getOptionalOutputShapeEnumValues () : array {
return [];
}
2024-04-29 10:21:07 -04:00
};
$newProviders [ $provider -> getId ()] = $provider ;
}
return $newProviders ;
}
/**
2024-05-02 09:12:35 -04:00
* @ return ITaskType []
2024-04-29 10:21:07 -04:00
*/
private function _getTextProcessingTaskTypes () : array {
2024-08-28 05:50:23 -04:00
$oldProviders = $this -> _getRawTextProcessingProviders ();
2024-04-29 10:21:07 -04:00
$newTaskTypes = [];
foreach ( $oldProviders as $oldProvider ) {
// These are already implemented in the TaskProcessing realm
if ( in_array ( $oldProvider -> getTaskType (), [
\OCP\TextProcessing\FreePromptTaskType :: class ,
\OCP\TextProcessing\HeadlineTaskType :: class ,
\OCP\TextProcessing\TopicsTaskType :: class ,
\OCP\TextProcessing\SummaryTaskType :: class
], true )) {
continue ;
}
$taskType = new class ( $oldProvider -> getTaskType ()) implements ITaskType {
private string $oldTaskTypeClass ;
private \OCP\TextProcessing\ITaskType $oldTaskType ;
2024-05-03 05:46:36 -04:00
private IL10N $l ;
2024-04-29 10:21:07 -04:00
public function __construct ( string $oldTaskTypeClass ) {
$this -> oldTaskTypeClass = $oldTaskTypeClass ;
$this -> oldTaskType = \OCP\Server :: get ( $oldTaskTypeClass );
2024-05-03 05:46:36 -04:00
$this -> l = \OCP\Server :: get ( IFactory :: class ) -> get ( 'core' );
2024-04-29 10:21:07 -04:00
}
public function getId () : string {
return Manager :: LEGACY_PREFIX_TEXTPROCESSING . $this -> oldTaskTypeClass ;
}
public function getName () : string {
return $this -> oldTaskType -> getName ();
}
public function getDescription () : string {
return $this -> oldTaskType -> getDescription ();
}
public function getInputShape () : array {
2024-05-03 05:46:36 -04:00
return [ 'input' => new ShapeDescriptor ( $this -> l -> t ( 'Input text' ), $this -> l -> t ( 'The input text' ), EShapeType :: Text )];
2024-04-29 10:21:07 -04:00
}
public function getOutputShape () : array {
2024-05-03 05:46:36 -04:00
return [ 'output' => new ShapeDescriptor ( $this -> l -> t ( 'Input text' ), $this -> l -> t ( 'The input text' ), EShapeType :: Text )];
2024-04-29 10:21:07 -04:00
}
};
$newTaskTypes [ $taskType -> getId ()] = $taskType ;
}
return $newTaskTypes ;
}
/**
* @ return IProvider []
*/
private function _getTextToImageProviders () : array {
$oldProviders = $this -> textToImageManager -> getProviders ();
$newProviders = [];
foreach ( $oldProviders as $oldProvider ) {
$newProvider = new class ( $oldProvider , $this -> appData ) implements IProvider , ISynchronousProvider {
private \OCP\TextToImage\IProvider $provider ;
private IAppData $appData ;
public function __construct ( \OCP\TextToImage\IProvider $provider , IAppData $appData ) {
$this -> provider = $provider ;
$this -> appData = $appData ;
}
public function getId () : string {
return Manager :: LEGACY_PREFIX_TEXTTOIMAGE . $this -> provider -> getId ();
}
public function getName () : string {
return $this -> provider -> getName ();
}
2024-05-03 06:23:59 -04:00
public function getTaskTypeId () : string {
2024-04-29 10:21:07 -04:00
return TextToImage :: ID ;
}
public function getExpectedRuntime () : int {
return $this -> provider -> getExpectedRuntime ();
}
public function getOptionalInputShape () : array {
return [];
}
public function getOptionalOutputShape () : array {
return [];
}
2024-05-07 07:04:44 -04:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-04-29 10:21:07 -04:00
try {
$folder = $this -> appData -> getFolder ( 'text2image' );
2024-09-05 15:23:38 -04:00
} catch ( \OCP\Files\NotFoundException ) {
2024-04-29 10:21:07 -04:00
$folder = $this -> appData -> newFolder ( 'text2image' );
}
$resources = [];
$files = [];
for ( $i = 0 ; $i < $input [ 'numberOfImages' ]; $i ++ ) {
2024-09-19 05:10:31 -04:00
$file = $folder -> newFile ( time () . '-' . rand ( 1 , 100000 ) . '-' . $i );
2024-04-29 10:21:07 -04:00
$files [] = $file ;
$resource = $file -> write ();
if ( $resource !== false && $resource !== true && is_resource ( $resource )) {
$resources [] = $resource ;
} else {
throw new ProcessingException ( 'Text2Image generation using provider "' . $this -> getName () . '" failed: Couldn\'t open file to write.' );
}
}
if ( $this -> provider instanceof \OCP\TextToImage\IProviderWithUserId ) {
$this -> provider -> setUserId ( $userId );
}
try {
$this -> provider -> generate ( $input [ 'input' ], $resources );
2024-04-30 09:48:00 -04:00
} catch ( \RuntimeException $e ) {
2024-04-29 10:21:07 -04:00
throw new ProcessingException ( $e -> getMessage (), 0 , $e );
}
2024-05-10 00:51:41 -04:00
for ( $i = 0 ; $i < $input [ 'numberOfImages' ]; $i ++ ) {
if ( is_resource ( $resources [ $i ])) {
// If $resource hasn't been closed yet, we'll do that here
fclose ( $resources [ $i ]);
}
}
2024-05-03 08:15:03 -04:00
return [ 'images' => array_map ( fn ( ISimpleFile $file ) => $file -> getContent (), $files )];
2024-04-29 10:21:07 -04:00
}
2024-07-24 09:34:51 -04:00
public function getInputShapeEnumValues () : array {
return [];
}
public function getInputShapeDefaults () : array {
return [];
}
public function getOptionalInputShapeEnumValues () : array {
return [];
}
public function getOptionalInputShapeDefaults () : array {
return [];
}
public function getOutputShapeEnumValues () : array {
return [];
}
public function getOptionalOutputShapeEnumValues () : array {
return [];
}
2024-04-29 10:21:07 -04:00
};
$newProviders [ $newProvider -> getId ()] = $newProvider ;
}
return $newProviders ;
}
2024-08-30 04:01:21 -04:00
/**
* This is almost a copy of SpeechToTextManager -> getProviders
* to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
*/
2024-08-28 11:26:32 -04:00
private function _getRawSpeechToTextProviders () : array {
2024-08-30 04:01:21 -04:00
$context = $this -> coordinator -> getRegistrationContext ();
if ( $context === null ) {
return [];
}
$providers = [];
foreach ( $context -> getSpeechToTextProviders () as $providerServiceRegistration ) {
$class = $providerServiceRegistration -> getService ();
try {
$providers [ $class ] = $this -> serverContainer -> get ( $class );
} catch ( NotFoundExceptionInterface | ContainerExceptionInterface | \Throwable $e ) {
$this -> logger -> error ( 'Failed to load SpeechToText provider ' . $class , [
'exception' => $e ,
]);
}
}
return $providers ;
2024-08-28 11:26:32 -04:00
}
2024-04-29 10:21:07 -04:00
/**
* @ return IProvider []
*/
private function _getSpeechToTextProviders () : array {
2024-08-28 11:26:32 -04:00
$oldProviders = $this -> _getRawSpeechToTextProviders ();
2024-04-29 10:21:07 -04:00
$newProviders = [];
foreach ( $oldProviders as $oldProvider ) {
$newProvider = new class ( $oldProvider , $this -> rootFolder , $this -> appData ) implements IProvider , ISynchronousProvider {
private ISpeechToTextProvider $provider ;
private IAppData $appData ;
2024-05-02 09:12:35 -04:00
private IRootFolder $rootFolder ;
2024-04-29 10:21:07 -04:00
public function __construct ( ISpeechToTextProvider $provider , IRootFolder $rootFolder , IAppData $appData ) {
$this -> provider = $provider ;
$this -> rootFolder = $rootFolder ;
$this -> appData = $appData ;
}
public function getId () : string {
if ( $this -> provider instanceof ISpeechToTextProviderWithId ) {
return Manager :: LEGACY_PREFIX_SPEECHTOTEXT . $this -> provider -> getId ();
}
return Manager :: LEGACY_PREFIX_SPEECHTOTEXT . $this -> provider :: class ;
}
public function getName () : string {
return $this -> provider -> getName ();
}
2024-05-03 06:23:59 -04:00
public function getTaskTypeId () : string {
2024-04-29 10:21:07 -04:00
return AudioToText :: ID ;
}
public function getExpectedRuntime () : int {
return 60 ;
}
public function getOptionalInputShape () : array {
return [];
}
public function getOptionalOutputShape () : array {
return [];
}
2024-05-07 07:04:44 -04:00
public function process ( ? string $userId , array $input , callable $reportProgress ) : array {
2024-07-31 05:05:25 -04:00
if ( $this -> provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId ) {
$this -> provider -> setUserId ( $userId );
}
2024-04-29 10:21:07 -04:00
try {
2024-05-13 05:50:14 -04:00
$result = $this -> provider -> transcribeFile ( $input [ 'input' ]);
2024-04-30 09:48:00 -04:00
} catch ( \RuntimeException $e ) {
2024-04-29 10:21:07 -04:00
throw new ProcessingException ( $e -> getMessage (), 0 , $e );
}
return [ 'output' => $result ];
}
2024-07-24 09:34:51 -04:00
public function getInputShapeEnumValues () : array {
return [];
}
public function getInputShapeDefaults () : array {
return [];
}
public function getOptionalInputShapeEnumValues () : array {
return [];
}
public function getOptionalInputShapeDefaults () : array {
return [];
}
public function getOutputShapeEnumValues () : array {
return [];
}
public function getOptionalOutputShapeEnumValues () : array {
return [];
}
2024-04-29 10:21:07 -04:00
};
$newProviders [ $newProvider -> getId ()] = $newProvider ;
}
return $newProviders ;
}
2025-04-08 13:45:37 -04:00
/**
* Dispatches the event to collect external providers and task types .
* Caches the result within the request .
*/
private function dispatchGetProvidersEvent () : GetTaskProcessingProvidersEvent {
if ( $this -> eventResult !== null ) {
return $this -> eventResult ;
}
$this -> eventResult = new GetTaskProcessingProvidersEvent ();
$this -> dispatcher -> dispatchTyped ( $this -> eventResult );
return $this -> eventResult ;
}
2024-04-29 10:21:07 -04:00
/**
* @ return IProvider []
*/
private function _getProviders () : array {
$context = $this -> coordinator -> getRegistrationContext ();
if ( $context === null ) {
return [];
}
$providers = [];
foreach ( $context -> getTaskProcessingProviders () as $providerServiceRegistration ) {
$class = $providerServiceRegistration -> getService ();
try {
/** @var IProvider $provider */
$provider = $this -> serverContainer -> get ( $class );
2024-05-06 07:03:03 -04:00
if ( isset ( $providers [ $provider -> getId ()])) {
$this -> logger -> warning ( 'Task processing provider ' . $class . ' is using ID ' . $provider -> getId () . ' which is already used by ' . $providers [ $provider -> getId ()] :: class );
}
2024-04-29 10:21:07 -04:00
$providers [ $provider -> getId ()] = $provider ;
} catch ( \Throwable $e ) {
$this -> logger -> error ( 'Failed to load task processing provider ' . $class , [
'exception' => $e ,
]);
}
}
2025-04-08 13:45:37 -04:00
$event = $this -> dispatchGetProvidersEvent ();
$externalProviders = $event -> getProviders ();
foreach ( $externalProviders as $provider ) {
if ( ! isset ( $providers [ $provider -> getId ()])) {
$providers [ $provider -> getId ()] = $provider ;
} else {
$this -> logger -> info ( 'Skipping external task processing provider with ID ' . $provider -> getId () . ' because a local provider with the same ID already exists.' );
}
}
2024-04-29 10:21:07 -04:00
$providers += $this -> _getTextProcessingProviders () + $this -> _getTextToImageProviders () + $this -> _getSpeechToTextProviders ();
return $providers ;
}
/**
* @ return ITaskType []
*/
private function _getTaskTypes () : array {
$context = $this -> coordinator -> getRegistrationContext ();
if ( $context === null ) {
return [];
}
2025-04-08 13:45:37 -04:00
if ( $this -> taskTypes !== null ) {
return $this -> taskTypes ;
}
2024-04-29 10:21:07 -04:00
// Default task types
$taskTypes = [
\OCP\TaskProcessing\TaskTypes\TextToText :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToText :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextTopics :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextTopics :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextHeadline :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextHeadline :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextSummary :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextSummary :: class ),
2024-07-02 10:26:52 -04:00
\OCP\TaskProcessing\TaskTypes\TextToTextFormalization :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextFormalization :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextSimplification :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextSimplification :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextChat :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextChat :: class ),
2024-07-25 06:09:47 -04:00
\OCP\TaskProcessing\TaskTypes\TextToTextTranslate :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextTranslate :: class ),
2024-07-31 05:05:25 -04:00
\OCP\TaskProcessing\TaskTypes\TextToTextReformulation :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextReformulation :: class ),
2024-04-29 10:21:07 -04:00
\OCP\TaskProcessing\TaskTypes\TextToImage :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToImage :: class ),
\OCP\TaskProcessing\TaskTypes\AudioToText :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\AudioToText :: class ),
2024-06-20 06:27:08 -04:00
\OCP\TaskProcessing\TaskTypes\ContextWrite :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\ContextWrite :: class ),
2024-06-20 06:55:36 -04:00
\OCP\TaskProcessing\TaskTypes\GenerateEmoji :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\GenerateEmoji :: class ),
2024-12-06 06:09:18 -05:00
\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextChangeTone :: class ),
\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools :: class ),
\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction :: class ),
2024-12-19 04:51:26 -05:00
\OCP\TaskProcessing\TaskTypes\TextToTextProofread :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToTextProofread :: class ),
2025-04-26 00:41:45 -04:00
\OCP\TaskProcessing\TaskTypes\TextToSpeech :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\TextToSpeech :: class ),
2025-07-02 06:10:01 -04:00
\OCP\TaskProcessing\TaskTypes\AudioToAudioChat :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\AudioToAudioChat :: class ),
2025-07-07 09:28:15 -04:00
\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction :: class ),
2025-07-03 09:28:45 -04:00
\OCP\TaskProcessing\TaskTypes\AnalyzeImages :: ID => \OCP\Server :: get ( \OCP\TaskProcessing\TaskTypes\AnalyzeImages :: class ),
2024-04-29 10:21:07 -04:00
];
foreach ( $context -> getTaskProcessingTaskTypes () as $providerServiceRegistration ) {
$class = $providerServiceRegistration -> getService ();
try {
/** @var ITaskType $provider */
$taskType = $this -> serverContainer -> get ( $class );
2024-05-06 07:03:03 -04:00
if ( isset ( $taskTypes [ $taskType -> getId ()])) {
$this -> logger -> warning ( 'Task processing task type ' . $class . ' is using ID ' . $taskType -> getId () . ' which is already used by ' . $taskTypes [ $taskType -> getId ()] :: class );
}
2024-04-29 10:21:07 -04:00
$taskTypes [ $taskType -> getId ()] = $taskType ;
} catch ( \Throwable $e ) {
$this -> logger -> error ( 'Failed to load task processing task type ' . $class , [
'exception' => $e ,
]);
}
}
2025-04-08 13:45:37 -04:00
$event = $this -> dispatchGetProvidersEvent ();
$externalTaskTypes = $event -> getTaskTypes ();
foreach ( $externalTaskTypes as $taskType ) {
if ( isset ( $taskTypes [ $taskType -> getId ()])) {
$this -> logger -> warning ( 'External task processing task type is using ID ' . $taskType -> getId () . ' which is already used by a locally registered task type (' . get_class ( $taskTypes [ $taskType -> getId ()]) . ')' );
}
$taskTypes [ $taskType -> getId ()] = $taskType ;
}
2024-04-29 10:21:07 -04:00
$taskTypes += $this -> _getTextProcessingTaskTypes ();
2025-04-08 13:45:37 -04:00
$this -> taskTypes = $taskTypes ;
return $this -> taskTypes ;
2024-04-29 10:21:07 -04:00
}
2024-12-08 13:16:19 -05:00
/**
* @ return array
*/
private function _getTaskTypeSettings () : array {
2024-12-17 11:00:07 -05:00
try {
2025-08-01 05:56:10 -04:00
$json = $this -> appConfig -> getValueString ( 'core' , 'ai.taskprocessing_type_preferences' , '' , lazy : true );
2024-12-17 11:00:07 -05:00
if ( $json === '' ) {
return [];
}
return json_decode ( $json , true , flags : JSON_THROW_ON_ERROR );
} catch ( \JsonException $e ) {
$this -> logger -> error ( 'Failed to get settings. JSON Error in ai.taskprocessing_type_preferences' , [ 'exception' => $e ]);
$taskTypeSettings = [];
$taskTypes = $this -> _getTaskTypes ();
foreach ( $taskTypes as $taskType ) {
$taskTypeSettings [ $taskType -> getId ()] = false ;
};
2025-01-23 03:36:29 -05:00
2024-12-17 11:00:07 -05:00
return $taskTypeSettings ;
2024-12-09 02:31:18 -05:00
}
2025-01-23 03:36:29 -05:00
2024-12-08 13:16:19 -05:00
}
2024-04-29 10:21:07 -04:00
/**
2024-04-30 09:48:00 -04:00
* @ param ShapeDescriptor [] $spec
2024-07-24 09:34:51 -04:00
* @ param array < array - key , string | numeric > $defaults
* @ param array < array - key , ShapeEnumValue [] > $enumValues
2024-04-30 09:48:00 -04:00
* @ param array $io
2024-07-24 09:34:51 -04:00
* @ param bool $optional
2024-04-29 10:21:07 -04:00
* @ return void
* @ throws ValidationException
*/
2024-07-24 09:34:51 -04:00
private static function validateInput ( array $spec , array $defaults , array $enumValues , array $io , bool $optional = false ) : void {
2024-04-29 10:21:07 -04:00
foreach ( $spec as $key => $descriptor ) {
$type = $descriptor -> getShapeType ();
if ( ! isset ( $io [ $key ])) {
if ( $optional ) {
continue ;
}
2024-07-24 09:34:51 -04:00
if ( isset ( $defaults [ $key ])) {
if ( EShapeType :: getScalarType ( $type ) !== $type ) {
throw new ValidationException ( 'Provider tried to set a default value for a non-scalar slot' );
}
if ( EShapeType :: isFileType ( $type )) {
throw new ValidationException ( 'Provider tried to set a default value for a slot that is not text or number' );
}
$type -> validateInput ( $defaults [ $key ]);
continue ;
}
2024-05-02 05:15:51 -04:00
throw new ValidationException ( 'Missing key: "' . $key . '"' );
2024-04-29 10:21:07 -04:00
}
2024-05-02 05:15:51 -04:00
try {
$type -> validateInput ( $io [ $key ]);
2024-07-25 04:23:53 -04:00
if ( $type === EShapeType :: Enum ) {
if ( ! isset ( $enumValues [ $key ])) {
2024-09-19 05:10:31 -04:00
throw new ValidationException ( 'Provider did not provide enum values for an enum slot: "' . $key . '"' );
2024-07-25 04:23:53 -04:00
}
2024-07-24 09:34:51 -04:00
$type -> validateEnum ( $io [ $key ], $enumValues [ $key ]);
}
2024-05-02 05:15:51 -04:00
} catch ( ValidationException $e ) {
throw new ValidationException ( 'Failed to validate input key "' . $key . '": ' . $e -> getMessage ());
2024-04-29 10:21:07 -04:00
}
}
}
2024-07-24 09:34:51 -04:00
/**
* Takes task input data and replaces fileIds with File objects
*
* @ param array < array - key , list < numeric | string >| numeric | string > $input
* @ param array < array - key , numeric | string > ... $defaultSpecs the specs
* @ return array < array - key , list < numeric | string >| numeric | string >
*/
public function fillInputDefaults ( array $input , ... $defaultSpecs ) : array {
2024-07-25 02:06:04 -04:00
$spec = array_reduce ( $defaultSpecs , fn ( $carry , $spec ) => array_merge ( $carry , $spec ), []);
return array_merge ( $spec , $input );
2024-07-24 09:34:51 -04:00
}
2024-04-29 10:21:07 -04:00
/**
2024-04-30 09:48:00 -04:00
* @ param ShapeDescriptor [] $spec
2024-07-24 09:34:51 -04:00
* @ param array < array - key , ShapeEnumValue [] > $enumValues
2024-04-29 10:21:07 -04:00
* @ param array $io
2024-04-30 09:48:00 -04:00
* @ param bool $optional
2024-04-29 10:21:07 -04:00
* @ return void
* @ throws ValidationException
*/
2024-07-24 09:34:51 -04:00
private static function validateOutputWithFileIds ( array $spec , array $enumValues , array $io , bool $optional = false ) : void {
2024-04-29 10:21:07 -04:00
foreach ( $spec as $key => $descriptor ) {
$type = $descriptor -> getShapeType ();
if ( ! isset ( $io [ $key ])) {
if ( $optional ) {
continue ;
}
2024-05-02 05:15:51 -04:00
throw new ValidationException ( 'Missing key: "' . $key . '"' );
2024-04-29 10:21:07 -04:00
}
2024-05-02 05:15:51 -04:00
try {
2024-07-09 06:43:31 -04:00
$type -> validateOutputWithFileIds ( $io [ $key ]);
2024-07-24 09:34:51 -04:00
if ( isset ( $enumValues [ $key ])) {
$type -> validateEnum ( $io [ $key ], $enumValues [ $key ]);
}
2024-07-09 06:43:31 -04:00
} catch ( ValidationException $e ) {
throw new ValidationException ( 'Failed to validate output key "' . $key . '": ' . $e -> getMessage ());
}
}
}
/**
* @ param ShapeDescriptor [] $spec
2024-07-24 09:34:51 -04:00
* @ param array < array - key , ShapeEnumValue [] > $enumValues
2024-07-09 06:43:31 -04:00
* @ param array $io
* @ param bool $optional
* @ return void
* @ throws ValidationException
*/
2024-07-24 09:34:51 -04:00
private static function validateOutputWithFileData ( array $spec , array $enumValues , array $io , bool $optional = false ) : void {
2024-07-09 06:43:31 -04:00
foreach ( $spec as $key => $descriptor ) {
$type = $descriptor -> getShapeType ();
if ( ! isset ( $io [ $key ])) {
if ( $optional ) {
continue ;
}
throw new ValidationException ( 'Missing key: "' . $key . '"' );
}
try {
$type -> validateOutputWithFileData ( $io [ $key ]);
2024-07-24 09:34:51 -04:00
if ( isset ( $enumValues [ $key ])) {
$type -> validateEnum ( $io [ $key ], $enumValues [ $key ]);
}
2024-05-02 05:15:51 -04:00
} catch ( ValidationException $e ) {
throw new ValidationException ( 'Failed to validate output key "' . $key . '": ' . $e -> getMessage ());
2024-04-29 10:21:07 -04:00
}
}
}
/**
2024-05-06 10:36:35 -04:00
* @ param array < array - key , T > $array The array to filter
* @ param ShapeDescriptor [] ... $specs the specs that define which keys to keep
* @ return array < array - key , T >
* @ psalm - template T
2024-04-29 10:21:07 -04:00
*/
private function removeSuperfluousArrayKeys ( array $array , ... $specs ) : array {
2024-06-23 16:47:32 -04:00
$keys = array_unique ( array_reduce ( $specs , fn ( $carry , $spec ) => array_merge ( $carry , array_keys ( $spec )), []));
2024-07-01 05:32:34 -04:00
$keys = array_filter ( $keys , fn ( $key ) => array_key_exists ( $key , $array ));
2024-04-30 09:48:00 -04:00
$values = array_map ( fn ( string $key ) => $array [ $key ], $keys );
2024-04-29 10:21:07 -04:00
return array_combine ( $keys , $values );
}
public function hasProviders () : bool {
return count ( $this -> getProviders ()) !== 0 ;
}
public function getProviders () : array {
if ( $this -> providers === null ) {
$this -> providers = $this -> _getProviders ();
}
return $this -> providers ;
}
2024-08-12 07:11:41 -04:00
public function getPreferredProvider ( string $taskTypeId ) {
2024-07-13 09:07:22 -04:00
try {
2025-01-23 04:40:42 -05:00
if ( $this -> preferences === null ) {
$this -> preferences = $this -> distributedCache -> get ( 'ai.taskprocessing_provider_preferences' );
if ( $this -> preferences === null ) {
2025-08-01 05:56:10 -04:00
$this -> preferences = json_decode (
$this -> appConfig -> getValueString ( 'core' , 'ai.taskprocessing_provider_preferences' , 'null' , lazy : true ),
associative : true ,
flags : JSON_THROW_ON_ERROR ,
);
2025-01-23 04:40:42 -05:00
$this -> distributedCache -> set ( 'ai.taskprocessing_provider_preferences' , $this -> preferences , 60 * 3 );
}
}
2024-07-17 09:23:18 -04:00
$providers = $this -> getProviders ();
2025-01-21 14:05:51 -05:00
if ( isset ( $this -> preferences [ $taskTypeId ])) {
2025-01-24 04:12:56 -05:00
$providersById = $this -> providersById ? ? array_reduce ( $providers , static function ( array $carry , IProvider $provider ) {
$carry [ $provider -> getId ()] = $provider ;
return $carry ;
}, []);
$this -> providersById = $providersById ;
if ( isset ( $providersById [ $this -> preferences [ $taskTypeId ]])) {
return $providersById [ $this -> preferences [ $taskTypeId ]];
2024-07-13 09:07:22 -04:00
}
}
// By default, use the first available provider
foreach ( $providers as $provider ) {
2024-08-12 07:11:41 -04:00
if ( $provider -> getTaskTypeId () === $taskTypeId ) {
2024-07-13 09:07:22 -04:00
return $provider ;
}
2024-05-17 05:54:31 -04:00
}
2024-07-13 09:07:22 -04:00
} catch ( \JsonException $e ) {
2024-08-12 07:11:41 -04:00
$this -> logger -> warning ( 'Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId , [ 'exception' => $e ]);
2024-05-17 05:54:31 -04:00
}
throw new \OCP\TaskProcessing\Exception\Exception ( 'No matching provider found' );
}
2025-07-02 12:10:51 -04:00
public function getAvailableTaskTypes ( bool $showDisabled = false , ? string $userId = null ) : array {
2025-09-17 05:05:28 -04:00
// We cache by language, because some task type fields are translated
$cacheKey = self :: TASK_TYPES_CACHE_KEY . ':' . $this -> l10nFactory -> findLanguage ();
2025-07-02 12:10:51 -04:00
// userId will be obtained from the session if left to null
2025-07-03 03:59:28 -04:00
if ( ! $this -> checkGuestAccess ( $userId )) {
2025-07-02 12:10:51 -04:00
return [];
}
2025-02-04 07:03:59 -05:00
if ( $this -> availableTaskTypes === null ) {
2025-09-17 05:05:28 -04:00
$cachedValue = $this -> distributedCache -> get ( $cacheKey );
2025-02-04 07:03:59 -05:00
if ( $cachedValue !== null ) {
$this -> availableTaskTypes = unserialize ( $cachedValue );
2025-02-04 07:04:43 -05:00
}
2025-01-23 03:36:29 -05:00
}
2024-12-12 05:50:39 -05:00
// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
if ( $this -> availableTaskTypes === null || $showDisabled ) {
$taskTypes = $this -> _getTaskTypes ();
$taskTypeSettings = $this -> _getTaskTypeSettings ();
$availableTaskTypes = [];
foreach ( $taskTypes as $taskType ) {
if (( ! $showDisabled ) && isset ( $taskTypeSettings [ $taskType -> getId ()]) && ! $taskTypeSettings [ $taskType -> getId ()]) {
continue ;
}
try {
$provider = $this -> getPreferredProvider ( $taskType -> getId ());
} catch ( \OCP\TaskProcessing\Exception\Exception $e ) {
continue ;
}
try {
$availableTaskTypes [ $provider -> getTaskTypeId ()] = [
'name' => $taskType -> getName (),
'description' => $taskType -> getDescription (),
'optionalInputShape' => $provider -> getOptionalInputShape (),
'inputShapeEnumValues' => $provider -> getInputShapeEnumValues (),
'inputShapeDefaults' => $provider -> getInputShapeDefaults (),
'inputShape' => $taskType -> getInputShape (),
'optionalInputShapeEnumValues' => $provider -> getOptionalInputShapeEnumValues (),
'optionalInputShapeDefaults' => $provider -> getOptionalInputShapeDefaults (),
'outputShape' => $taskType -> getOutputShape (),
'outputShapeEnumValues' => $provider -> getOutputShapeEnumValues (),
'optionalOutputShape' => $provider -> getOptionalOutputShape (),
'optionalOutputShapeEnumValues' => $provider -> getOptionalOutputShapeEnumValues (),
2025-10-09 05:54:35 -04:00
'isInternal' => $taskType instanceof IInternalTaskType ,
2024-12-12 05:50:39 -05:00
];
} catch ( \Throwable $e ) {
$this -> logger -> error ( 'Failed to set up TaskProcessing provider ' . $provider :: class , [ 'exception' => $e ]);
}
2024-12-11 11:29:45 -05:00
}
2024-12-12 05:50:39 -05:00
if ( $showDisabled ) {
// Do not cache showDisabled, ever.
return $availableTaskTypes ;
2024-04-29 10:21:07 -04:00
}
2024-12-12 05:50:39 -05:00
$this -> availableTaskTypes = $availableTaskTypes ;
2025-09-17 05:05:28 -04:00
$this -> distributedCache -> set ( $cacheKey , serialize ( $this -> availableTaskTypes ), 60 );
2024-04-29 10:21:07 -04:00
}
2024-12-11 11:29:45 -05:00
2024-04-29 10:21:07 -04:00
return $this -> availableTaskTypes ;
}
2025-09-03 11:10:12 -04:00
public function getAvailableTaskTypeIds ( bool $showDisabled = false , ? string $userId = null ) : array {
// userId will be obtained from the session if left to null
if ( ! $this -> checkGuestAccess ( $userId )) {
return [];
}
if ( $this -> availableTaskTypeIds === null ) {
$cachedValue = $this -> distributedCache -> get ( self :: TASK_TYPE_IDS_CACHE_KEY );
if ( $cachedValue !== null ) {
$this -> availableTaskTypeIds = $cachedValue ;
}
}
// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
if ( $this -> availableTaskTypeIds === null || $showDisabled ) {
$taskTypes = $this -> _getTaskTypes ();
$taskTypeSettings = $this -> _getTaskTypeSettings ();
$availableTaskTypeIds = [];
foreach ( $taskTypes as $taskType ) {
if (( ! $showDisabled ) && isset ( $taskTypeSettings [ $taskType -> getId ()]) && ! $taskTypeSettings [ $taskType -> getId ()]) {
continue ;
}
try {
$provider = $this -> getPreferredProvider ( $taskType -> getId ());
} catch ( \OCP\TaskProcessing\Exception\Exception $e ) {
continue ;
}
$availableTaskTypeIds [] = $taskType -> getId ();
}
if ( $showDisabled ) {
// Do not cache showDisabled, ever.
return $availableTaskTypeIds ;
}
$this -> availableTaskTypeIds = $availableTaskTypeIds ;
$this -> distributedCache -> set ( self :: TASK_TYPE_IDS_CACHE_KEY , $this -> availableTaskTypeIds , 60 );
}
return $this -> availableTaskTypeIds ;
}
2024-04-29 10:21:07 -04:00
public function canHandleTask ( Task $task ) : bool {
2024-05-03 06:23:59 -04:00
return isset ( $this -> getAvailableTaskTypes ()[ $task -> getTaskTypeId ()]);
2024-04-29 10:21:07 -04:00
}
2025-07-02 12:10:51 -04:00
private function checkGuestAccess ( ? string $userId = null ) : bool {
if ( $userId === null && ! $this -> userSession -> isLoggedIn ()) {
return true ;
}
if ( $userId === null ) {
$user = $this -> userSession -> getUser ();
} else {
$user = $this -> userManager -> get ( $userId );
}
2025-07-03 03:59:28 -04:00
2025-08-01 05:56:10 -04:00
$guestsAllowed = $this -> appConfig -> getValueString ( 'core' , 'ai.taskprocessing_guests' , 'false' );
2025-07-02 12:10:51 -04:00
if ( $guestsAllowed == 'true' || ! class_exists ( \OCA\Guests\UserBackend :: class ) || ! ( $user -> getBackend () instanceof \OCA\Guests\UserBackend )) {
return true ;
}
return false ;
}
2024-04-29 10:21:07 -04:00
public function scheduleTask ( Task $task ) : void {
2025-07-02 12:10:51 -04:00
if ( ! $this -> checkGuestAccess ( $task -> getUserId ())) {
throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException ( 'Access to this resource is forbidden for guests.' );
}
2024-04-29 10:21:07 -04:00
if ( ! $this -> canHandleTask ( $task )) {
2024-05-06 04:03:24 -04:00
throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException ( 'No task processing provider is installed that can handle this task type: ' . $task -> getTaskTypeId ());
2024-04-29 10:21:07 -04:00
}
2024-08-27 07:11:51 -04:00
$this -> prepareTask ( $task );
2024-04-29 10:21:07 -04:00
$task -> setStatus ( Task :: STATUS_SCHEDULED );
2024-08-27 07:11:51 -04:00
$this -> storeTask ( $task );
2024-04-29 10:21:07 -04:00
// schedule synchronous job if the provider is synchronous
2024-08-27 07:11:51 -04:00
$provider = $this -> getPreferredProvider ( $task -> getTaskTypeId ());
2024-04-29 10:21:07 -04:00
if ( $provider instanceof ISynchronousProvider ) {
$this -> jobList -> add ( SynchronousBackgroundJob :: class , null );
}
2025-10-14 05:01:39 -04:00
if ( $provider instanceof ITriggerableProvider ) {
try {
if ( ! $this -> taskMapper -> hasRunningTasksForTaskType ( $task -> getTaskTypeId ())) {
// If no tasks are currently running for this task type, nudge the provider to ask for tasks
$provider -> trigger ();
}
} catch ( Exception $e ) {
$this -> logger -> error ( 'Failed to check DB for running tasks after a task was scheduled for a triggerable provider. Not triggering the provider.' , [ 'exception' => $e ]);
}
}
2024-04-29 10:21:07 -04:00
}
2024-08-27 07:11:51 -04:00
public function runTask ( Task $task ) : Task {
2025-07-02 12:10:51 -04:00
if ( ! $this -> checkGuestAccess ( $task -> getUserId ())) {
throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException ( 'Access to this resource is forbidden for guests.' );
}
2024-08-27 07:11:51 -04:00
if ( ! $this -> canHandleTask ( $task )) {
throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException ( 'No task processing provider is installed that can handle this task type: ' . $task -> getTaskTypeId ());
}
$provider = $this -> getPreferredProvider ( $task -> getTaskTypeId ());
if ( $provider instanceof ISynchronousProvider ) {
$this -> prepareTask ( $task );
$task -> setStatus ( Task :: STATUS_SCHEDULED );
$this -> storeTask ( $task );
$this -> processTask ( $task , $provider );
$task = $this -> getTask ( $task -> getId ());
} else {
$this -> scheduleTask ( $task );
// poll task
while ( $task -> getStatus () === Task :: STATUS_SCHEDULED || $task -> getStatus () === Task :: STATUS_RUNNING ) {
sleep ( 1 );
$task = $this -> getTask ( $task -> getId ());
}
}
return $task ;
}
public function processTask ( Task $task , ISynchronousProvider $provider ) : bool {
try {
try {
$input = $this -> prepareInputData ( $task );
} catch ( GenericFileException | NotPermittedException | LockedException | ValidationException | UnauthorizedException $e ) {
$this -> logger -> warning ( 'Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider -> getId (), [ 'exception' => $e ]);
$this -> setTaskResult ( $task -> getId (), $e -> getMessage (), null );
return false ;
}
try {
$this -> setTaskStatus ( $task , Task :: STATUS_RUNNING );
$output = $provider -> process ( $task -> getUserId (), $input , fn ( float $progress ) => $this -> setTaskProgress ( $task -> getId (), $progress ));
} catch ( ProcessingException $e ) {
$this -> logger -> warning ( 'Failed to process a TaskProcessing task with synchronous provider ' . $provider -> getId (), [ 'exception' => $e ]);
$this -> setTaskResult ( $task -> getId (), $e -> getMessage (), null );
return false ;
} catch ( \Throwable $e ) {
$this -> logger -> error ( 'Unknown error while processing TaskProcessing task' , [ 'exception' => $e ]);
$this -> setTaskResult ( $task -> getId (), $e -> getMessage (), null );
return false ;
}
$this -> setTaskResult ( $task -> getId (), null , $output );
} catch ( NotFoundException $e ) {
$this -> logger -> info ( 'Could not find task anymore after execution. Moving on.' , [ 'exception' => $e ]);
} catch ( Exception $e ) {
$this -> logger -> error ( 'Failed to report result of TaskProcessing task' , [ 'exception' => $e ]);
}
return true ;
}
2024-04-29 10:21:07 -04:00
public function deleteTask ( Task $task ) : void {
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
$this -> taskMapper -> delete ( $taskEntity );
}
public function getTask ( int $id ) : Task {
try {
$taskEntity = $this -> taskMapper -> find ( $id );
return $taskEntity -> toPublicTask ();
} catch ( DoesNotExistException $e ) {
throw new NotFoundException ( 'Couldn\'t find task with id ' . $id , 0 , $e );
} catch ( MultipleObjectsReturnedException | \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , 0 , $e );
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the task' , 0 , $e );
}
}
public function cancelTask ( int $id ) : void {
$task = $this -> getTask ( $id );
2024-05-07 07:23:53 -04:00
if ( $task -> getStatus () !== Task :: STATUS_SCHEDULED && $task -> getStatus () !== Task :: STATUS_RUNNING ) {
return ;
}
2024-04-29 10:21:07 -04:00
$task -> setStatus ( Task :: STATUS_CANCELLED );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-04-29 10:21:07 -04:00
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
try {
$this -> taskMapper -> update ( $taskEntity );
2024-07-17 06:35:13 -04:00
$this -> runWebhook ( $task );
2024-04-29 10:21:07 -04:00
} catch ( \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , 0 , $e );
}
}
public function setTaskProgress ( int $id , float $progress ) : bool {
// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
$task = $this -> getTask ( $id );
if ( $task -> getStatus () === Task :: STATUS_CANCELLED ) {
return false ;
}
2024-07-08 12:04:46 -04:00
// only set the start time if the task is going from scheduled to running
if ( $task -> getstatus () === Task :: STATUS_SCHEDULED ) {
$task -> setStartedAt ( time ());
}
2024-04-29 10:21:07 -04:00
$task -> setStatus ( Task :: STATUS_RUNNING );
$task -> setProgress ( $progress );
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
try {
$this -> taskMapper -> update ( $taskEntity );
} catch ( \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , 0 , $e );
}
return true ;
}
2024-07-09 05:43:11 -04:00
public function setTaskResult ( int $id , ? string $error , ? array $result , bool $isUsingFileIds = false ) : void {
2024-04-29 10:21:07 -04:00
// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
$task = $this -> getTask ( $id );
if ( $task -> getStatus () === Task :: STATUS_CANCELLED ) {
2024-05-03 06:23:59 -04:00
$this -> logger -> info ( 'A TaskProcessing ' . $task -> getTaskTypeId () . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.' );
2024-04-29 10:21:07 -04:00
return ;
}
if ( $error !== null ) {
$task -> setStatus ( Task :: STATUS_FAILED );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-09-06 07:53:31 -04:00
// truncate error message to 1000 characters
$task -> setErrorMessage ( mb_substr ( $error , 0 , 1000 ));
2024-05-03 06:23:59 -04:00
$this -> logger -> warning ( 'A TaskProcessing ' . $task -> getTaskTypeId () . ' task with id ' . $id . ' failed with the following message: ' . $error );
2024-04-30 09:48:00 -04:00
} elseif ( $result !== null ) {
2024-04-29 10:21:07 -04:00
$taskTypes = $this -> getAvailableTaskTypes ();
2024-05-03 06:23:59 -04:00
$outputShape = $taskTypes [ $task -> getTaskTypeId ()][ 'outputShape' ];
2024-07-24 09:34:51 -04:00
$outputShapeEnumValues = $taskTypes [ $task -> getTaskTypeId ()][ 'outputShapeEnumValues' ];
2024-05-03 06:23:59 -04:00
$optionalOutputShape = $taskTypes [ $task -> getTaskTypeId ()][ 'optionalOutputShape' ];
2024-07-24 09:34:51 -04:00
$optionalOutputShapeEnumValues = $taskTypes [ $task -> getTaskTypeId ()][ 'optionalOutputShapeEnumValues' ];
2024-04-29 10:21:07 -04:00
try {
// validate output
2024-07-09 06:43:31 -04:00
if ( ! $isUsingFileIds ) {
2024-07-24 09:34:51 -04:00
$this -> validateOutputWithFileData ( $outputShape , $outputShapeEnumValues , $result );
$this -> validateOutputWithFileData ( $optionalOutputShape , $optionalOutputShapeEnumValues , $result , true );
2024-07-09 06:43:31 -04:00
} else {
2024-07-24 09:34:51 -04:00
$this -> validateOutputWithFileIds ( $outputShape , $outputShapeEnumValues , $result );
$this -> validateOutputWithFileIds ( $optionalOutputShape , $optionalOutputShapeEnumValues , $result , true );
2024-07-09 06:43:31 -04:00
}
2024-04-29 10:21:07 -04:00
$output = $this -> removeSuperfluousArrayKeys ( $result , $outputShape , $optionalOutputShape );
2024-05-06 10:36:35 -04:00
// extract raw data and put it in files, replace it with file ids
2024-07-09 05:43:11 -04:00
if ( ! $isUsingFileIds ) {
$output = $this -> encapsulateOutputFileData ( $output , $outputShape , $optionalOutputShape );
} else {
2024-07-13 04:49:53 -04:00
$this -> validateOutputFileIds ( $output , $outputShape , $optionalOutputShape );
2024-07-09 07:35:46 -04:00
}
2024-07-09 07:35:46 -04:00
// Turn file objects into IDs
foreach ( $output as $key => $value ) {
if ( $value instanceof Node ) {
$output [ $key ] = $value -> getId ();
}
2024-10-02 02:38:20 -04:00
if ( is_array ( $value ) && isset ( $value [ 0 ]) && $value [ 0 ] instanceof Node ) {
2024-07-13 06:23:05 -04:00
$output [ $key ] = array_map ( fn ( $node ) => $node -> getId (), $value );
2024-07-09 07:35:46 -04:00
}
}
2024-04-29 10:21:07 -04:00
$task -> setOutput ( $output );
$task -> setProgress ( 1 );
$task -> setStatus ( Task :: STATUS_SUCCESSFUL );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-04-29 10:21:07 -04:00
} catch ( ValidationException $e ) {
$task -> setProgress ( 1 );
$task -> setStatus ( Task :: STATUS_FAILED );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-04-29 10:21:07 -04:00
$error = 'The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec' ;
$task -> setErrorMessage ( $error );
2025-02-17 05:59:23 -05:00
$this -> logger -> error ( $error , [ 'exception' => $e , 'output' => $result ]);
2024-04-29 10:21:07 -04:00
} catch ( NotPermittedException $e ) {
$task -> setProgress ( 1 );
$task -> setStatus ( Task :: STATUS_FAILED );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-04-29 10:21:07 -04:00
$error = 'The task was processed successfully but storing the output in a file failed' ;
$task -> setErrorMessage ( $error );
$this -> logger -> error ( $error , [ 'exception' => $e ]);
2024-07-13 05:42:06 -04:00
} catch ( InvalidPathException | \OCP\Files\NotFoundException $e ) {
$task -> setProgress ( 1 );
$task -> setStatus ( Task :: STATUS_FAILED );
2024-07-08 12:04:46 -04:00
$task -> setEndedAt ( time ());
2024-07-13 05:42:06 -04:00
$error = 'The task was processed successfully but the result file could not be found' ;
$task -> setErrorMessage ( $error );
$this -> logger -> error ( $error , [ 'exception' => $e ]);
2024-04-29 10:21:07 -04:00
}
}
2024-07-26 07:22:41 -04:00
try {
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'The task was processed successfully but the provider\'s output could not be encoded as JSON for the database.' , 0 , $e );
}
2024-04-29 10:21:07 -04:00
try {
$this -> taskMapper -> update ( $taskEntity );
2024-07-17 06:35:13 -04:00
$this -> runWebhook ( $task );
2024-04-29 10:21:07 -04:00
} catch ( \OCP\DB\Exception $e ) {
2024-09-06 07:53:31 -04:00
throw new \OCP\TaskProcessing\Exception\Exception ( $e -> getMessage ());
2024-04-29 10:21:07 -04:00
}
if ( $task -> getStatus () === Task :: STATUS_SUCCESSFUL ) {
$event = new TaskSuccessfulEvent ( $task );
2024-04-30 09:48:00 -04:00
} else {
2024-04-29 10:21:07 -04:00
$event = new TaskFailedEvent ( $task , $error );
}
$this -> dispatcher -> dispatchTyped ( $event );
}
2024-05-17 05:54:31 -04:00
public function getNextScheduledTask ( array $taskTypeIds = [], array $taskIdsToIgnore = []) : Task {
2024-04-29 10:21:07 -04:00
try {
2024-05-17 05:54:31 -04:00
$taskEntity = $this -> taskMapper -> findOldestScheduledByType ( $taskTypeIds , $taskIdsToIgnore );
2024-04-29 10:21:07 -04:00
return $taskEntity -> toPublicTask ();
} catch ( DoesNotExistException $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\NotFoundException ( 'Could not find the task' , previous : $e );
2024-04-29 10:21:07 -04:00
} catch ( \OCP\DB\Exception $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , previous : $e );
2025-10-14 03:30:42 -04:00
} catch ( \JsonException $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the task' , previous : $e );
2025-10-14 03:30:42 -04:00
}
}
public function getNextScheduledTasks ( array $taskTypeIds = [], array $taskIdsToIgnore = [], int $numberOfTasks = 1 ) : array {
try {
2025-10-14 09:51:31 -04:00
return array_map ( fn ( $taskEntity ) => $taskEntity -> toPublicTask (), $this -> taskMapper -> findNOldestScheduledByType ( $taskTypeIds , $taskIdsToIgnore , $numberOfTasks ));
2025-10-14 03:30:42 -04:00
} catch ( DoesNotExistException $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\NotFoundException ( 'Could not find the task' , previous : $e );
2025-10-14 03:30:42 -04:00
} catch ( \OCP\DB\Exception $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , previous : $e );
2024-04-29 10:21:07 -04:00
} catch ( \JsonException $e ) {
2025-10-14 09:51:31 -04:00
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the task' , previous : $e );
2024-04-29 10:21:07 -04:00
}
}
/**
2024-07-09 05:43:11 -04:00
* Takes task input data and replaces fileIds with File objects
2024-04-29 10:21:07 -04:00
*
2024-05-13 03:04:56 -04:00
* @ param string | null $userId
2024-05-06 10:36:35 -04:00
* @ param array < array - key , list < numeric | string >| numeric | string > $input
2024-04-30 09:48:00 -04:00
* @ param ShapeDescriptor [] ... $specs the specs
2024-05-06 10:36:35 -04:00
* @ return array < array - key , list < File | numeric | string >| numeric | string | File >
2024-07-09 05:43:11 -04:00
* @ throws GenericFileException | LockedException | NotPermittedException | ValidationException | UnauthorizedException
2024-04-30 09:48:00 -04:00
*/
2024-05-10 01:42:03 -04:00
public function fillInputFileData ( ? string $userId , array $input , ... $specs ) : array {
if ( $userId !== null ) {
2024-05-13 03:04:56 -04:00
\OC_Util :: setupFS ( $userId );
2024-05-10 01:42:03 -04:00
}
2024-04-29 10:21:07 -04:00
$newInputOutput = [];
2024-04-30 09:48:00 -04:00
$spec = array_reduce ( $specs , fn ( $carry , $spec ) => $carry + $spec , []);
2024-09-05 15:23:38 -04:00
foreach ( $spec as $key => $descriptor ) {
2024-04-29 10:21:07 -04:00
$type = $descriptor -> getShapeType ();
2024-04-30 09:48:00 -04:00
if ( ! isset ( $input [ $key ])) {
2024-04-29 10:21:07 -04:00
continue ;
}
2024-05-02 05:15:51 -04:00
if ( ! in_array ( EShapeType :: getScalarType ( $type ), [ EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video , EShapeType :: File ], true )) {
2024-04-30 09:48:00 -04:00
$newInputOutput [ $key ] = $input [ $key ];
2024-04-29 10:21:07 -04:00
continue ;
}
2024-07-13 06:22:22 -04:00
if ( EShapeType :: getScalarType ( $type ) === $type ) {
// is scalar
2024-07-09 05:43:11 -04:00
$node = $this -> validateFileId (( int ) $input [ $key ]);
$this -> validateUserAccessToFile ( $input [ $key ], $userId );
2024-04-30 09:48:00 -04:00
$newInputOutput [ $key ] = $node ;
2024-04-29 10:21:07 -04:00
} else {
2024-07-13 06:22:22 -04:00
// is list
2024-04-29 10:21:07 -04:00
$newInputOutput [ $key ] = [];
2024-04-30 09:48:00 -04:00
foreach ( $input [ $key ] as $item ) {
2024-07-09 05:43:11 -04:00
$node = $this -> validateFileId (( int ) $item );
$this -> validateUserAccessToFile ( $item , $userId );
2024-04-30 09:48:00 -04:00
$newInputOutput [ $key ][] = $node ;
2024-04-29 10:21:07 -04:00
}
}
}
return $newInputOutput ;
}
2024-05-06 10:36:35 -04:00
public function getUserTask ( int $id , ? string $userId ) : Task {
try {
$taskEntity = $this -> taskMapper -> findByIdAndUser ( $id , $userId );
return $taskEntity -> toPublicTask ();
} catch ( DoesNotExistException $e ) {
throw new \OCP\TaskProcessing\Exception\NotFoundException ( 'Could not find the task' , 0 , $e );
} catch ( MultipleObjectsReturnedException | \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the task' , 0 , $e );
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the task' , 0 , $e );
}
}
2024-05-10 01:08:31 -04:00
public function getUserTasks ( ? string $userId , ? string $taskTypeId = null , ? string $customId = null ) : array {
try {
2024-05-13 02:24:31 -04:00
$taskEntities = $this -> taskMapper -> findByUserAndTaskType ( $userId , $taskTypeId , $customId );
2024-05-10 01:08:31 -04:00
return array_map ( fn ( $taskEntity ) : Task => $taskEntity -> toPublicTask (), $taskEntities );
} catch ( \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the tasks' , 0 , $e );
2024-07-15 07:30:59 -04:00
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the tasks' , 0 , $e );
}
}
public function getTasks (
2024-07-23 05:06:42 -04:00
? string $userId , ? string $taskTypeId = null , ? string $appId = null , ? string $customId = null ,
2024-09-19 05:10:31 -04:00
? int $status = null , ? int $scheduleAfter = null , ? int $endedBefore = null ,
2024-07-15 07:30:59 -04:00
) : array {
try {
2024-07-23 05:06:42 -04:00
$taskEntities = $this -> taskMapper -> findTasks ( $userId , $taskTypeId , $appId , $customId , $status , $scheduleAfter , $endedBefore );
2024-07-15 07:30:59 -04:00
return array_map ( fn ( $taskEntity ) : Task => $taskEntity -> toPublicTask (), $taskEntities );
} catch ( \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding the tasks' , 0 , $e );
2024-05-10 01:08:31 -04:00
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding the tasks' , 0 , $e );
}
}
2024-05-07 06:46:21 -04:00
public function getUserTasksByApp ( ? string $userId , string $appId , ? string $customId = null ) : array {
2024-05-06 10:36:35 -04:00
try {
2024-05-07 06:46:21 -04:00
$taskEntities = $this -> taskMapper -> findUserTasksByApp ( $userId , $appId , $customId );
2024-05-06 10:36:35 -04:00
return array_map ( fn ( $taskEntity ) : Task => $taskEntity -> toPublicTask (), $taskEntities );
} catch ( \OCP\DB\Exception $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem finding a task' , 0 , $e );
} catch ( \JsonException $e ) {
throw new \OCP\TaskProcessing\Exception\Exception ( 'There was a problem parsing JSON after finding a task' , 0 , $e );
}
}
2024-04-29 10:21:07 -04:00
/**
* Takes task input or output and replaces base64 data with file ids
*
2024-04-30 09:48:00 -04:00
* @ param array $output
* @ param ShapeDescriptor [] ... $specs the specs that define which keys to keep
* @ return array
2024-04-29 10:21:07 -04:00
* @ throws NotPermittedException
*/
2024-04-30 09:48:00 -04:00
public function encapsulateOutputFileData ( array $output , ... $specs ) : array {
$newOutput = [];
2024-04-29 10:21:07 -04:00
try {
$folder = $this -> appData -> getFolder ( 'TaskProcessing' );
} catch ( \OCP\Files\NotFoundException ) {
$folder = $this -> appData -> newFolder ( 'TaskProcessing' );
}
2024-04-30 09:48:00 -04:00
$spec = array_reduce ( $specs , fn ( $carry , $spec ) => $carry + $spec , []);
2024-09-05 15:23:38 -04:00
foreach ( $spec as $key => $descriptor ) {
2024-04-29 10:21:07 -04:00
$type = $descriptor -> getShapeType ();
2024-04-30 09:48:00 -04:00
if ( ! isset ( $output [ $key ])) {
2024-04-29 10:21:07 -04:00
continue ;
}
2024-05-02 05:15:51 -04:00
if ( ! in_array ( EShapeType :: getScalarType ( $type ), [ EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video , EShapeType :: File ], true )) {
2024-04-30 09:48:00 -04:00
$newOutput [ $key ] = $output [ $key ];
2024-04-29 10:21:07 -04:00
continue ;
}
2024-07-13 06:22:22 -04:00
if ( EShapeType :: getScalarType ( $type ) === $type ) {
2024-04-30 09:48:00 -04:00
/** @var SimpleFile $file */
2024-07-13 05:41:44 -04:00
$file = $folder -> newFile ( time () . '-' . rand ( 1 , 100000 ), $output [ $key ]);
2024-04-30 09:48:00 -04:00
$newOutput [ $key ] = $file -> getId (); // polymorphic call to SimpleFile
2024-04-29 10:21:07 -04:00
} else {
2024-04-30 09:48:00 -04:00
$newOutput = [];
foreach ( $output [ $key ] as $item ) {
/** @var SimpleFile $file */
2024-07-13 05:41:44 -04:00
$file = $folder -> newFile ( time () . '-' . rand ( 1 , 100000 ), $item );
2024-04-30 09:48:00 -04:00
$newOutput [ $key ][] = $file -> getId ();
2024-04-29 10:21:07 -04:00
}
}
}
2024-04-30 09:48:00 -04:00
return $newOutput ;
2024-04-29 10:21:07 -04:00
}
2024-05-06 10:36:35 -04:00
/**
* @ param Task $task
* @ return array < array - key , list < numeric | string | File >| numeric | string | File >
* @ throws GenericFileException
* @ throws LockedException
* @ throws NotPermittedException
2024-07-09 05:43:11 -04:00
* @ throws ValidationException | UnauthorizedException
2024-05-06 10:36:35 -04:00
*/
2024-04-29 10:21:07 -04:00
public function prepareInputData ( Task $task ) : array {
$taskTypes = $this -> getAvailableTaskTypes ();
2024-05-03 06:23:59 -04:00
$inputShape = $taskTypes [ $task -> getTaskTypeId ()][ 'inputShape' ];
$optionalInputShape = $taskTypes [ $task -> getTaskTypeId ()][ 'optionalInputShape' ];
2024-04-29 10:21:07 -04:00
$input = $task -> getInput ();
$input = $this -> removeSuperfluousArrayKeys ( $input , $inputShape , $optionalInputShape );
2024-05-10 01:42:03 -04:00
$input = $this -> fillInputFileData ( $task -> getUserId (), $input , $inputShape , $optionalInputShape );
2024-04-29 10:21:07 -04:00
return $input ;
}
2024-05-17 05:54:31 -04:00
public function lockTask ( Task $task ) : bool {
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
if ( $this -> taskMapper -> lockTask ( $taskEntity ) === 0 ) {
return false ;
}
$task -> setStatus ( Task :: STATUS_RUNNING );
return true ;
}
2024-07-03 10:08:13 -04:00
/**
* @ throws \JsonException
* @ throws Exception
*/
public function setTaskStatus ( Task $task , int $status ) : void {
2024-07-08 12:04:46 -04:00
$currentTaskStatus = $task -> getStatus ();
if ( $currentTaskStatus === Task :: STATUS_SCHEDULED && $status === Task :: STATUS_RUNNING ) {
$task -> setStartedAt ( time ());
} elseif ( $currentTaskStatus === Task :: STATUS_RUNNING && ( $status === Task :: STATUS_FAILED || $status === Task :: STATUS_CANCELLED )) {
$task -> setEndedAt ( time ());
} elseif ( $currentTaskStatus === Task :: STATUS_UNKNOWN && $status === Task :: STATUS_SCHEDULED ) {
$task -> setScheduledAt ( time ());
}
2024-07-03 10:08:13 -04:00
$task -> setStatus ( $status );
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
$this -> taskMapper -> update ( $taskEntity );
}
2024-07-09 05:43:11 -04:00
2024-08-27 07:11:51 -04:00
/**
* Validate input , fill input default values , set completionExpectedAt , set scheduledAt
*
* @ param Task $task
* @ return void
* @ throws UnauthorizedException
* @ throws ValidationException
* @ throws \OCP\TaskProcessing\Exception\Exception
*/
private function prepareTask ( Task $task ) : void {
$taskTypes = $this -> getAvailableTaskTypes ();
$taskType = $taskTypes [ $task -> getTaskTypeId ()];
$inputShape = $taskType [ 'inputShape' ];
$inputShapeDefaults = $taskType [ 'inputShapeDefaults' ];
$inputShapeEnumValues = $taskType [ 'inputShapeEnumValues' ];
$optionalInputShape = $taskType [ 'optionalInputShape' ];
$optionalInputShapeEnumValues = $taskType [ 'optionalInputShapeEnumValues' ];
$optionalInputShapeDefaults = $taskType [ 'optionalInputShapeDefaults' ];
// validate input
$this -> validateInput ( $inputShape , $inputShapeDefaults , $inputShapeEnumValues , $task -> getInput ());
$this -> validateInput ( $optionalInputShape , $optionalInputShapeDefaults , $optionalInputShapeEnumValues , $task -> getInput (), true );
// authenticate access to mentioned files
$ids = [];
foreach ( $inputShape + $optionalInputShape as $key => $descriptor ) {
if ( in_array ( EShapeType :: getScalarType ( $descriptor -> getShapeType ()), [ EShapeType :: File , EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video ], true )) {
/** @var list<int>|int $inputSlot */
$inputSlot = $task -> getInput ()[ $key ];
if ( is_array ( $inputSlot )) {
$ids += $inputSlot ;
} else {
$ids [] = $inputSlot ;
}
}
}
foreach ( $ids as $fileId ) {
$this -> validateFileId ( $fileId );
$this -> validateUserAccessToFile ( $fileId , $task -> getUserId ());
}
// remove superfluous keys and set input
$input = $this -> removeSuperfluousArrayKeys ( $task -> getInput (), $inputShape , $optionalInputShape );
$inputWithDefaults = $this -> fillInputDefaults ( $input , $inputShapeDefaults , $optionalInputShapeDefaults );
$task -> setInput ( $inputWithDefaults );
$task -> setScheduledAt ( time ());
$provider = $this -> getPreferredProvider ( $task -> getTaskTypeId ());
// calculate expected completion time
$completionExpectedAt = new \DateTime ( 'now' );
2024-09-19 05:10:31 -04:00
$completionExpectedAt -> add ( new \DateInterval ( 'PT' . $provider -> getExpectedRuntime () . 'S' ));
2024-08-27 07:11:51 -04:00
$task -> setCompletionExpectedAt ( $completionExpectedAt );
}
/**
* Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
*
* @ param Task $task
* @ return void
* @ throws Exception
* @ throws \JsonException
*/
private function storeTask ( Task $task ) : void {
// create a db entity and insert into db table
$taskEntity = \OC\TaskProcessing\Db\Task :: fromPublicTask ( $task );
$this -> taskMapper -> insert ( $taskEntity );
// make sure the scheduler knows the id
$task -> setId ( $taskEntity -> getId ());
}
2024-07-09 05:43:11 -04:00
/**
* @ param array $output
* @ param ShapeDescriptor [] ... $specs the specs that define which keys to keep
* @ return array
* @ throws NotPermittedException
*/
private function validateOutputFileIds ( array $output , ... $specs ) : array {
$newOutput = [];
$spec = array_reduce ( $specs , fn ( $carry , $spec ) => $carry + $spec , []);
2024-09-05 15:23:38 -04:00
foreach ( $spec as $key => $descriptor ) {
2024-07-09 05:43:11 -04:00
$type = $descriptor -> getShapeType ();
if ( ! isset ( $output [ $key ])) {
continue ;
}
if ( ! in_array ( EShapeType :: getScalarType ( $type ), [ EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video , EShapeType :: File ], true )) {
$newOutput [ $key ] = $output [ $key ];
continue ;
}
2024-07-13 06:22:22 -04:00
if ( EShapeType :: getScalarType ( $type ) === $type ) {
2024-07-09 05:43:11 -04:00
// Is scalar file ID
$newOutput [ $key ] = $this -> validateFileId ( $output [ $key ]);
} else {
// Is list of file IDs
$newOutput = [];
foreach ( $output [ $key ] as $item ) {
$newOutput [ $key ][] = $this -> validateFileId ( $item );
}
}
}
return $newOutput ;
}
/**
* @ param mixed $id
2024-07-13 06:22:22 -04:00
* @ return File
2024-07-09 05:43:11 -04:00
* @ throws ValidationException
*/
2024-07-13 06:22:22 -04:00
private function validateFileId ( mixed $id ) : File {
2024-07-09 05:43:11 -04:00
$node = $this -> rootFolder -> getFirstNodeById ( $id );
if ( $node === null ) {
$node = $this -> rootFolder -> getFirstNodeByIdInPath ( $id , '/' . $this -> rootFolder -> getAppDataDirectoryName () . '/' );
if ( $node === null ) {
throw new ValidationException ( 'Could not find file ' . $id );
} elseif ( ! $node instanceof File ) {
throw new ValidationException ( 'File with id "' . $id . '" is not a file' );
}
} elseif ( ! $node instanceof File ) {
throw new ValidationException ( 'File with id "' . $id . '" is not a file' );
}
return $node ;
}
/**
* @ param mixed $fileId
2024-07-17 06:35:13 -04:00
* @ param string | null $userId
2024-07-09 05:43:11 -04:00
* @ return void
* @ throws UnauthorizedException
*/
private function validateUserAccessToFile ( mixed $fileId , ? string $userId ) : void {
if ( $userId === null ) {
throw new UnauthorizedException ( 'User does not have access to file ' . $fileId );
}
$mounts = $this -> userMountCache -> getMountsForFileId ( $fileId );
$userIds = array_map ( fn ( $mount ) => $mount -> getUser () -> getUID (), $mounts );
if ( ! in_array ( $userId , $userIds )) {
throw new UnauthorizedException ( 'User ' . $userId . ' does not have access to file ' . $fileId );
}
}
2024-07-17 06:35:13 -04:00
2025-08-06 06:21:23 -04:00
/**
* @ param Task $task
* @ return list < int >
* @ throws NotFoundException
*/
public function extractFileIdsFromTask ( Task $task ) : array {
$ids = [];
$taskTypes = $this -> getAvailableTaskTypes ();
if ( ! isset ( $taskTypes [ $task -> getTaskTypeId ()])) {
throw new NotFoundException ( 'Could not find task type' );
}
$taskType = $taskTypes [ $task -> getTaskTypeId ()];
foreach ( $taskType [ 'inputShape' ] + $taskType [ 'optionalInputShape' ] as $key => $descriptor ) {
if ( in_array ( EShapeType :: getScalarType ( $descriptor -> getShapeType ()), [ EShapeType :: File , EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video ], true )) {
/** @var int|list<int> $inputSlot */
$inputSlot = $task -> getInput ()[ $key ];
if ( is_array ( $inputSlot )) {
$ids = array_merge ( $inputSlot , $ids );
} else {
$ids [] = $inputSlot ;
}
}
}
if ( $task -> getOutput () !== null ) {
foreach ( $taskType [ 'outputShape' ] + $taskType [ 'optionalOutputShape' ] as $key => $descriptor ) {
if ( in_array ( EShapeType :: getScalarType ( $descriptor -> getShapeType ()), [ EShapeType :: File , EShapeType :: Image , EShapeType :: Audio , EShapeType :: Video ], true )) {
/** @var int|list<int> $outputSlot */
$outputSlot = $task -> getOutput ()[ $key ];
if ( is_array ( $outputSlot )) {
$ids = array_merge ( $outputSlot , $ids );
} else {
$ids [] = $outputSlot ;
}
}
}
}
return $ids ;
}
2025-08-06 09:34:15 -04:00
/**
* @ param ISimpleFolder $folder
* @ param int $ageInSeconds
* @ return \Generator
*/
2025-08-07 08:58:37 -04:00
public function clearFilesOlderThan ( ISimpleFolder $folder , int $ageInSeconds = self :: MAX_TASK_AGE_SECONDS ) : \Generator {
2025-08-06 09:34:15 -04:00
foreach ( $folder -> getDirectoryListing () as $file ) {
if ( $file -> getMTime () < time () - $ageInSeconds ) {
try {
$fileName = $file -> getName ();
$file -> delete ();
2025-08-07 08:58:37 -04:00
yield $fileName ;
2025-08-06 09:34:15 -04:00
} catch ( NotPermittedException $e ) {
$this -> logger -> warning ( 'Failed to delete a stale task processing file' , [ 'exception' => $e ]);
}
}
}
}
/**
* @ param int $ageInSeconds
* @ return \Generator
* @ throws Exception
* @ throws InvalidPathException
* @ throws NotFoundException
* @ throws \JsonException
* @ throws \OCP\Files\NotFoundException
*/
2025-08-07 08:58:37 -04:00
public function cleanupTaskProcessingTaskFiles ( int $ageInSeconds = self :: MAX_TASK_AGE_SECONDS ) : \Generator {
2025-08-06 10:00:44 -04:00
$taskIdsToCleanup = [];
2025-08-06 09:34:15 -04:00
foreach ( $this -> taskMapper -> getTasksToCleanup ( $ageInSeconds ) as $task ) {
2025-08-06 10:00:44 -04:00
$taskIdsToCleanup [] = $task -> getId ();
2025-08-06 09:34:15 -04:00
$ocpTask = $task -> toPublicTask ();
$fileIds = $this -> extractFileIdsFromTask ( $ocpTask );
foreach ( $fileIds as $fileId ) {
// only look for output files stored in appData/TaskProcessing/
$file = $this -> rootFolder -> getFirstNodeByIdInPath ( $fileId , '/' . $this -> rootFolder -> getAppDataDirectoryName () . '/core/TaskProcessing/' );
if ( $file instanceof File ) {
try {
$fileId = $file -> getId ();
$fileName = $file -> getName ();
$file -> delete ();
yield [ 'task_id' => $task -> getId (), 'file_id' => $fileId , 'file_name' => $fileName ];
} catch ( NotPermittedException $e ) {
$this -> logger -> warning ( 'Failed to delete a stale task processing file' , [ 'exception' => $e ]);
}
}
}
}
2025-08-06 10:00:44 -04:00
return $taskIdsToCleanup ;
2025-08-06 09:34:15 -04:00
}
2024-07-17 06:35:13 -04:00
/**
* Make a request to the task ' s webhookUri if necessary
*
* @ param Task $task
*/
private function runWebhook ( Task $task ) : void {
$uri = $task -> getWebhookUri ();
$method = $task -> getWebhookMethod ();
if ( ! $uri || ! $method ) {
return ;
}
if ( in_array ( $method , [ 'HTTP:GET' , 'HTTP:POST' , 'HTTP:PUT' , 'HTTP:DELETE' ], true )) {
$client = $this -> clientService -> newClient ();
$httpMethod = preg_replace ( '/^HTTP:/' , '' , $method );
$options = [
'timeout' => 30 ,
'body' => json_encode ([
'task' => $task -> jsonSerialize (),
]),
'headers' => [ 'Content-Type' => 'application/json' ],
];
try {
$client -> request ( $httpMethod , $uri , $options );
2024-08-23 09:10:27 -04:00
} catch ( ClientException | ServerException $e ) {
2024-07-17 06:35:13 -04:00
$this -> logger -> warning ( 'Task processing HTTP webhook failed for task ' . $task -> getId () . '. Request failed' , [ 'exception' => $e ]);
2024-08-23 09:10:27 -04:00
} catch ( \Exception | \Throwable $e ) {
2024-07-17 06:35:13 -04:00
$this -> logger -> warning ( 'Task processing HTTP webhook failed for task ' . $task -> getId () . '. Unknown error' , [ 'exception' => $e ]);
}
} elseif ( str_starts_with ( $method , 'AppAPI:' ) && str_starts_with ( $uri , '/' )) {
$parsedMethod = explode ( ':' , $method , 4 );
if ( count ( $parsedMethod ) < 3 ) {
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. Invalid method: ' . $method );
}
[, $exAppId , $httpMethod ] = $parsedMethod ;
2025-02-10 05:25:50 -05:00
if ( ! $this -> appManager -> isEnabledForAnyone ( 'app_api' )) {
2024-07-17 06:35:13 -04:00
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. AppAPI is disabled or not installed.' );
return ;
}
try {
$appApiFunctions = \OCP\Server :: get ( \OCA\AppAPI\PublicFunctions :: class );
} catch ( ContainerExceptionInterface | NotFoundExceptionInterface ) {
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. Could not get AppAPI public functions.' );
return ;
}
$exApp = $appApiFunctions -> getExApp ( $exAppId );
if ( $exApp === null ) {
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. ExApp ' . $exAppId . ' is missing.' );
return ;
} elseif ( ! $exApp [ 'enabled' ]) {
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. ExApp ' . $exAppId . ' is disabled.' );
return ;
}
$requestParams = [
'task' => $task -> jsonSerialize (),
];
$requestOptions = [
'timeout' => 30 ,
];
$response = $appApiFunctions -> exAppRequest ( $exAppId , $uri , $task -> getUserId (), $httpMethod , $requestParams , $requestOptions );
if ( is_array ( $response ) && isset ( $response [ 'error' ])) {
$this -> logger -> warning ( 'Task processing AppAPI webhook failed for task ' . $task -> getId () . '. Error during request to ExApp(' . $exAppId . '): ' , $response [ 'error' ]);
}
}
}
2024-04-29 10:21:07 -04:00
}