diff options
Diffstat (limited to 'lib/private')
-rw-r--r-- | lib/private/Activity/Event.php | 1 | ||||
-rw-r--r-- | lib/private/Activity/Manager.php | 39 | ||||
-rw-r--r-- | lib/private/Files/Node/Root.php | 2 | ||||
-rw-r--r-- | lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php | 13 | ||||
-rw-r--r-- | lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php | 121 | ||||
-rw-r--r-- | lib/private/Files/View.php | 44 | ||||
-rw-r--r-- | lib/private/Search/SearchComposer.php | 21 | ||||
-rw-r--r-- | lib/private/Server.php | 3 | ||||
-rw-r--r-- | lib/private/Share20/Manager.php | 2 | ||||
-rw-r--r-- | lib/private/Streamer.php | 13 | ||||
-rw-r--r-- | lib/private/TaskProcessing/Db/Task.php | 10 | ||||
-rw-r--r-- | lib/private/TaskProcessing/Db/TaskMapper.php | 25 | ||||
-rw-r--r-- | lib/private/TaskProcessing/Manager.php | 94 | ||||
-rw-r--r-- | lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php | 44 | ||||
-rw-r--r-- | lib/private/Template/JSConfigHelper.php | 6 | ||||
-rw-r--r-- | lib/private/TemplateLayout.php | 3 |
16 files changed, 336 insertions, 105 deletions
diff --git a/lib/private/Activity/Event.php b/lib/private/Activity/Event.php index 39cdc12b3fb..0ccad1d0a4e 100644 --- a/lib/private/Activity/Event.php +++ b/lib/private/Activity/Event.php @@ -450,7 +450,6 @@ class Event implements IEvent { return $this->getApp() !== '' && $this->getType() !== '' - && $this->getAffectedUser() !== '' && $this->getTimestamp() !== 0 /** * Disabled for BC with old activities diff --git a/lib/private/Activity/Manager.php b/lib/private/Activity/Manager.php index 4e10f8a0c1a..7471b7d708a 100644 --- a/lib/private/Activity/Manager.php +++ b/lib/private/Activity/Manager.php @@ -11,12 +11,14 @@ use OCP\Activity\ActivitySettings; use OCP\Activity\Exceptions\FilterNotFoundException; use OCP\Activity\Exceptions\IncompleteActivityException; use OCP\Activity\Exceptions\SettingNotFoundException; +use OCP\Activity\IBulkConsumer; use OCP\Activity\IConsumer; use OCP\Activity\IEvent; use OCP\Activity\IFilter; use OCP\Activity\IManager; use OCP\Activity\IProvider; use OCP\Activity\ISetting; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; @@ -46,6 +48,7 @@ class Manager implements IManager { protected IValidator $validator, protected IRichTextFormatter $richTextFormatter, protected IL10N $l10n, + protected ITimeFactory $timeFactory, ) { } @@ -96,6 +99,31 @@ class Manager implements IManager { * {@inheritDoc} */ public function publish(IEvent $event): void { + if ($event->getAuthor() === '' && $this->session->getUser() instanceof IUser) { + $event->setAuthor($this->session->getUser()->getUID()); + } + + if (!$event->getTimestamp()) { + $event->setTimestamp($this->timeFactory->getTime()); + } + + if ($event->getAffectedUser() === '' || !$event->isValid()) { + throw new IncompleteActivityException('The given event is invalid'); + } + + foreach ($this->getConsumers() as $c) { + $c->receive($event); + } + } + + /** + * {@inheritDoc} + */ + public function bulkPublish(IEvent $event, array $affectedUserIds, ISetting $setting): void { + if (empty($affectedUserIds)) { + throw new IncompleteActivityException('The given event is invalid'); + } + if ($event->getAuthor() === '') { if ($this->session->getUser() instanceof IUser) { $event->setAuthor($this->session->getUser()->getUID()); @@ -103,7 +131,7 @@ class Manager implements IManager { } if (!$event->getTimestamp()) { - $event->setTimestamp(time()); + $event->setTimestamp($this->timeFactory->getTime()); } if (!$event->isValid()) { @@ -111,10 +139,17 @@ class Manager implements IManager { } foreach ($this->getConsumers() as $c) { - $c->receive($event); + if ($c instanceof IBulkConsumer) { + $c->bulkReceive($event, $affectedUserIds, $setting); + } + foreach ($affectedUserIds as $affectedUserId) { + $event->setAffectedUser($affectedUserId); + $c->receive($event); + } } } + /** * In order to improve lazy loading a closure can be registered which will be called in case * activity consumers are actually requested diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index d82ec08f362..76afca9dee8 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -415,7 +415,7 @@ class Root extends Folder implements IRootFolder { */ public function getByIdInPath(int $id, string $path): array { $mountCache = $this->getUserMountCache(); - if (strpos($path, '/', 1) > 0) { + if ($path !== '' && strpos($path, '/', 1) > 0) { [, $user] = explode('/', $path); } else { $user = null; diff --git a/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php new file mode 100644 index 00000000000..369182b069d --- /dev/null +++ b/lib/private/Files/ObjectStore/InvalidObjectStoreConfigurationException.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Files\ObjectStore; + +class InvalidObjectStoreConfigurationException extends \Exception { + +} diff --git a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php index fdfe989addc..ffc33687340 100644 --- a/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php +++ b/lib/private/Files/ObjectStore/PrimaryObjectStoreConfig.php @@ -34,9 +34,13 @@ class PrimaryObjectStoreConfig { * @return ?ObjectStoreConfig */ public function getObjectStoreConfigForRoot(): ?array { - $config = $this->getObjectStoreConfig(); + if (!$this->hasObjectStore()) { + return null; + } + + $config = $this->getObjectStoreConfiguration('root'); - if ($config && $config['arguments']['multibucket']) { + if ($config['arguments']['multibucket']) { if (!isset($config['arguments']['bucket'])) { $config['arguments']['bucket'] = ''; } @@ -51,38 +55,102 @@ class PrimaryObjectStoreConfig { * @return ?ObjectStoreConfig */ public function getObjectStoreConfigForUser(IUser $user): ?array { - $config = $this->getObjectStoreConfig(); + if (!$this->hasObjectStore()) { + return null; + } - if ($config && $config['arguments']['multibucket']) { + $store = $this->getObjectStoreForUser($user); + $config = $this->getObjectStoreConfiguration($store); + + if ($config['arguments']['multibucket']) { $config['arguments']['bucket'] = $this->getBucketForUser($user, $config); } return $config; } /** - * @return ?ObjectStoreConfig + * @param string $name + * @return ObjectStoreConfig */ - private function getObjectStoreConfig(): ?array { + public function getObjectStoreConfiguration(string $name): array { + $configs = $this->getObjectStoreConfigs(); + $name = $this->resolveAlias($name); + if (!isset($configs[$name])) { + throw new \Exception("Object store configuration for '$name' not found"); + } + if (is_string($configs[$name])) { + throw new \Exception("Object store configuration for '{$configs[$name]}' not found"); + } + return $configs[$name]; + } + + public function resolveAlias(string $name): string { + $configs = $this->getObjectStoreConfigs(); + + while (isset($configs[$name]) && is_string($configs[$name])) { + $name = $configs[$name]; + } + return $name; + } + + public function hasObjectStore(): bool { + $objectStore = $this->config->getSystemValue('objectstore', null); + $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); + return $objectStore || $objectStoreMultiBucket; + } + + public function hasMultipleObjectStorages(): bool { + $objectStore = $this->config->getSystemValue('objectstore', []); + return isset($objectStore['default']); + } + + /** + * @return ?array<string, ObjectStoreConfig|string> + * @throws InvalidObjectStoreConfigurationException + */ + public function getObjectStoreConfigs(): ?array { $objectStore = $this->config->getSystemValue('objectstore', null); $objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); // new-style multibucket config uses the same 'objectstore' key but sets `'multibucket' => true`, transparently upgrade older style config if ($objectStoreMultiBucket) { $objectStoreMultiBucket['arguments']['multibucket'] = true; - return $this->validateObjectStoreConfig($objectStoreMultiBucket); + return [ + 'default' => 'server1', + 'server1' => $this->validateObjectStoreConfig($objectStoreMultiBucket), + 'root' => 'server1', + ]; } elseif ($objectStore) { - return $this->validateObjectStoreConfig($objectStore); + if (!isset($objectStore['default'])) { + $objectStore = [ + 'default' => 'server1', + 'root' => 'server1', + 'server1' => $objectStore, + ]; + } + if (!isset($objectStore['root'])) { + $objectStore['root'] = 'default'; + } + + if (!is_string($objectStore['default'])) { + throw new InvalidObjectStoreConfigurationException('The \'default\' object storage configuration is required to be a reference to another configuration.'); + } + return array_map($this->validateObjectStoreConfig(...), $objectStore); } else { return null; } } /** - * @return ObjectStoreConfig + * @param array|string $config + * @return string|ObjectStoreConfig */ - private function validateObjectStoreConfig(array $config) { + private function validateObjectStoreConfig(array|string $config): array|string { + if (is_string($config)) { + return $config; + } if (!isset($config['class'])) { - throw new \Exception('No class configured for object store'); + throw new InvalidObjectStoreConfigurationException('No class configured for object store'); } if (!isset($config['arguments'])) { $config['arguments'] = []; @@ -90,17 +158,17 @@ class PrimaryObjectStoreConfig { $class = $config['class']; $arguments = $config['arguments']; if (!is_array($arguments)) { - throw new \Exception('Configured object store arguments are not an array'); + throw new InvalidObjectStoreConfigurationException('Configured object store arguments are not an array'); } if (!isset($arguments['multibucket'])) { $arguments['multibucket'] = false; } if (!is_bool($arguments['multibucket'])) { - throw new \Exception('arguments.multibucket must be a boolean in object store configuration'); + throw new InvalidObjectStoreConfigurationException('arguments.multibucket must be a boolean in object store configuration'); } if (!is_string($class)) { - throw new \Exception('Configured class for object store is not a string'); + throw new InvalidObjectStoreConfigurationException('Configured class for object store is not a string'); } if (str_starts_with($class, 'OCA\\') && substr_count($class, '\\') >= 2) { @@ -109,7 +177,7 @@ class PrimaryObjectStoreConfig { } if (!is_a($class, IObjectStore::class, true)) { - throw new \Exception('Configured class for object store is not an object store'); + throw new InvalidObjectStoreConfigurationException('Configured class for object store is not an object store'); } return [ 'class' => $class, @@ -117,8 +185,8 @@ class PrimaryObjectStoreConfig { ]; } - private function getBucketForUser(IUser $user, array $config): string { - $bucket = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null); + public function getBucketForUser(IUser $user, array $config): string { + $bucket = $this->getSetBucketForUser($user); if ($bucket === null) { /* @@ -129,7 +197,7 @@ class PrimaryObjectStoreConfig { $config['arguments']['bucket'] = ''; } $mapper = new Mapper($user, $this->config); - $numBuckets = isset($config['arguments']['num_buckets']) ? $config['arguments']['num_buckets'] : 64; + $numBuckets = $config['arguments']['num_buckets'] ?? 64; $bucket = $config['arguments']['bucket'] . $mapper->getBucket($numBuckets); $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $bucket); @@ -137,4 +205,21 @@ class PrimaryObjectStoreConfig { return $bucket; } + + public function getSetBucketForUser(IUser $user): ?string { + return $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null); + } + + public function getObjectStoreForUser(IUser $user): string { + if ($this->hasMultipleObjectStorages()) { + $value = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'objectstore', null); + if ($value === null) { + $value = $this->resolveAlias('default'); + $this->config->setUserValue($user->getUID(), 'homeobjectstore', 'objectstore', $value); + } + return $value; + } else { + return 'default'; + } + } } diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 63eecf5e1d6..a852f453963 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1828,43 +1828,25 @@ class View { * @return string * @throws NotFoundException */ - public function getPath($id, ?int $storageId = null) { + public function getPath($id, ?int $storageId = null): string { $id = (int)$id; - $manager = Filesystem::getMountManager(); - $mounts = $manager->findIn($this->fakeRoot); - $mounts[] = $manager->find($this->fakeRoot); - $mounts = array_filter($mounts); - // reverse the array, so we start with the storage this view is in - // which is the most likely to contain the file we're looking for - $mounts = array_reverse($mounts); - - // put non-shared mounts in front of the shared mount - // this prevents unneeded recursion into shares - usort($mounts, function (IMountPoint $a, IMountPoint $b) { - return $a instanceof SharedMount && (!$b instanceof SharedMount) ? 1 : -1; - }); + $rootFolder = Server::get(Files\IRootFolder::class); - if (!is_null($storageId)) { - $mounts = array_filter($mounts, function (IMountPoint $mount) use ($storageId) { - return $mount->getNumericStorageId() === $storageId; - }); + $node = $rootFolder->getFirstNodeByIdInPath($id, $this->getRoot()); + if ($node) { + if ($storageId === null || $storageId === $node->getStorage()->getCache()->getNumericStorageId()) { + return $this->getRelativePath($node->getPath()) ?? ''; + } + } else { + throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id)); } - foreach ($mounts as $mount) { - /** - * @var \OC\Files\Mount\MountPoint $mount - */ - if ($mount->getStorage()) { - $cache = $mount->getStorage()->getCache(); - $internalPath = $cache->getPathById($id); - if (is_string($internalPath)) { - $fullPath = $mount->getMountPoint() . $internalPath; - if (!is_null($path = $this->getRelativePath($fullPath))) { - return $path; - } - } + foreach ($rootFolder->getByIdInPath($id, $this->getRoot()) as $node) { + if ($storageId === $node->getStorage()->getCache()->getNumericStorageId()) { + return $this->getRelativePath($node->getPath()) ?? ''; } } + throw new NotFoundException(sprintf('File with id "%s" has not been found.', $id)); } diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php index 77d7ca62311..be366e8ba6c 100644 --- a/lib/private/Search/SearchComposer.php +++ b/lib/private/Search/SearchComposer.php @@ -15,6 +15,7 @@ use OCP\IAppConfig; use OCP\IURLGenerator; use OCP\IUser; use OCP\Search\FilterDefinition; +use OCP\Search\IExternalProvider; use OCP\Search\IFilter; use OCP\Search\IFilteringProvider; use OCP\Search\IInAppSearch; @@ -118,7 +119,7 @@ class SearchComposer { } } - $this->providers = $this->filterProviders($this->providers); + $this->filterProviders(); $this->loadFilters(); } @@ -178,6 +179,7 @@ class SearchComposer { if ($order === null) { return; } + $isExternalProvider = $provider instanceof IExternalProvider ? $provider->isExternalProvider() : false; $triggers = [$provider->getId()]; if ($provider instanceof IFilteringProvider) { $triggers += $provider->getAlternateIds(); @@ -192,6 +194,7 @@ class SearchComposer { 'name' => $provider->getName(), 'icon' => $this->fetchIcon($appId, $provider->getId()), 'order' => $order, + 'isExternalProvider' => $isExternalProvider, 'triggers' => array_values($triggers), 'filters' => $this->getFiltersType($filters, $provider->getId()), 'inAppSearch' => $provider instanceof IInAppSearch, @@ -211,19 +214,21 @@ class SearchComposer { /** * Filter providers based on 'unified_search.providers_allowed' core app config array - * @param array $providers - * @return array + * Will remove providers that are not in the allowed list */ - private function filterProviders(array $providers): array { + private function filterProviders(): void { $allowedProviders = $this->appConfig->getValueArray('core', 'unified_search.providers_allowed'); if (empty($allowedProviders)) { - return $providers; + return; } - return array_values(array_filter($providers, function ($p) use ($allowedProviders) { - return in_array($p['id'], $allowedProviders); - })); + foreach (array_keys($this->providers) as $providerId) { + if (!in_array($providerId, $allowedProviders, true)) { + unset($this->providers[$providerId]); + unset($this->handlers[$providerId]); + } + } } private function fetchIcon(string $appId, string $providerId): string { diff --git a/lib/private/Server.php b/lib/private/Server.php index 171fee2afa1..22cd13438b8 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -657,7 +657,8 @@ class Server extends ServerContainer implements IServerContainer { $c->get(\OCP\IConfig::class), $c->get(IValidator::class), $c->get(IRichTextFormatter::class), - $l10n + $l10n, + $c->get(ITimeFactory::class), ); }); diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 855bb173d56..28f29d6b20f 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1778,7 +1778,7 @@ class Manager implements IManager { } } } - return $this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes'; + return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_ENFORCED); } /** diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php index de663f66e0d..e579c42c0d7 100644 --- a/lib/private/Streamer.php +++ b/lib/private/Streamer.php @@ -170,11 +170,16 @@ class Streamer { /** * Add an empty directory entry to the archive. * - * @param string $dirName Directory Path and name to be added to the archive. - * @return bool $success + * @param $dirName - Directory Path and name to be added to the archive. + * @param $timestamp - Modification time of the directory (defaults to current time) */ - public function addEmptyDir($dirName) { - return $this->streamerInstance->addEmptyDir($dirName); + public function addEmptyDir(string $dirName, int $timestamp = 0): bool { + $options = null; + if ($timestamp > 0) { + $options = ['timestamp' => $timestamp]; + } + + return $this->streamerInstance->addEmptyDir($dirName, $options); } /** diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php index 4d919deaf94..05c0ae9ac74 100644 --- a/lib/private/TaskProcessing/Db/Task.php +++ b/lib/private/TaskProcessing/Db/Task.php @@ -45,6 +45,8 @@ use OCP\TaskProcessing\Task as OCPTask; * @method int getStartedAt() * @method setEndedAt(int $endedAt) * @method int getEndedAt() + * @method setAllowCleanup(int $allowCleanup) + * @method int getAllowCleanup() */ class Task extends Entity { protected $lastUpdated; @@ -63,16 +65,17 @@ class Task extends Entity { protected $scheduledAt; protected $startedAt; protected $endedAt; + protected $allowCleanup; /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at']; + public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt']; + public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup']; public function __construct() { @@ -94,6 +97,7 @@ class Task extends Entity { $this->addType('scheduledAt', 'integer'); $this->addType('startedAt', 'integer'); $this->addType('endedAt', 'integer'); + $this->addType('allowCleanup', 'integer'); } public function toRow(): array { @@ -122,6 +126,7 @@ class Task extends Entity { 'scheduledAt' => $task->getScheduledAt(), 'startedAt' => $task->getStartedAt(), 'endedAt' => $task->getEndedAt(), + 'allowCleanup' => $task->getAllowCleanup() ? 1 : 0, ]); return $taskEntity; } @@ -144,6 +149,7 @@ class Task extends Entity { $task->setScheduledAt($this->getScheduledAt()); $task->setStartedAt($this->getStartedAt()); $task->setEndedAt($this->getEndedAt()); + $task->setAllowCleanup($this->getAllowCleanup() !== 0); return $task; } } diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index 91fd68820ae..fee96534633 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -183,16 +183,39 @@ class TaskMapper extends QBMapper { /** * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag * @return int the number of deleted tasks * @throws Exception */ - public function deleteOlderThan(int $timeout): int { + public function deleteOlderThan(int $timeout, bool $force = false): int { $qb = $this->db->getQueryBuilder(); $qb->delete($this->tableName) ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } return $qb->executeStatement(); } + /** + * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag + * @return \Generator<Task> + * @throws Exception + */ + public function getTasksToCleanup(int $timeout, bool $force = false): \Generator { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } + foreach ($this->yieldEntities($qb) as $entity) { + yield $entity; + }; + } + public function update(Entity $entity): Entity { $entity->setLastUpdated($this->timeFactory->now()->getTimestamp()); return parent::update($entity); diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index 11fb2bed559..e288f2981a8 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -30,6 +30,7 @@ use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; use OCP\Http\Client\IClientService; use OCP\IAppConfig; use OCP\ICache; @@ -78,6 +79,8 @@ class Manager implements IManager { 'ai.taskprocessing_provider_preferences', ]; + public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months + /** @var list<IProvider>|null */ private ?array $providers = null; @@ -1449,6 +1452,97 @@ class Manager implements IManager { } /** + * @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; + } + + /** + * @param ISimpleFolder $folder + * @param int $ageInSeconds + * @return \Generator + */ + public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + foreach ($folder->getDirectoryListing() as $file) { + if ($file->getMTime() < time() - $ageInSeconds) { + try { + $fileName = $file->getName(); + $file->delete(); + yield $fileName; + } 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 + */ + public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + $taskIdsToCleanup = []; + foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) { + $taskIdsToCleanup[] = $task->getId(); + $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]); + } + } + } + } + return $taskIdsToCleanup; + } + + /** * Make a request to the task's webhookUri if necessary * * @param Task $task diff --git a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php index 42d073a024d..52fc204b752 100644 --- a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php +++ b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php @@ -10,17 +10,14 @@ use OC\TaskProcessing\Db\TaskMapper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; use OCP\Files\AppData\IAppDataFactory; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\Files\SimpleFS\ISimpleFolder; use Psr\Log\LoggerInterface; class RemoveOldTasksBackgroundJob extends TimedJob { - public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months private \OCP\Files\IAppData $appData; public function __construct( ITimeFactory $timeFactory, + private Manager $taskProcessingManager, private TaskMapper $taskMapper, private LoggerInterface $logger, IAppDataFactory $appDataFactory, @@ -32,48 +29,29 @@ class RemoveOldTasksBackgroundJob extends TimedJob { $this->appData = $appDataFactory->get('core'); } - /** * @inheritDoc */ protected function run($argument): void { try { - $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); - } catch (\OCP\DB\Exception $e) { - $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + iterator_to_array($this->taskProcessingManager->cleanupTaskProcessingTaskFiles()); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('text2image'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { - // noop + $this->taskMapper->deleteOlderThan(Manager::MAX_TASK_AGE_SECONDS); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('audio2text'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } try { - $this->clearFilesOlderThan($this->appData->getFolder('TaskProcessing'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } } - - /** - * @param ISimpleFolder $folder - * @param int $ageInSeconds - * @return void - */ - private function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds): void { - foreach ($folder->getDirectoryListing() as $file) { - if ($file->getMTime() < time() - $ageInSeconds) { - try { - $file->delete(); - } catch (NotPermittedException $e) { - $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); - } - } - } - } - } diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index 07e557d0706..044fa8147a0 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -10,6 +10,7 @@ namespace OC\Template; use bantu\IniGetWrapper\IniGetWrapper; use OC\Authentication\Token\IProvider; use OC\CapabilitiesManager; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\FilenameValidator; use OC\Share\Share; use OCA\Provisioning_API\Controller\AUserDataOCSController; @@ -22,6 +23,7 @@ use OCP\Authentication\Token\IToken; use OCP\Constants; use OCP\Defaults; use OCP\Files\FileInfo; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroupManager; use OCP\IInitialStateService; @@ -50,6 +52,7 @@ class JSConfigHelper { protected ISession $session, protected ?IUser $currentUser, protected IConfig $config, + protected readonly IAppConfig $appConfig, protected IGroupManager $groupManager, protected IniGetWrapper $iniWrapper, protected IURLGenerator $urlGenerator, @@ -94,8 +97,7 @@ class JSConfigHelper { } } - $enableLinkPasswordByDefault = $this->config->getAppValue('core', 'shareapi_enable_link_password_by_default', 'no'); - $enableLinkPasswordByDefault = $enableLinkPasswordByDefault === 'yes'; + $enableLinkPasswordByDefault = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT); $defaultExpireDateEnabled = $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes'; $defaultExpireDate = $enforceDefaultExpireDate = null; if ($defaultExpireDateEnabled) { diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index cfc387d2164..42861eddc0d 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -20,6 +20,7 @@ use OC\Template\JSResourceLocator; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IInitialStateService; use OCP\INavigationManager; @@ -44,6 +45,7 @@ class TemplateLayout { public function __construct( private IConfig $config, + private readonly IAppConfig $appConfig, private IAppManager $appManager, private InitialStateService $initialState, private INavigationManager $navigationManager, @@ -223,6 +225,7 @@ class TemplateLayout { \OC::$server->getSession(), \OC::$server->getUserSession()->getUser(), $this->config, + $this->appConfig, \OC::$server->getGroupManager(), \OC::$server->get(IniGetWrapper::class), \OC::$server->getURLGenerator(), |