mirror of
https://github.com/nextcloud/server.git
synced 2026-11-14 06:32:09 +01:00
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>
176 lines
5.5 KiB
PHP
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];
|
|
}
|
|
}
|