You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

225 lines
6.6 KiB

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Files\ObjectStore;
use OCP\App\IAppManager;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\IConfig;
use OCP\IUser;
/**
* @psalm-type ObjectStoreConfig array{class: class-string<IObjectStore>, arguments: array{multibucket: bool, ...}}
*/
class PrimaryObjectStoreConfig {
public function __construct(
private readonly IConfig $config,
private readonly IAppManager $appManager,
) {
}
/**
* @param ObjectStoreConfig $config
*/
public function buildObjectStore(array $config): IObjectStore {
return new $config['class']($config['arguments']);
}
/**
* @return ?ObjectStoreConfig
*/
public function getObjectStoreConfigForRoot(): ?array {
if (!$this->hasObjectStore()) {
return null;
}
$config = $this->getObjectStoreConfiguration('root');
if ($config['arguments']['multibucket']) {
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
// put the root FS always in first bucket for multibucket configuration
$config['arguments']['bucket'] .= '0';
}
return $config;
}
/**
* @return ?ObjectStoreConfig
*/
public function getObjectStoreConfigForUser(IUser $user): ?array {
if (!$this->hasObjectStore()) {
return null;
}
$store = $this->getObjectStoreForUser($user);
$config = $this->getObjectStoreConfiguration($store);
if ($config['arguments']['multibucket']) {
$config['arguments']['bucket'] = $this->getBucketForUser($user, $config);
}
return $config;
}
/**
* @param string $name
* @return ObjectStoreConfig
*/
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 [
'default' => 'server1',
'server1' => $this->validateObjectStoreConfig($objectStoreMultiBucket),
'root' => 'server1',
];
} elseif ($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;
}
}
/**
* @param array|string $config
* @return string|ObjectStoreConfig
*/
private function validateObjectStoreConfig(array|string $config): array|string {
if (is_string($config)) {
return $config;
}
if (!isset($config['class'])) {
throw new InvalidObjectStoreConfigurationException('No class configured for object store');
}
if (!isset($config['arguments'])) {
$config['arguments'] = [];
}
$class = $config['class'];
$arguments = $config['arguments'];
if (!is_array($arguments)) {
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 InvalidObjectStoreConfigurationException('arguments.multibucket must be a boolean in object store configuration');
}
if (!is_string($class)) {
throw new InvalidObjectStoreConfigurationException('Configured class for object store is not a string');
}
if (str_starts_with($class, 'OCA\\') && substr_count($class, '\\') >= 2) {
[$appId] = explode('\\', $class);
$this->appManager->loadApp(strtolower($appId));
}
if (!is_a($class, IObjectStore::class, true)) {
throw new InvalidObjectStoreConfigurationException('Configured class for object store is not an object store');
}
return [
'class' => $class,
'arguments' => $arguments,
];
}
public function getBucketForUser(IUser $user, array $config): string {
$bucket = $this->getSetBucketForUser($user);
if ($bucket === null) {
/*
* Use any provided bucket argument as prefix
* and add the mapping from username => bucket
*/
if (!isset($config['arguments']['bucket'])) {
$config['arguments']['bucket'] = '';
}
$mapper = new Mapper($user, $this->config);
$numBuckets = $config['arguments']['num_buckets'] ?? 64;
$bucket = $config['arguments']['bucket'] . $mapper->getBucket($numBuckets);
$this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $bucket);
}
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';
}
}
}