|  | 
|  | 1 | +<?php | 
|  | 2 | + | 
|  | 3 | +declare(strict_types=1); | 
|  | 4 | +/** | 
|  | 5 | + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | 
|  | 6 | + * SPDX-License-Identifier: AGPL-3.0-only | 
|  | 7 | + */ | 
|  | 8 | + | 
|  | 9 | +namespace OC\Files\ObjectStore; | 
|  | 10 | + | 
|  | 11 | +use OCP\App\IAppManager; | 
|  | 12 | +use OCP\Files\ObjectStore\IObjectStore; | 
|  | 13 | +use OCP\IConfig; | 
|  | 14 | +use OCP\IUser; | 
|  | 15 | + | 
|  | 16 | +/** | 
|  | 17 | + * @psalm-type ObjectStoreConfig array{class: class-string<IObjectStore>, arguments: array{multibucket: bool, ...}} | 
|  | 18 | + */ | 
|  | 19 | +class PrimaryObjectStoreConfig { | 
|  | 20 | +	public function __construct( | 
|  | 21 | +		private readonly IConfig $config, | 
|  | 22 | +		private readonly IAppManager $appManager, | 
|  | 23 | +	) { | 
|  | 24 | +	} | 
|  | 25 | + | 
|  | 26 | +	/** | 
|  | 27 | +	 * @param ObjectStoreConfig $config | 
|  | 28 | +	 */ | 
|  | 29 | +	public function buildObjectStore(array $config): IObjectStore { | 
|  | 30 | +		return new $config['class']($config['arguments']); | 
|  | 31 | +	} | 
|  | 32 | + | 
|  | 33 | +	/** | 
|  | 34 | +	 * @return ?ObjectStoreConfig | 
|  | 35 | +	 */ | 
|  | 36 | +	public function getObjectStoreConfigForRoot(): ?array { | 
|  | 37 | +		$config = $this->getObjectStoreConfig(); | 
|  | 38 | + | 
|  | 39 | +		if ($config && $config['arguments']['multibucket']) { | 
|  | 40 | +			if (!isset($config['arguments']['bucket'])) { | 
|  | 41 | +				$config['arguments']['bucket'] = ''; | 
|  | 42 | +			} | 
|  | 43 | + | 
|  | 44 | +			// put the root FS always in first bucket for multibucket configuration | 
|  | 45 | +			$config['arguments']['bucket'] .= '0'; | 
|  | 46 | +		} | 
|  | 47 | +		return $config; | 
|  | 48 | +	} | 
|  | 49 | + | 
|  | 50 | +	/** | 
|  | 51 | +	 * @return ?ObjectStoreConfig | 
|  | 52 | +	 */ | 
|  | 53 | +	public function getObjectStoreConfigForUser(IUser $user): ?array { | 
|  | 54 | +		$config = $this->getObjectStoreConfig(); | 
|  | 55 | + | 
|  | 56 | +		if ($config && $config['arguments']['multibucket']) { | 
|  | 57 | +			$config['arguments']['bucket'] = $this->getBucketForUser($user, $config); | 
|  | 58 | +		} | 
|  | 59 | +		return $config; | 
|  | 60 | +	} | 
|  | 61 | + | 
|  | 62 | +	/** | 
|  | 63 | +	 * @return ?ObjectStoreConfig | 
|  | 64 | +	 */ | 
|  | 65 | +	private function getObjectStoreConfig(): ?array { | 
|  | 66 | +		$objectStore = $this->config->getSystemValue('objectstore', null); | 
|  | 67 | +		$objectStoreMultiBucket = $this->config->getSystemValue('objectstore_multibucket', null); | 
|  | 68 | + | 
|  | 69 | +		// new-style multibucket config uses the same 'objectstore' key but sets `'multibucket' => true`, transparently upgrade older style config | 
|  | 70 | +		if ($objectStoreMultiBucket) { | 
|  | 71 | +			$objectStoreMultiBucket['arguments']['multibucket'] = true; | 
|  | 72 | +			return $this->validateObjectStoreConfig($objectStoreMultiBucket); | 
|  | 73 | +		} elseif ($objectStore) { | 
|  | 74 | +			return $this->validateObjectStoreConfig($objectStore); | 
|  | 75 | +		} else { | 
|  | 76 | +			return null; | 
|  | 77 | +		} | 
|  | 78 | +	} | 
|  | 79 | + | 
|  | 80 | +	/** | 
|  | 81 | +	 * @return ObjectStoreConfig | 
|  | 82 | +	 */ | 
|  | 83 | +	private function validateObjectStoreConfig(array $config) { | 
|  | 84 | +		if (!isset($config['class'])) { | 
|  | 85 | +			throw new \Exception('No class configured for object store'); | 
|  | 86 | +		} | 
|  | 87 | +		if (!isset($config['arguments'])) { | 
|  | 88 | +			$config['arguments'] = []; | 
|  | 89 | +		} | 
|  | 90 | +		$class = $config['class']; | 
|  | 91 | +		$arguments = $config['arguments']; | 
|  | 92 | +		if (!is_array($arguments)) { | 
|  | 93 | +			throw new \Exception('Configured object store arguments are not an array'); | 
|  | 94 | +		} | 
|  | 95 | +		if (!isset($arguments['multibucket'])) { | 
|  | 96 | +			$arguments['multibucket'] = false; | 
|  | 97 | +		} | 
|  | 98 | +		if (!is_bool($arguments['multibucket'])) { | 
|  | 99 | +			throw new \Exception('arguments.multibucket must be a boolean in object store configuration'); | 
|  | 100 | +		} | 
|  | 101 | + | 
|  | 102 | +		if (!is_string($class)) { | 
|  | 103 | +			throw new \Exception('Configured class for object store is not a string'); | 
|  | 104 | +		} | 
|  | 105 | + | 
|  | 106 | +		if (str_starts_with($class, 'OCA\\') && substr_count($class, '\\') >= 2) { | 
|  | 107 | +			[$appId] = explode('\\', $class); | 
|  | 108 | +			$this->appManager->loadApp(strtolower($appId)); | 
|  | 109 | +		} | 
|  | 110 | + | 
|  | 111 | +		if (!is_a($class, IObjectStore::class, true)) { | 
|  | 112 | +			throw new \Exception('Configured class for object store is not an object store'); | 
|  | 113 | +		} | 
|  | 114 | +		return [ | 
|  | 115 | +			'class' => $class, | 
|  | 116 | +			'arguments' => $arguments, | 
|  | 117 | +		]; | 
|  | 118 | +	} | 
|  | 119 | + | 
|  | 120 | +	private function getBucketForUser(IUser $user, array $config): string { | 
|  | 121 | +		$bucket = $this->config->getUserValue($user->getUID(), 'homeobjectstore', 'bucket', null); | 
|  | 122 | + | 
|  | 123 | +		if ($bucket === null) { | 
|  | 124 | +			/* | 
|  | 125 | +			 * Use any provided bucket argument as prefix | 
|  | 126 | +			 * and add the mapping from username => bucket | 
|  | 127 | +			 */ | 
|  | 128 | +			if (!isset($config['arguments']['bucket'])) { | 
|  | 129 | +				$config['arguments']['bucket'] = ''; | 
|  | 130 | +			} | 
|  | 131 | +			$mapper = new Mapper($user, $this->config); | 
|  | 132 | +			$numBuckets = isset($config['arguments']['num_buckets']) ? $config['arguments']['num_buckets'] : 64; | 
|  | 133 | +			$bucket = $config['arguments']['bucket'] . $mapper->getBucket($numBuckets); | 
|  | 134 | + | 
|  | 135 | +			$this->config->setUserValue($user->getUID(), 'homeobjectstore', 'bucket', $bucket); | 
|  | 136 | +		} | 
|  | 137 | + | 
|  | 138 | +		return $bucket; | 
|  | 139 | +	} | 
|  | 140 | +} | 
0 commit comments