Files
nextcloud-server-mirror/lib/private/Files/NodeAttributes/NodeAttributeRegistry.php
John Molakvoæ 88526c69e0 refactor: track preloaded nodes in the node attribute registry
The serializer and the DAV plugin both preload through the registry,
which only passes the nodes a provider has not preloaded yet for the
same context and attributes. Providers no longer need to skip known
nodes or preload lazily from getAttributes().

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com>
2026-10-27 16:17:40 +01:00

176 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Files\NodeAttributes;
use NCU\Files\NodeAttributes\INodeAttributeProvider;
use NCU\Files\NodeAttributes\NodeAttributeContext;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\NodeAttributeProviderRegistration;
use OCP\Files\Node;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Throwable;
/**
* Maps the registered attributes to their providers, loading providers only when needed
*/
class NodeAttributeRegistry {
/** @var array<string, NodeAttributeProviderRegistration>|null registration by attribute in Clark notation */
private ?array $registrations = null;
/** @var array<class-string, INodeAttributeProvider|null> */
private array $providers = [];
/** @var array<string, array<string, array<int, true>>> preloaded file ids by context and attribute */
private array $preloaded = [];
public function __construct(
private readonly Coordinator $coordinator,
private readonly ContainerInterface $container,
private readonly LoggerInterface $logger,
) {
}
/**
* @return list<string> the registered attributes in Clark notation
*/
public function getAttributeNames(): array {
return array_keys($this->getRegistrations());
}
/**
* Group the requested attributes by the provider serving them, unknown attributes are skipped
*
* @param list<string> $attributes in Clark notation
* @return list<array{provider: INodeAttributeProvider, attributes: list<string>}>
*/
public function getProvidersFor(array $attributes): array {
$registrations = $this->getRegistrations();
$groups = [];
foreach (array_unique($attributes) as $attribute) {
$registration = $registrations[$attribute] ?? null;
if ($registration === null) {
continue;
}
$service = $registration->getService();
$provider = $this->getProvider($service);
if ($provider === null) {
continue;
}
$groups[$service] ??= ['provider' => $provider, 'attributes' => []];
$groups[$service]['attributes'][] = $attribute;
}
return array_values($groups);
}
/**
* Preload the requested attributes of these nodes, skipping the ones already preloaded for this context
*
* @param list<Node> $nodes
* @param list<string> $attributes in Clark notation
*/
public function preload(NodeAttributeContext $context, array $nodes, array $attributes): void {
$contextKey = $this->getContextKey($context);
foreach ($this->getProvidersFor($attributes) as $group) {
$missing = array_values(array_filter($nodes, function (Node $node) use ($contextKey, $group): bool {
foreach ($group['attributes'] as $attribute) {
if (!isset($this->preloaded[$contextKey][$attribute][$node->getId()])) {
return true;
}
}
return false;
}));
if ($missing === []) {
continue;
}
$group['provider']->preload($context, $missing, $group['attributes']);
foreach ($group['attributes'] as $attribute) {
foreach ($missing as $node) {
$this->preloaded[$contextKey][$attribute][$node->getId()] = true;
}
}
}
}
/**
* Get the local name of an attribute in Clark notation, e.g. `has-preview` for `{http://nextcloud.org/ns}has-preview`
*/
public static function getLocalName(string $attribute): string {
$position = strrpos($attribute, '}');
return $position === false ? $attribute : substr($attribute, $position + 1);
}
/**
* @return array<string, NodeAttributeProviderRegistration>
*/
private function getRegistrations(): array {
if ($this->registrations !== null) {
return $this->registrations;
}
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
// Too early, apps are not registered yet: do not cache the empty result
return [];
}
$this->registrations = [];
$localNames = [];
foreach ($context->getNodeAttributeProviders() as $registration) {
foreach ($registration->getAttributes() as $attribute) {
$localName = self::getLocalName($attribute);
// Attributes are keyed by local name in the serialized nodes, so they must be unique
if (isset($localNames[$localName])) {
$this->logger->error('Node attribute ' . $attribute . ' of ' . $registration->getService() . ' is ignored, ' . $localNames[$localName] . ' already uses the name ' . $localName, [
'app' => $registration->getAppId(),
]);
continue;
}
$localNames[$localName] = $attribute;
$this->registrations[$attribute] = $registration;
}
}
return $this->registrations;
}
private function getContextKey(NodeAttributeContext $context): string {
$user = $context->getUser();
if ($user !== null) {
return 'user:' . $user->getUID();
}
return 'share:' . ($context->getShare()?->getToken() ?? '');
}
/**
* @param class-string $service
*/
private function getProvider(string $service): ?INodeAttributeProvider {
if (!array_key_exists($service, $this->providers)) {
try {
$provider = $this->container->get($service);
$this->providers[$service] = $provider instanceof INodeAttributeProvider ? $provider : null;
if ($this->providers[$service] === null) {
$this->logger->error($service . ' is registered as node attribute provider but does not implement ' . INodeAttributeProvider::class);
}
} catch (Throwable $e) {
$this->logger->error('Could not load node attribute provider ' . $service . ': ' . $e->getMessage(), [
'exception' => $e,
]);
$this->providers[$service] = null;
}
}
return $this->providers[$service];
}
}